Skip to content

Commit 8b2fdc2

Browse files
Improve combine layers + some tiny fixes (#410)
* improve debug messaging in layer_combine_top_bot * improve combine_layer_ds: - check if layers are consecutive and give a useful error message otherwise - deal with single layer entries (#406) - accept both string and layer index input in combine_layers as dictionary or list * remove OrderedDict dictionaries maintain order since python 3.6 i believe * docstrings * add xmargin/ymargin to get_extent * minor improvement in add_xsec_line_and_labels subtract offset from one of the x-sec label locations * older numpy printoption * make usage of "active_domain" more explicit in get_idomain: - allow passing of str or xr.DataArray - default is "active_domain" - no warning is shown if it is not present in ds. * Update nlmod/dims/layers.py Co-authored-by: OnnoEbbens <onnoebbens@gmail.com> * Update nlmod/dims/layers.py Co-authored-by: OnnoEbbens <onnoebbens@gmail.com> * fix message, and remove layer dim from top in the returned result * codacy --------- Co-authored-by: OnnoEbbens <onnoebbens@gmail.com>
1 parent c089aaa commit 8b2fdc2

File tree

3 files changed

+130
-44
lines changed

3 files changed

+130
-44
lines changed

nlmod/dims/grid.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2266,10 +2266,33 @@ def affine_transform_gdf(gdf, affine):
22662266
return gdfm
22672267

22682268

2269-
def get_extent(ds, rotated=True):
2270-
"""Get the model extent, corrected for angrot if necessary."""
2269+
def get_extent(ds, rotated=True, xmargin=0.0, ymargin=0.0):
2270+
"""Get the model extent, corrected for angrot if necessary.
2271+
2272+
Parameters
2273+
----------
2274+
ds : xr.Dataset
2275+
model dataset.
2276+
rotated : bool, optional
2277+
if True, the extent is corrected for angrot. The default is True.
2278+
xmargin : float, optional
2279+
margin to add to the x-extent. The default is 0.0.
2280+
ymargin : float, optional
2281+
margin to add to the y-extent. The default is 0.0.
2282+
2283+
Returns
2284+
-------
2285+
extent : list
2286+
[xmin, xmax, ymin, ymax]
2287+
"""
22712288
attrs = _get_attrs(ds)
22722289
extent = attrs["extent"]
2290+
extent = [
2291+
extent[0] - xmargin,
2292+
extent[1] + xmargin,
2293+
extent[2] - ymargin,
2294+
extent[3] + ymargin,
2295+
]
22732296
if rotated and "angrot" in attrs and attrs["angrot"] != 0.0:
22742297
affine = get_affine_mod_to_world(ds)
22752298
xc = np.array([extent[0], extent[1], extent[1], extent[0]])

nlmod/dims/layers.py

Lines changed: 103 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import logging
22
import warnings
3-
from collections import OrderedDict
43

54
import numpy as np
65
import xarray as xr
76

8-
from ..util import LayerError
7+
from ..util import LayerError, _get_value_from_ds_datavar
98
from . import resample
109

1110
logger = logging.getLogger(__name__)
@@ -240,7 +239,7 @@ def split_layers_ds(
240239
bot : str, optional
241240
name of data variable containing bottom of layers, by default 'botm'
242241
return_reindexer : bool, optional
243-
Return a OrderedDict that can be used to reindex variables from the original
242+
Return a dictionary that can be used to reindex variables from the original
244243
layer-dimension to the new layer-dimension when True. The default is False.
245244
start_suffix_at : int, optional
246245
The suffix that the first splitted layer will receive, for layers that were
@@ -322,7 +321,7 @@ def split_layers_ds(
322321

323322
if return_reindexer:
324323
# determine reindexer
325-
reindexer = OrderedDict(zip(layers, layers_org))
324+
reindexer = dict(zip(layers, layers_org))
326325
for lay0 in split_dict:
327326
reindexer.pop(lay0)
328327
return ds, reindexer
@@ -369,7 +368,7 @@ def layer_combine_top_bot(ds, combine_layers, layer="layer", top="top", bot="bot
369368
-------
370369
new_top, new_bot : xarray.DataArrays
371370
DataArrays containing new tops and bottoms after splitting layers.
372-
reindexer : OrderedDict
371+
reindexer : dict
373372
dictionary mapping new to old layer indices.
374373
"""
375374
# calculate new number of layers
@@ -390,7 +389,7 @@ def layer_combine_top_bot(ds, combine_layers, layer="layer", top="top", bot="bot
390389
)
391390

392391
# dict to keep track of old and new layer indices
393-
reindexer = OrderedDict()
392+
reindexer = {}
394393

395394
j = 0 # new layer index
396395
icomb = 0 # combine layer index
@@ -401,14 +400,17 @@ def layer_combine_top_bot(ds, combine_layers, layer="layer", top="top", bot="bot
401400
if i in np.concatenate(combine_layers):
402401
# get indices of layers
403402
c = combine_layers[icomb]
403+
old_names = ds.layer.isel(layer=list(c)).to_numpy().tolist()
404404
# store new and original layer indices
405405
reindexer[j] = c
406406
# only need to calculate new top/bot once for each merged layer
407407
if i == np.min(c):
408-
logger.debug(
409-
f"{j:2d}: Merge layers {c} as layer {j}, calculate new top/bot."
410-
)
411-
tops = ds[top].data[c, :, :]
408+
with np.printoptions(legacy="1.21"):
409+
logger.debug(
410+
f"{j:2d}: Merge layers {c} ({old_names}) as layer {j}, "
411+
"calculate new top/bot."
412+
)
413+
tops = ds[top].data[c, ...]
412414
bots = ds[bot].data[c, :, :]
413415
new_top.data[j] = np.nanmax(tops, axis=0)
414416
new_bot.data[j] = np.nanmin(bots, axis=0)
@@ -425,7 +427,7 @@ def layer_combine_top_bot(ds, combine_layers, layer="layer", top="top", bot="bot
425427
else:
426428
# do not merge, only map old layer index to new layer index
427429
logger.debug(
428-
f"{j:2d}: Do not merge, map old layer index to new layer index."
430+
f"{j:2d}: Do not merge, map old layer index {i} to new layer index {j}."
429431
)
430432
new_top.data[j] = ds[top].data[i]
431433
new_bot.data[j] = ds[bot].data[i]
@@ -442,7 +444,7 @@ def sum_param_combined_layers(da, reindexer):
442444
----------
443445
da : xarray.DataArray
444446
data array to calculate combined parameters for
445-
reindexer : OrderedDict
447+
reindexer : dict
446448
dictionary mapping new layer indices to old layer indices
447449
448450
Returns
@@ -570,11 +572,11 @@ def combine_layers_ds(
570572
ds : xarray.Dataset
571573
xarray Dataset containing information about layers
572574
(layers, top and bot)
573-
combine_layers : list of tuple of ints, or dict of layer names
574-
list of tuples, with each tuple containing integers indicating
575-
layer indices to combine into one layer. E.g. [(0, 1), (2, 3)] will
576-
combine layers 0 and 1 into a single layer (with index 0) and layers
577-
2 and 3 into a second layer (with index 1).
575+
combine_layers : dict or list of iterables
576+
dictionary with new layer names as keys and a collection of layer names
577+
or indices to merge as values. Alternatively a list of iterables, with
578+
each iterable containing strings or integers indicating layers to
579+
merge. The new layer will be named after the first of the layers to be merged.
578580
layer : str, optional
579581
name of layer dimension, by default 'layer'
580582
top : str, optional
@@ -599,6 +601,32 @@ def combine_layers_ds(
599601
ds_combine : xarray.Dataset
600602
Dataset with new tops and bottoms taking into account combined layers,
601603
and recalculated values for parameters (kh, kv, kD, c).
604+
605+
Examples
606+
--------
607+
Given some layer model Dataset with named layers. Specifying which layers to merge
608+
can be done the following ways.
609+
610+
As a dictionary:
611+
612+
>>> combine_layers = {
613+
"new_layer_name": [0, 1] # as layer indices
614+
"PZWAz": ["PZWAz2", "PZWAz3", "PZWAz4"], # as strings
615+
}
616+
617+
As a list of iterables:
618+
619+
>>> combine_layers = [
620+
(0, 1), # as layer indices
621+
("PZWAz2", "PZWAz3", "PZWAz4"), # as strings
622+
]
623+
624+
Note
625+
----
626+
When passing integers to combine_layers, these are always intepreted as the
627+
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
629+
can be used to specify which layers to merge.
602630
"""
603631
data_vars = []
604632
for dv in [kh, kv, kD, c]:
@@ -608,33 +636,61 @@ def combine_layers_ds(
608636

609637
dropped_dv = set(ds.data_vars.keys()) - parsed_dv
610638
if len(dropped_dv) > 0:
611-
logger.warning(f"Following data variables will be dropped: {dropped_dv}")
639+
msg = f"Following data variables will be dropped: {dropped_dv}"
640+
logger.warning(msg)
612641

613642
# calculate new tops/bots
614643
logger.info("Calculating new layer tops and bottoms...")
615644

616-
if "layer" in ds["top"].dims:
617-
msg = "Top in ds has a layer dimension. combine_layers_ds will remove the layer dimension from top in ds."
618-
logger.warning(msg)
645+
if "layer" in ds[top].dims:
646+
msg = (
647+
f"Datavar {top} has a layer dimension. combine_layers_ds will"
648+
" remove the layer dimension from {top}."
649+
)
650+
logger.info(msg)
619651
else:
620652
ds = ds.copy()
621-
ds["top"] = ds["botm"] + calculate_thickness(ds)
653+
ds[top] = ds[bot] + calculate_thickness(ds)
622654

623655
if isinstance(combine_layers, dict):
656+
# remove single layer entries if they exist:
657+
combine_layers = {k: v for k, v in combine_layers.items() if len(v) > 1}
624658
new_layer_names = combine_layers.keys()
625-
combine_layers = [
626-
tuple(np.where(ds.layer.isin(x))[0]) for x in combine_layers.values()
659+
combine_layers_integer = [
660+
tuple(np.where(ds.layer.isin(x))[0]) if isinstance(x[0], str) else x
661+
for x in combine_layers.values()
627662
]
628-
# make sure there are no layers in between
629-
assert np.all([(np.diff(x) == 1).all() for x in combine_layers])
630663
else:
631-
new_layer_names = [ds.layer.data[x[0]] for x in combine_layers]
632-
new_layer_names = dict(zip(combine_layers, new_layer_names))
664+
# remove single layer entries if they exist:
665+
combine_layers = [x for x in combine_layers if len(x) > 1]
666+
combine_layers_integer = [
667+
tuple(np.where(ds.layer.isin(x))[0]) if isinstance(x[0], str) else x
668+
for x in combine_layers
669+
]
670+
new_layer_names = [ds.layer.data[x[0]] for x in combine_layers_integer]
671+
672+
# make sure there are no layers in between
673+
check = [(np.diff(x) == 1).all() for x in combine_layers_integer]
674+
if not np.all(check):
675+
msg = ""
676+
with np.printoptions(legacy="1.21"):
677+
for m in np.nonzero(~np.array(check))[0]:
678+
if isinstance(combine_layers, dict):
679+
layer_names = combine_layers[list(combine_layers.keys())[m]]
680+
else:
681+
layer_names = combine_layers[m]
682+
msg += f"\n{m}: {layer_names} {combine_layers_integer[m]}"
683+
raise AssertionError(
684+
f"Only consecutive layers can be combined. Check input: {msg}"
685+
)
686+
# set new layer name dictionary
687+
new_layer_names = dict(zip(combine_layers_integer, new_layer_names))
633688

689+
# collection for data arrays
634690
da_dict = {}
635691

636692
new_top, new_bot, reindexer = layer_combine_top_bot(
637-
ds, combine_layers, layer=layer, top=top, bot=bot
693+
ds, combine_layers_integer, layer=layer, top=top, bot=bot
638694
)
639695
da_dict[top] = new_top
640696
da_dict[bot] = new_bot
@@ -677,8 +733,8 @@ def combine_layers_ds(
677733
logger.info("Done! Created new dataset with combined layers!")
678734
ds_combine = xr.Dataset(da_dict, attrs=attrs)
679735

680-
# remove layer dimension from top again
681-
ds = remove_layer_dim_from_top(ds, inconsistency_threshold=0.001)
736+
# remove layer dimension from top
737+
ds_combine = remove_layer_dim_from_top(ds_combine, inconsistency_threshold=1e-5)
682738

683739
return ds_combine
684740

@@ -947,7 +1003,7 @@ def remove_thin_layers(
9471003

9481004

9491005
def get_kh_kv(kh, kv, anisotropy, fill_value_kh=1.0, fill_value_kv=0.1, idomain=None):
950-
"""Create kh en kv grid data for flopy from existing kh, kv and anistropy grids with
1006+
"""Create kh and kv grid data for flopy from existing kh, kv and anistropy grids with
9511007
nan values (typically from REGIS).
9521008
9531009
fill nans in kh grid in these steps:
@@ -1148,6 +1204,7 @@ def fill_nan_top_botm(ds):
11481204
top_max = ds["top"].max("layer")
11491205
else:
11501206
top_max = ds["top"]
1207+
11511208
# fill nans in botm of the first layer
11521209
ds["botm"][0] = ds["botm"][0].where(~ds["botm"][0].isnull(), top_max)
11531210

@@ -1201,8 +1258,8 @@ def remove_layer_dim_from_top(
12011258
"""Change top from 3d to 2d, removing NaNs in top and botm in the process.
12021259
12031260
This method sets variable `top` to the top of the upper layer (like the definition
1204-
in MODFLOW). This removes redundant data, as the top of all layers exept the most
1205-
upper one is also defined as the bottom of lower layers.
1261+
in MODFLOW). This removes redundant data, as the top of all layers (except
1262+
the first layer) is equal to the botm of the layer above.
12061263
12071264
Parameters
12081265
----------
@@ -1319,19 +1376,22 @@ def remove_inactive_layers(ds):
13191376
return ds
13201377

13211378

1322-
def get_idomain(ds):
1379+
def get_idomain(ds, active_domain="active_domain"):
13231380
"""Get idomain from a model Dataset.
13241381
13251382
Idomain is calculated from the thickness of the layers, and will be 1 for all layers
1326-
with a positive thickness, and -1 (pass-through) otherwise. On top of this, an
1327-
"active_domain" DataArray is applied, which is taken from ds, and can be 2d or 3d.
1383+
with a positive thickness, and -1 (pass-through) otherwise. Additionally, an
1384+
"active_domain" DataArray can be applied which can be 2d or 3d.
13281385
Idomain is set to 0 where "active_domain" is False or 0.
13291386
1330-
13311387
Parameters
13321388
----------
13331389
ds : xr.Dataset
13341390
The model Dataset.
1391+
active_domain : str or xr.DataArray, optional
1392+
boolean array indicating the active model domain (True = active). Idomain is
1393+
set to 0 everywhere else. If passed as str, this variable is taken from ds, if
1394+
passed as xr.DataArray, this DataArray is used. The default is "active_domain".
13351395
13361396
Returns
13371397
-------
@@ -1353,8 +1413,11 @@ def get_idomain(ds):
13531413
idomain.data[idomain.where(idomain > 0).ffill(dim="layer").isnull()] = 0
13541414
idomain.data[idomain.where(idomain > 0).bfill(dim="layer").isnull()] = 0
13551415
# set idomain to 0 in the inactive part of the model
1356-
if "active_domain" in ds:
1357-
idomain = idomain.where(ds["active_domain"], 0)
1416+
active = _get_value_from_ds_datavar(
1417+
ds, "active_domain", datavar=active_domain, default=None, warn=False
1418+
)
1419+
if active is not None:
1420+
idomain = idomain.where(active, 0)
13581421
return idomain
13591422

13601423

nlmod/plot/plotutil.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,8 @@ def add_xsec_line_and_labels(
438438
mapax.plot(x, y, **kwargs)
439439
stroke = [patheffects.withStroke(linewidth=2, foreground="w")]
440440
mapax.text(
441-
x[0] + x_offset,
442-
y[0] + y_offset,
441+
x[0] - x_offset,
442+
y[0] - y_offset,
443443
f"{label}",
444444
fontweight="bold",
445445
path_effects=stroke,

0 commit comments

Comments
 (0)