11import logging
22import warnings
3- from collections import OrderedDict
43
54import numpy as np
65import xarray as xr
76
8- from ..util import LayerError
7+ from ..util import LayerError , _get_value_from_ds_datavar
98from . import resample
109
1110logger = 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
9491005def 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
0 commit comments