Skip to content

Commit ce1aab5

Browse files
wpbonelliwpbonelli
authored andcommitted
feat(prp): support local, offset and transformed coords
1 parent a3155cd commit ce1aab5

9 files changed

Lines changed: 371 additions & 25 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""
2+
Tests ability to transform release point coordinates from
3+
global, local, or local offset coordinate representations
4+
to model coordinates, as well as to transform output data
5+
from model to global cooordinates.
6+
7+
The grid is a 10x10 square with a single layer,
8+
the same flow system shown on the FloPy readme.
9+
10+
Test cases are defined for each coordinate system option.
11+
These are:
12+
13+
- local_xy
14+
- local_xy_offset
15+
- dev_global_xy
16+
17+
"""
18+
19+
import flopy
20+
import matplotlib.pyplot as plt
21+
import numpy as np
22+
import pandas as pd
23+
import pytest
24+
from flopy.utils.gridintersect import GridIntersect
25+
from framework import TestFramework
26+
from prt_test_utils import (
27+
DEFAULT_EXIT_SOLVE_TOL,
28+
FlopyReadmeCase,
29+
get_model_name,
30+
)
31+
from shapely.geometry import Point
32+
33+
simname = "prtcrd"
34+
cases = [f"{simname}mdl", f"{simname}lcl", f"{simname}ofs"] # todo global
35+
nodes = list(range(100))
36+
37+
38+
def get_start_points():
39+
return np.add(
40+
np.transpose(np.array(np.meshgrid(range(10), range(10))).reshape(2, -1)),
41+
0.5,
42+
)
43+
44+
45+
def get_prp(prt):
46+
gi = GridIntersect(prt.modelgrid, method="vertex", rtree=True)
47+
if "mdl" in prt.name:
48+
startpts = get_start_points()
49+
releasepts = [
50+
(i, (0, *gi.intersect(Point(rpt))[0].cellids), *rpt, 0.5)
51+
for i, rpt in enumerate(startpts)
52+
]
53+
elif "lcl" in prt.name:
54+
releasepts = [
55+
(nn, tuple([*prt.modelgrid.get_lrc([nn])[0]]), 0.5, 0.5, 0.5)
56+
for nn in nodes
57+
]
58+
elif "ofs" in prt.name:
59+
releasepts = [
60+
(nn, tuple([*prt.modelgrid.get_lrc([nn])[0]]), 0.0, 0.0, 0.0)
61+
for nn in nodes
62+
]
63+
elif "gbl" in prt.name:
64+
# todo global
65+
pass
66+
67+
prp_track_file = f"{prt.name}_prt.prp.trk"
68+
prp_track_csv_file = f"{prt.name}_prt.prp.trk.csv"
69+
return flopy.mf6.ModflowPrtprp(
70+
prt,
71+
pname="prp1",
72+
filename=f"{prt.name}_1.prp",
73+
nreleasepts=len(releasepts),
74+
packagedata=releasepts,
75+
perioddata={0: ["FIRST"]},
76+
track_filerecord=[prp_track_file],
77+
trackcsv_filerecord=[prp_track_csv_file],
78+
local_xy="lcl" in prt.name,
79+
local_xy_offset="ofs" in prt.name,
80+
exit_solve_tolerance=DEFAULT_EXIT_SOLVE_TOL,
81+
extend_tracking=True,
82+
)
83+
84+
85+
def build_prt_sim(name, gwf_ws, prt_ws, mf6):
86+
# create simulation
87+
sim = flopy.mf6.MFSimulation(
88+
sim_name=name,
89+
exe_name=mf6,
90+
version="mf6",
91+
sim_ws=prt_ws,
92+
)
93+
94+
# create tdis package
95+
flopy.mf6.modflow.mftdis.ModflowTdis(
96+
sim,
97+
pname="tdis",
98+
time_units="DAYS",
99+
nper=FlopyReadmeCase.nper,
100+
perioddata=[
101+
(
102+
FlopyReadmeCase.perlen,
103+
FlopyReadmeCase.nstp,
104+
FlopyReadmeCase.tsmult,
105+
)
106+
],
107+
)
108+
109+
# create prt model
110+
prt_name = get_model_name(name, "prt")
111+
prt = flopy.mf6.ModflowPrt(sim, modelname=prt_name, save_flows=True)
112+
113+
# create prt discretization
114+
flopy.mf6.modflow.mfgwfdis.ModflowGwfdis(
115+
prt,
116+
pname="dis",
117+
nlay=FlopyReadmeCase.nlay,
118+
nrow=FlopyReadmeCase.nrow,
119+
ncol=FlopyReadmeCase.ncol,
120+
)
121+
122+
# create mip package
123+
flopy.mf6.ModflowPrtmip(prt, pname="mip", porosity=FlopyReadmeCase.porosity)
124+
125+
# create prp package
126+
prp = get_prp(prt)
127+
128+
# create output control package
129+
prt_budget_file = f"{prt_name}.bud"
130+
prt_track_file = f"{prt_name}.trk"
131+
prt_track_csv_file = f"{prt_name}.trk.csv"
132+
flopy.mf6.ModflowPrtoc(
133+
prt,
134+
pname="oc",
135+
budget_filerecord=[prt_budget_file],
136+
track_filerecord=[prt_track_file],
137+
trackcsv_filerecord=[prt_track_csv_file],
138+
saverecord=[("BUDGET", "ALL")],
139+
)
140+
141+
# create the flow model interface
142+
gwf_name = get_model_name(name, "gwf")
143+
gwf_budget_file = gwf_ws / f"{gwf_name}.bud"
144+
gwf_head_file = gwf_ws / f"{gwf_name}.hds"
145+
flopy.mf6.ModflowPrtfmi(
146+
prt,
147+
packagedata=[
148+
("GWFHEAD", gwf_head_file),
149+
("GWFBUDGET", gwf_budget_file),
150+
],
151+
)
152+
153+
# add explicit model solution
154+
ems = flopy.mf6.ModflowEms(
155+
sim,
156+
pname="ems",
157+
filename=f"{prt_name}.ems",
158+
)
159+
sim.register_solution_package(ems, [prt.name])
160+
161+
return sim
162+
163+
164+
def build_models(idx, test):
165+
gwf_ws = test.workspace / "gwf"
166+
prt_ws = test.workspace / "prt"
167+
gwf_sim = FlopyReadmeCase.get_gwf_sim(test.name, gwf_ws, test.targets["mf6"])
168+
prt_sim = build_prt_sim(test.name, gwf_ws, prt_ws, test.targets["mf6"])
169+
return gwf_sim, prt_sim
170+
171+
172+
def check_output(idx, test):
173+
name = test.name
174+
gwf_ws = test.workspace
175+
prt_ws = test.workspace / "prt"
176+
gwf_sim, prt_sim = test.sims[:2]
177+
gwf = gwf_sim.get_model()
178+
prt = prt_sim.get_model()
179+
hds = gwf.output.head().get_data()
180+
mg = gwf.modelgrid
181+
pathlines = pd.read_csv(prt_ws / f"{prt.name}.trk.csv")
182+
startpts = pathlines[pathlines.ireason == 0]
183+
endpts = pathlines[pathlines.ireason == 3]
184+
185+
# check start points
186+
assert np.allclose(
187+
np.sort(startpts[["x", "y"]].to_numpy(), axis=0),
188+
np.sort(np.array(get_start_points()), axis=0),
189+
)
190+
191+
# setup plot
192+
plot_data = False
193+
if plot_data:
194+
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10, 10))
195+
for a in ax:
196+
a.set_aspect("equal")
197+
198+
# plot pathline in map view
199+
pmv = flopy.plot.PlotMapView(modelgrid=mg, ax=ax[0])
200+
pmv.plot_grid()
201+
pmv.plot_array(hds[0], alpha=0.1, cmap="jet")
202+
pmv.plot_pathline(pathlines)
203+
204+
# view/save plot
205+
plt.show()
206+
plt.savefig(gwf_ws / f"test_{simname}.png")
207+
208+
209+
@pytest.mark.parametrize("idx, name", enumerate(cases))
210+
def test_mf6model(idx, name, function_tmpdir, targets):
211+
test = TestFramework(
212+
name=name,
213+
workspace=function_tmpdir,
214+
build=lambda t: build_models(idx, t),
215+
check=lambda t: check_output(idx, t),
216+
targets=targets,
217+
compare=None,
218+
)
219+
test.run()

autotest/test_prt_fmi.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""
22
Tests ability to run a GWF model then a PRT model
33
in separate simulations via flow model interface,
4-
as well as
54
65
The grid is a 10x10 square with a single layer,
76
the same flow system shown on the FloPy readme.

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,31 @@ type keyword
4141
reader urword
4242
optional true
4343
longname whether to use local z coordinates
44-
description indicates that ``zrpt'' defines the local z coordinate of the release point within the cell, with value of 0 at the bottom and 1 at the top of the cell. If the cell is partially saturated at release time, the top of the cell is considered to be the water table elevation (the head in the cell) rather than the top defined by the user.
44+
description indicates that ``zrpt'' defines the local z coordinate of the release point within the cell scaled to the unit interval, with value of 0 at the bottom and 1 at the top of the cell. If the cell is partially saturated at release time, the top of the cell is considered to be the water table elevation (the head in the cell) rather than the top defined by the user.
45+
46+
block options
47+
name local_xy
48+
type keyword
49+
reader urword
50+
optional true
51+
longname whether to use local x and y coordinates
52+
description indicates that ``xrpt'' and ``yrpt'' define the local x and y coordinates of the release point within the cell scaled to the unit interval, with value of 0 at the west and south faces and 1 at the east and north faces of the cell. This option may only be used with structured models.
53+
54+
block options
55+
name local_xy_offset
56+
type keyword
57+
reader urword
58+
optional true
59+
longname whether to use local x and y offsets
60+
description indicates that ``xrpt'' and ``rpt'' define the local x and y offsets of the release point within the cell, where the local coordinate system's origin is the cell center and distances are not rescaled. This option may be used with structured or unstructured models.
61+
62+
block options
63+
name dev_global_xy
64+
type keyword
65+
reader urword
66+
optional true
67+
longname whether to transform x and y coordinates
68+
description indicates that ``xrpt'' and ``yrpt'' define global coordinates and should be transformed to the model coordinate system if georeferencing information (origin and rotation) is available on the model grid. By default, release points are assumed to be in model coordinates. If this option is enabled, particle pathlines will also be reported in global coordinates in output files.
4569

4670
block options
4771
name extend_tracking

src/Model/ModelUtilities/TrackControl.f90

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,11 @@ end subroutine expand
110110
!! any PRP-level files with PRP index matching the particle's
111111
!! PRP index.
112112
!<
113-
subroutine save(this, particle, kper, kstp, reason, level)
113+
subroutine save(this, particle, dis, kper, kstp, reason, level)
114114
! dummy
115115
class(TrackControlType), intent(inout) :: this
116116
type(ParticleType), pointer, intent(in) :: particle
117+
class(DisBaseType), pointer, intent(in) :: dis
117118
integer(I4B), intent(in) :: kper
118119
integer(I4B), intent(in) :: kstp
119120
integer(I4B), intent(in) :: reason
@@ -146,7 +147,7 @@ subroutine save(this, particle, kper, kstp, reason, level)
146147
if (file%iun > 0 .and. &
147148
(file%iprp == -1 .or. &
148149
file%iprp == particle%iprp)) &
149-
call save_record(file%iun, particle, &
150+
call save_record(file%iun, particle, dis, &
150151
kper, kstp, reason, csv=file%csv)
151152
end do
152153
end subroutine save

src/Model/ModelUtilities/TrackFile.f90

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module TrackFileModule
22
use KindModule, only: DP, I4B, LGP
33
use ConstantsModule, only: DZERO, DPIO180
44
use ParticleModule, only: ParticleType, ACTIVE
5+
use BaseDisModule, only: DisBaseType
56
use GeomUtilModule, only: transform
67

78
implicit none
@@ -43,21 +44,39 @@ module TrackFileModule
4344
contains
4445

4546
!> @brief Save a particle track record to a binary or CSV file.
46-
subroutine save_record(iun, particle, kper, kstp, reason, csv)
47+
subroutine save_record(iun, particle, dis, kper, kstp, reason, csv)
4748
! dummy
4849
integer(I4B), intent(in) :: iun
4950
type(ParticleType), pointer, intent(in) :: particle
51+
class(DisBaseType), pointer, intent(in) :: dis
5052
integer(I4B), intent(in) :: kper
5153
integer(I4B), intent(in) :: kstp
5254
integer(I4B), intent(in) :: reason
5355
logical(LGP), intent(in) :: csv
5456
! local
55-
real(DP) :: x, y, z
57+
real(DP) :: x, y, z, xout, yout, zout
5658
integer(I4B) :: status
5759

5860
! Convert from cell-local to model coordinates if needed
5961
call particle%get_model_coords(x, y, z)
6062

63+
! Apply model grid offset/rotation if needed
64+
if (particle%icoords == 1 .and. &
65+
(dis%angrot /= DZERO .or. &
66+
dis%xorigin /= DZERO .or. &
67+
dis%yorigin /= DZERO)) then
68+
call transform(x, y, z, &
69+
xout, yout, zout, &
70+
dis%xorigin, &
71+
dis%yorigin, &
72+
DZERO, &
73+
sin(dis%angrot * DPIO180), &
74+
cos(dis%angrot * DPIO180), &
75+
invert=.true.)
76+
x = xout
77+
y = yout
78+
end if
79+
6180
! Set status
6281
if (particle%istatus .lt. 0) then
6382
status = ACTIVE

0 commit comments

Comments
 (0)