Skip to content

Commit 58d8c40

Browse files
authored
Combine layers unstructured (#426)
* add functions to check ds or da grid type - is_structured - is_vertex - is_layered * add _get_empty_layered_da - add method to get empty data array with new layer coord but maintaining other dims/coords from source data array * use new method to get empty da when combining layers * fix combine_layers to work for vertex datasets * improve combine_layers - only drop datavars with layer coord - add the other datavars back to final combined result - lazy formatting in logging * add import * ruff * ruff * Add a GridTypeDims enum - specificies dimensions for grid types * ruff * add test for combining layers unstructured * add option to return reindexer: - no longer add reindexer to attributes (this was annoying when saving the dataset) * make enum even more complicated - add parse_dims class method - use this in is_structured and is_vertex grid-methods * codacy * docstring
1 parent 9482a17 commit 58d8c40

11 files changed

Lines changed: 238 additions & 115 deletions

nlmod/dims/attributes_encodings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,9 @@ def get_encodings(
167167
if np.issubdtype(da.dtype, np.character):
168168
continue
169169

170-
assert "_FillValue" not in da.attrs, (
171-
f"Custom fillvalues are not supported. {varname} has a fillvalue set."
172-
)
170+
assert (
171+
"_FillValue" not in da.attrs
172+
), f"Custom fillvalues are not supported. {varname} has a fillvalue set."
173173

174174
encoding = {
175175
"zlib": True,

nlmod/dims/grid.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,65 @@
3636
remove_inactive_layers,
3737
)
3838
from .rdp import rdp
39-
from .shared import get_area, get_delc, get_delr # noqa: F401
39+
from .shared import GridTypeDims, get_area, get_delc, get_delr # noqa: F401
4040

4141
logger = logging.getLogger(__name__)
4242

4343

44+
def is_structured(ds):
45+
"""Check if a dataset is structured.
46+
47+
Parameters
48+
----------
49+
ds : xr.Dataset or xr.Dataarray
50+
dataset or dataarray
51+
52+
Returns
53+
-------
54+
bool
55+
True if the dataset is structured.
56+
"""
57+
return GridTypeDims.parse_dims(ds) in (
58+
GridTypeDims.STRUCTURED,
59+
GridTypeDims.STRUCTURED_LAYERED,
60+
)
61+
62+
63+
def is_vertex(ds):
64+
"""Check if a dataset is vertex.
65+
66+
Parameters
67+
----------
68+
ds : xr.Dataset or xr.Dataarray
69+
dataset or dataarray
70+
71+
Returns
72+
-------
73+
bool
74+
True if the dataset is structured.
75+
"""
76+
return GridTypeDims.parse_dims(ds) in (
77+
GridTypeDims.VERTEX,
78+
GridTypeDims.VERTEX_LAYERED,
79+
)
80+
81+
82+
def is_layered(ds):
83+
"""Check if a dataset is layered.
84+
85+
Parameters
86+
----------
87+
ds : xr.Dataset or xr.Dataarray
88+
dataset or dataarray
89+
90+
Returns
91+
-------
92+
bool
93+
True if the dataset is layered.
94+
"""
95+
return "layer" in ds.dims
96+
97+
4498
def snap_extent(extent, delr, delc):
4599
"""Snap the extent in such a way that an integer number of columns and rows fit in
46100
the extent. The new extent is always equal to, or bigger than the original extent.
@@ -1513,9 +1567,9 @@ def aggregate_vector_per_cell(gdf, fields_methods, modelgrid=None):
15131567
else:
15141568
raise TypeError("cannot aggregate geometries of different types")
15151569
if bool({"length_weighted", "max_length"} & set(fields_methods.values())):
1516-
assert geom_types[0] == "LineString", (
1517-
"can only use length methods with line geometries"
1518-
)
1570+
assert (
1571+
geom_types[0] == "LineString"
1572+
), "can only use length methods with line geometries"
15191573
if bool({"area_weighted", "max_area"} & set(fields_methods.values())):
15201574
if ("Polygon" in geom_types) or ("MultiPolygon" in geom_types):
15211575
pass

nlmod/dims/layers.py

Lines changed: 71 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from ..util import LayerError, _get_value_from_ds_datavar
88
from .resample import fillnan_da
9+
from .shared import GridTypeDims
910

1011
logger = logging.getLogger(__name__)
1112

@@ -269,9 +270,9 @@ def split_layers_ds(
269270
split_dict[lay0] = [1 / split_dict[lay0]] * split_dict[lay0]
270271
elif hasattr(split_dict[lay0], "__iter__"):
271272
# make sure the fractions add up to 1
272-
assert np.isclose(np.sum(split_dict[lay0]), 1), (
273-
f"Fractions for splitting layer '{lay0}' do not add up to 1."
274-
)
273+
assert np.isclose(
274+
np.sum(split_dict[lay0]), 1
275+
), f"Fractions for splitting layer '{lay0}' do not add up to 1."
275276
split_dict[lay0] = split_dict[lay0] / np.sum(split_dict[lay0])
276277
else:
277278
raise ValueError(
@@ -375,18 +376,9 @@ def layer_combine_top_bot(ds, combine_layers, layer="layer", top="top", bot="bot
375376
new_nlay = (
376377
ds[layer].size - sum((len(c) for c in combine_layers)) + len(combine_layers)
377378
)
378-
379379
# create new DataArrays for storing new top/bot
380-
new_bot = xr.DataArray(
381-
data=np.nan,
382-
dims=["layer", "y", "x"],
383-
coords={"layer": np.arange(new_nlay), "y": ds.y.data, "x": ds.x.data},
384-
)
385-
new_top = xr.DataArray(
386-
data=np.nan,
387-
dims=["layer", "y", "x"],
388-
coords={"layer": np.arange(new_nlay), "y": ds.y.data, "x": ds.x.data},
389-
)
380+
new_bot = _get_empty_layered_da(ds[bot], nlay=new_nlay)
381+
new_top = _get_empty_layered_da(ds[top], nlay=new_nlay)
390382

391383
# dict to keep track of old and new layer indices
392384
reindexer = {}
@@ -411,7 +403,7 @@ def layer_combine_top_bot(ds, combine_layers, layer="layer", top="top", bot="bot
411403
"calculate new top/bot."
412404
)
413405
tops = ds[top].data[c, ...]
414-
bots = ds[bot].data[c, :, :]
406+
bots = ds[bot].data[c, ...]
415407
new_top.data[j] = np.nanmax(tops, axis=0)
416408
new_bot.data[j] = np.nanmin(bots, axis=0)
417409

@@ -453,25 +445,49 @@ def sum_param_combined_layers(da, reindexer):
453445
data array containing new parameters for combined layers and old
454446
parameters for unmodified layers.
455447
"""
456-
da_new = xr.DataArray(
457-
data=np.nan,
458-
dims=["layer", "y", "x"],
459-
coords={
460-
"layer": np.arange(list(reindexer.keys())[-1] + 1),
461-
"y": da["y"],
462-
"x": da["x"],
463-
},
464-
)
465-
448+
da_new = _get_empty_layered_da(da, nlay=list(reindexer.keys())[-1] + 1)
466449
for k, v in reindexer.items():
467450
if isinstance(v, tuple):
468-
psum = np.sum(da.data[v, :, :], axis=0)
451+
psum = np.sum(da.data[v, ...], axis=0)
469452
else:
470453
psum = da.data[v]
471454
da_new.data[k] = psum
472455
return da_new
473456

474457

458+
def _get_empty_layered_da(da, nlay):
459+
"""Get empty DataArray with number of layer specified by nlay.
460+
461+
Parameters
462+
----------
463+
da : xarray.DataArray
464+
original data array
465+
nlay : int
466+
number of layers
467+
468+
Returns
469+
-------
470+
da_new : xarray.DataArray
471+
new empty data array with updated number of layers
472+
"""
473+
if set(GridTypeDims.STRUCTURED_LAYERED.value).issubset(da.dims):
474+
dims = GridTypeDims.STRUCTURED_LAYERED.value
475+
coords = {
476+
"layer": np.arange(nlay),
477+
"y": da["y"],
478+
"x": da["x"],
479+
}
480+
elif set(GridTypeDims.VERTEX_LAYERED.value).issubset(da.dims):
481+
dims = GridTypeDims.VERTEX_LAYERED.value
482+
coords = {
483+
"layer": np.arange(nlay),
484+
"icell2d": da["icell2d"],
485+
}
486+
else:
487+
raise TypeError("Cannot determine grid type of data array.")
488+
return xr.DataArray(data=np.nan, dims=dims, coords=coords)
489+
490+
475491
def kheq_combined_layers(kh, thickness, reindexer):
476492
"""Calculate equivalent horizontal hydraulic conductivity.
477493
@@ -491,21 +507,12 @@ def kheq_combined_layers(kh, thickness, reindexer):
491507
for combined layers and original hydraulic conductivity in unmodified
492508
layers
493509
"""
494-
da_kh = xr.DataArray(
495-
data=np.nan,
496-
dims=["layer", "y", "x"],
497-
coords={
498-
"layer": np.arange(list(reindexer.keys())[-1] + 1),
499-
"y": kh["y"],
500-
"x": kh["x"],
501-
},
502-
)
503-
510+
da_kh = _get_empty_layered_da(kh, nlay=list(reindexer.keys())[-1] + 1)
504511
for k, v in reindexer.items():
505512
if isinstance(v, tuple):
506513
kheq = np.nansum(
507-
thickness.data[v, :, :] * kh.data[v, :, :], axis=0
508-
) / np.nansum(thickness.data[v, :, :], axis=0)
514+
thickness.data[v, ...] * kh.data[v, ...], axis=0
515+
) / np.nansum(thickness.data[v, ...], axis=0)
509516
kheq[np.isinf(kheq)] = np.nan
510517
else:
511518
kheq = kh.data[v]
@@ -532,20 +539,11 @@ def kveq_combined_layers(kv, thickness, reindexer):
532539
for combined layers and original hydraulic conductivity in unmodified
533540
layers
534541
"""
535-
da_kv = xr.DataArray(
536-
data=np.nan,
537-
dims=["layer", "y", "x"],
538-
coords={
539-
"layer": np.arange(list(reindexer.keys())[-1] + 1),
540-
"y": kv["y"],
541-
"x": kv["x"],
542-
},
543-
)
544-
542+
da_kv = _get_empty_layered_da(kv, nlay=list(reindexer.keys())[-1] + 1)
545543
for k, v in reindexer.items():
546544
if isinstance(v, tuple):
547-
kveq = np.nansum(thickness.data[v, :, :], axis=0) / np.nansum(
548-
thickness.data[v, :, :] / kv.data[v, :, :], axis=0
545+
kveq = np.nansum(thickness.data[v, ...], axis=0) / np.nansum(
546+
thickness.data[v, ...] / kv.data[v, ...], axis=0
549547
)
550548
kveq[np.isinf(kveq)] = np.nan
551549
else:
@@ -564,6 +562,7 @@ def combine_layers_ds(
564562
kv="kv",
565563
kD="kD",
566564
c="c",
565+
return_reindexer=False,
567566
):
568567
"""Combine layers in Dataset.
569568
@@ -595,6 +594,9 @@ def combine_layers_ds(
595594
c : str, optional
596595
name of data variable containg resistance or c,
597596
by default 'c'. Not parsed if set to None.
597+
return_reindexer : bool, optional
598+
Return a dictionary that can be used to reindex variables from the original
599+
layer-dimension to the new layer-dimension when True. The default is False.
598600
599601
Returns
600602
-------
@@ -625,7 +627,7 @@ def combine_layers_ds(
625627
----
626628
When passing integers to combine_layers, these are always intepreted as the
627629
layer index (i.e. starting at 0 and numbered consecutively), and not the
628-
layer "name". If the dataset layer index is integer, only the layer index
630+
layer "name". If the dataset layer names are integers, only the layer index
629631
can be used to specify which layers to merge.
630632
"""
631633
data_vars = []
@@ -634,10 +636,14 @@ def combine_layers_ds(
634636
data_vars.append(dv)
635637
parsed_dv = set([top, bot] + data_vars)
636638

637-
dropped_dv = set(ds.data_vars.keys()) - parsed_dv
638-
if len(dropped_dv) > 0:
639-
msg = f"Following data variables will be dropped: {dropped_dv}"
640-
logger.warning(msg)
639+
check_remaining_dv = set(ds.data_vars.keys()) - parsed_dv
640+
keep_dv = []
641+
for dv in check_remaining_dv:
642+
if "layer" in ds[dv].dims:
643+
msg = f"Data variable has 'layer' dimension and will be dropped: {dv}"
644+
logger.warning(msg)
645+
else:
646+
keep_dv.append(dv)
641647

642648
# calculate new tops/bots
643649
logger.info("Calculating new layer tops and bottoms...")
@@ -700,16 +706,16 @@ def combine_layers_ds(
700706

701707
# calculate equivalent kh/kv
702708
if kh is not None:
703-
logger.info(f"Calculate equivalent '{kh}' for combined layers.")
709+
logger.info("Calculate equivalent '%s' for combined layers.", kh)
704710
da_dict[kh] = kheq_combined_layers(ds[kh], thickness, reindexer)
705711
if kv is not None:
706-
logger.info(f"Calculate equivalent '{kv}' for combined layers.")
712+
logger.info("Calculate equivalent '%s' for combined layers.", kv)
707713
da_dict[kv] = kveq_combined_layers(ds[kv], thickness, reindexer)
708714
if kD is not None and kD in ds:
709-
logger.info(f"Calculate value '{kD}' for combined layers with sum.")
715+
logger.info("Calculate value '%s' for combined layers with sum.", kD)
710716
da_dict[kD] = sum_param_combined_layers(ds[kD], reindexer)
711717
if c is not None and c in ds:
712-
logger.info(f"Calculate value '{c}' for combined layers with sum.")
718+
logger.info("Calculate value '%s' for combined layers with sum.", c)
713719
da_dict[c] = sum_param_combined_layers(ds[c], reindexer)
714720

715721
# get new layer names, based on first sub-layer from each combined layer
@@ -725,17 +731,19 @@ def combine_layers_ds(
725731
for k, da in da_dict.items():
726732
da_dict[k] = da.assign_coords(layer=layer_names)
727733

728-
# add reindexer to attributes
729-
attrs = ds.attrs.copy()
730-
attrs["combine_reindexer"] = reindexer
734+
# add original data variables to new dataset
735+
for dv in keep_dv:
736+
da_dict[dv] = ds[dv]
731737

732738
# create new dataset
733739
logger.info("Done! Created new dataset with combined layers!")
734-
ds_combine = xr.Dataset(da_dict, attrs=attrs)
740+
ds_combine = xr.Dataset(da_dict, attrs=ds.attrs)
735741

736742
# remove layer dimension from top
737743
ds_combine = remove_layer_dim_from_top(ds_combine, inconsistency_threshold=1e-5)
738744

745+
if return_reindexer:
746+
return ds_combine, reindexer
739747
return ds_combine
740748

741749

nlmod/dims/shared.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,46 @@
1+
from enum import Enum
2+
13
import numpy as np
24
import xarray as xr
35

46

7+
class GridTypeDims(Enum):
8+
"""Enum for grid dimensions."""
9+
10+
STRUCTURED_LAYERED = ("layer", "y", "x")
11+
VERTEX_LAYERED = ("layer", "icell2d")
12+
STRUCTURED = ("y", "x")
13+
VERTEX = ("icell2d",)
14+
15+
@classmethod
16+
def parse_dims(cls, ds):
17+
"""Get GridTypeDim from dataset or dataarray.
18+
19+
Parameters
20+
----------
21+
ds : xr.Dataset or xr.DataArray
22+
Dataset or DataArray to parse.
23+
24+
Returns
25+
-------
26+
gridtype : GridTypeDims
27+
type of grid
28+
29+
Raises
30+
------
31+
ValueError
32+
If no partially matching gridtype is found.
33+
"""
34+
for gridtype in GridTypeDims:
35+
if set(gridtype.value).issubset(ds.dims):
36+
return gridtype
37+
# raises ValueError if no gridtype is found
38+
return cls(ds.dims)
39+
40+
541
def get_delr(ds):
642
"""
7-
Get the distance along rows (delr) from the x-coordinate of a structured model
8-
dataset.
43+
Get the distance along rows (delr) from the x-coordinate of a structured model ds.
944
1045
Parameters
1146
----------

0 commit comments

Comments
 (0)