Skip to content

Commit d36bbd0

Browse files
committed
Land cover types configurable, memory improvements
1 parent 862611c commit d36bbd0

4 files changed

Lines changed: 97 additions & 51 deletions

File tree

config/config.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@
44
# A good option is "epsg:8857" (WGS 84 / Equal Earth Greenwich) for global coverage
55
buffer_crs: "epsg:8857"
66

7+
land_cover_types:
8+
POST_FLOODING: FARM
9+
RAINFED_CROPLANDS: FARM
10+
MOSAIC_CROPLAND: FARM
11+
MOSAIC_VEGETATION: FARM
12+
CLOSED_TO_OPEN_BROADLEAVED_FOREST: FOREST
13+
CLOSED_BROADLEAVED_FOREST: FOREST
14+
OPEN_BROADLEAVED_FOREST: FOREST
15+
CLOSED_NEEDLELEAVED_FOREST: FOREST
16+
OPEN_NEEDLELEAVED_FOREST: FOREST
17+
CLOSED_TO_OPEN_MIXED_FOREST: FOREST
18+
MOSAIC_FOREST: FOREST
19+
CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST: FOREST
20+
CLOSED_REGULARLY_FLOODED_FOREST: FOREST
21+
MOSAIC_GRASSLAND: OTHER
22+
CLOSED_TO_OPEN_SHRUBLAND: OTHER
23+
CLOSED_TO_OPEN_HERBS: OTHER
24+
SPARSE_VEGETATION: OTHER
25+
CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND: OTHER
26+
BARE_AREAS: OTHER
27+
ARTIFICIAL_SURFACES_AND_URBAN_AREAS: URBAN
28+
WATER_BODIES: WATER
29+
PERMANENT_SNOW: NOT_SUITABLE
30+
NO_DATA: NOT_SUITABLE
31+
732
techs:
833
pv_rooftop:
934
initial_area: settlement_area

workflow/internal/config.schema.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ properties:
1616
required: [projection, resolution]
1717
additionalProperties: false
1818

19+
land_cover_types:
20+
type: object
21+
additionalProperties:
22+
type: string
23+
description: "Mapping of land cover types to their categories."
24+
1925
techs:
2026
type: object
2127
additionalProperties:

workflow/rules/process.smk

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
rule prepare_resampled_inputs:
22
message:
33
"Resample inputs for {wildcards.shape} to the projection and resolution of the land cover data, while aggregating land cover types."
4+
params:
5+
land_cover_types_yaml_string=config["land_cover_types"],
46
input:
57
script=workflow.source_path("../scripts/resample.py"),
68
shapes="resources/user/shapes/{shape}.parquet",
@@ -23,6 +25,7 @@ rule prepare_resampled_inputs:
2325
"""
2426
python "{input.script}" \
2527
"{input.shapes}" "{input.land_cover_path}" "{input.slope_path}" "{input.settlement_path}" "{input.bathymetry_path}" "{input.protected_area_path}" \
28+
"{params.land_cover_types_yaml_string}" \
2629
"{output.resampled_input}" "{output.plot}" 2> "{log}"
2730
"""
2831

workflow/scripts/resample.py

Lines changed: 63 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import rioxarray as rxr
99
import script_utils
1010
import xarray as xr
11+
import yaml
1112
from rasterio.enums import Resampling
1213
from rasterio.features import rasterize
1314

@@ -16,7 +17,7 @@
1617
# From Troendle et al. (2019) https://github.com/timtroendle/possibility-for-electricity-autarky
1718

1819

19-
GlobCover = {
20+
GLOBCOVER_TYPES = {
2021
11: "POST_FLOODING",
2122
14: "RAINFED_CROPLANDS",
2223
20: "MOSAIC_CROPLAND",
@@ -32,57 +33,33 @@
3233
130: "CLOSED_TO_OPEN_SHRUBLAND",
3334
140: "CLOSED_TO_OPEN_HERBS",
3435
150: "SPARSE_VEGETATION",
35-
160: "CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST", # doesn't exist in Europe
36-
170: "CLOSED_REGULARLY_FLOODED_FOREST", # doesn't exist in Europe
37-
180: "CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND", # roughly 2.3% of land in Europe
38-
190: "ARTIFICAL_SURFACES_AND_URBAN_AREAS",
36+
160: "CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST",
37+
170: "CLOSED_REGULARLY_FLOODED_FOREST",
38+
180: "CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND",
39+
190: "ARTIFICIAL_SURFACES_AND_URBAN_AREAS",
3940
200: "BARE_AREAS",
4041
210: "WATER_BODIES",
4142
220: "PERMANENT_SNOW",
4243
230: "NO_DATA",
4344
}
4445

45-
CoverType = {
46-
"POST_FLOODING": "FARM",
47-
"RAINFED_CROPLANDS": "FARM",
48-
"MOSAIC_CROPLAND": "FARM",
49-
"MOSAIC_VEGETATION": "FARM",
50-
"CLOSED_TO_OPEN_BROADLEAVED_FOREST": "FOREST",
51-
"CLOSED_BROADLEAVED_FOREST": "FOREST",
52-
"OPEN_BROADLEAVED_FOREST": "FOREST",
53-
"CLOSED_NEEDLELEAVED_FOREST": "FOREST",
54-
"OPEN_NEEDLELEAVED_FOREST": "FOREST",
55-
"CLOSED_TO_OPEN_MIXED_FOREST": "FOREST",
56-
"MOSAIC_FOREST": "FOREST",
57-
"CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST": "FOREST",
58-
"CLOSED_REGULARLY_FLOODED_FOREST": "FOREST",
59-
"MOSAIC_GRASSLAND": "OTHER", # vegetation
60-
"CLOSED_TO_OPEN_SHRUBLAND": "OTHER", # vegetation
61-
"CLOSED_TO_OPEN_HERBS": "OTHER", # vegetation
62-
"SPARSE_VEGETATION": "OTHER", # vegetation
63-
"CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND": "OTHER", # vegetation
64-
"BARE_AREAS": "OTHER",
65-
"ARTIFICAL_SURFACES_AND_URBAN_AREAS": "URBAN",
66-
"WATER_BODIES": "WATER",
67-
"PERMANENT_SNOW": "NOT_SUITABLE",
68-
"NO_DATA": "NOT_SUITABLE",
69-
}
70-
7146

72-
def get_suitable_land_cover_type(ds_land_cover, suitable_land_cover_types):
47+
def aggregate_land_cover_types(ds_land_cover, land_cover_types):
7348
"""Convert raw GlobCover data to a dataset with suitable land cover types."""
7449
suitable_land_cover = xr.Dataset(coords=ds_land_cover.coords)
7550

7651
# convert the input value to land cover type of interest
7752
for value in np.unique(ds_land_cover.data):
78-
if value in GlobCover:
53+
if value in GLOBCOVER_TYPES:
7954
ds_land_cover = ds_land_cover.where(
80-
ds_land_cover != value, other=CoverType[GlobCover[value]], drop=False
55+
ds_land_cover != value,
56+
other=land_cover_types[GLOBCOVER_TYPES[value]],
57+
drop=False,
8158
)
8259

8360
# check if each pixel is in the list of suitable land cover types
84-
for type in suitable_land_cover_types:
85-
suitable_land_cover[type] = (ds_land_cover == type).astype(float)
61+
for type_ in sorted(list(set(land_cover_types.values()))):
62+
suitable_land_cover[type_] = (ds_land_cover == type_).astype(np.byte)
8663

8764
return suitable_land_cover
8865

@@ -159,6 +136,7 @@ def _rasterize_regions(shapes, reference_raster):
159136
@click.argument("settlement_path", type=str)
160137
@click.argument("bathymetry_path", type=str)
161138
@click.argument("protected_area_path", type=str)
139+
@click.argument("land_cover_configuration_yaml_string", type=str)
162140
@click.argument("output_path", type=str)
163141
@click.argument("plot_path", type=str)
164142
def resample_inputs(
@@ -168,6 +146,7 @@ def resample_inputs(
168146
settlement_path,
169147
bathymetry_path,
170148
protected_area_path,
149+
land_cover_configuration_yaml_string,
171150
output_path,
172151
plot_path,
173152
):
@@ -178,23 +157,21 @@ def resample_inputs(
178157
179158
"""
180159
shapes = gpd.read_parquet(shapes_path)
160+
resampled = xr.Dataset()
181161

182162
##
183163
# Land cover
184164
##
185-
suitable_land_cover_types = sorted(list(set(CoverType.values())))
186165
ds_land_cover = rxr.open_rasterio(land_cover_path)
187-
reference_raster = xr.ones_like(ds_land_cover)
166+
land_cover_types = yaml.safe_load(land_cover_configuration_yaml_string)
167+
reference_raster = xr.ones_like(ds_land_cover, dtype=np.byte)
188168
reference_resolution = ds_land_cover.rio.resolution()
189-
print(f"Land cover resolution: {reference_resolution}")
190-
land_cover = get_suitable_land_cover_type(ds_land_cover, suitable_land_cover_types)
191-
192-
resampled = xr.Dataset()
169+
print(f"Land cover resolution used as reference resolution: {reference_resolution}")
170+
land_cover = aggregate_land_cover_types(ds_land_cover, land_cover_types)
193171

194-
for land_type in suitable_land_cover_types:
195-
resampled[f"landcover_{land_type}"] = land_cover[land_type].rio.reproject_match(
196-
reference_raster, resampling=Resampling.average
197-
)
172+
for land_type in sorted(list(set(land_cover_types.values()))):
173+
resampled[f"landcover_{land_type}"] = land_cover[land_type]
174+
del ds_land_cover, land_cover
198175

199176
##
200177
# Pixel area
@@ -204,6 +181,7 @@ def resample_inputs(
204181
resampled["pixel_area"] = pixel_area.expand_dims({"x": resampled.x}).transpose(
205182
"y", "x"
206183
)
184+
del pixel_area
207185

208186
##
209187
# Regions
@@ -227,17 +205,21 @@ def resample_inputs(
227205
dims=resampled["regions"].dims,
228206
coords=resampled["regions"].coords,
229207
)
230-
resampled["regions_land"] = xr.where(mask_land, 1.0, np.nan)
231-
resampled["regions_maritime"] = xr.where(mask_maritime, 1.0, np.nan)
208+
resampled["regions_land"] = xr.where(mask_land, np.half(1.0), np.half(np.nan))
209+
resampled["regions_maritime"] = xr.where(
210+
mask_maritime, np.half(1.0), np.half(np.nan)
211+
)
212+
del mask_land, mask_maritime
232213

233214
##
234215
# Slope
235216
##
236217
da_slope = rxr.open_rasterio(slope_path, masked=True) / 100
237218
print(f"Slope resolution: {da_slope.rio.resolution()}")
238-
resampled["slope"] = da_slope.astype(float).rio.reproject_match(
219+
resampled["slope"] = da_slope.rio.reproject_match(
239220
reference_raster, resampling=Resampling.average
240221
)
222+
del da_slope
241223

242224
##
243225
# Settlement in sum of area of built-up surface (m2)
@@ -260,6 +242,7 @@ def resample_inputs(
260242
resampled["settlement_area"] = (
261243
resampled["settlement_share"] * resampled["pixel_area"]
262244
)
245+
del ds_settlement, ds_settlement_pixel_area
263246

264247
##
265248
# Bathymetry
@@ -272,6 +255,7 @@ def resample_inputs(
272255
resampled["bathymetry"] = ds_bathymetry.rio.reproject_match(
273256
reference_raster, resampling=Resampling.average
274257
)
258+
del ds_bathymetry
275259

276260
##
277261
# Protected areas
@@ -287,14 +271,42 @@ def resample_inputs(
287271
protected_areas.geometry, protected_areas.crs
288272
)
289273
resampled["protected"] = resampled["protected"].fillna(0)
274+
del protected_areas
290275

291-
compression = {
276+
netcdf4_encoding = {
292277
var: {"zlib": True, "complevel": 1}
293278
for var in resampled.data_vars
294279
if var not in ["spatial_ref", "band"]
295280
}
281+
for v in ["regions_land", "regions_maritime"]:
282+
netcdf4_encoding[v]["dtype"] = "int8"
283+
netcdf4_encoding[v]["scale_factor"] = 1
284+
netcdf4_encoding[v]["add_offset"] = 0
285+
netcdf4_encoding[v]["_FillValue"] = -128
286+
287+
print("Saving result to output path:", output_path)
288+
resampled.to_netcdf(output_path, encoding=netcdf4_encoding)
289+
290+
print("Saving image to plot path:", plot_path)
291+
# If needed, resample `resampled` to fit within a maximum of `max_pixels` pixels
292+
max_pixels = 1000000
293+
total_pixels = resampled.sizes["y"] * resampled.sizes["x"]
294+
if total_pixels > max_pixels:
295+
# Calculate the new resolution to fit within the max_pixels limit
296+
resolution_multiplier = 1 / math.sqrt(total_pixels / max_pixels)
297+
new_y_size = int(resampled.sizes["y"] * resolution_multiplier)
298+
new_x_size = int(resampled.sizes["x"] * resolution_multiplier)
299+
print(
300+
f"Resampling old size {resampled.sizes['y']} x {resampled.sizes['x']} "
301+
f"to new size: {new_y_size} x {new_x_size} "
302+
f"to fit within {max_pixels} pixels."
303+
)
296304

297-
resampled.to_netcdf(output_path, encoding=compression)
305+
resampled = resampled.coarsen(
306+
x=round(resampled.sizes["x"] / new_x_size),
307+
y=round(resampled.sizes["y"] / new_y_size),
308+
boundary="trim",
309+
).mean()
298310

299311
script_utils.plot_all_dataset_variables(resampled, ncols=3, savefig=plot_path)
300312

0 commit comments

Comments
 (0)