Skip to content

Commit 696b47d

Browse files
authored
support for layer and grid array packages (#262)
1 parent 8e8ee7c commit 696b47d

File tree

18 files changed

+646
-122
lines changed

18 files changed

+646
-122
lines changed

docs/examples/quickstart.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545

4646
assert chd.data["head"][0, 0] == 1.0
4747
assert chd.data.head.sel(per=0)[99] == 0.0
48-
assert np.allclose(chd.data.head[:, 1:99], np.full(98, 1e30))
48+
assert np.allclose(chd.data.head[:, 1:99], np.full(98, 3e30))
4949

5050
assert gwf.dis.data.botm.sel(lay=0, col=0, row=0) == 0.0
5151

docs/examples/twri.py

Lines changed: 113 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@
88

99
import flopy4
1010

11+
12+
def plot_head(head, workspace):
13+
import matplotlib.pyplot as plt
14+
15+
# Plot head results
16+
plt.figure(figsize=(10, 6))
17+
head.isel(layer=0, time=0).plot.contourf()
18+
plt.title("Filled Contour Plot TWRI Head")
19+
plt.xlabel("x")
20+
plt.ylabel("y")
21+
plt.grid(True)
22+
plt.savefig(workspace / "head.png", dpi=300, bbox_inches="tight")
23+
plt.close()
24+
25+
1126
# Timing
1227
time = flopy4.mf6.utils.time.Time.from_timestamps(
1328
["2000-01-01", "2000-01-02", "2000-01-03", "2000-01-04"]
@@ -28,7 +43,7 @@
2843
grid = flopy4.mf6.utils.grid.StructuredGrid(
2944
nlay=nlay, nrow=nrow, ncol=ncol, top=top, botm=bottom, delr=delr, delc=delc, idomain=idomain
3045
)
31-
dims = {"nper": nper, **dict(grid.dataset.sizes)} # TODO: temporary
46+
dims = {"nper": nper, "ncpl": nrow * ncol, **dict(grid.dataset.sizes)} # TODO: temporary
3247

3348
# Grid discretization
3449
# TODO: xorigin, yorigin
@@ -84,9 +99,9 @@
8499
)
85100

86101
# Uniform recharge on the top layer
87-
rch_rate = np.stack(np.full((nlay, nrow, ncol), flopy4.mf6.constants.FILL_DNODATA))
102+
rch_rate = np.full((nlay, nrow, ncol), flopy4.mf6.constants.FILL_DNODATA)
88103
rate = np.repeat(np.expand_dims(rch_rate, axis=0), repeats=nper, axis=0)
89-
rate[0, 0, :] = 3.0e-8
104+
rate[0, 0, ...] = 3.0e-8
90105
rch = flopy4.mf6.gwf.Rch(recharge=rate.reshape(nper, -1), dims=dims)
91106

92107
# Output control
@@ -103,24 +118,24 @@
103118
# Wells scattered throughout the model
104119
wel_q = -5.0
105120
wel_nodes = [
106-
[2, 4, 10, -5.0],
107-
[1, 3, 5, -5.0],
108-
[1, 5, 11, -5.0],
109-
[0, 8, 7, -5.0],
110-
[0, 8, 9, -5.0],
111-
[0, 8, 11, -5.0],
112-
[0, 8, 13, -5.0],
113-
[0, 10, 7, -5.0],
114-
[0, 10, 9, -5.0],
115-
[0, 10, 11, -5.0],
116-
[0, 10, 13, -5.0],
117-
[0, 12, 7, -5.0],
118-
[0, 12, 9, -5.0],
119-
[0, 12, 11, -5.0],
120-
[0, 12, 13, -5.0],
121+
[2, 4, 10],
122+
[1, 3, 5],
123+
[1, 5, 11],
124+
[0, 8, 7],
125+
[0, 8, 9],
126+
[0, 8, 11],
127+
[0, 8, 13],
128+
[0, 10, 7],
129+
[0, 10, 9],
130+
[0, 10, 11],
131+
[0, 10, 13],
132+
[0, 12, 7],
133+
[0, 12, 9],
134+
[0, 12, 11],
135+
[0, 12, 13],
121136
]
122137
wel = flopy4.mf6.gwf.Wel(
123-
q={"*": {(layer, row, col): wel_q for layer, row, col, wel_q in wel_nodes}},
138+
q={"*": {(layer, row, col): wel_q for layer, row, col in wel_nodes}},
124139
dims=dims,
125140
)
126141

@@ -181,4 +196,82 @@
181196
)
182197

183198
# Plot head results
184-
head.isel(layer=0, time=0).plot.contourf()
199+
plot_head(head, workspace)
200+
201+
# UPDATE SIM for array based inputs
202+
203+
# update simulation with array based inputs
204+
LAYER_NODATA = np.full((nrow, ncol), flopy4.mf6.constants.FILL_DNODATA, dtype=float)
205+
GRID_NODATA = np.full((nlay, nrow, ncol), flopy4.mf6.constants.FILL_DNODATA, dtype=float)
206+
207+
head = np.repeat(np.expand_dims(GRID_NODATA, axis=0), repeats=nper, axis=0)
208+
for i in range(nrow):
209+
for k in range(nlay - 1):
210+
head[0, k, i, 0] = 0.0
211+
chdg = flopy4.mf6.gwf.Chdg(
212+
print_input=True,
213+
print_flows=True,
214+
save_flows=True,
215+
head=head.reshape(nper, -1),
216+
dims=dims,
217+
)
218+
219+
# Drain in the center left of the model
220+
elev = np.repeat(np.expand_dims(GRID_NODATA, axis=0), repeats=nper, axis=0)
221+
cond = np.repeat(np.expand_dims(GRID_NODATA, axis=0), repeats=nper, axis=0)
222+
for j in range(9):
223+
elev[0, 0, 7, j + 1] = elevation[j]
224+
cond[0, 0, 7, j + 1] = conductance
225+
drng = flopy4.mf6.gwf.Drng(
226+
print_input=True,
227+
print_flows=True,
228+
save_flows=True,
229+
elev=elev.reshape(nper, -1),
230+
cond=cond.reshape(nper, -1),
231+
dims=dims,
232+
)
233+
234+
# well
235+
q = np.repeat(np.expand_dims(GRID_NODATA, axis=0), repeats=nper, axis=0)
236+
for layer, row, col in wel_nodes:
237+
q[0, layer, row, col] = wel_q
238+
welg = flopy4.mf6.gwf.Welg(
239+
q=q.reshape(nper, -1),
240+
dims=dims,
241+
)
242+
243+
# recharge
244+
recharge = np.repeat(np.expand_dims(LAYER_NODATA, axis=0), repeats=nper, axis=0)
245+
recharge[0, ...] = 3.0e-8
246+
rcha = flopy4.mf6.gwf.Rcha(recharge=recharge.reshape(nper, -1), dims=dims)
247+
248+
# remove list based inputs
249+
# TODO: show variations on removing packages
250+
gwf.chd.remove(chd)
251+
del gwf.drn[0]
252+
del gwf.wel[0]
253+
del gwf.rch[0]
254+
255+
# add array based inputs
256+
# TODO: needs type checking and list consolidation support (see comments in gwf init)
257+
gwf.chd = [chdg]
258+
gwf.drng = [drng]
259+
gwf.welg = [welg]
260+
gwf.rcha = [rcha]
261+
262+
# create new workspace
263+
workspace = Path(__file__).parent / "twri2"
264+
workspace.mkdir(parents=True, exist_ok=True)
265+
sim.workspace = workspace
266+
267+
sim.write()
268+
sim.run()
269+
270+
# Load head results
271+
head = flopy4.mf6.utils.open_hds(
272+
workspace / f"{gwf.name}.hds",
273+
workspace / f"{gwf.name}.dis.grb",
274+
)
275+
276+
# Plot head results
277+
plot_head(head, workspace)

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].upper()}6"
3638
return f"{cls_name.upper()}6"
3739

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

flopy4/mf6/codec/writer/filters.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from flopy4.mf6.constants import FILL_DNODATA
1212

13-
ArrayHow = Literal["constant", "internal", "external", "layered internal"]
13+
ArrayHow = Literal["constant", "internal", "external", "layered constant", "layered internal"]
1414

1515

1616
def array_how(value: xr.DataArray) -> ArrayHow:
@@ -29,6 +29,14 @@ def array_how(value: xr.DataArray) -> ArrayHow:
2929
if value.ndim <= 2:
3030
return "internal"
3131
if value.ndim == 3:
32+
layer_const = True
33+
for layer in range(value.shape[0]):
34+
val_layer = value.isel(nlay=layer)
35+
if val_layer.max() != val_layer.min():
36+
layer_const = False
37+
break
38+
if layer_const:
39+
return "layered constant"
3240
return "layered internal"
3341
raise ValueError(f"Arrays with ndim > 3 are not supported, got ndim={value.ndim}")
3442

flopy4/mf6/codec/writer/templates/macros.jinja

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
{{ inset }}CONSTANT {{ value|array2const }}
3636
{% elif how == "layered constant" %}
3737
{% for layer in value -%}
38-
{{ inset }}CONSTANT {{ layer|array2const }}
38+
{{ "\n" ~ inset }}CONSTANT {{ layer|array2const }}
3939
{%- endfor %}
4040
{% elif how == "layered internal" %}
4141
{% for layer in value %}

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 = 3e30
66
LENBOUNDNAME = 40

flopy4/mf6/context.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,29 @@
1010
from flopy4.utils import to_path
1111

1212

13+
def update_child_attr(instance, attribute, new_value):
14+
"""
15+
Generalized function to update child attribute (e.g. workspace).
16+
17+
Args:
18+
instance: The model instance
19+
attribute: The attribute being set (from attrs on_setattr)
20+
new_value: The new value being set
21+
22+
Returns:
23+
The new_value (unchanged)
24+
"""
25+
26+
for child in instance.children.values(): # type: ignore
27+
if hasattr(child, attribute.name):
28+
setattr(child, attribute.name, new_value)
29+
30+
return new_value
31+
32+
1333
@xattree
1434
class Context(Component, ABC):
15-
workspace: Path = field(default=None, converter=to_path)
35+
workspace: Path = field(default=None, converter=to_path, on_setattr=update_child_attr)
1636

1737
def __attrs_post_init__(self):
1838
super().__attrs_post_init__()

0 commit comments

Comments
 (0)