@@ -31,6 +31,59 @@ def _scope_for(
3131 return "component"
3232
3333
34+ def _raw_dim_names (blocks : dict [str , v2 .Block ]) -> set [str ]:
35+ """Names of all Integer fields in the dimensions block."""
36+ dim_block = blocks .get ("dimensions" )
37+ if not dim_block :
38+ return set ()
39+ return {fname for fname , f in dim_block .fields .items () if isinstance (f , v2 .Integer )}
40+
41+
42+ def _parse_list_shape (s : str ) -> list [str ]:
43+ """
44+ Parse a v1 recarray shape string into a ``List.shape`` value.
45+
46+ Only a bare identifier is accepted — complex expressions such as
47+ ``sum(nlakeconn)`` cannot be represented in ``List.shape`` and are dropped.
48+ """
49+ if not s :
50+ return []
51+ s_clean = s .strip ()
52+ if s_clean .startswith ("(" ) and s_clean .endswith (")" ):
53+ s_clean = s_clean [1 :- 1 ].strip ()
54+ if _IDENT_RE .fullmatch (s_clean ):
55+ return [s_clean ]
56+ return []
57+
58+
59+ def _normalize_n_prefix_shapes (
60+ blocks : dict [str , v2 .Block ],
61+ raw_dim_names : set [str ],
62+ ) -> dict [str , v2 .Block ]:
63+ """
64+ Fix List shapes that use ``nFoo`` where the actual dimension is ``maxFoo``.
65+
66+ Some v1 DFNs (e.g. ``gwf-mvr [packages]`` with ``shape (npackages)``) use
67+ an ``n``-prefixed name while the dimensions block defines the same quantity
68+ under a ``max``-prefixed name. Normalise before building explicit dims.
69+ """
70+ result = {}
71+ for bname , block in blocks .items ():
72+ new_fields = {}
73+ changed = False
74+ for fname , field in block .fields .items ():
75+ if isinstance (field , v2 .List ) and field .shape :
76+ elem = field .shape [0 ]
77+ if elem not in raw_dim_names and elem .startswith ("n" ) and len (elem ) > 1 :
78+ candidate = "max" + elem [1 :]
79+ if candidate in raw_dim_names :
80+ field = field .model_copy (update = {"shape" : [candidate ]})
81+ changed = True
82+ new_fields [fname ] = field
83+ result [bname ] = block .model_copy (update = {"fields" : new_fields }) if changed else block
84+ return result
85+
86+
3487def _build_explicit_dims (
3588 parent : "str | list[str] | None" ,
3689 blocks : dict [str , v2 .Block ],
@@ -65,6 +118,33 @@ def _build_explicit_dims(
65118 return dims
66119
67120
121+ def _sanitize_list_shapes (
122+ blocks : dict [str , v2 .Block ],
123+ known_dims : set [str ],
124+ ) -> dict [str , v2 .Block ]:
125+ """
126+ Clear the shape of any List whose shape element doesn't resolve to a known
127+ dim.
128+
129+ Advanced packages (LAK, SFR, GNC, transport packages, etc.) often carry
130+ ``shape (maxbound)`` in their v1 DFNs as a convention even though
131+ ``maxbound`` is not declared as a dimension. The structurally correct v2
132+ representation for such lists is ``shape=[]``.
133+ """
134+ result = {}
135+ for bname , block in blocks .items ():
136+ new_fields = {}
137+ changed = False
138+ for fname , field in block .fields .items ():
139+ if isinstance (field , v2 .List ) and field .shape :
140+ if any (elem not in known_dims for elem in field .shape ):
141+ field = field .model_copy (update = {"shape" : []})
142+ changed = True
143+ new_fields [fname ] = field
144+ result [bname ] = block .model_copy (update = {"fields" : new_fields }) if changed else block
145+ return result
146+
147+
68148def _resolve_dimensions (
69149 blocks : dict [str , v2 .Block ],
70150) -> tuple [dict [str , v2 .Block ], dict [str , v2 .Dim ]]:
@@ -176,6 +256,34 @@ def _resolve_record(record: v2.Record) -> v2.Record:
176256 }
177257
178258
259+ def _fill_period_list_shapes (
260+ blocks : dict [str , v2 .Block ],
261+ explicit_dims : dict [str , v2 .Dim ],
262+ ) -> dict [str , v2 .Block ]:
263+ """
264+ For period blocks whose List field has no shape expression, infer the shape
265+ from the component's explicit dims. Currently handles ``maxbound`` only:
266+ if the component defines a ``maxbound`` dimension but the period list omits
267+ it, add ``shape=["maxbound"]``.
268+ """
269+ if "maxbound" not in explicit_dims :
270+ return blocks
271+ result = {}
272+ for bname , block in blocks .items ():
273+ if "period" not in bname :
274+ result [bname ] = block
275+ continue
276+ new_fields = {}
277+ changed = False
278+ for fname , field in block .fields .items ():
279+ if isinstance (field , v2 .List ) and not field .shape :
280+ field = field .model_copy (update = {"shape" : ["maxbound" ]})
281+ changed = True
282+ new_fields [fname ] = field
283+ result [bname ] = block .model_copy (update = {"fields" : new_fields }) if changed else block
284+ return result
285+
286+
179287def map (dfn : v1 .Dfn ) -> v2 .Component :
180288 """Map a component definition from the v1 schema to v2."""
181289
@@ -401,6 +509,7 @@ def _record_fields() -> dict:
401509
402510 if _type .startswith ("recarray" ):
403511 item = _row_field ()
512+ list_shape = _parse_list_shape (shape_str ) if shape_str else []
404513 return v2 .List (
405514 name = _name ,
406515 longname = longname ,
@@ -410,6 +519,7 @@ def _record_fields() -> dict:
410519 developmode = developmode ,
411520 netcdf = netcdf ,
412521 item = item ,
522+ shape = list_shape ,
413523 )
414524
415525 if _type .startswith ("keystring" ):
@@ -572,7 +682,12 @@ def _record_fields() -> dict:
572682
573683 blocks , array_dims = _resolve_dimensions (blocks )
574684 blocks = _resolve_relations (blocks )
685+ raw_dim_names = _raw_dim_names (blocks )
686+ blocks = _normalize_n_prefix_shapes (blocks , raw_dim_names )
575687 explicit_dims = _build_explicit_dims (dfn ["parent" ], blocks )
688+ known_dims = set (explicit_dims ) | set (array_dims )
689+ blocks = _sanitize_list_shapes (blocks , known_dims )
690+ blocks = _fill_period_list_shapes (blocks , explicit_dims )
576691 dims = {** explicit_dims , ** array_dims } or None
577692
578693 d : dict [str , Any ] = {
0 commit comments