Skip to content

Commit f85656d

Browse files
mjrenomjreno
authored andcommitted
chdg baseline
1 parent 3516b69 commit f85656d

File tree

6 files changed

+241
-64
lines changed

6 files changed

+241
-64
lines changed

flopy4/mf6/binding.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ def _get_binding_type(component: Component) -> str:
3333
elif isinstance(component, Solution):
3434
return f"{component.slntype}6"
3535
else:
36+
if len(cls_name) == 4 and (cls_name[3] == "g" or cls_name[3] == "a"):
37+
return f"{cls_name[0:3]}6"
3638
return f"{cls_name.upper()}6"
3739

3840
def _get_binding_terms(component: Component) -> tuple[str, ...] | None:

flopy4/mf6/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
MF6 = "mf6"
44
FILL_DEFAULT = np.nan
5-
FILL_DNODATA = 1e30
5+
FILL_DNODATA = 3.0e30
66
LENBOUNDNAME = 40

flopy4/mf6/converter/unstructure.py

Lines changed: 132 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import xarray as xr
88
import xattree
99
from modflow_devtools.dfn.schema.block import block_sort_key
10+
from xattree import XatSpec
1011

1112
from flopy4.mf6.binding import Binding
1213
from flopy4.mf6.component import Component
@@ -135,7 +136,91 @@ def oc_setting_data(rec):
135136
return data
136137

137138

139+
def _unstructure_block_param(
140+
block_name: str,
141+
field_name: str,
142+
xatspec: XatSpec,
143+
value: Component,
144+
data: dict[str, Any],
145+
blocks: dict,
146+
period_data: dict,
147+
) -> None:
148+
# Skip child components that have been processed as bindings
149+
if isinstance(value, Context) and field_name in xatspec.children:
150+
child_spec = xatspec.children[field_name]
151+
if hasattr(child_spec, "metadata") and "block" in child_spec.metadata: # type: ignore
152+
if child_spec.metadata["block"] == block_name: # type: ignore
153+
return
154+
155+
# filter out empty values and false keywords, and convert:
156+
# - paths to records
157+
# - datetimes to ISO format
158+
# - filter out false keywords
159+
# - 'auxiliary' fields to tuples
160+
# - xarray DataArrays with 'nper' dim to dict of kper-sliced datasets
161+
# - other values to their original form
162+
# TODO: use cattrs converters for field unstructuring?
163+
match field_value := data[field_name]:
164+
case None:
165+
return
166+
case bool():
167+
if field_value:
168+
blocks[block_name][field_name] = field_value
169+
case Path():
170+
field_spec = xatspec.attrs[field_name]
171+
field_meta = getattr(field_spec, "metadata", {})
172+
t = _path_to_tuple(field_name, field_value, inout=field_meta.get("inout", "fileout"))
173+
# name may have changed e.g dropping '_file' suffix
174+
blocks[block_name][t[0]] = t
175+
case datetime():
176+
blocks[block_name][field_name] = field_value.isoformat()
177+
case t if (
178+
field_name == "auxiliary" and hasattr(field_value, "values") and field_value is not None
179+
):
180+
blocks[block_name][field_name] = tuple(field_value.values.tolist())
181+
case xr.DataArray() if "nper" in field_value.dims:
182+
has_spatial_dims = any(
183+
dim in field_value.dims for dim in ["nlay", "nrow", "ncol", "nodes"]
184+
)
185+
if has_spatial_dims:
186+
field_value = _hack_structured_grid_dims(
187+
field_value,
188+
structured_grid_dims=value.parent.data.dims, # type: ignore
189+
)
190+
if block_name == "period":
191+
if not np.issubdtype(field_value.dtype, np.number):
192+
dat = _hack_period_non_numeric(field_name, field_value)
193+
for n, v in dat.items():
194+
period_data[n] = v
195+
else:
196+
period_data[field_name] = {
197+
kper: field_value.isel(nper=kper) # type: ignore
198+
for kper in range(field_value.sizes["nper"])
199+
}
200+
else:
201+
blocks[block_name][field_name] = field_value
202+
203+
case _:
204+
blocks[block_name][field_name] = field_value
205+
206+
138207
def unstructure_component(value: Component) -> dict[str, Any]:
208+
xatspec = xattree.get_xatspec(type(value))
209+
if "readarraygrid" in xatspec.attrs:
210+
return _unstructure_grid_component(value)
211+
elif "readasarrays" in xatspec.attrs:
212+
return _unstructure_layer_component(value)
213+
else:
214+
return _unstructure_component(value)
215+
216+
217+
def _unstructure_layer_component(value: Component) -> dict[str, Any]:
218+
return {}
219+
220+
221+
def _unstructure_grid_component(value: Component) -> dict[str, Any]:
222+
from flopy4.mf6.constants import FILL_DNODATA
223+
139224
blockspec = dict(sorted(value.dfn.blocks.items(), key=block_sort_key)) # type: ignore
140225
blocks: dict[str, dict[str, Any]] = {}
141226
xatspec = xattree.get_xatspec(type(value))
@@ -156,67 +241,53 @@ def unstructure_component(value: Component) -> dict[str, Any]:
156241
blocks[block_name] = {}
157242

158243
for field_name in block.keys():
159-
# Skip child components that have been processed as bindings
160-
if isinstance(value, Context) and field_name in xatspec.children:
161-
child_spec = xatspec.children[field_name]
162-
if hasattr(child_spec, "metadata") and "block" in child_spec.metadata: # type: ignore
163-
if child_spec.metadata["block"] == block_name: # type: ignore
164-
continue
165-
166-
# filter out empty values and false keywords, and convert:
167-
# - paths to records
168-
# - datetimes to ISO format
169-
# - filter out false keywords
170-
# - 'auxiliary' fields to tuples
171-
# - xarray DataArrays with 'nper' dim to dict of kper-sliced datasets
172-
# - other values to their original form
173-
# TODO: use cattrs converters for field unstructuring?
174-
match field_value := data[field_name]:
175-
case None:
176-
continue
177-
case bool():
178-
if field_value:
179-
blocks[block_name][field_name] = field_value
180-
case Path():
181-
field_spec = xatspec.attrs[field_name]
182-
field_meta = getattr(field_spec, "metadata", {})
183-
t = _path_to_tuple(
184-
field_name, field_value, inout=field_meta.get("inout", "fileout")
185-
)
186-
# name may have changed e.g dropping '_file' suffix
187-
blocks[block_name][t[0]] = t
188-
case datetime():
189-
blocks[block_name][field_name] = field_value.isoformat()
190-
case t if (
191-
field_name == "auxiliary"
192-
and hasattr(field_value, "values")
193-
and field_value is not None
194-
):
195-
blocks[block_name][field_name] = tuple(field_value.values.tolist())
196-
case xr.DataArray() if "nper" in field_value.dims:
197-
has_spatial_dims = any(
198-
dim in field_value.dims for dim in ["nlay", "nrow", "ncol", "nodes"]
199-
)
200-
if has_spatial_dims:
201-
field_value = _hack_structured_grid_dims(
202-
field_value,
203-
structured_grid_dims=value.parent.data.dims, # type: ignore
204-
)
205-
if block_name == "period":
206-
if not np.issubdtype(field_value.dtype, np.number):
207-
dat = _hack_period_non_numeric(field_name, field_value)
208-
for n, v in dat.items():
209-
period_data[n] = v
210-
else:
211-
period_data[field_name] = {
212-
kper: field_value.isel(nper=kper) # type: ignore
213-
for kper in range(field_value.sizes["nper"])
214-
}
215-
else:
216-
blocks[block_name][field_name] = field_value
217-
218-
case _:
219-
blocks[block_name][field_name] = field_value
244+
_unstructure_block_param(
245+
block_name, field_name, xatspec, value, data, blocks, period_data
246+
)
247+
248+
# invert key order, (arr_name, kper) -> (kper, arr_name)
249+
for arr_name, periods in period_data.items():
250+
for kper, arr in periods.items():
251+
if kper not in period_blocks:
252+
period_blocks[kper] = {}
253+
period_blocks[kper][arr_name] = arr
254+
255+
# setup indexed period blocks, combine arrays into datasets
256+
for kper, block in period_blocks.items():
257+
key = f"period {kper + 1}"
258+
for arr_name, val in block.items():
259+
if not np.all(val == FILL_DNODATA):
260+
if key not in blocks:
261+
blocks[key] = {}
262+
blocks[f"period {kper + 1}"][arr_name] = val
263+
264+
return {name: block for name, block in blocks.items() if name != "period"}
265+
266+
267+
def _unstructure_component(value: Component) -> dict[str, Any]:
268+
blockspec = dict(sorted(value.dfn.blocks.items(), key=block_sort_key)) # type: ignore
269+
blocks: dict[str, dict[str, Any]] = {}
270+
xatspec = xattree.get_xatspec(type(value))
271+
data = xattree.asdict(value)
272+
273+
# create child component binding blocks
274+
blocks.update(_make_binding_blocks(value))
275+
276+
# process blocks in order, unstructuring fields as needed,
277+
# then slice period data into separate kper-indexed blocks
278+
# each of which contains a dataset indexed for that period.
279+
for block_name, block in blockspec.items():
280+
period_data = {} # type: ignore
281+
period_blocks = {} # type: ignore
282+
period_block_name = None
283+
284+
if block_name not in blocks:
285+
blocks[block_name] = {}
286+
287+
for field_name in block.keys():
288+
_unstructure_block_param(
289+
block_name, field_name, xatspec, value, data, blocks, period_data
290+
)
220291

221292
# invert key order, (arr_name, kper) -> (kper, arr_name)
222293
for arr_name, periods in period_data.items():

flopy4/mf6/gwf/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from xattree import xattree
99

1010
from flopy4.mf6.gwf.chd import Chd
11+
from flopy4.mf6.gwf.chdg import Chdg
1112
from flopy4.mf6.gwf.dis import Dis
1213
from flopy4.mf6.gwf.drn import Drn
1314
from flopy4.mf6.gwf.ic import Ic
@@ -20,7 +21,7 @@
2021
from flopy4.mf6.utils import open_cbc, open_hds
2122
from flopy4.utils import to_path
2223

23-
__all__ = ["Gwf", "Chd", "Dis", "Drn", "Ic", "Npf", "Oc", "Sto", "Wel"]
24+
__all__ = ["Gwf", "Chd", "Chdg", "Dis", "Drn", "Ic", "Npf", "Oc", "Sto", "Wel"]
2425

2526

2627
def convert_grid(value):
@@ -78,6 +79,7 @@ def budget(self):
7879
npf: Npf | None = field(block="packages", default=None)
7980
sto: Sto | None = field(block="packages", default=None)
8081
chd: list[Chd] = field(block="packages")
82+
chdg: list[Chdg] = field(block="packages")
8183
wel: list[Wel] = field(block="packages")
8284
drn: list[Drn] = field(block="packages")
8385
output: Output = attrs.field(

flopy4/mf6/gwf/chdg.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from pathlib import Path
2+
from typing import ClassVar, Optional
3+
4+
import numpy as np
5+
6+
# from attrs import Converter
7+
from numpy.typing import NDArray
8+
from xattree import xattree
9+
10+
# from flopy4.mf6.converter import structure_array
11+
from flopy4.mf6.package import Package
12+
from flopy4.mf6.spec import array, field, path
13+
from flopy4.mf6.utils.grid import update_maxbound
14+
from flopy4.utils import to_path
15+
16+
17+
@xattree
18+
class Chdg(Package):
19+
multi_package: ClassVar[bool] = True
20+
auxiliary: Optional[list[str]] = array(block="options", default=None)
21+
auxmultname: Optional[str] = field(block="options", default=None)
22+
print_input: bool = field(block="options", default=False)
23+
print_flows: bool = field(block="options", default=False)
24+
readarraygrid: bool = field(block="options", default=True)
25+
save_flows: bool = field(block="options", default=False)
26+
obs_filerecord: Optional[Path] = path(
27+
block="options", default=None, converter=to_path, inout="fileout"
28+
)
29+
export_array_netcdf: bool = field(block="options", default=False)
30+
dev_no_newton: bool = field(default=False, block="options")
31+
maxbound: Optional[int] = field(block="dimensions", default=None, init=False)
32+
head: Optional[NDArray[np.float64]] = array(
33+
block="period",
34+
dims=(
35+
"nper",
36+
"nodes",
37+
),
38+
default=None,
39+
# converter=Converter(structure_array, takes_self=True, takes_field=True),
40+
on_setattr=update_maxbound,
41+
)
42+
aux: Optional[NDArray[np.float64]] = array(
43+
block="period",
44+
dims=(
45+
"nper",
46+
"nodes",
47+
),
48+
default=None,
49+
# converter=Converter(structure_array, takes_self=True, takes_field=True),
50+
on_setattr=update_maxbound,
51+
)

test/test_mf6_component.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from flopy4.mf6.component import COMPONENTS
1313
from flopy4.mf6.constants import FILL_DNODATA, LENBOUNDNAME
14-
from flopy4.mf6.gwf import Chd, Dis, Gwf, Ic, Npf, Oc
14+
from flopy4.mf6.gwf import Chd, Chdg, Dis, Gwf, Ic, Npf, Oc
1515
from flopy4.mf6.ims import Ims
1616
from flopy4.mf6.simulation import Simulation
1717
from flopy4.mf6.tdis import Tdis
@@ -398,6 +398,57 @@ def test_quickstart(function_tmpdir):
398398
sim.run()
399399

400400

401+
def test_quickstart_grid(function_tmpdir):
402+
sim_name = "quickstart"
403+
gwf_name = "mymodel"
404+
405+
# dimensions
406+
nlay = 1
407+
nrow = 10
408+
ncol = 10
409+
nstp = 1
410+
411+
time = Time(perlen=[1.0], nstp=[1], tsmult=[1.0])
412+
# time = Time(perlen=[1.0, 1.0], nstp=[1, 1], tsmult=[1.0, 1.0])
413+
ims = Ims(models=[gwf_name])
414+
dis = Dis(
415+
nlay=nlay,
416+
nrow=nrow,
417+
ncol=ncol,
418+
top=1.0,
419+
botm=0.0,
420+
)
421+
sim = Simulation(
422+
tdis=time,
423+
workspace=function_tmpdir,
424+
name=sim_name,
425+
solutions={"ims": ims},
426+
)
427+
gwf = Gwf(parent=sim, dis=dis, name=gwf_name)
428+
ic = Ic(parent=gwf)
429+
oc = Oc(
430+
parent=gwf,
431+
budget_file=f"{gwf_name}.bud",
432+
head_file=f"{gwf_name}.hds",
433+
save_head=["all"],
434+
save_budget=["all"],
435+
)
436+
npf = Npf(parent=gwf, icelltype=0, k=1.0)
437+
438+
# chd grid based input, step 1 data head array
439+
head = np.full((nlay, nrow, ncol), FILL_DNODATA, dtype=float)
440+
head[0, 0, 0] = 1.0
441+
head[0, 9, 9] = 0.0
442+
# TODO: support dict style input keyed on SP with separate grid arrays
443+
chd = Chdg(parent=gwf, head=np.expand_dims(head.ravel(), axis=0))
444+
# headnone = np.full((nlay, nrow, ncol), FILL_DNODATA, dtype=float)
445+
# ts_head = np.stack((head.ravel(), headnone.ravel()), axis=0)
446+
# chd = Chdg(parent=gwf, head=ts_head)
447+
448+
sim.write()
449+
sim.run()
450+
451+
401452
def test_write_ascii(function_tmpdir):
402453
sim_name = "sim"
403454
gwf_name = "gwf"

0 commit comments

Comments
 (0)