1+ from collections .abc import MutableMapping
12from datetime import datetime
23from pathlib import Path
34from typing import Any
45
56import xarray as xr
67import xattree
8+ from attrs import define
79from cattrs import Converter
810
911from flopy4 .mf6 .component import Component
12+ from flopy4 .mf6 .context import Context
13+ from flopy4 .mf6 .exchange import Exchange
14+ from flopy4 .mf6 .model import Model
15+ from flopy4 .mf6 .package import Package
16+ from flopy4 .mf6 .solution import Solution
1017from flopy4 .mf6 .spec import fields_dict , get_blocks
1118
1219
20+ @define
21+ class _Binding :
22+ """
23+ An MF6 component binding: a record representation of the
24+ component for writing to a parent component's name file.
25+ """
26+
27+ type : str
28+ fname : str
29+ terms : tuple [str , ...] | None = None
30+
31+ def to_tuple (self ):
32+ if self .terms and any (self .terms ):
33+ return (self .type , self .fname , * self .terms )
34+ else :
35+ return (self .type , self .fname )
36+
37+ @classmethod
38+ def from_component (cls , component : Component ) -> "_Binding" :
39+ def _get_binding_type (component : Component ) -> str :
40+ cls_name = component .__class__ .__name__
41+ if isinstance (component , Exchange ):
42+ return f"{ '-' .join ([cls_name [:2 ], cls_name [3 :]]).upper ()} 6"
43+ else :
44+ return f"{ cls_name .upper ()} 6"
45+
46+ def _get_binding_terms (component : Component ) -> tuple [str , ...] | None :
47+ if isinstance (component , Exchange ):
48+ return (component .exgmnamea , component .exgmnameb ) # type: ignore
49+ elif isinstance (component , Solution ):
50+ return tuple (component .models )
51+ elif isinstance (component , (Model , Package )):
52+ return (component .name ,) # type: ignore
53+ return None
54+
55+ return cls (
56+ type = _get_binding_type (component ),
57+ fname = component .filename or component .default_filename (),
58+ terms = _get_binding_terms (component ),
59+ )
60+
61+
1362def _attach_field_metadata (
1463 dataset : xr .Dataset , component_type : type , field_names : list [str ]
1564) -> None :
@@ -29,14 +78,56 @@ def _path_to_record(field_name: str, path_value: Path) -> tuple:
2978
3079
3180def unstructure_component (value : Component ) -> dict [str , Any ]:
32- data = xattree .asdict (value )
3381 blockspec = get_blocks (value .dfn )
3482 blocks : dict [str , dict [str , Any ]] = {}
83+ xatspec = xattree .get_xatspec (type (value ))
84+
85+ # Handle child component bindings before converting to dict
86+ if isinstance (value , Context ):
87+ for field_name , child_spec in xatspec .children .items ():
88+ if hasattr (child_spec , "metadata" ) and "block" in child_spec .metadata : # type: ignore
89+ block_name = child_spec .metadata ["block" ] # type: ignore
90+ field_value = getattr (value , field_name , None )
91+
92+ if block_name not in blocks :
93+ blocks [block_name ] = {}
94+
95+ if isinstance (field_value , Component ):
96+ components = [_Binding .from_component (field_value ).to_tuple ()]
97+ elif isinstance (field_value , MutableMapping ):
98+ components = [
99+ _Binding .from_component (comp ).to_tuple ()
100+ for comp in field_value .values ()
101+ if comp is not None
102+ ]
103+ elif isinstance (field_value , (list , tuple )):
104+ components = [
105+ _Binding .from_component (comp ).to_tuple ()
106+ for comp in field_value
107+ if comp is not None
108+ ]
109+ else :
110+ continue
111+
112+ if components :
113+ blocks [block_name ][field_name ] = components
114+
115+ data = xattree .asdict (value )
116+
35117 for block_name , block in blockspec .items ():
36- blocks [block_name ] = {}
118+ if block_name not in blocks :
119+ blocks [block_name ] = {}
37120 period_data = {}
38121 period_blocks = {} # type: ignore
122+
39123 for field_name in block .keys ():
124+ # Skip child components that have been processed as bindings
125+ if isinstance (value , Context ) and field_name in xatspec .children :
126+ child_spec = xatspec .children [field_name ]
127+ if hasattr (child_spec , "metadata" ) and "block" in child_spec .metadata : # type: ignore
128+ if child_spec .metadata ["block" ] == block_name : # type: ignore
129+ continue
130+
40131 field_value = data [field_name ]
41132 # convert:
42133 # - paths to records
@@ -89,7 +180,18 @@ def unstructure_component(value: Component) -> dict[str, Any]:
89180 _attach_field_metadata (dataset , type (value ), list (block .keys ()))
90181 blocks [f"{ block_name } { kper + 1 } " ] = {block_name : dataset }
91182
92- return {name : block for name , block in blocks .items () if block }
183+ # make sure options block always comes first
184+ if "options" in blocks :
185+ options_block = blocks .pop ("options" )
186+ blocks = {"options" : options_block , ** blocks }
187+
188+ # total temporary hack! manually set solutiongroup 1. still need to support multiple..
189+ if "solutiongroup" in blocks :
190+ sg = blocks ["solutiongroup" ]
191+ blocks ["solutiongroup 1" ] = sg
192+ del blocks ["solutiongroup" ]
193+
194+ return {name : block for name , block in blocks .items () if name != "period" }
93195
94196
95197def _make_converter () -> Converter :
0 commit comments