Skip to content

Commit d891348

Browse files
committed
refactor(dfn): extract/separate period block arrays
1 parent 548e5e8 commit d891348

2 files changed

Lines changed: 100 additions & 25 deletions

File tree

modflow_devtools/dfn.py

Lines changed: 84 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,10 @@
2525
from boltons.iterutils import remap
2626

2727
from modflow_devtools.download import download_and_unzip
28+
from modflow_devtools.misc import try_literal_eval
2829

29-
# TODO: use dataclasses instead of typed dicts? static
30-
# methods on typed dicts are evidently not allowed
31-
# mypy: ignore-errors
3230

33-
34-
def _try_literal_eval(value: str) -> Any:
35-
"""
36-
Try to parse a string as a literal. If this fails,
37-
return the value unaltered.
38-
"""
39-
try:
40-
return literal_eval(value)
41-
except (SyntaxError, ValueError):
42-
return value
43-
44-
45-
def _try_parse_bool(value: Any) -> Any:
31+
def try_parse_bool(value: Any) -> Any:
4632
"""
4733
Try to parse a boolean from a string as represented
4834
in a DFN file, otherwise return the value unaltered.
@@ -54,7 +40,7 @@ def _try_parse_bool(value: Any) -> Any:
5440
return value
5541

5642

57-
def _field_attr_sort_key(item) -> int:
43+
def field_attr_sort_key(item) -> int:
5844
"""
5945
Sort key for input field attributes. The order is:
6046
-1. block
@@ -90,6 +76,22 @@ def _field_attr_sort_key(item) -> int:
9076
return 8
9177

9278

79+
def block_sort_key(item: tuple[str, dict]) -> int:
80+
k, _ = item
81+
if k == "options":
82+
return 0
83+
elif k == "dimensions":
84+
return 1
85+
elif k == "griddata":
86+
return 2
87+
elif k == "packagedata":
88+
return 3
89+
elif "period" in k:
90+
return 4
91+
else:
92+
return 5
93+
94+
9395
FormatVersion = Literal[1, 2]
9496
"""DFN format version number."""
9597

@@ -118,6 +120,21 @@ def _field_attr_sort_key(item) -> int:
118120

119121
Dfns = dict[str, "Dfn"]
120122
Fields = dict[str, "Field"]
123+
Block = Fields
124+
Blocks = dict[str, Block]
125+
126+
127+
def get_blocks(dfn: "Dfn") -> Blocks:
128+
"""
129+
Extract blocks from an input definition. Any entry whose key
130+
is not explicitly defined in `Dfn` is a block.
131+
"""
132+
return dict(
133+
sorted(
134+
{k: v for k, v in dfn.items() if k not in Dfn.__annotations__}.items(),
135+
key=block_sort_key,
136+
)
137+
)
121138

122139

123140
class Field(TypedDict):
@@ -279,6 +296,49 @@ def _load_v1(cls, f, name, **kwargs) -> "Dfn":
279296
refs = kwargs.pop("refs", {})
280297
flat, meta = Dfn._load_v1_flat(f, **kwargs)
281298

299+
def _convert_period_block(block: Block) -> Block:
300+
"""
301+
Convert a period block recarray to individual arrays, one per column.
302+
303+
Extracts recarray fields and creates separate array variables. Gives
304+
each an appropriate grid- or tdis-aligned shape as opposed to sparse
305+
list shape in terms of maxbound (as previously), and sets the reader
306+
field to "list" to indicate to MF6 that the arrays are to be loaded
307+
together via list-based input. (This could just be inferred from the
308+
block name "period" or the presence of an "iper" variable, but seems
309+
better to be explicit than rely on implicit rules.)
310+
"""
311+
312+
fields = list(block.values())
313+
if fields[0]["type"] == "recarray":
314+
assert len(fields) == 1
315+
recarray_name = fields[0]["name"]
316+
item = fields[0]["item"]
317+
columns = item.get("fields", None)
318+
if not columns:
319+
assert item["type"] == "keystring"
320+
columns = item["choices"]
321+
else:
322+
recarray_name = None
323+
columns = block
324+
block.pop(recarray_name, None)
325+
cellid = columns.pop("cellid", None)
326+
for col_name, column in columns.items():
327+
col_copy = column.copy()
328+
old_dims = col_copy.get("shape")
329+
if old_dims:
330+
old_dims = old_dims[1:-1].split(",")
331+
new_dims = ["nper"]
332+
if cellid:
333+
new_dims.append("nnodes")
334+
if old_dims:
335+
new_dims.extend([dim for dim in old_dims if dim != "maxbound"])
336+
col_copy["shape"] = f"({', '.join(new_dims)})"
337+
col_copy["reader"] = "list"
338+
block[col_name] = col_copy
339+
340+
return block
341+
282342
def _convert_field(var: dict[str, Any]) -> Field:
283343
"""
284344
Convert an input field specification from its representation
@@ -300,15 +360,15 @@ def _load(field) -> Field:
300360
# stay a string except default values, which we'll
301361
# try to parse as arbitrary literals below, and at
302362
# some point types, once we introduce type hinting
303-
field = {k: _try_parse_bool(v) for k, v in field.items()}
363+
field = {k: try_parse_bool(v) for k, v in field.items()}
304364

305365
_name = field.pop("name")
306366
_type = field.pop("type", None)
307367
shape = field.pop("shape", None)
308368
shape = None if shape == "" else shape
309369
block = field.pop("block", None)
310370
default = field.pop("default", None)
311-
default = _try_literal_eval(default) if _type != "string" else default
371+
default = try_literal_eval(default) if _type != "string" else default
312372
description = field.pop("description", "")
313373
reader = field.pop("reader", "urword")
314374
ref = refs.get(_name, None)
@@ -453,7 +513,7 @@ def _fields() -> Fields:
453513

454514
return var_
455515

456-
return dict(sorted(_load(var).items(), key=_field_attr_sort_key))
516+
return dict(sorted(_load(var).items(), key=field_attr_sort_key))
457517

458518
# load top-level fields. any nested
459519
# fields will be loaded recursively
@@ -469,11 +529,10 @@ def _fields() -> Fields:
469529
for block_name, block in groupby(fields.values(), lambda v: v["block"])
470530
}
471531

472-
# mark transient blocks
473-
transient_index_vars = flat.getlist("iper")
474-
for transient_index in transient_index_vars:
475-
transient_block = transient_index["block"]
476-
blocks[transient_block]["transient_block"] = True
532+
# if there's a period block, extract distinct arrays from
533+
# the recarray-style definition
534+
if (period_block := blocks.get("period", None)) is not None:
535+
blocks["period"] = _convert_period_block(period_block)
477536

478537
# remove unneeded variable attributes
479538
def remove_attrs(path, key, value):

modflow_devtools/misc.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,3 +565,19 @@ def try_get_enum_value(v: Any) -> Any:
565565
of an enumeration, otherwise return it unaltered.
566566
"""
567567
return v.value if isinstance(v, Enum) else v
568+
569+
570+
# TODO: use dataclasses instead of typed dicts? static
571+
# methods on typed dicts are evidently not allowed
572+
# mypy: ignore-errors
573+
574+
575+
def try_literal_eval(value: str) -> Any:
576+
"""
577+
Try to parse a string as a literal. If this fails,
578+
return the value unaltered.
579+
"""
580+
try:
581+
return literal_eval(value)
582+
except (SyntaxError, ValueError):
583+
return value

0 commit comments

Comments
 (0)