Skip to content

Commit 9654d5f

Browse files
authored
add to_dict method to component (#212)
supports either fields or blocks (nested). useful for the writer internally and also presumably for users. includes a hack to omit "derived" dimensions from the dict like nodes, which is referenced in DFNs (therefore by components in their array definitions) but isn't actually a variable in any DFN. a better way would be an option on the dim field decorator, maybe. come back to it.
1 parent 2657659 commit 9654d5f

File tree

3 files changed

+141
-8
lines changed

3 files changed

+141
-8
lines changed

flopy4/mf6/component.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from abc import ABC
22
from collections.abc import MutableMapping
33
from pathlib import Path
4-
from typing import ClassVar
4+
from typing import Any, ClassVar
55

66
import numpy as np
77
from attrs import fields
88
from modflow_devtools.dfn import Dfn, Field
99
from packaging.version import Version
10+
from xattree import asdict as xattree_asdict
1011
from xattree import xattree
1112

1213
from flopy4.mf6.constants import FILL_DNODATA, MF6
@@ -179,21 +180,40 @@ def get_dfn(cls) -> Dfn:
179180
blocks=blocks,
180181
)
181182

182-
def _preio(self, format: str = MF6) -> None:
183-
# prep for io operations
184-
if not self.filename:
185-
self.filename = self.default_filename()
186-
187183
def load(self, format: str = MF6) -> None:
188184
"""Load the component and any children."""
189-
self._preio(format=format)
185+
# TODO: setting filename is a temp hack to get the parent's
186+
# name as this component's filename stem, if it has one. an
187+
# actual solution is to auto-set the filename when children
188+
# are attached to parents.
189+
self.filename = self.filename or self.default_filename()
190190
self._load(format=format)
191191
for child in self.children.values(): # type: ignore
192192
child.load(format=format)
193193

194194
def write(self, format: str = MF6) -> None:
195195
"""Write the component and any children."""
196-
self._preio(format=format)
196+
# TODO: setting filename is a temp hack to get the parent's
197+
# name as this component's filename stem, if it has one. an
198+
# actual solution is to auto-set the filename when children
199+
# are attached to parents.
200+
self.filename = self.filename or self.default_filename()
197201
self._write(format=format)
198202
for child in self.children.values(): # type: ignore
199203
child.write(format=format)
204+
205+
def to_dict(self, blocks: bool = False) -> dict[str, Any]:
206+
"""Convert the component to a dictionary representation."""
207+
data = xattree_asdict(self)
208+
data.pop("filename")
209+
data.pop("workspace", None) # might be a Context
210+
data.pop("nodes", None) # TODO: find a better way to omit
211+
if blocks:
212+
blocks_ = {} # type: ignore
213+
for field_name, field_value in data.items():
214+
block_name = self.dfn.fields[field_name].block
215+
if block_name not in blocks_:
216+
blocks_[block_name] = {}
217+
blocks_[block_name][field_name] = field_value
218+
return blocks_
219+
return data

flopy4/spec.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
Wrap `xattree` and `attrs` specification utilities.
33
These include field decorators and introspection functions.
4+
TODO: add `derived` option to dims? or more generic option
5+
to any field indicating it is not part of the formal spec?
46
"""
57

68
from attrs import NOTHING, Attribute

test/test_component.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,114 @@ def test_write_ascii(function_tmpdir):
416416
assert f"{gwf_name}.oc" in file_names
417417
assert f"{gwf_name}.npf" in file_names
418418
assert f"{gwf_name}.chd" in file_names
419+
420+
421+
def test_to_dict_fields():
422+
time = ModelTime(perlen=[1.0], nstp=[1], tsmult=[1.0])
423+
grid = StructuredGrid(nlay=1, nrow=10, ncol=10)
424+
dims = {
425+
"nlay": grid.nlay,
426+
"nrow": grid.nrow,
427+
"ncol": grid.ncol,
428+
"nper": time.nper,
429+
"nodes": grid.nnodes,
430+
}
431+
432+
chd = Chd(dims=dims, head={0: {(0, 0, 0): 1.0, (0, 9, 9): 0.0}})
433+
result = chd.to_dict()
434+
435+
assert "head" in result
436+
assert result["head"][0, 0] == 1.0
437+
assert result["head"][0, 99] == 0.0
438+
439+
npf = Npf(dims=dims, k=5.0)
440+
result = npf.to_dict()
441+
442+
assert "filename" not in result
443+
assert "k" in result
444+
assert "icelltype" in result
445+
assert "k33" in result
446+
assert np.array_equal(result["k"], np.full(100, 5.0))
447+
448+
449+
def test_to_dict_blocks():
450+
time = ModelTime(perlen=[1.0], nstp=[1], tsmult=[1.0])
451+
grid = StructuredGrid(nlay=1, nrow=10, ncol=10)
452+
dims = {
453+
"nlay": grid.nlay,
454+
"nrow": grid.nrow,
455+
"ncol": grid.ncol,
456+
"nper": time.nper,
457+
"nodes": grid.nnodes,
458+
}
459+
460+
chd = Chd(
461+
dims=dims,
462+
print_flows=True,
463+
head={0: {(0, 0, 0): 1.0, (0, 9, 9): 0.0}},
464+
)
465+
result = chd.to_dict(blocks=True)
466+
467+
assert "options" in result
468+
assert "period" in result
469+
assert "print_flows" in result["options"]
470+
assert result["options"]["print_flows"] is True
471+
assert "head" in result["period"]
472+
assert result["period"]["head"][0, 0] == 1.0
473+
assert result["period"]["head"][0, 99] == 0.0
474+
475+
npf = Npf(dims=dims, save_flows=True, k=2.0)
476+
result = npf.to_dict(blocks=True)
477+
478+
assert "options" in result
479+
assert "griddata" in result
480+
assert "save_flows" in result["options"]
481+
assert result["options"]["save_flows"] is True
482+
assert "k" in result["griddata"]
483+
assert np.array_equal(result["griddata"]["k"], np.full(100, 2.0))
484+
485+
486+
def test_to_dict_on_component():
487+
dims = {
488+
"nper": 1,
489+
"nlay": 1,
490+
"nrow": 2,
491+
"ncol": 2,
492+
"nodes": 4,
493+
}
494+
dis = Dis(dims=dims)
495+
result = dis.to_dict()
496+
497+
assert "filename" not in result
498+
assert "nlay" in result
499+
500+
501+
def test_to_dict_on_context():
502+
time = ModelTime(perlen=[1.0], nstp=[1], tsmult=[1.0])
503+
ims = Ims(models=["gwf"])
504+
sim = Simulation(tdis=time, solutions={"ims": ims})
505+
506+
result = sim.to_dict()
507+
508+
assert "filename" not in result
509+
assert "workspace" not in result
510+
assert "tdis" in result
511+
512+
513+
def test_to_dict_excludes_derived_dims():
514+
# TODO eventually revise to test exclusion of all derived dimensions,
515+
# once we have a mechanism to mark them as such
516+
dims = {
517+
"nper": 1,
518+
"nlay": 1,
519+
"nrow": 2,
520+
"ncol": 2,
521+
"nodes": 4,
522+
}
523+
dis = Dis(dims=dims)
524+
result = dis.to_dict()
525+
526+
assert "nlay" in result
527+
assert "nrow" in result
528+
assert "ncol" in result
529+
assert "nodes" not in result

0 commit comments

Comments
 (0)