Skip to content

Commit d44af13

Browse files
committed
fix(prt): support exchange coupling to ATS flow models
1 parent 5fe023f commit d44af13

14 files changed

Lines changed: 401 additions & 74 deletions
4.59 KB
Binary file not shown.
4.59 KB
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

autotest/test_gwf_ats01.py

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
dtfailadj = 5.0
2121

2222

23-
def build_models(idx, test):
23+
def build_gwf_sim(name, ws, mf6):
2424
perlen = [10]
2525
nper = len(perlen)
2626
nstp = [1]
@@ -39,13 +39,8 @@ def build_models(idx, test):
3939
for id in range(nper):
4040
tdis_rc.append((perlen[id], nstp[id], tsmult[id]))
4141

42-
name = cases[idx]
43-
4442
# build MODFLOW 6 files
45-
ws = test.workspace
46-
sim = flopy.mf6.MFSimulation(
47-
sim_name=name, version="mf6", exe_name="mf6", sim_ws=ws
48-
)
43+
sim = flopy.mf6.MFSimulation(sim_name=name, version="mf6", exe_name=mf6, sim_ws=ws)
4944

5045
# create tdis package
5146
ats_filerecord = None
@@ -69,7 +64,7 @@ def build_models(idx, test):
6964
)
7065

7166
# create gwf model
72-
gwfname = name
67+
gwfname = f"{name}_gwf"
7368
newtonoptions = "NEWTON UNDER_RELAXATION"
7469
gwf = flopy.mf6.ModflowGwf(
7570
sim,
@@ -114,11 +109,18 @@ def build_models(idx, test):
114109
ic = flopy.mf6.ModflowGwfic(gwf, strt=strt)
115110

116111
# node property flow
117-
npf = flopy.mf6.ModflowGwfnpf(gwf, save_flows=False, icelltype=laytyp, k=hk)
112+
npf = flopy.mf6.ModflowGwfnpf(
113+
gwf,
114+
save_flows=True,
115+
save_saturation=True,
116+
save_specific_discharge=True,
117+
icelltype=laytyp,
118+
k=hk,
119+
)
118120
# storage
119121
sto = flopy.mf6.ModflowGwfsto(
120122
gwf,
121-
save_flows=False,
123+
save_flows=True,
122124
iconvert=laytyp,
123125
ss=ss,
124126
sy=sy,
@@ -135,7 +137,7 @@ def build_models(idx, test):
135137
print_input=True,
136138
print_flows=True,
137139
stress_period_data=welspdict,
138-
save_flows=False,
140+
save_flows=True,
139141
)
140142

141143
# ghb files
@@ -147,7 +149,7 @@ def build_models(idx, test):
147149
print_input=True,
148150
print_flows=True,
149151
stress_period_data=ghbspdict,
150-
save_flows=False,
152+
save_flows=True,
151153
)
152154

153155
# output control
@@ -156,7 +158,7 @@ def build_models(idx, test):
156158
budget_filerecord=f"{gwfname}.cbc",
157159
head_filerecord=f"{gwfname}.hds",
158160
headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")],
159-
saverecord=[("HEAD", "ALL")],
161+
saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")],
160162
printrecord=[("HEAD", "ALL"), ("BUDGET", "ALL")],
161163
)
162164

@@ -166,12 +168,22 @@ def build_models(idx, test):
166168
obs_dict = {f"{gwfname}.obs.csv": obs_lst}
167169
obs = flopy.mf6.ModflowUtlobs(gwf, pname="head_obs", digits=20, continuous=obs_dict)
168170

169-
return sim, None
171+
return sim
172+
173+
174+
def build_models(idx, test):
175+
return build_gwf_sim(
176+
name=test.name,
177+
ws=test.workspace,
178+
mf6=test.targets["mf6"],
179+
)
170180

171181

172182
def check_output(idx, test):
183+
gwfname = f"{test.name}_gwf"
184+
173185
# This will fail if budget numbers cannot be read
174-
fpth = os.path.join(test.workspace, f"{test.name}.lst")
186+
fpth = os.path.join(test.workspace, f"{gwfname}.lst")
175187
mflist = flopy.utils.Mf6ListBudget(fpth)
176188
names = mflist.get_record_names()
177189
inc = mflist.get_incremental()
@@ -181,7 +193,7 @@ def check_output(idx, test):
181193
assert v == 10.0, f"Last time should be 10. Found {v}"
182194

183195
# ensure obs results changing monotonically
184-
fpth = os.path.join(test.workspace, test.name + ".obs.csv")
196+
fpth = os.path.join(test.workspace, f"{gwfname}.obs.csv")
185197
try:
186198
tc = np.genfromtxt(fpth, names=True, delimiter=",")
187199
except:

autotest/test_prt_ats.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
"""
2+
Test a PRT model exchange-coupled to an ATS-enabled GWF model. Verify
3+
that PRT "stages" particle state during each attempted solve and only
4+
"commits" it after a successful solve. Also verify that PRT re-solves
5+
on Picard iterations, since each iteration may yield different flows.
6+
7+
There are four cases, covering both buffer strategies (memory and scratch
8+
file) and both solver configurations:
9+
mxiter=1: ATS retry only (no Picard loop)
10+
mxiter=2: Picard loop enabled
11+
12+
The GWF model is nonlinear (unconfined, icelltype=1), so with mxiter=2
13+
the GWF results are different on the 2nd iteration, and PRT also gives
14+
different particle trajectories.
15+
"""
16+
17+
import flopy
18+
import matplotlib.cm as cm
19+
import matplotlib.pyplot as plt
20+
import pandas as pd
21+
import pytest
22+
from flopy.utils.binaryfile import CellBudgetFile, HeadFile
23+
from framework import TestFramework
24+
from prt_test_utils import get_model_name
25+
from test_gwf_ats01 import build_gwf_sim
26+
27+
simname = "prtatsexg"
28+
29+
cases = {
30+
"mxiter=1, memory buffer": (simname, 1, False),
31+
"mxiter=2, memory buffer": (simname + "pic", 2, False),
32+
"mxiter=1, scratch buffer": (simname, 1, True),
33+
"mxiter=2, scratch buffer": (simname + "pic", 2, True),
34+
}
35+
36+
37+
def build_mf6_sim(name, ws, mf6, mxiter=1, scratch_buffer=False):
38+
gwf_name = get_model_name(name, "gwf")
39+
prt_name = get_model_name(name, "prt")
40+
41+
sim = build_gwf_sim(name, ws, mf6)
42+
43+
# create prt model
44+
gwf = sim.get_model()
45+
prt = flopy.mf6.ModflowPrt(sim, modelname=prt_name, save_flows=True)
46+
grid = gwf.modelgrid
47+
48+
# create prt discretization
49+
flopy.mf6.modflow.mfprtdis.ModflowPrtdis(
50+
prt,
51+
pname="dis",
52+
nlay=grid.nlay,
53+
nrow=grid.nrow,
54+
ncol=grid.ncol,
55+
)
56+
57+
# create mip package
58+
flopy.mf6.ModflowPrtmip(prt, pname="mip", porosity=0.1)
59+
60+
# create prp package
61+
rpts = [
62+
# particle index, k, i, j, x, y, z
63+
[i, 0, 0, 0, float(f"0.{i + 1}"), float(f"0.{i + 1}"), 0.5]
64+
for i in range(9)
65+
]
66+
flopy.mf6.ModflowPrtprp(
67+
prt,
68+
pname="prp1",
69+
filename=f"{prt_name}_1.prp",
70+
nreleasepts=len(rpts),
71+
packagedata=rpts,
72+
perioddata={0: ["FIRST"]},
73+
extend_tracking=True,
74+
)
75+
76+
# create output control package
77+
prt_budget_file = f"{prt_name}.cbb"
78+
prt_track_file = f"{prt_name}.trk"
79+
prt_track_csv_file = f"{prt_name}.trk.csv"
80+
flopy.mf6.ModflowPrtoc(
81+
prt,
82+
pname="oc",
83+
budget_filerecord=[prt_budget_file],
84+
track_filerecord=[prt_track_file],
85+
trackcsv_filerecord=[prt_track_csv_file],
86+
scratch_buffer=scratch_buffer,
87+
printrecord=[("BUDGET", "ALL")],
88+
saverecord=[("BUDGET", "ALL")],
89+
)
90+
91+
# create exchange
92+
gwf_name = get_model_name(name, "gwf")
93+
flopy.mf6.ModflowGwfprt(
94+
sim,
95+
exgtype="GWF6-PRT6",
96+
exgmnamea=gwf_name,
97+
exgmnameb=prt_name,
98+
filename=f"{gwf_name}.gwfprt",
99+
)
100+
101+
# add explicit model solution
102+
ems = flopy.mf6.ModflowEms(
103+
sim,
104+
pname="ems",
105+
filename=f"{prt_name}.ems",
106+
)
107+
sim.register_solution_package(ems, [prt.name])
108+
109+
if mxiter > 1:
110+
sim.name_file.mxiter.set_data(mxiter, key=0)
111+
112+
return sim
113+
114+
115+
def build_models(idx, test, mxiter, scratch):
116+
return build_mf6_sim(
117+
test.name,
118+
test.workspace,
119+
test.targets["mf6"],
120+
mxiter=mxiter,
121+
scratch_buffer=scratch,
122+
)
123+
124+
125+
def check_output(idx, test, snapshot):
126+
name = test.name
127+
ws = test.workspace
128+
gwf_name = get_model_name(name, "gwf")
129+
prt_name = get_model_name(name, "prt")
130+
131+
gwf_head_file = ws / f"{gwf_name}.hds"
132+
gwf_budget_file = ws / f"{gwf_name}.cbc"
133+
134+
gwf_hds = HeadFile(gwf_head_file)
135+
gwf_cbb = CellBudgetFile(gwf_budget_file)
136+
gwf_times = set(gwf_hds.get_times())
137+
assert gwf_times == set(gwf_cbb.get_times())
138+
139+
prt_track_csv_file = ws / f"{prt_name}.trk.csv"
140+
prt_budget_file = ws / f"{prt_name}.cbb"
141+
142+
prt_pls = pd.read_csv(prt_track_csv_file)
143+
prt_cbb = CellBudgetFile(prt_budget_file)
144+
assert gwf_times == set(prt_cbb.get_times())
145+
assert (prt_pls["ireason"] == 0).sum() == 9
146+
147+
# Compare particle tracks to snapshot. mxiter=1 and mxiter=2 produce
148+
# different trajectories because the GWF model is nonlinear (unconfined,
149+
# icelltype=1): with mxiter=2 PRT re-tracks on each Picard iteration with
150+
# updated GWF flows, so the final paths reflect more converged flow results.
151+
actual = prt_pls.drop("name", axis=1).round(3)
152+
assert snapshot == actual.to_records(index=False)
153+
154+
155+
def plot_output(idx, test):
156+
name = test.name
157+
ws = test.workspace
158+
gwf_name = get_model_name(name, "gwf")
159+
prt_name = get_model_name(name, "prt")
160+
sim = test.sims[0]
161+
gwf = sim.get_model(gwf_name)
162+
mg = gwf.modelgrid
163+
164+
# check mf6 output files exist
165+
gwf_head_file = ws / f"{gwf_name}.hds"
166+
prt_track_csv_file = ws / f"{prt_name}.trk.csv"
167+
prt_budget_file = ws / f"{prt_name}.cbb"
168+
169+
# extract head, budget, and specific discharge results from GWF model
170+
hds = HeadFile(ws / gwf_head_file).get_data()
171+
bud = gwf.output.budget()
172+
spdis = bud.get_data(text="DATA-SPDIS")[0]
173+
qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge(spdis, gwf)
174+
175+
prt_pls = pd.read_csv(ws / prt_track_csv_file, na_filter=False)
176+
prt_bud = flopy.utils.CellBudgetFile(prt_budget_file, precision="double")
177+
178+
# set up plot
179+
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 10))
180+
ax.set_aspect("equal")
181+
182+
# plot mf6 pathlines in map view
183+
pmv = flopy.plot.PlotMapView(modelgrid=mg, ax=ax)
184+
pmv.plot_grid()
185+
pmv.plot_array(hds[0], alpha=0.1)
186+
pmv.plot_vector(qx, qy, normalize=True, color="white")
187+
mf6_plines = prt_pls.groupby(["iprp", "irpt", "trelease"])
188+
for ipl, ((iprp, irpt, trelease), pl) in enumerate(mf6_plines):
189+
pl.plot(
190+
title="MF6 pathlines",
191+
kind="line",
192+
x="x",
193+
y="y",
194+
ax=ax,
195+
legend=False,
196+
color=cm.plasma(ipl / len(mf6_plines)),
197+
)
198+
199+
# view/save plot
200+
plt.show()
201+
plt.savefig(ws / f"{name}.png")
202+
203+
204+
@pytest.mark.snapshot
205+
@pytest.mark.parametrize("idx, case", enumerate(cases.values()), ids=cases.keys())
206+
def test_mf6model(idx, case, function_tmpdir, targets, array_snapshot, plot):
207+
name, mxiter, scratch = case
208+
test = TestFramework(
209+
name=name,
210+
workspace=function_tmpdir,
211+
build=lambda t: build_models(idx, t, mxiter, scratch),
212+
check=lambda t: check_output(idx, t, array_snapshot),
213+
plot=lambda t: plot_output(idx, t) if plot else None,
214+
targets=targets,
215+
)
216+
test.run()

0 commit comments

Comments
 (0)