33from pathlib import Path
44from typing import Any
55
6- import numpy as np
7- import sparse
86import xarray as xr
97import xattree
10- from cattrs import Converter
118from modflow_devtools .dfn .schema .block import block_sort_key
12- from numpy .typing import NDArray
13- from xattree import get_xatspec
149
15- from flopy4 .adapters import get_nn
1610from flopy4 .mf6 .binding import Binding
1711from flopy4 .mf6 .component import Component
18- from flopy4 .mf6 .config import SPARSE_THRESHOLD
19- from flopy4 .mf6 .constants import FILL_DNODATA
2012from flopy4 .mf6 .context import Context
2113from flopy4 .mf6 .spec import FileInOut
2214
2315
24- def path_to_tuple (name : str , value : Path , inout : FileInOut ) -> tuple [str , ...]:
16+ def _path_to_tuple (name : str , value : Path , inout : FileInOut ) -> tuple [str , ...]:
2517 t = [name .upper ()]
2618 if name .endswith ("_file" ):
2719 t [0 ] = name .replace ("_file" , "" ).upper ()
@@ -31,7 +23,7 @@ def path_to_tuple(name: str, value: Path, inout: FileInOut) -> tuple[str, ...]:
3123 return tuple (t )
3224
3325
34- def make_binding_blocks (value : Component ) -> dict [str , dict [str , list [tuple [str , ...]]]]:
26+ def _make_binding_blocks (value : Component ) -> dict [str , dict [str , list [tuple [str , ...]]]]:
3527 if not isinstance (value , Context ):
3628 return {}
3729
@@ -104,7 +96,7 @@ def unstructure_component(value: Component) -> dict[str, Any]:
10496 data = xattree .asdict (value )
10597
10698 # create child component binding blocks
107- blocks .update (make_binding_blocks (value ))
99+ blocks .update (_make_binding_blocks (value ))
108100
109101 # process blocks in order, unstructuring fields as needed,
110102 # then slice period data into separate kper-indexed blocks
@@ -132,6 +124,7 @@ def unstructure_component(value: Component) -> dict[str, Any]:
132124 # - 'auxiliary' fields to tuples
133125 # - xarray DataArrays with 'nper' dim to dict of kper-sliced datasets
134126 # - other values to their original form
127+ # TODO: use cattrs converters for field unstructuring?
135128 match field_value := data [field_name ]:
136129 case None :
137130 continue
@@ -141,7 +134,7 @@ def unstructure_component(value: Component) -> dict[str, Any]:
141134 case Path ():
142135 field_spec = xatspec .attrs [field_name ]
143136 field_meta = getattr (field_spec , "metadata" , {})
144- t = path_to_tuple (
137+ t = _path_to_tuple (
145138 field_name , field_value , inout = field_meta .get ("inout" , "fileout" )
146139 )
147140 # name may have changed e.g dropping '_file' suffix
@@ -197,98 +190,3 @@ def unstructure_component(value: Component) -> dict[str, Any]:
197190 del blocks ["solutiongroup" ]
198191
199192 return {name : block for name , block in blocks .items () if name != period_block_name }
200-
201-
202- def _make_converter () -> Converter :
203- converter = Converter ()
204- converter .register_unstructure_hook_factory (xattree .has , lambda _ : xattree .asdict )
205- converter .register_unstructure_hook (Component , unstructure_component )
206- return converter
207-
208-
209- COMPONENT_CONVERTER = _make_converter ()
210-
211-
212- def dict_to_array (value , self_ , field ) -> NDArray :
213- """
214- Convert a sparse dictionary representation of an array to a
215- dense numpy array or a sparse COO array.
216-
217- TODO: generalize this not only to dictionaries but to any
218- form that can be converted to an array (e.g. nested list)
219- """
220-
221- if not isinstance (value , dict ):
222- # if not a dict, assume it's a numpy array
223- # and let xarray deal with it if it isn't
224- return value
225-
226- spec = get_xatspec (type (self_ )).flat
227- field = spec [field .name ]
228- if not field .dims :
229- raise ValueError (f"Field { field } missing dims" )
230-
231- # resolve dims
232- explicit_dims = self_ .__dict__ .get ("dims" , {})
233- inherited_dims = dict (self_ .parent .data .dims ) if self_ .parent else {}
234- dims = inherited_dims | explicit_dims
235- shape = [dims .get (d , d ) for d in field .dims ]
236- unresolved = [d for d in shape if isinstance (d , str )]
237- if any (unresolved ):
238- raise ValueError (f"Couldn't resolve dims: { unresolved } " )
239-
240- if np .prod (shape ) > SPARSE_THRESHOLD :
241- a : dict [tuple [Any , ...], Any ] = dict ()
242-
243- def set_ (arr , val , * ind ):
244- arr [tuple (ind )] = val
245-
246- def final (arr ):
247- coords = np .array (list (map (list , zip (* arr .keys ()))))
248- return sparse .COO (
249- coords ,
250- list (arr .values ()),
251- shape = shape ,
252- fill_value = field .default or FILL_DNODATA ,
253- )
254- else :
255- a = np .full (shape , FILL_DNODATA , dtype = field .dtype ) # type: ignore
256-
257- def set_ (arr , val , * ind ):
258- arr [ind ] = val
259-
260- def final (arr ):
261- arr [arr == FILL_DNODATA ] = field .default or FILL_DNODATA
262- return arr
263-
264- if "nper" in dims :
265- for kper , period in value .items ():
266- if kper == "*" :
267- kper = 0
268- match len (shape ):
269- case 1 :
270- set_ (a , period , kper )
271- case _:
272- for cellid , v in period .items ():
273- nn = get_nn (cellid , ** dims )
274- set_ (a , v , kper , nn )
275- if kper == "*" :
276- break
277- else :
278- for cellid , v in value .items ():
279- nn = get_nn (cellid , ** dims )
280- set_ (a , v , nn )
281-
282- return final (a )
283-
284-
285- def structure (data : dict [str , Any ], path : Path ) -> Component :
286- component = COMPONENT_CONVERTER .structure (data , Component )
287- if isinstance (component , Context ):
288- component .workspace = path .parent
289- component .filename = path .name
290- return component
291-
292-
293- def unstructure (component : Component ) -> dict [str , Any ]:
294- return COMPONENT_CONVERTER .unstructure (component )
0 commit comments