Skip to content

Commit 6a4b6fb

Browse files
committed
add test with quad refinement
1 parent 9a72048 commit 6a4b6fb

3 files changed

Lines changed: 389 additions & 4 deletions

File tree

autotest/test_prt_prt_exg.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
88
The flow and tracking models are connected on their shared face.
99
IFLOWFACE is configured in the CHD cells and in the exchange cells,
10-
on the outer faces of the CHD cells and on the shared exchange face.
10+
on the outer faces of the CHD cells and on the shared exchange face
11+
at the exchange boundary.
1112
12-
Particles are released from the left-most cell (0, 0, 0) in the left
13-
model. They should reach and cross the exchange and terminate at the
13+
A single particle is released from the left-most cell in the left
14+
model. It should reach and cross the exchange and terminate at the
1415
rightmost edge of the right model.
1516
1617
left | right
Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
"""
2+
Test PRT-PRT exchange between two particle tracking models,
3+
where one model is quad-refined. This exercises logic that
4+
that transfers particles across the exchange boundary, and
5+
verifies that particles are sent to the correct cells and
6+
positions in the refined destination model.
7+
8+
Each base model domain has 2 cells, which together form a
9+
4-cell line. The right model is refined with one level of
10+
quad refinement, so it has 8 cells. Steady flow is set up
11+
from left to right with CHD packages in the left-most and
12+
right-most cells.
13+
14+
The flow and tracking models are connected on their shared
15+
faces, with the right-most cell in the left model connected
16+
to both left-most cells in the right model. IFLOWFACE is
17+
configured in the CHD cells and in the exchange cells, on
18+
the outer faces of the CHD cells and on the shared exchange
19+
faces at the exchange boundary.
20+
21+
Particles are released from the left-most cell in the left
22+
model. They should reach and cross the exchange boundary,
23+
and terminate at the rightmost edge of the right model.
24+
Two particles are released at Y coordinates such that one
25+
will pass through the upper row of cells in the right model
26+
and one through the lower row.
27+
28+
left | right
29+
____________________________|____________________________
30+
| | | | | | |
31+
| o------|-------------|------|------|------|----->x
32+
| | |______|______|______|______|
33+
| | | | | | |
34+
| o------|-------------|------|------|------|----->x
35+
|_____________|_____________|______|______|______|______|
36+
o release
37+
x termination
38+
39+
"""
40+
41+
from pathlib import Path
42+
43+
import flopy
44+
import numpy as np
45+
import pandas as pd
46+
import pytest
47+
from flopy.utils.gridgen import Gridgen
48+
from framework import TestFramework
49+
from test_prt_prt_exg import get_model_name, plot_output
50+
51+
nlay = 1
52+
nrow = 1
53+
ncol = 2
54+
55+
delr = 1.0
56+
delc = 1.0
57+
delz = 1.0
58+
top = 1.0
59+
botm = [0.0]
60+
61+
head_left = 2.0
62+
head_right = -1.0
63+
64+
hk = 1.0
65+
66+
porosity = 0.1
67+
nper = 1
68+
perlen = 10.0
69+
nstp = 10
70+
71+
simname = "prtprtqref"
72+
cases = [simname]
73+
74+
75+
def get_refined_gridprops(test):
76+
"""Create refined DISV grid for right model using gridgen."""
77+
workspace = test.workspace
78+
targets = test.targets
79+
80+
# Create base grid for right model (1x1x2)
81+
ms = flopy.modflow.Modflow()
82+
dis = flopy.modflow.ModflowDis(
83+
ms,
84+
nlay=nlay,
85+
nrow=nrow,
86+
ncol=ncol,
87+
delr=delr,
88+
delc=delc,
89+
top=top,
90+
botm=botm,
91+
)
92+
93+
# Create gridgen workspace
94+
gridgen_ws = workspace / "gridgen"
95+
gridgen_ws.mkdir(parents=True, exist_ok=True)
96+
97+
# Create Gridgen object
98+
g = Gridgen(
99+
ms.modelgrid,
100+
model_ws=gridgen_ws,
101+
exe_name=targets["gridgen"],
102+
)
103+
104+
# Refine entire right model with 1 level of refinement
105+
# This will turn 2 cells into 8 cells (each refined 2x2)
106+
polygon = [[(0.0, 0.0), (0.0, delc), (2 * delr, delc), (2 * delr, 0.0), (0.0, 0.0)]]
107+
g.add_refinement_features([polygon], "polygon", 1, range(nlay))
108+
g.build(verbose=False)
109+
110+
return g.get_gridprops_disv()
111+
112+
113+
def build_models(idx, test):
114+
name = cases[idx]
115+
ws = test.workspace
116+
mf6 = test.targets["mf6"]
117+
118+
sim = flopy.mf6.MFSimulation(
119+
sim_name=name,
120+
exe_name=mf6,
121+
sim_ws=ws,
122+
)
123+
flopy.mf6.ModflowTdis(
124+
sim,
125+
time_units="DAYS",
126+
nper=nper,
127+
perioddata=[(perlen, nstp, 1.0)],
128+
)
129+
flopy.mf6.ModflowIms(
130+
sim,
131+
pname="ims",
132+
complexity="simple",
133+
outer_dvclose=1e-8,
134+
inner_dvclose=1e-8,
135+
)
136+
137+
# Left GWF model - regular DIS grid (2 cells)
138+
gwfl = flopy.mf6.ModflowGwf(
139+
sim,
140+
modelname=get_model_name(idx, "gwfl"),
141+
save_flows=True,
142+
)
143+
flopy.mf6.ModflowGwfdis(
144+
gwfl,
145+
nlay=nlay,
146+
nrow=nrow,
147+
ncol=ncol,
148+
delr=delr,
149+
delc=delc,
150+
top=top,
151+
botm=botm,
152+
)
153+
flopy.mf6.ModflowGwfic(gwfl, strt=head_left)
154+
flopy.mf6.ModflowGwfnpf(
155+
gwfl,
156+
save_specific_discharge=True,
157+
icelltype=0,
158+
k=hk,
159+
)
160+
flopy.mf6.ModflowGwfchd(
161+
gwfl,
162+
stress_period_data=[[(0, 0, 0), head_left, 1]],
163+
pname="chd_left",
164+
auxiliary=["IFLOWFACE"],
165+
)
166+
flopy.mf6.ModflowGwfoc(
167+
gwfl,
168+
head_filerecord=f"{get_model_name(idx, 'gwfl')}.hds",
169+
budget_filerecord=f"{get_model_name(idx, 'gwfl')}.cbc",
170+
saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")],
171+
)
172+
173+
# Right GWF model - refined DISV grid (8 cells after refinement)
174+
gridprops = get_refined_gridprops(test)
175+
176+
gwfr = flopy.mf6.ModflowGwf(
177+
sim,
178+
modelname=get_model_name(idx, "gwfr"),
179+
save_flows=True,
180+
)
181+
flopy.mf6.ModflowGwfdisv(gwfr, **gridprops)
182+
flopy.mf6.ModflowGwfic(gwfr, strt=head_right)
183+
flopy.mf6.ModflowGwfnpf(
184+
gwfr,
185+
save_specific_discharge=True,
186+
icelltype=0,
187+
k=hk,
188+
)
189+
# CHD in rightmost cells of refined grid
190+
# From gridgen output: cells 6 and 8 (1-based) are at x=1.75-2.0 (rightmost)
191+
# In FloPy 0-based indexing: cells 5 and 7
192+
# IFLOWFACE 2 is east face for these cells
193+
flopy.mf6.ModflowGwfchd(
194+
gwfr,
195+
stress_period_data=[[(0, 5), head_right, 2], [(0, 7), head_right, 2]],
196+
pname="chd_right",
197+
auxiliary=["IFLOWFACE"],
198+
)
199+
flopy.mf6.ModflowGwfoc(
200+
gwfr,
201+
head_filerecord=f"{get_model_name(idx, 'gwfr')}.hds",
202+
budget_filerecord=f"{get_model_name(idx, 'gwfr')}.cbc",
203+
saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")],
204+
)
205+
206+
# GWF-GWF exchange
207+
# Left model cell (0,0,1) connects to right model cells 1 and 3 (0-based: 0 and 2)
208+
# These are the leftmost cells of the refined grid
209+
gwfgwf_data = [
210+
# Left cell (0,0,1) to refined right upper cell (cell 1, 0-based: 0)
211+
(
212+
(0, 0, ncol - 1),
213+
(0, 0),
214+
1,
215+
delr / 2.0,
216+
delr / 4.0,
217+
delc / 2.0,
218+
0.0,
219+
delr / 2.0,
220+
),
221+
# Left cell (0,0,1) to refined right lower cell (cell 3, 0-based: 2)
222+
(
223+
(0, 0, ncol - 1),
224+
(0, 2),
225+
1,
226+
delr / 2.0,
227+
delr / 4.0,
228+
delc / 2.0,
229+
0.0,
230+
delr / 2.0,
231+
),
232+
]
233+
flopy.mf6.ModflowGwfgwf(
234+
sim,
235+
exgtype="GWF6-GWF6",
236+
nexg=len(gwfgwf_data),
237+
exgmnamea=get_model_name(idx, "gwfl"),
238+
exgmnameb=get_model_name(idx, "gwfr"),
239+
exchangedata=gwfgwf_data,
240+
auxiliary=["ANGLDEGX", "CDIST"],
241+
filename="gwf-gwf.exg",
242+
)
243+
244+
# Left PRT model
245+
prtl = flopy.mf6.ModflowPrt(sim, modelname=get_model_name(idx, "prtl"))
246+
flopy.mf6.ModflowPrtdis(
247+
prtl,
248+
nlay=nlay,
249+
nrow=nrow,
250+
ncol=ncol,
251+
delr=delr,
252+
delc=delc,
253+
top=top,
254+
botm=botm,
255+
)
256+
flopy.mf6.ModflowPrtmip(prtl, pname="mip", porosity=porosity)
257+
258+
# Release 2 particles: one in upper half, one in lower half
259+
# This ensures particles go through different rows in refined right model
260+
releasepts = [
261+
(0, (0, 0, 0), 0.5, 0.75, 0.5), # Upper particle
262+
(1, (0, 0, 0), 0.5, 0.25, 0.5), # Lower particle
263+
]
264+
flopy.mf6.ModflowPrtprp(
265+
prtl,
266+
pname="prp1",
267+
filename=f"{get_model_name(idx, 'prtl')}.prp",
268+
nreleasepts=len(releasepts),
269+
packagedata=releasepts,
270+
perioddata={0: ["FIRST"]},
271+
extend_tracking=True,
272+
stoptime=0.4,
273+
)
274+
flopy.mf6.ModflowPrtoc(
275+
prtl,
276+
pname="oc",
277+
trackcsv_filerecord=f"{get_model_name(idx, 'prtl')}.trk.csv",
278+
dev_dump_event_trace=True,
279+
)
280+
flopy.mf6.ModflowGwfprt(
281+
sim,
282+
exgtype="GWF6-PRT6",
283+
exgmnamea=get_model_name(idx, "gwfl"),
284+
exgmnameb=get_model_name(idx, "prtl"),
285+
filename=f"{get_model_name(idx, 'gwfl')}.gwfprt",
286+
)
287+
288+
# Right PRT model - refined DISV grid
289+
prtr = flopy.mf6.ModflowPrt(sim, modelname=get_model_name(idx, "prtr"))
290+
flopy.mf6.ModflowPrtdisv(prtr, **gridprops)
291+
flopy.mf6.ModflowPrtmip(prtr, pname="mip", porosity=porosity)
292+
# Right model has no PRP, it just receives particles from left
293+
flopy.mf6.ModflowPrtoc(
294+
prtr,
295+
pname="oc",
296+
trackcsv_filerecord=f"{get_model_name(idx, 'prtr')}.trk.csv",
297+
dev_dump_event_trace=True,
298+
)
299+
flopy.mf6.ModflowGwfprt(
300+
sim,
301+
exgtype="GWF6-PRT6",
302+
exgmnamea=get_model_name(idx, "gwfr"),
303+
exgmnameb=get_model_name(idx, "prtr"),
304+
filename=f"{get_model_name(idx, 'gwfr')}.gwfprt",
305+
)
306+
307+
# PRT-PRT exchange
308+
# For DISV grids, IFLOWFACE proceeds clockwise from first vertex
309+
# Left model DIS: face 3 is east (right) face
310+
# Right model DISV cells 1 and 3 both have west face = face 4
311+
# (verified from vertex coordinates - face 4 connects leftmost vertices)
312+
prtprt_data = [
313+
# Left cell (0,0,1) to refined upper right cell (cell 1, 0-based: 0)
314+
((0, 0, ncol - 1), (0, 0), 1, 3, 4), # DIS face 3 (east) to DISV face 4 (west)
315+
# Left cell (0,0,1) to refined lower right cell (cell 3, 0-based: 2)
316+
((0, 0, ncol - 1), (0, 2), 1, 3, 4), # DIS face 3 (east) to DISV face 4 (west)
317+
]
318+
flopy.mf6.ModflowPrtprt(
319+
sim,
320+
exgtype="PRT6-PRT6",
321+
nexg=len(prtprt_data),
322+
exgmnamea=get_model_name(idx, "prtl"),
323+
exgmnameb=get_model_name(idx, "prtr"),
324+
gwfmodelname1=get_model_name(idx, "gwfl"),
325+
gwfmodelname2=get_model_name(idx, "gwfr"),
326+
exchangedata=prtprt_data,
327+
filename="prt-prt.exg",
328+
auxiliary=["IFLOWFACE1", "IFLOWFACE2"],
329+
)
330+
331+
ems = flopy.mf6.ModflowEms(
332+
sim,
333+
pname="ems",
334+
filename="prt.ems",
335+
)
336+
sim.register_solution_package(
337+
ems, [get_model_name(idx, "prtl"), get_model_name(idx, "prtr")]
338+
)
339+
340+
return sim
341+
342+
343+
def check_output(idx, test):
344+
ws = Path(test.workspace)
345+
hds_l = flopy.utils.HeadFile(ws / f"{get_model_name(idx, 'gwfl')}.hds")
346+
hds_r = flopy.utils.HeadFile(ws / f"{get_model_name(idx, 'gwfr')}.hds")
347+
head_l = hds_l.get_data().squeeze()
348+
head_r = hds_r.get_data().squeeze()
349+
assert np.all(head_l <= head_left)
350+
assert np.all(head_r >= head_right)
351+
352+
pls_l = pd.read_csv(ws / f"{get_model_name(idx, 'prtl')}.trk.csv")
353+
pls_r = pd.read_csv(ws / f"{get_model_name(idx, 'prtr')}.trk.csv")
354+
assert len(pls_l) > 0
355+
assert len(pls_r) > 0
356+
357+
irpts_l = pls_l["irpt"].unique()
358+
irpts_r = pls_r["irpt"].unique()
359+
assert set(irpts_l) == set(irpts_r)
360+
361+
terminations = pls_r[pls_r.ireason == 3]
362+
assert len(terminations) == len(irpts_l)
363+
assert all(terminations.istatus == 2)
364+
365+
final_x = terminations["x"]
366+
final_y = terminations["y"]
367+
final_z = terminations["z"]
368+
assert np.allclose(final_x, 2.0)
369+
assert all(np.isclose(y, 0.25) or np.isclose(y, 0.75) for y in final_y)
370+
assert np.allclose(final_z, 0.5)
371+
372+
373+
@pytest.mark.parametrize("idx, name", enumerate(cases))
374+
def test_mf6model(idx, name, function_tmpdir, targets, plot):
375+
test = TestFramework(
376+
name=name,
377+
workspace=function_tmpdir,
378+
targets=targets,
379+
build=lambda t: build_models(idx, t),
380+
check=lambda t: check_output(idx, t),
381+
plot=lambda t: plot_output(idx, t) if plot else None,
382+
compare=None,
383+
)
384+
test.run()

0 commit comments

Comments
 (0)