77import xarray as xr
88import xattree
99from modflow_devtools .dfn .schema .block import block_sort_key
10+ from xattree import XatSpec
1011
1112from flopy4 .mf6 .binding import Binding
1213from flopy4 .mf6 .component import Component
@@ -135,7 +136,91 @@ def oc_setting_data(rec):
135136 return data
136137
137138
139+ def _unstructure_block_param (
140+ block_name : str ,
141+ field_name : str ,
142+ xatspec : XatSpec ,
143+ value : Component ,
144+ data : dict [str , Any ],
145+ blocks : dict ,
146+ period_data : dict ,
147+ ) -> None :
148+ # Skip child components that have been processed as bindings
149+ if isinstance (value , Context ) and field_name in xatspec .children :
150+ child_spec = xatspec .children [field_name ]
151+ if hasattr (child_spec , "metadata" ) and "block" in child_spec .metadata : # type: ignore
152+ if child_spec .metadata ["block" ] == block_name : # type: ignore
153+ return
154+
155+ # filter out empty values and false keywords, and convert:
156+ # - paths to records
157+ # - datetimes to ISO format
158+ # - filter out false keywords
159+ # - 'auxiliary' fields to tuples
160+ # - xarray DataArrays with 'nper' dim to dict of kper-sliced datasets
161+ # - other values to their original form
162+ # TODO: use cattrs converters for field unstructuring?
163+ match field_value := data [field_name ]:
164+ case None :
165+ return
166+ case bool ():
167+ if field_value :
168+ blocks [block_name ][field_name ] = field_value
169+ case Path ():
170+ field_spec = xatspec .attrs [field_name ]
171+ field_meta = getattr (field_spec , "metadata" , {})
172+ t = _path_to_tuple (field_name , field_value , inout = field_meta .get ("inout" , "fileout" ))
173+ # name may have changed e.g dropping '_file' suffix
174+ blocks [block_name ][t [0 ]] = t
175+ case datetime ():
176+ blocks [block_name ][field_name ] = field_value .isoformat ()
177+ case t if (
178+ field_name == "auxiliary" and hasattr (field_value , "values" ) and field_value is not None
179+ ):
180+ blocks [block_name ][field_name ] = tuple (field_value .values .tolist ())
181+ case xr .DataArray () if "nper" in field_value .dims :
182+ has_spatial_dims = any (
183+ dim in field_value .dims for dim in ["nlay" , "nrow" , "ncol" , "nodes" ]
184+ )
185+ if has_spatial_dims :
186+ field_value = _hack_structured_grid_dims (
187+ field_value ,
188+ structured_grid_dims = value .parent .data .dims , # type: ignore
189+ )
190+ if block_name == "period" :
191+ if not np .issubdtype (field_value .dtype , np .number ):
192+ dat = _hack_period_non_numeric (field_name , field_value )
193+ for n , v in dat .items ():
194+ period_data [n ] = v
195+ else :
196+ period_data [field_name ] = {
197+ kper : field_value .isel (nper = kper ) # type: ignore
198+ for kper in range (field_value .sizes ["nper" ])
199+ }
200+ else :
201+ blocks [block_name ][field_name ] = field_value
202+
203+ case _:
204+ blocks [block_name ][field_name ] = field_value
205+
206+
138207def unstructure_component (value : Component ) -> dict [str , Any ]:
208+ xatspec = xattree .get_xatspec (type (value ))
209+ if "readarraygrid" in xatspec .attrs :
210+ return _unstructure_grid_component (value )
211+ elif "readasarrays" in xatspec .attrs :
212+ return _unstructure_layer_component (value )
213+ else :
214+ return _unstructure_component (value )
215+
216+
217+ def _unstructure_layer_component (value : Component ) -> dict [str , Any ]:
218+ return {}
219+
220+
221+ def _unstructure_grid_component (value : Component ) -> dict [str , Any ]:
222+ from flopy4 .mf6 .constants import FILL_DNODATA
223+
139224 blockspec = dict (sorted (value .dfn .blocks .items (), key = block_sort_key )) # type: ignore
140225 blocks : dict [str , dict [str , Any ]] = {}
141226 xatspec = xattree .get_xatspec (type (value ))
@@ -156,67 +241,53 @@ def unstructure_component(value: Component) -> dict[str, Any]:
156241 blocks [block_name ] = {}
157242
158243 for field_name in block .keys ():
159- # Skip child components that have been processed as bindings
160- if isinstance (value , Context ) and field_name in xatspec .children :
161- child_spec = xatspec .children [field_name ]
162- if hasattr (child_spec , "metadata" ) and "block" in child_spec .metadata : # type: ignore
163- if child_spec .metadata ["block" ] == block_name : # type: ignore
164- continue
165-
166- # filter out empty values and false keywords, and convert:
167- # - paths to records
168- # - datetimes to ISO format
169- # - filter out false keywords
170- # - 'auxiliary' fields to tuples
171- # - xarray DataArrays with 'nper' dim to dict of kper-sliced datasets
172- # - other values to their original form
173- # TODO: use cattrs converters for field unstructuring?
174- match field_value := data [field_name ]:
175- case None :
176- continue
177- case bool ():
178- if field_value :
179- blocks [block_name ][field_name ] = field_value
180- case Path ():
181- field_spec = xatspec .attrs [field_name ]
182- field_meta = getattr (field_spec , "metadata" , {})
183- t = _path_to_tuple (
184- field_name , field_value , inout = field_meta .get ("inout" , "fileout" )
185- )
186- # name may have changed e.g dropping '_file' suffix
187- blocks [block_name ][t [0 ]] = t
188- case datetime ():
189- blocks [block_name ][field_name ] = field_value .isoformat ()
190- case t if (
191- field_name == "auxiliary"
192- and hasattr (field_value , "values" )
193- and field_value is not None
194- ):
195- blocks [block_name ][field_name ] = tuple (field_value .values .tolist ())
196- case xr .DataArray () if "nper" in field_value .dims :
197- has_spatial_dims = any (
198- dim in field_value .dims for dim in ["nlay" , "nrow" , "ncol" , "nodes" ]
199- )
200- if has_spatial_dims :
201- field_value = _hack_structured_grid_dims (
202- field_value ,
203- structured_grid_dims = value .parent .data .dims , # type: ignore
204- )
205- if block_name == "period" :
206- if not np .issubdtype (field_value .dtype , np .number ):
207- dat = _hack_period_non_numeric (field_name , field_value )
208- for n , v in dat .items ():
209- period_data [n ] = v
210- else :
211- period_data [field_name ] = {
212- kper : field_value .isel (nper = kper ) # type: ignore
213- for kper in range (field_value .sizes ["nper" ])
214- }
215- else :
216- blocks [block_name ][field_name ] = field_value
217-
218- case _:
219- blocks [block_name ][field_name ] = field_value
244+ _unstructure_block_param (
245+ block_name , field_name , xatspec , value , data , blocks , period_data
246+ )
247+
248+ # invert key order, (arr_name, kper) -> (kper, arr_name)
249+ for arr_name , periods in period_data .items ():
250+ for kper , arr in periods .items ():
251+ if kper not in period_blocks :
252+ period_blocks [kper ] = {}
253+ period_blocks [kper ][arr_name ] = arr
254+
255+ # setup indexed period blocks, combine arrays into datasets
256+ for kper , block in period_blocks .items ():
257+ key = f"period { kper + 1 } "
258+ for arr_name , val in block .items ():
259+ if not np .all (val == FILL_DNODATA ):
260+ if key not in blocks :
261+ blocks [key ] = {}
262+ blocks [f"period { kper + 1 } " ][arr_name ] = val
263+
264+ return {name : block for name , block in blocks .items () if name != "period" }
265+
266+
267+ def _unstructure_component (value : Component ) -> dict [str , Any ]:
268+ blockspec = dict (sorted (value .dfn .blocks .items (), key = block_sort_key )) # type: ignore
269+ blocks : dict [str , dict [str , Any ]] = {}
270+ xatspec = xattree .get_xatspec (type (value ))
271+ data = xattree .asdict (value )
272+
273+ # create child component binding blocks
274+ blocks .update (_make_binding_blocks (value ))
275+
276+ # process blocks in order, unstructuring fields as needed,
277+ # then slice period data into separate kper-indexed blocks
278+ # each of which contains a dataset indexed for that period.
279+ for block_name , block in blockspec .items ():
280+ period_data = {} # type: ignore
281+ period_blocks = {} # type: ignore
282+ period_block_name = None
283+
284+ if block_name not in blocks :
285+ blocks [block_name ] = {}
286+
287+ for field_name in block .keys ():
288+ _unstructure_block_param (
289+ block_name , field_name , xatspec , value , data , blocks , period_data
290+ )
220291
221292 # invert key order, (arr_name, kper) -> (kper, arr_name)
222293 for arr_name , periods in period_data .items ():
0 commit comments