Skip to content

Commit c764bf2

Browse files
committed
restore rp lst table, add shutoff test
1 parent fdd8982 commit c764bf2

3 files changed

Lines changed: 295 additions & 15 deletions

File tree

autotest/test_gwf_maw_shutoff.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
"""
2+
Test MAW SHUT_OFF option.
3+
4+
Two cases share the same Thiem-well setup (rate=-2000, head_limit=-0.4):
5+
a) HEAD_LIMIT only (baseline) — rate tapers but never reaches exactly 0
6+
b) HEAD_LIMIT + SHUT_OFF — well shuts off (rate == 0) when potential flow
7+
falls below minrate and the well head is below head_limit
8+
9+
The SHUT_OFF keyword takes minrate and maxrate. minrate is the threshold
10+
below which the well shuts off; maxrate is the threshold above which it
11+
reactivates. For the shut-off case to trigger, minrate must be chosen large
12+
enough that the taper will cross it during the stress period.
13+
"""
14+
15+
import os
16+
17+
import flopy
18+
import numpy as np
19+
import pytest
20+
from framework import TestFramework
21+
22+
cases = ["maw_shutoff_a", "maw_shutoff_b"]
23+
24+
# Grid / solver parameters shared by both cases
25+
nlay, nrow, ncol = 1, 101, 101
26+
nper = 1
27+
perlen = [500.0]
28+
nstp = [50]
29+
tsmult = [1.2]
30+
delr = delc = 142.0
31+
top = 0.0
32+
botm = [-1000.0]
33+
strt = 0.0
34+
hk = 10.0
35+
36+
nouter, ninner = 100, 100
37+
hclose, rclose, relax = 1e-6, 1e-6, 1.0
38+
39+
# MAW parameters
40+
wellbottom = -1000.0
41+
rate = -2000.0
42+
head_limit = -0.4
43+
# SHUT_OFF thresholds: well shuts off once potential flow < minrate
44+
# and reactivates only if it exceeds maxrate. With the Thiem setup used here,
45+
# the HEAD_LIMIT-throttled rate drops from 2000 to ~616 over 500 days, so
46+
# minrate=900 ensures the shutoff fires before the simulation ends.
47+
shutoff_minrate = 900.0
48+
shutoff_maxrate = 1200.0
49+
50+
51+
def build_models(idx, test):
52+
name = cases[idx]
53+
ws = test.workspace
54+
55+
tdis_rc = [(perlen[0], nstp[0], tsmult[0])]
56+
57+
sim = flopy.mf6.MFSimulation(sim_name=name, sim_ws=ws)
58+
59+
flopy.mf6.ModflowTdis(sim, time_units="DAYS", nper=nper, perioddata=tdis_rc)
60+
61+
gwf = flopy.mf6.MFModel(
62+
sim,
63+
model_type="gwf6",
64+
modelname=name,
65+
model_nam_file=f"{name}.nam",
66+
)
67+
68+
flopy.mf6.ModflowIms(
69+
sim,
70+
print_option="SUMMARY",
71+
outer_dvclose=hclose,
72+
outer_maximum=nouter,
73+
under_relaxation="NONE",
74+
inner_maximum=ninner,
75+
inner_dvclose=hclose,
76+
rcloserecord=rclose,
77+
linear_acceleration="CG",
78+
scaling_method="NONE",
79+
reordering_method="NONE",
80+
relaxation_factor=relax,
81+
)
82+
sim.register_ims_package(sim.ims, [gwf.name])
83+
84+
flopy.mf6.ModflowGwfdis(
85+
gwf,
86+
nlay=nlay,
87+
nrow=nrow,
88+
ncol=ncol,
89+
delr=delr,
90+
delc=delc,
91+
top=top,
92+
botm=botm,
93+
idomain=1,
94+
filename=f"{name}.dis",
95+
)
96+
97+
flopy.mf6.ModflowGwfic(gwf, strt=strt, filename=f"{name}.ic")
98+
99+
flopy.mf6.ModflowGwfnpf(
100+
gwf,
101+
save_flows=True,
102+
icelltype=1,
103+
k=hk,
104+
k33=hk,
105+
filename=f"{name}.npf",
106+
)
107+
108+
flopy.mf6.ModflowGwfsto(
109+
gwf,
110+
save_flows=True,
111+
iconvert=0,
112+
ss=1.0e-5,
113+
sy=0.1,
114+
steady_state={0: False},
115+
transient={0: True},
116+
filename=f"{name}.sto",
117+
)
118+
119+
# Period data: case a has HEAD_LIMIT only; case b adds SHUT_OFF
120+
perioddata = {
121+
0: [
122+
[0, "rate", rate],
123+
[0, "head_limit", head_limit],
124+
]
125+
}
126+
if idx == 1:
127+
perioddata[0].append([0, "shut_off", shutoff_minrate, shutoff_maxrate])
128+
129+
mawo_dict = {
130+
f"{name}.maw.obs.csv": [
131+
("m1head", "head", (0,)),
132+
("m1rate", "rate", (0,)),
133+
]
134+
}
135+
136+
flopy.mf6.ModflowGwfmaw(
137+
gwf,
138+
filename=f"{name}.maw",
139+
print_input=True,
140+
print_head=True,
141+
print_flows=True,
142+
save_flows=True,
143+
observations=mawo_dict,
144+
packagedata=[[0, 0.15, wellbottom, strt, "THIEM", 1]],
145+
connectiondata=[[0, 0, (0, 50, 50), 0.0, wellbottom, 0.0, 0.0]],
146+
perioddata=perioddata,
147+
)
148+
149+
flopy.mf6.ModflowGwfoc(
150+
gwf,
151+
budget_filerecord=f"{name}.cbc",
152+
head_filerecord=f"{name}.hds",
153+
saverecord=[("HEAD", "ALL")],
154+
printrecord=[("BUDGET", "ALL")],
155+
filename=f"{name}.oc",
156+
)
157+
158+
return sim, None
159+
160+
161+
def check_output(idx, test):
162+
name = cases[idx]
163+
fpth = os.path.join(test.workspace, f"{name}.maw.obs.csv")
164+
try:
165+
tc = np.genfromtxt(fpth, names=True, delimiter=",")
166+
except Exception:
167+
assert False, f'could not load data from "{fpth}"'
168+
169+
rates = tc["M1RATE"]
170+
heads = tc["M1HEAD"]
171+
172+
if idx == 0:
173+
# HEAD_LIMIT only: rate tapers once the well head reaches head_limit, but
174+
# never reaches exactly 0 (qpot stays positive as long as h_aq > head_limit).
175+
assert rates.min() < -shutoff_minrate, (
176+
"Extraction rate should drop below shutoff_minrate "
177+
f"({-shutoff_minrate}) with HEAD_LIMIT throttling active; "
178+
f"min rate = {rates.min()}"
179+
)
180+
assert not np.any(np.isclose(rates, 0.0)), (
181+
"Without SHUT_OFF, the well rate should not snap to exactly 0; "
182+
f"rates = {rates}"
183+
)
184+
else:
185+
# SHUT_OFF: well must shut off (rate == 0) at some timestep
186+
assert np.any(np.isclose(rates, 0.0)), (
187+
"With SHUT_OFF active, the well rate should reach 0 at some timestep "
188+
f"but min rate = {rates.min()}"
189+
)
190+
191+
192+
@pytest.mark.parametrize("idx, name", enumerate(cases))
193+
def test_mf6model(idx, name, function_tmpdir, targets):
194+
test = TestFramework(
195+
name=name,
196+
workspace=function_tmpdir,
197+
build=lambda t: build_models(idx, t),
198+
check=lambda t: check_output(idx, t),
199+
targets=targets,
200+
)
201+
test.run()

autotest/test_gwf_ts_maw01.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
from framework import TestFramework
88

99
paktest = "maw"
10-
cases = [f"ts_{paktest}01"]
10+
cases = [f"ts_{paktest}01", f"ts_{paktest}01_reorder"]
1111

1212

13-
def get_model(ws, name, timeseries=False):
13+
def get_model(ws, name, timeseries=False, reorder=False):
1414
# static model data
1515
# temporal discretization
1616
nper = 3
@@ -21,7 +21,9 @@ def get_model(ws, name, timeseries=False):
2121

2222
auxnames = ["temp", "conc"]
2323
temp, conc = 32.5, 0.1
24-
strt_well1 = 0.0
24+
# reorder case uses a non-zero strt so that swapping PACKAGEDATA rows
25+
# produces distinct input strt values
26+
strt_well1 = -5.0 if reorder else 0.0
2527

2628
# spatial discretization data
2729
nlay, nrow, ncol = 3, 10, 10
@@ -243,6 +245,10 @@ def get_model(ws, name, timeseries=False):
243245
perioddata.append([0, "rate", rate])
244246
perioddata.append([0, "AUXILIARY", "conc", conc])
245247
perioddata.append([0, "AUXILIARY", "temp", temp])
248+
if timeseries and reorder:
249+
# reverse PACKAGEDATA row order (WELLNO=1 in row 0, WELLNO=0 in row 1)
250+
# so that input strt is indexed differently from feature order;
251+
packagedata = [packagedata[1], packagedata[0]]
246252

247253
budpth = f"{name}.{paktest}.cbc"
248254
maw = flopy.mf6.ModflowGwfmaw(
@@ -406,14 +412,15 @@ def get_model(ws, name, timeseries=False):
406412

407413
def build_models(idx, test):
408414
name = cases[idx]
415+
reorder = idx == 1
409416

410417
# build MODFLOW 6 files
411418
ws = test.workspace
412-
sim = get_model(ws, name)
419+
sim = get_model(ws, name, reorder=reorder)
413420

414421
# build MODFLOW 6 files with timeseries
415422
ws = os.path.join(test.workspace, "mf6")
416-
mc = get_model(ws, name, timeseries=True)
423+
mc = get_model(ws, name, timeseries=True, reorder=reorder)
417424

418425
return sim, mc
419426

0 commit comments

Comments
 (0)