Skip to content

Commit c94c7a3

Browse files
authored
feat(prt): make release coordinate checks optional (#2424)
While it's on the user to provide both release coordinates and cell IDs, for some time we have checked at release time that release coordinates fall within the specified cell. Alternative ways to specify release points are in the works in #1901 e.g. local coordinates, which would remove the redundancy, but so long as release points are in global coordinates and we don't do cell identification ourselves, the coordinate checks are a performance bottleneck when particle count is high. Add a COORDINATE_CHECK_METHOD option to allow opting out. Checks on (by default) with eager, trust me I know what I'm doing mode with none. Later, consider adding a lazy mode where validation happens at tracking time, this might save some time if we can reuse terms of other calculations for the checks, but contradicts "fail early" philosophy so should probably not become default. Need to test and get some performance numbers. If in future we did start computing cell IDs (e.g. with something fast like the celltree algorithm) this option could just be ignored when cell IDs are not provided by the user. So one could decide whether to do it in preprocessing or let PRT handle it.
1 parent 5a6af78 commit c94c7a3

4 files changed

Lines changed: 256 additions & 3 deletions

File tree

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""
2+
Test coordinate checking functionality for PRT release points.
3+
4+
Tests the COORDINATE_CHECK_METHOD option which can be set to:
5+
- 'eager' (default): check at release time
6+
- 'none': skip release coordinate validation
7+
8+
Two test cases:
9+
1. checks on ('eager'): Should catch mismatching points/cellids with informative error
10+
2. no checks ('none'): Should allow mismatches and let tracking proceed. This produces
11+
incorrect pathlines where particles jump to the specified cells after reporting initial
12+
positions at the release coordinates.
13+
"""
14+
15+
import flopy
16+
import matplotlib.cm as cm
17+
import matplotlib.pyplot as plt
18+
import pandas as pd
19+
import pytest
20+
from flopy.utils import HeadFile
21+
from framework import TestFramework
22+
from prt_test_utils import FlopyReadmeCase, get_model_name
23+
24+
simname = "prtchk"
25+
cases = [
26+
f"{simname}_eager",
27+
f"{simname}_none",
28+
]
29+
30+
31+
def build_prt_sim(name, gwf_ws, prt_ws, mf6):
32+
# create simulation
33+
sim = flopy.mf6.MFSimulation(
34+
sim_name=name,
35+
exe_name=mf6,
36+
version="mf6",
37+
sim_ws=prt_ws,
38+
)
39+
40+
# create tdis package
41+
flopy.mf6.modflow.mftdis.ModflowTdis(
42+
sim,
43+
pname="tdis",
44+
time_units="DAYS",
45+
nper=FlopyReadmeCase.nper,
46+
perioddata=[
47+
(
48+
FlopyReadmeCase.perlen,
49+
FlopyReadmeCase.nstp,
50+
FlopyReadmeCase.tsmult,
51+
)
52+
],
53+
)
54+
55+
# create prt model
56+
prt_name = get_model_name(name, "prt")
57+
prt = flopy.mf6.ModflowPrt(sim, modelname=prt_name)
58+
59+
# create prt discretization
60+
flopy.mf6.modflow.mfgwfdis.ModflowGwfdis(
61+
prt,
62+
pname="dis",
63+
nlay=FlopyReadmeCase.nlay,
64+
nrow=FlopyReadmeCase.nrow,
65+
ncol=FlopyReadmeCase.ncol,
66+
top=FlopyReadmeCase.top,
67+
botm=FlopyReadmeCase.botm,
68+
)
69+
70+
# create mip package
71+
flopy.mf6.ModflowPrtmip(prt, pname="mip", porosity=FlopyReadmeCase.porosity)
72+
73+
releasepts = [
74+
# particle index, k, i, j, x, y, z (wrong coordinates for cell 0,0,0)
75+
[0, 0, 0, 0, 5.5, 5.5, 0.5],
76+
]
77+
78+
# create prp package
79+
prp_track_file = f"{prt_name}.prp.trk"
80+
prp_track_csv_file = f"{prt_name}.prp.trk.csv"
81+
82+
if "eager" in name:
83+
coordinate_check_method = "eager"
84+
else:
85+
coordinate_check_method = "none"
86+
87+
flopy.mf6.ModflowPrtprp(
88+
prt,
89+
pname="prp1",
90+
filename=f"{prt_name}_1.prp",
91+
nreleasepts=len(releasepts),
92+
packagedata=releasepts,
93+
perioddata={0: [("FIRST",)]},
94+
track_filerecord=[prp_track_file],
95+
trackcsv_filerecord=[prp_track_csv_file],
96+
coordinate_check_method=coordinate_check_method,
97+
print_input=True,
98+
extend_tracking=True,
99+
)
100+
101+
# create output control package
102+
prt_track_file = f"{prt_name}.trk"
103+
prt_track_csv_file = f"{prt_name}.trk.csv"
104+
flopy.mf6.ModflowPrtoc(
105+
prt,
106+
pname="oc",
107+
track_filerecord=[prt_track_file],
108+
trackcsv_filerecord=[prt_track_csv_file],
109+
)
110+
111+
# create the flow model interface
112+
gwf_name = get_model_name(name, "gwf")
113+
gwf_budget_file = gwf_ws / f"{gwf_name}.bud"
114+
gwf_head_file = gwf_ws / f"{gwf_name}.hds"
115+
flopy.mf6.ModflowPrtfmi(
116+
prt,
117+
packagedata=[
118+
("GWFHEAD", gwf_head_file),
119+
("GWFBUDGET", gwf_budget_file),
120+
],
121+
)
122+
123+
# add explicit model solution
124+
ems = flopy.mf6.ModflowEms(
125+
sim,
126+
pname="ems",
127+
filename=f"{prt_name}.ems",
128+
)
129+
sim.register_solution_package(ems, [prt.name])
130+
131+
return sim
132+
133+
134+
def build_models(test):
135+
gwf_sim = FlopyReadmeCase.get_gwf_sim(
136+
test.name, test.workspace, test.targets["mf6"]
137+
)
138+
prt_sim = build_prt_sim(
139+
test.name,
140+
test.workspace,
141+
test.workspace / "prt",
142+
test.targets["mf6"],
143+
)
144+
return gwf_sim, prt_sim
145+
146+
147+
def check_output(test):
148+
name = test.name
149+
150+
if "eager" in name:
151+
buff = test.buffs[1]
152+
assert any("Error: release point" in l for l in buff)
153+
154+
155+
def plot_output(test):
156+
name = test.name
157+
gwf_ws = test.workspace
158+
prt_ws = test.workspace / "prt"
159+
gwf_name = get_model_name(name, "gwf")
160+
prt_name = get_model_name(name, "prt")
161+
gwf_sim = test.sims[0]
162+
gwf = gwf_sim.get_model(gwf_name)
163+
mg = gwf.modelgrid
164+
drape = "drp" in name
165+
166+
# check mf6 output files exist
167+
gwf_head_file = f"{gwf_name}.hds"
168+
prt_track_csv_file = f"{prt_name}.trk.csv"
169+
170+
# extract head, budget, and specific discharge results from GWF model
171+
hds = HeadFile(gwf_ws / gwf_head_file).get_data()
172+
bud = gwf.output.budget()
173+
spdis = bud.get_data(text="DATA-SPDIS")[0]
174+
qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge(spdis, gwf)
175+
176+
# load mf6 pathline results
177+
mf6_pls = pd.read_csv(prt_ws / prt_track_csv_file, na_filter=False)
178+
179+
# set up plot
180+
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 10))
181+
ax.set_aspect("equal")
182+
183+
# plot mf6 pathlines in map view
184+
pmv = flopy.plot.PlotMapView(modelgrid=mg, ax=ax)
185+
pmv.plot_grid()
186+
pmv.plot_array(hds[0], alpha=0.1)
187+
pmv.plot_vector(qx, qy, normalize=True, color="white")
188+
mf6_plines = mf6_pls.groupby(["iprp", "irpt", "trelease"])
189+
for ipl, ((iprp, irpt, trelease), pl) in enumerate(mf6_plines):
190+
pl.plot(
191+
title=f"MF6 pathlines{' (drape)' if drape else ''}",
192+
kind="line",
193+
x="x",
194+
y="y",
195+
ax=ax,
196+
legend=False,
197+
color=cm.plasma(ipl / len(mf6_plines)),
198+
)
199+
200+
# view/save plot
201+
plt.show()
202+
plt.savefig(prt_ws / f"{name}.png")
203+
204+
205+
@pytest.mark.parametrize("name", cases)
206+
def test_mf6model(name, function_tmpdir, targets, plot):
207+
test = TestFramework(
208+
name=name,
209+
workspace=function_tmpdir,
210+
build=build_models,
211+
check=check_output,
212+
plot=plot_output if plot else None,
213+
targets=targets,
214+
compare=None,
215+
xfail=[False, "eager" in name],
216+
)
217+
test.run()

doc/ReleaseNotes/develop.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ section = "fixes"
3232
subsection = "stress"
3333
description = "Fixed viscosity ratio assignment in the GWT MAW package when the GWF VSC package is active."
3434

35-
3635
[[items]]
3736
section = "fixes"
3837
subsection = "internal"
@@ -46,4 +45,9 @@ description = "With the PRT model's Particle Release Package's EXTEND\\_TRACKING
4645
[[items]]
4746
section = "fixes"
4847
subsection = "parallel"
49-
description = "Fixed a memory access exception that can occur when writing convergence information to a CSV file (through the CSV\\_OUTER\\_OUTPUT option in IMS) for a parallel solution."
48+
description = "Fixed a memory access exception that can occur when writing convergence information to a CSV file (through the CSV\\_OUTER\\_OUTPUT option in IMS) for a parallel solution."
49+
50+
[[items]]
51+
section = "features"
52+
subsection = "solution"
53+
description = "Added an optional new COORDINATE\\_CHECK\\_METHOD option to the PRT model's Particle Release Package, accepting values 'EAGER' (default) or 'NONE'. 'NONE' disables release-time verification that release point coordinates and release point cell IDs match, which can significantly reduce runtime when releasing many particles."

doc/mf6io/mf6ivar/dfn/prt-prp.dfn

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,16 @@ optional true
271271
longname release time frequency
272272
description real number indicating the time frequency at which to release particles. This option can be used to schedule releases at a regular interval for the duration of the simulation, starting at the simulation start time. The release schedule is the union of this option, the RELEASETIMES block, and PERIOD block RELEASESETTING selections. If none of these are provided, a single release time is configured at the beginning of the first time step of the simulation's first stress period.
273273

274+
block options
275+
name coordinate_check_method
276+
type string
277+
valid none eager
278+
reader urword
279+
optional true
280+
longname coordinate checking method
281+
description approach for verifying that release point coordinates are in the cell with the specified ID. By default, release point coordinates are checked at release time.
282+
default eager
283+
274284
# --------------------- prt prp dimensions ---------------------
275285

276286
block dimensions

src/Model/ParticleTracking/prt-prp.f90

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ module PrtPrpModule
5959
integer(I4B), pointer :: iextend => null() !< extend tracking beyond simulation's end
6060
integer(I4B), pointer :: ifrctrn => null() !< force ternary solution for quad grids
6161
integer(I4B), pointer :: iexmeth => null() !< method for iterative solution of particle exit location and time in generalized Pollock's method
62+
integer(I4B), pointer :: ichkmeth => null() !< method for checking particle release coordinates are in the specified cells, 0 = none, 1 = eager
6263
real(DP), pointer :: extol => null() !< tolerance for iterative solution of particle exit location and time in generalized Pollock's method
6364
real(DP), pointer :: rttol => null() !< tolerance for coincident particle release times
6465
real(DP), pointer :: rtfreq => null() !< frequency for regularly spaced release times
@@ -170,6 +171,7 @@ subroutine prp_da(this)
170171
call mem_deallocate(this%irlstls)
171172
call mem_deallocate(this%ifrctrn)
172173
call mem_deallocate(this%iexmeth)
174+
call mem_deallocate(this%ichkmeth)
173175
call mem_deallocate(this%extol)
174176
call mem_deallocate(this%rttol)
175177
call mem_deallocate(this%rtfreq)
@@ -260,6 +262,7 @@ subroutine prp_allocate_scalars(this)
260262
call mem_allocate(this%irlstls, 'IRLSTLS', this%memoryPath)
261263
call mem_allocate(this%ifrctrn, 'IFRCTRN', this%memoryPath)
262264
call mem_allocate(this%iexmeth, 'IEXMETH', this%memoryPath)
265+
call mem_allocate(this%ichkmeth, 'ICHKMETH', this%memoryPath)
263266
call mem_allocate(this%extol, 'EXTOL', this%memoryPath)
264267
call mem_allocate(this%rttol, 'RTTOL', this%memoryPath)
265268
call mem_allocate(this%rtfreq, 'RTFREQ', this%memoryPath)
@@ -283,6 +286,7 @@ subroutine prp_allocate_scalars(this)
283286
this%irlstls = 0
284287
this%ifrctrn = 0
285288
this%iexmeth = 0
289+
this%ichkmeth = 1
286290
this%extol = DEM5
287291
this%rttol = DSAME * DEP9
288292
this%rtfreq = DZERO
@@ -514,7 +518,8 @@ subroutine initialize_particle(this, particle, ip, trelease)
514518
z = this%rptz(ip)
515519
end if
516520

517-
call this%validate_release_point(ic, x, y, z)
521+
if (this%ichkmeth > 0) &
522+
call this%validate_release_point(ic, x, y, z)
518523

519524
particle%x = x
520525
particle%y = y
@@ -818,6 +823,23 @@ subroutine prp_options(this, option, found)
818823
call store_error('DEV_EXIT_SOLVE_METHOD MUST BE &
819824
&1 (BRENT) OR 2 (CHANDRUPATLA)')
820825
found = .true.
826+
case ('COORDINATE_CHECK_METHOD')
827+
call this%parser%GetStringCaps(keyword)
828+
select case (keyword)
829+
case ('NONE')
830+
this%ichkmeth = 0
831+
case ('EAGER')
832+
this%ichkmeth = 1
833+
case default
834+
write (errmsg, '(a, a)') &
835+
'Unsupported coordinate check method: ', trim(keyword)
836+
call store_error(errmsg)
837+
write (errmsg, '(a, a)') &
838+
'COORDINATE_CHECK_METHOD must be "NONE" or "EAGER"'
839+
call store_error(errmsg)
840+
call this%parser%StoreErrorUnit()
841+
end select
842+
found = .true.
821843
case default
822844
found = .false.
823845
end select

0 commit comments

Comments
 (0)