Skip to content

Commit 15142f5

Browse files
author
wpbonelli
committed
back to original dict->array converter
1 parent 83bd261 commit 15142f5

File tree

18 files changed

+2189
-1715
lines changed

18 files changed

+2189
-1715
lines changed

flopy4/mf6/codec/__init__.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@
88
from jinja2 import Environment, PackageLoader
99

1010
from flopy4.mf6 import filters
11-
from flopy4.mf6.codec.converter import (
12-
unstructure_array,
13-
unstructure_chd,
14-
unstructure_component,
15-
unstructure_oc,
16-
unstructure_tdis,
17-
)
11+
from flopy4.mf6.codec.converter import unstructure_component
1812

1913
_JINJA_ENV = Environment(
2014
loader=PackageLoader("flopy4.mf6"),
@@ -28,6 +22,8 @@
2822
_JINJA_ENV.filters["array_how"] = filters.array_how
2923
_JINJA_ENV.filters["array_chunks"] = filters.array_chunks
3024
_JINJA_ENV.filters["array2string"] = filters.array2string
25+
_JINJA_ENV.filters["to_sparse_dict"] = filters.to_sparse_dict
26+
_JINJA_ENV.filters["to_period_records"] = filters.to_period_records
3127

3228
_JINJA_TEMPLATE_NAME = "blocks.jinja"
3329

@@ -39,21 +35,12 @@
3935

4036

4137
def _make_converter() -> Converter:
42-
# TODO: document what is converter's responsibility vs Jinja's
43-
# TODO: how can we make sure writing remains lazy for list input?
44-
# don't eagerly unstructure to dict, lazily access from the template?
45-
38+
"""Create a simple converter that just handles structure conversion."""
4639
from flopy4.mf6.component import Component
47-
from flopy4.mf6.gwf.chd import Chd
48-
from flopy4.mf6.gwf.oc import Oc
49-
from flopy4.mf6.tdis import Tdis
5040

5141
converter = Converter()
5242
converter.register_unstructure_hook_factory(xattree.has, lambda _: xattree.asdict)
5343
converter.register_unstructure_hook(Component, unstructure_component)
54-
converter.register_unstructure_hook(Tdis, unstructure_tdis)
55-
converter.register_unstructure_hook(Chd, unstructure_chd)
56-
converter.register_unstructure_hook(Oc, unstructure_oc)
5744
return converter
5845

5946

@@ -72,19 +59,20 @@ def load(path: str | PathLike) -> Any:
7259

7360
def dumps(data) -> str:
7461
template = _JINJA_ENV.get_template(_JINJA_TEMPLATE_NAME)
62+
unstructured_data = _CONVERTER.unstructure(data)
7563
with np.printoptions(**_PRINT_OPTIONS): # type: ignore
76-
return template.render(dfn=type(data).dfn, data=_CONVERTER.unstructure(data))
64+
return template.render(dfn=type(data).dfn, data=unstructured_data, component=data)
7765

7866

7967
def dump(data, path: str | PathLike) -> None:
8068
template = _JINJA_ENV.get_template(_JINJA_TEMPLATE_NAME)
81-
iterator = template.generate(dfn=type(data).dfn, data=_CONVERTER.unstructure(data))
69+
unstructured_data = _CONVERTER.unstructure(data)
70+
iterator = template.generate(dfn=type(data).dfn, data=unstructured_data, component=data)
8271
with np.printoptions(**_PRINT_OPTIONS), open(path, "w") as f: # type: ignore
8372
f.writelines(iterator)
8473

8574

8675
__all__ = [
87-
"unstructure_array",
8876
"loads",
8977
"load",
9078
"dumps",

flopy4/mf6/codec/converter.py

Lines changed: 5 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,154 +1,15 @@
11
from typing import Any
22

3-
import numpy as np
4-
import sparse
53
import xattree
6-
from xarray import DataArray
74

8-
from flopy4.adapters import get_cellid
95
from flopy4.mf6.component import Component
10-
from flopy4.mf6.constants import FILL_DNODATA
11-
from flopy4.mf6.spec import get_blocks, is_list_field
12-
13-
14-
def unstructure_array(value: DataArray) -> dict:
15-
"""
16-
Convert a dense numpy array or a sparse COO array to a sparse
17-
dictionary representation suitable for serialization into the
18-
MF6 list-based input format.
19-
20-
The input array must have a time dimension named 'nper', i.e.
21-
it must be stress period data for some MODFLOW 6 component.
22-
23-
Returns:
24-
dict: {kper: {spatial indices: value, ...}, ...}
25-
"""
26-
if (time_dim := "nper") not in value.dims:
27-
raise ValueError(f"Array must have dimension '{time_dim}'")
28-
if isinstance(value.data, sparse.COO):
29-
coords = value.coords
30-
data = value.data
31-
else:
32-
coords = np.array(np.where(value.data != FILL_DNODATA)).T # type: ignore
33-
data = value.data[tuple(coords.T)] # type: ignore
34-
if not coords.size: # type: ignore
35-
return {}
36-
result = {}
37-
match value.ndim:
38-
case 1:
39-
# Only kper, no spatial dims
40-
for kper, v in zip(coords[:, 0], data):
41-
result[int(kper)] = v
42-
case _:
43-
# kper + spatial dims
44-
for row, v in zip(coords, data):
45-
kper = int(row[0]) # type: ignore
46-
spatial = tuple(int(x) for x in row[1:]) # type: ignore
47-
if kper not in result:
48-
result[kper] = {}
49-
# flatten spatial index if only one spatial dim
50-
key = spatial[0] if len(spatial) == 1 else spatial
51-
result[kper][key] = v
52-
return result
536

547

558
def unstructure_component(value: Component) -> dict[str, Any]:
56-
data = xattree.asdict(value)
57-
blocks = get_blocks(value.dfn)
58-
for block in blocks.values():
59-
for field_name, field in block.items():
60-
if is_list_field(field):
61-
data[field_name] = unstructure_array(data[field_name])
62-
return data
63-
64-
65-
def unstructure_tdis(value: Any) -> dict[str, Any]:
66-
data = xattree.asdict(value)
67-
blocks = get_blocks(value.dfn)
68-
for block_name, block in blocks.items():
69-
if block_name == "perioddata":
70-
arrs_d = {}
71-
periods = set() # type: ignore
72-
for field_name in block.keys():
73-
arr = data.get(field_name, None)
74-
arr_d = {} if arr is None else unstructure_array(arr)
75-
arrs_d[field_name] = arr_d
76-
periods.update(arr_d.keys())
77-
periods = sorted(periods) # type: ignore
78-
perioddata = {} # type: ignore
79-
for kper in periods:
80-
line = []
81-
if kper not in perioddata:
82-
perioddata[kper] = [] # type: ignore
83-
for arr_d in arrs_d.values():
84-
if val := arr_d.get(kper, None):
85-
line.append(val)
86-
perioddata[kper] = tuple(line)
87-
data["perioddata"] = perioddata
88-
return data
89-
90-
91-
def unstructure_chd(value: Any) -> dict[str, Any]:
92-
if (parent := value.parent) is None:
93-
raise ValueError(
94-
"CHD cannot be unstructured without a parent "
95-
"model and corresponding grid discretization."
96-
)
97-
grid = parent.grid
98-
data = xattree.asdict(value)
99-
blocks = get_blocks(value.dfn)
100-
for block_name, block in blocks.items():
101-
if block_name == "period":
102-
arrs_d = {}
103-
periods = set() # type: ignore
104-
for field_name in block.keys():
105-
arr = data.get(field_name, None)
106-
arr_d = {} if arr is None else unstructure_array(arr)
107-
arrs_d[field_name] = arr_d
108-
periods.update(arr_d.keys())
109-
periods = sorted(periods) # type: ignore
110-
perioddata = {} # type: ignore
111-
for kper in periods:
112-
line = []
113-
if kper not in perioddata:
114-
perioddata[kper] = [] # type: ignore
115-
for arr_d in arrs_d.values():
116-
if val := arr_d.get(kper, None):
117-
for nn, v in val.items():
118-
cellid = get_cellid(nn, grid)
119-
line.append((*cellid, v))
120-
perioddata[kper] = tuple(line)
121-
data["period"] = perioddata
122-
return data
9+
"""Simple converter: component -> dict with validation."""
10+
return xattree.asdict(value)
12311

12412

125-
def unstructure_oc(value: Any) -> dict[str, Any]:
126-
data = xattree.asdict(value)
127-
blocks = get_blocks(value.dfn)
128-
for block_name, block in blocks.items():
129-
if block_name == "period":
130-
fields = []
131-
for field_name, field in block.items():
132-
action, rtype = field_name.split("_")
133-
fields.append((action, rtype, field_name))
134-
arrs_d = {}
135-
periods = set() # type: ignore
136-
for action, rtype, field_name in fields:
137-
arr = data.get(field_name, None)
138-
arr_d = {} if arr is None else unstructure_array(arr)
139-
arrs_d[(action, rtype)] = arr_d
140-
periods.update(arr_d.keys())
141-
periods = sorted(periods) # type: ignore
142-
perioddata = {} # type: ignore
143-
for kper in periods:
144-
if kper not in perioddata:
145-
perioddata[kper] = []
146-
for (action, rtype), arr_d in arrs_d.items():
147-
if arr := arr_d.get(kper, None):
148-
perioddata[kper].append((action, rtype, arr))
149-
data["period"] = perioddata
150-
else:
151-
for field_name, field in block.items():
152-
if is_list_field(field):
153-
data[field_name] = unstructure_array(data[field_name])
154-
return data
13+
def structure_component(cls: type[Component], data: dict[str, Any]) -> Component:
14+
"""Simple converter: dict -> component with validation."""
15+
return xattree.structure(data, cls)

0 commit comments

Comments
 (0)