2525from boltons .iterutils import remap
2626
2727from 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+
9395FormatVersion = Literal [1 , 2 ]
9496"""DFN format version number."""
9597
@@ -118,6 +120,21 @@ def _field_attr_sort_key(item) -> int:
118120
119121Dfns = dict [str , "Dfn" ]
120122Fields = 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
123140class 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 ):
0 commit comments