Skip to content

Commit f225f55

Browse files
committed
simplified, working?
1 parent d1ac9ee commit f225f55

File tree

2 files changed

+111
-185
lines changed

2 files changed

+111
-185
lines changed

docs/examples/attrs_demo.py

Lines changed: 110 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -3,164 +3,135 @@
33
# This example demonstrates a tentative `attrs`-based object model.
44

55
from pathlib import Path
6-
from typing import List, Literal, Optional, Union
6+
from typing import Any, List, Literal, NamedTuple, Optional
77

8-
# ## GWF IC
98
import numpy as np
109
from attr import asdict, define, field, fields_dict
1110
from cattr import Converter
11+
from flopy.discretization import StructuredGrid
1212
from numpy.typing import NDArray
13-
14-
# We can define block classes where variable descriptions become
15-
# the variable's docstring. Ideally we can come up with a Python
16-
# input specification that is equivalent to (and convertible to)
17-
# the original MF6 input specification, while knowing as little
18-
# as possible about the MF6 input format; but anything we can't
19-
# get rid of can go in field `metadata`.
13+
from xarray import Dataset, DataTree
2014

2115

2216
@define
23-
class Options:
17+
class GwfIc:
18+
strt: NDArray[np.float64] = field(
19+
metadata={"block": "packagedata", "shape": "(nodes)"}
20+
)
2421
export_array_ascii: bool = field(
25-
default=False,
26-
metadata={"longname": "export array variables to netcdf output files"},
22+
default=False, metadata={"block": "options"}
2723
)
28-
"""
29-
keyword that specifies input griddata arrays should be
30-
written to layered ascii output files.
31-
"""
32-
3324
export_array_netcdf: bool = field(
34-
default=False, metadata={"longname": "starting head"}
25+
default=False,
26+
metadata={"block": "options"},
3527
)
36-
"""
37-
keyword that specifies input griddata arrays should be
38-
written to the model output netcdf file.
39-
"""
40-
4128

42-
# Eventually we may be able to take advantage of NumPy
43-
# support for shape parameters:
44-
# https://github.com/numpy/numpy/issues/16544
45-
#
46-
# We can still take advantage of type parameters.
29+
def __attrs_post_init__(self):
30+
# TODO: setup attributes for blocks?
31+
self.data = DataTree(Dataset({"strt": self.strt}), name="ic")
4732

4833

4934
@define
50-
class PackageData:
51-
strt: NDArray[np.float64] = field(
52-
metadata={"longname": "starting head", "shape": ("nodes")}
35+
class GwfOc:
36+
@define
37+
class Format:
38+
columns: int
39+
width: int
40+
digits: int
41+
format: Literal["exponential", "fixed", "general", "scientific"]
42+
43+
periods: List[List[tuple]] = field(
44+
metadata={"block": "perioddata"}
45+
)
46+
budget_file: Optional[Path] = field(
47+
default=None, metadata={"block": "options"}
48+
)
49+
budget_csv_file: Optional[Path] = field(
50+
default=None, metadata={"block": "options"}
51+
)
52+
head_file: Optional[Path] = field(
53+
default=None, metadata={"block": "options"}
54+
)
55+
printhead: Optional[Format] = field(
56+
default=None, metadata={"block": "options"}
5357
)
54-
"""
55-
is the initial (starting) head---that is, head at the
56-
beginning of the GWF Model simulation. STRT must be specified for
57-
all simulations, including steady-state simulations. One value is
58-
read for every model cell. For simulations in which the first stress
59-
period is steady state, the values used for STRT generally do not
60-
affect the simulation (exceptions may occur if cells go dry and (or)
61-
rewet). The execution time, however, will be less if STRT includes
62-
hydraulic heads that are close to the steady-state solution. A head
63-
value lower than the cell bottom can be provided if a cell should
64-
start as dry.
65-
"""
66-
67-
68-
# Putting it all together:
69-
70-
71-
@define
72-
class GwfIc:
73-
options: Options = field()
74-
packagedata: PackageData = field()
75-
76-
77-
# ## GWF OC
78-
#
79-
# The output control package has a more complicated variable structure.
80-
# Below docstrings/descriptions are omitted for space-saving.
81-
82-
83-
@define
84-
class Format:
85-
columns: int = field()
86-
width: int = field()
87-
digits: int = field()
88-
format: Literal["exponential", "fixed", "general", "scientific"] = field()
89-
90-
91-
@define
92-
class Options:
93-
budget_file: Optional[Path] = field(default=None)
94-
budget_csv_file: Optional[Path] = field(default=None)
95-
head_file: Optional[Path] = field(default=None)
96-
printhead: Optional[Format] = field(default=None)
97-
98-
99-
# It's awkward to have single-parameter classes, but
100-
# it's the only way I got `cattrs` to distinguish a
101-
# number of choices with the same shape in a union
102-
# like `OCSetting`. There may be a better way.
103-
104-
105-
@define
106-
class All:
107-
all: bool = field()
108-
109-
110-
@define
111-
class First:
112-
first: bool = field()
11358

11459

11560
@define
116-
class Last:
117-
last: bool = field()
118-
61+
class GwfDis:
62+
nlay: int = field(metadata={"block": "dimensions"})
63+
ncol: int = field(metadata={"block": "dimensions"})
64+
nrow: int = field(metadata={"block": "dimensions"})
65+
delr: NDArray[np.float64] = field(
66+
metadata={"block": "griddata", "shape": "(ncol,)"}
67+
)
68+
delc: NDArray[np.float64] = field(
69+
metadata={"block": "griddata", "shape": "(nrow,)"}
70+
)
71+
top: NDArray[np.float64] = field(
72+
metadata={"block": "griddata", "shape": "(ncol, nrow)"}
73+
)
74+
botm: NDArray[np.float64] = field(
75+
metadata={"block": "griddata", "shape": "(ncol, nrow, nlay)"}
76+
)
77+
idomain: NDArray[np.float64] = field(
78+
metadata={"block": "griddata", "shape": "(ncol, nrow, nlay)"}
79+
)
80+
length_units: str = field(default=None, metadata={"block": "options"})
81+
nogrb: bool = field(default=False, metadata={"block": "options"})
82+
xorigin: float = field(default=None, metadata={"block": "options"})
83+
yorigin: float = field(default=None, metadata={"block": "options"})
84+
angrot: float = field(default=None, metadata={"block": "options"})
85+
export_array_netcdf: bool = field(
86+
default=False, metadata={"block": "options"}
87+
)
11988

120-
@define
121-
class Steps:
122-
steps: List[int] = field()
89+
def __attrs_post_init__(self):
90+
self.data = DataTree(
91+
Dataset(
92+
{
93+
"nlay": self.nlay,
94+
"ncol": self.ncol,
95+
"nrow": self.nrow,
96+
"delr": self.delr,
97+
"delc": self.delc,
98+
"top": self.top,
99+
"botm": self.botm,
100+
"idomain": self.idomain,
101+
}
102+
),
103+
name="dis",
104+
)
105+
# TODO: check for parent and update dimensions
106+
# then try to realign any existing packages?
123107

124108

125109
@define
126-
class Frequency:
127-
frequency: int = field()
110+
class Gwf:
111+
dis: GwfDis = field()
112+
ic: GwfIc = field()
128113

114+
def __attrs_post_init__(self):
115+
self.data = DataTree.from_dict(
116+
{"/dis": self.dis, "/ic": self.ic}, name="gwf"
117+
)
118+
self.grid = StructuredGrid(**asdict(self.dis))
129119

130-
PrintSave = Literal["print", "save"]
131-
RType = Literal["budget", "head"]
132-
OCSetting = Union[All, First, Last, Steps, Frequency]
120+
@ic.validator
121+
def _check_dims(self, attribute, value):
122+
assert value.strt.shape == (
123+
self.dis.nlay * self.dis.nrow * self.dis.ncol
124+
)
133125

134126

135-
@define
136-
class OutputControlData:
137-
printsave: PrintSave = field()
138-
rtype: RType = field()
139-
ocsetting: OCSetting = field()
140-
141-
@classmethod
142-
def from_tuple(cls, t):
143-
t = list(t)
144-
printsave = t.pop(0)
145-
rtype = t.pop(0)
146-
ocsetting = {
147-
"all": All,
148-
"first": First,
149-
"last": Last,
150-
"steps": Steps,
151-
"frequency": Frequency,
152-
}[t.pop(0).lower()](t)
153-
return cls(printsave, rtype, ocsetting)
154-
155-
156-
Period = List[OutputControlData]
157-
Periods = List[Period]
127+
# We can define a package with some data.
158128

159129

160-
@define
161-
class GwfOc:
162-
options: Options = field()
163-
periods: Periods = field()
130+
oc = GwfOc(
131+
budget_file="some/file/path.cbc",
132+
periods=[[("print", "budget", "steps", 1, 3, 5)]]
133+
)
134+
assert isinstance(oc.budget_file, str) # TODO path
164135

165136

166137
# We now set up a `cattrs` converter to convert an unstructured
@@ -169,63 +140,19 @@ class GwfOc:
169140
converter = Converter()
170141

171142

172-
# Register a hook for the `OutputControlData.from_tuple` method.
173-
# MODFLOW 6 defines records as tuples, from which we'll need to
174-
# instantiate objects.
175-
176-
177-
def output_control_data_hook(value, _) -> OutputControlData:
178-
return OutputControlData.from_tuple(value)
179-
180-
181-
converter.register_structure_hook(OutputControlData, output_control_data_hook)
182-
183-
184-
# We can inspect the input specification with `attrs` machinery.
185-
186-
187-
spec = fields_dict(OutputControlData)
188-
assert len(spec) == 3
189-
190-
ocsetting = spec["ocsetting"]
191-
assert ocsetting.type is OCSetting
192-
193-
194-
# We can define a block with some data.
195-
196-
197-
options = Options(
198-
budget_file="some/file/path.cbc",
199-
)
200-
assert isinstance(options.budget_file, str) # TODO path
201-
assert len(asdict(options)) == 4
202-
203-
204-
# We can load a record from a tuple.
205-
206-
207-
ocdata = OutputControlData.from_tuple(("print", "budget", "steps", 1, 3, 5))
208-
assert ocdata.printsave == "print"
209-
assert ocdata.rtype == "budget"
210-
assert ocdata.ocsetting == Steps([1, 3, 5])
211-
212-
213143
# We can load the full package from an unstructured dictionary,
214144
# as would be returned by a separate IO layer in the future.
215145
# (Either hand-written or using e.g. lark.)
216146

217-
218147
gwfoc = converter.structure(
219148
{
220-
"options": {
221-
"budget_file": "some/file/path.cbc",
222-
"head_file": "some/file/path.hds",
223-
"printhead": {
224-
"columns": 1,
225-
"width": 10,
226-
"digits": 8,
227-
"format": "scientific",
228-
},
149+
"budget_file": "some/file/path.cbc",
150+
"head_file": "some/file/path.hds",
151+
"printhead": {
152+
"columns": 1,
153+
"width": 10,
154+
"digits": 8,
155+
"format": "scientific",
229156
},
230157
"periods": [
231158
[
@@ -236,11 +163,9 @@ def output_control_data_hook(value, _) -> OutputControlData:
236163
},
237164
GwfOc,
238165
)
239-
assert gwfoc.options.budget_file == Path("some/file/path.cbc")
240-
assert gwfoc.options.printhead.width == 10
241-
assert gwfoc.options.printhead.format == "scientific"
166+
assert gwfoc.budget_file == Path("some/file/path.cbc")
167+
assert gwfoc.printhead.width == 10
168+
assert gwfoc.printhead.format == "scientific"
242169
period = gwfoc.periods[0]
243170
assert len(period) == 2
244-
assert period[0] == OutputControlData.from_tuple(
245-
("print", "budget", "steps", 1, 3, 5)
246-
)
171+
assert period[0] == ("print", "budget", "steps", 1, 3, 5)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies = [
4343
"numpy>=1.20.3",
4444
"pandas>=2.0.0",
4545
"toml>=0.10",
46+
"xarray"
4647
]
4748
dynamic = ["version"]
4849

0 commit comments

Comments
 (0)