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
1016from flopy4 .mf6 .spec import fields_dict , get_blocks
1117
1218
19+ @define
20+ class _Binding :
21+ """
22+ An MF6 component binding: a record representation of the
23+ component for writing to a parent component's name file.
24+ """
25+
26+ type : str
27+ fname : str
28+ terms : tuple [str , ...] | None = None
29+
30+ def to_tuple (self ):
31+ if self .terms and any (self .terms ):
32+ return (self .type , self .fname , * self .terms )
33+ else :
34+ return (self .type , self .fname )
35+
36+ @classmethod
37+ def from_component (cls , component : Component ) -> "_Binding" :
38+ def _get_binding_type (component : Component ) -> str :
39+ cls_name = component .__class__ .__name__
40+ if isinstance (component , Exchange ):
41+ return f"{ '-' .join ([cls_name [:2 ], cls_name [3 :]]).upper ()} 6"
42+ else :
43+ return f"{ cls_name .upper ()} 6"
44+
45+ def _get_binding_terms (component : Component ) -> tuple [str , ...] | None :
46+ if isinstance (component , Exchange ):
47+ return (component .exgmnamea , component .exgmnameb ) # type: ignore
48+ elif isinstance (component , (Model , Package )):
49+ return (component .name ,) # type: ignore
50+ # TODO solutions
51+ return None
52+
53+ return cls (
54+ type = _get_binding_type (component ),
55+ fname = component .filename or component .default_filename (),
56+ terms = _get_binding_terms (component ),
57+ )
58+
59+
1360def _attach_field_metadata (
1461 dataset : xr .Dataset , component_type : type , field_names : list [str ]
1562) -> None :
@@ -29,14 +76,56 @@ def _path_to_record(field_name: str, path_value: Path) -> tuple:
2976
3077
3178def unstructure_component (value : Component ) -> dict [str , Any ]:
32- data = xattree .asdict (value )
3379 blockspec = get_blocks (value .dfn )
3480 blocks : dict [str , dict [str , Any ]] = {}
81+ xatspec = xattree .get_xatspec (type (value ))
82+
83+ # Handle child component bindings before converting to dict
84+ if isinstance (value , Context ):
85+ for field_name , child_spec in xatspec .children .items ():
86+ if hasattr (child_spec , "metadata" ) and "block" in child_spec .metadata : # type: ignore
87+ block_name = child_spec .metadata ["block" ] # type: ignore
88+ field_value = getattr (value , field_name , None )
89+
90+ if block_name not in blocks :
91+ blocks [block_name ] = {}
92+
93+ if isinstance (field_value , Component ):
94+ components = [_Binding .from_component (field_value ).to_tuple ()]
95+ elif isinstance (field_value , MutableMapping ):
96+ components = [
97+ _Binding .from_component (comp ).to_tuple ()
98+ for comp in field_value .values ()
99+ if comp is not None
100+ ]
101+ elif isinstance (field_value , (list , tuple )):
102+ components = [
103+ _Binding .from_component (comp ).to_tuple ()
104+ for comp in field_value
105+ if comp is not None
106+ ]
107+ else :
108+ continue
109+
110+ if components :
111+ blocks [block_name ][field_name ] = components
112+
113+ data = xattree .asdict (value )
114+
35115 for block_name , block in blockspec .items ():
36- blocks [block_name ] = {}
116+ if block_name not in blocks :
117+ blocks [block_name ] = {}
37118 period_data = {}
38119 period_blocks = {} # type: ignore
120+
39121 for field_name in block .keys ():
122+ # Skip child components that have been processed as bindings
123+ if isinstance (value , Context ) and field_name in xatspec .children :
124+ child_spec = xatspec .children [field_name ]
125+ if hasattr (child_spec , "metadata" ) and "block" in child_spec .metadata : # type: ignore
126+ if child_spec .metadata ["block" ] == block_name : # type: ignore
127+ continue
128+
40129 field_value = data [field_name ]
41130 # convert:
42131 # - paths to records
0 commit comments