Skip to content

Commit c839739

Browse files
mjrenomjreno
authored andcommitted
chdg baseline
1 parent 8e8ee7c commit c839739

File tree

6 files changed

+239
-64
lines changed

6 files changed

+239
-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: 130 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
@@ -144,7 +145,89 @@ def oc_setting_data(rec):
144145
return data
145146

146147

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

150233
blockspec = dict(sorted(value.dfn.blocks.items(), key=block_sort_key)) # type: ignore
@@ -167,67 +250,53 @@ def unstructure_component(value: Component) -> dict[str, Any]:
167250
blocks[block_name] = {}
168251

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

232301
# invert key order, (arr_name, kper) -> (kper, arr_name)
233302
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
@@ -21,7 +22,7 @@
2122
from flopy4.mf6.utils import open_cbc, open_hds
2223
from flopy4.utils import to_path
2324

24-
__all__ = ["Gwf", "Chd", "Dis", "Drn", "Ic", "Npf", "Oc", "Sto", "Wel", "Rch"]
25+
__all__ = ["Gwf", "Chd", "Chdg", "Dis", "Drn", "Ic", "Npf", "Oc", "Rch", "Sto", "Wel"]
2526

2627

2728
def convert_grid(value):
@@ -79,6 +80,7 @@ def budget(self):
7980
npf: Npf | None = field(block="packages", default=None)
8081
sto: Sto | None = field(block="packages", default=None)
8182
chd: list[Chd] = field(block="packages")
83+
chdg: list[Chdg] = field(block="packages")
8284
wel: list[Wel] = field(block="packages")
8385
drn: list[Drn] = field(block="packages")
8486
rch: list[Rch] = field(block="packages")

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)