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