Skip to content

Commit 9caec7b

Browse files
committed
support static (no-reset) ts links; add test
1 parent eb1f42d commit 9caec7b

7 files changed

Lines changed: 259 additions & 58 deletions

File tree

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
"""test_gwf_maw_pkgdata_ts.py
2+
3+
Focused tests for MAW PACKAGEDATA IDM enabled time-series linkage for two
4+
distinct TS mechanisms:
5+
Case 0: STRT package data timeseries variable
6+
Case 1: AUX package data timeseries variable
7+
8+
Model geometry: 1-layer, 1-row, 3-column; CHD at flanking columns; single
9+
MAW well in the centre column; 3 steady-state stress periods, 1 step each.
10+
"""
11+
12+
import os
13+
14+
import flopy
15+
import numpy as np
16+
import pytest
17+
from flopy.utils.compare import eval_bud_diff
18+
from framework import TestFramework
19+
20+
paktest = "maw"
21+
cases = ["maw_strt_pd_ts", "maw_aux_pd_ts"]
22+
23+
# Shared geometry and temporal discretisation
24+
nper = 3
25+
perlen = 1.0
26+
period_data = [(perlen, 1, 1.0)] * nper # steady-state, 1 timestep each
27+
28+
nlay, nrow, ncol = 1, 1, 3
29+
delr = delc = 100.0
30+
top = 200.0
31+
botm = [0.0]
32+
k11 = 1.0
33+
chd_head = 100.0
34+
35+
# MAW geometry
36+
radius = 0.1
37+
well_bot = -10.0
38+
39+
# Per-period values — chosen to be distinct so any failure to update is visible
40+
strt_vals = [90.0, 95.0, 85.0]
41+
conc_vals = [10.0, 20.0, 30.0]
42+
43+
# TS times: entries at period-end times so LINEAREND gives exact per-period values.
44+
# tsmanager%ad() is called with totim = end of current timestep (1.0, 2.0, 3.0).
45+
ts_times = [0.0, 1.0, 2.0, 3.0]
46+
ts_strt = [strt_vals[0]] + strt_vals # [90, 90, 95, 85]
47+
ts_conc = [conc_vals[0]] + conc_vals # [10, 10, 20, 30]
48+
49+
50+
def _base_sim(ws, name):
51+
"""Return a minimal GWF sim ready for a MAW package to be attached."""
52+
sim = flopy.mf6.MFSimulation(sim_name=name, exe_name="mf6", sim_ws=ws)
53+
flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=period_data)
54+
flopy.mf6.ModflowIms(sim, print_option="NONE")
55+
gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True)
56+
flopy.mf6.ModflowGwfdis(
57+
gwf,
58+
nlay=nlay,
59+
nrow=nrow,
60+
ncol=ncol,
61+
delr=delr,
62+
delc=delc,
63+
top=top,
64+
botm=botm,
65+
)
66+
flopy.mf6.ModflowGwfnpf(gwf, k=k11)
67+
flopy.mf6.ModflowGwfic(gwf, strt=chd_head)
68+
flopy.mf6.ModflowGwfchd(
69+
gwf,
70+
stress_period_data=[[(0, 0, 0), chd_head], [(0, 0, 2), chd_head]],
71+
)
72+
flopy.mf6.ModflowGwfoc(
73+
gwf,
74+
budget_filerecord=f"{name}.cbc",
75+
saverecord=[("BUDGET", "ALL")],
76+
)
77+
return sim, gwf
78+
79+
80+
# Case 0 — STRT PACKAGEDATA TS
81+
def _get_strt_ts(ws, name):
82+
"""STRT as PACKAGEDATA TS; STATUS CONSTANT; HEAD obs recorded."""
83+
sim, gwf = _base_sim(ws, name)
84+
maw = flopy.mf6.ModflowGwfmaw(
85+
gwf,
86+
nmawwells=1,
87+
budget_filerecord=f"{name}.{paktest}.cbc",
88+
print_input=True,
89+
packagedata=[(0, radius, well_bot, "strt_ts", "THIEM", 1)],
90+
connectiondata=[(0, 0, (0, 0, 1), top, well_bot, 0.0, 0.0)],
91+
perioddata={0: [(0, "status", "constant")]},
92+
pname="maw-1",
93+
)
94+
maw.obs.initialize(
95+
filename=f"{name}.{paktest}.obs",
96+
digits=10,
97+
print_input=True,
98+
continuous={"maw_head.csv": [("well_head", "HEAD", (0,))]},
99+
)
100+
maw.ts.initialize(
101+
filename=f"{name}.{paktest}.ts",
102+
timeseries=list(zip(ts_times, ts_strt)),
103+
time_series_namerecord=["strt_ts"],
104+
interpolation_methodrecord=["linearend"],
105+
)
106+
return sim
107+
108+
109+
# Case 1 — PACKAGEDATA AUX TS
110+
def _get_aux_period(ws, name):
111+
"""Reference: AUX changes via PERIOD AUXILIARY each period; STRT fixed."""
112+
sim, gwf = _base_sim(ws, name)
113+
maw_spd = {
114+
0: [(0, "status", "constant"), (0, "AUXILIARY", "concentration", conc_vals[0])],
115+
1: [(0, "AUXILIARY", "concentration", conc_vals[1])],
116+
2: [(0, "AUXILIARY", "concentration", conc_vals[2])],
117+
}
118+
flopy.mf6.ModflowGwfmaw(
119+
gwf,
120+
nmawwells=1,
121+
auxiliary=["concentration"],
122+
budget_filerecord=f"{name}.{paktest}.cbc",
123+
packagedata=[(0, radius, well_bot, strt_vals[0], "THIEM", 1, conc_vals[0])],
124+
connectiondata=[(0, 0, (0, 0, 1), top, well_bot, 0.0, 0.0)],
125+
perioddata=maw_spd,
126+
pname="maw-1",
127+
)
128+
return sim
129+
130+
131+
def _get_aux_pkgdata_ts(ws, name):
132+
"""TS: AUX changes via PACKAGEDATA AUX TS; STRT fixed (same as reference)."""
133+
sim, gwf = _base_sim(ws, name)
134+
maw = flopy.mf6.ModflowGwfmaw(
135+
gwf,
136+
nmawwells=1,
137+
auxiliary=["concentration"],
138+
budget_filerecord=f"{name}.{paktest}.cbc",
139+
packagedata=[(0, radius, well_bot, strt_vals[0], "THIEM", 1, "conc_ts")],
140+
connectiondata=[(0, 0, (0, 0, 1), top, well_bot, 0.0, 0.0)],
141+
perioddata={0: [(0, "status", "constant")]},
142+
pname="maw-1",
143+
)
144+
maw.ts.initialize(
145+
filename=f"{name}.{paktest}.ts",
146+
timeseries=list(zip(ts_times, ts_conc)),
147+
time_series_namerecord=["conc_ts"],
148+
interpolation_methodrecord=["linearend"],
149+
)
150+
return sim
151+
152+
153+
# Build the model
154+
def build_models(idx, test):
155+
name = cases[idx]
156+
ws0 = test.workspace
157+
ws1 = os.path.join(test.workspace, "mf6")
158+
if idx == 0:
159+
# Case 0: baseline is a fixed-STRT model (not compared); TS model
160+
# is in mf6/ sub-directory where HEAD obs are checked.
161+
return _get_strt_ts(ws0, name), _get_strt_ts(ws1, name)
162+
else:
163+
# Case 1: baseline uses PERIOD AUXILIARY; TS uses PACKAGEDATA AUX TS.
164+
return _get_aux_period(ws0, name), _get_aux_pkgdata_ts(ws1, name)
165+
166+
167+
# Check output
168+
def check_output(idx, test):
169+
name = os.path.basename(test.name)
170+
ws0 = test.workspace
171+
ws1 = os.path.join(test.workspace, "mf6")
172+
173+
if idx == 0:
174+
# -- Case 0: assert that MAW HEAD obs (in mf6/) match STRT TS values --
175+
obs_path = os.path.join(ws1, "maw_head.csv")
176+
obs = np.genfromtxt(obs_path, delimiter=",", names=True, skip_header=0)
177+
# One output time per stress period; values should equal strt_vals exactly
178+
# (LINEAREND at period-end totim gives the exact TS entry value)
179+
heads = obs["WELL_HEAD"]
180+
assert len(heads) == nper, f"expected {nper} obs records, got {len(heads)}"
181+
assert np.allclose(heads, strt_vals, atol=1e-6), (
182+
f"STRT TS: well_head {heads} does not match expected {strt_vals}"
183+
)
184+
else:
185+
# -- Case 1: GWF and MAW budgets must be identical between the two models --
186+
ia = (
187+
flopy.mf6.utils.MfGrdFile(os.path.join(ws0, f"{name}.dis.grb"))._datadict[
188+
"IA"
189+
]
190+
- 1
191+
)
192+
193+
eval_bud_diff(
194+
os.path.join(ws0, f"{name}.cbc.cmp.out"),
195+
flopy.utils.CellBudgetFile(
196+
os.path.join(ws0, f"{name}.cbc"), precision="double"
197+
),
198+
flopy.utils.CellBudgetFile(
199+
os.path.join(ws1, f"{name}.cbc"), precision="double"
200+
),
201+
ia,
202+
)
203+
eval_bud_diff(
204+
os.path.join(ws0, f"{name}.{paktest}.cbc.cmp.out"),
205+
flopy.utils.CellBudgetFile(
206+
os.path.join(ws0, f"{name}.{paktest}.cbc"), precision="double"
207+
),
208+
flopy.utils.CellBudgetFile(
209+
os.path.join(ws1, f"{name}.{paktest}.cbc"), precision="double"
210+
),
211+
)
212+
213+
214+
@pytest.mark.parametrize("idx, name", enumerate(cases))
215+
def test_mf6model(idx, name, function_tmpdir, targets):
216+
test = TestFramework(
217+
name=name,
218+
workspace=function_tmpdir,
219+
build=lambda t: build_models(idx, t),
220+
check=lambda t: check_output(idx, t),
221+
targets=targets,
222+
)
223+
test.run()

src/Model/GroundWaterFlow/gwf-maw.f90

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,7 +1619,7 @@ end subroutine maw_ar
16191619
!! Read itmp and new boundaries if itmp > 0
16201620
!<
16211621
subroutine maw_rp(this)
1622-
use ConstantsModule, only: LINELENGTH, DNODATA, DZERO, DEP20
1622+
use ConstantsModule, only: LENVARNAME
16231623
use TdisModule, only: kper
16241624
use MemoryManagerModule, only: mem_setptr
16251625
use CharacterStringModule, only: CharacterStringType
@@ -1631,6 +1631,7 @@ subroutine maw_rp(this)
16311631
character(len=LINELENGTH) :: text
16321632
character(len=LINELENGTH) :: csteady
16331633
character(len=LINELENGTH) :: str
1634+
character(len=LENVARNAME) :: setting
16341635
character(len=LINELENGTH) :: cstr
16351636
character(len=LINELENGTH) :: errmsgr
16361637
integer(I4B) :: node
@@ -1680,10 +1681,10 @@ subroutine maw_rp(this)
16801681
end if
16811682
!
16821683
! -- dispatch key for this row
1683-
str = this%input%setting(n)
1684+
setting = this%input%setting(n)
16841685
!
16851686
! -- STATUS
1686-
if (trim(str) == 'STATUS') then
1687+
if (trim(setting) == 'STATUS') then
16871688
str = this%input%status(n)
16881689
this%status(imaw) = str(1:8)
16891690
select case (trim(str))
@@ -1702,12 +1703,12 @@ subroutine maw_rp(this)
17021703
end if
17031704
!
17041705
! -- RATE
1705-
if (trim(str) == 'RATE') then
1706+
if (trim(setting) == 'RATE') then
17061707
this%rate(imaw) = this%input%rate(n)
17071708
end if
17081709
!
17091710
! -- WELL_HEAD
1710-
if (trim(str) == 'WELL_HEAD') then
1711+
if (trim(setting) == 'WELL_HEAD') then
17111712
this%well_head(imaw) = this%input%well_head(n)
17121713
this%xnewpak(imaw) = this%well_head(imaw)
17131714
if (this%well_head(imaw) < this%bot(imaw)) then
@@ -1717,7 +1718,7 @@ subroutine maw_rp(this)
17171718
end if
17181719
!
17191720
! -- HEAD_LIMIT
1720-
if (trim(str) == 'HEAD_LIMIT') then
1721+
if (trim(setting) == 'HEAD_LIMIT') then
17211722
str = this%input%head_limit(n)
17221723
if (trim(str) == 'OFF') then
17231724
this%shutofflevel(imaw) = DEP20
@@ -1735,7 +1736,7 @@ subroutine maw_rp(this)
17351736
end if
17361737
!
17371738
! -- FLOWING_WELL (compound group)
1738-
if (trim(str) == 'FLOWING_WELL') then
1739+
if (trim(setting) == 'FLOWING_WELL') then
17391740
this%fwelev(imaw) = this%input%fwelev(n)
17401741
this%fwcond(imaw) = this%input%fwcond(n)
17411742
this%fwrlen(imaw) = this%input%fwrlen(n)
@@ -1749,13 +1750,13 @@ subroutine maw_rp(this)
17491750
end if
17501751
!
17511752
! -- SHUT_OFF (compound group)
1752-
if (trim(str) == 'SHUT_OFF') then
1753+
if (trim(setting) == 'SHUT_OFF') then
17531754
this%shutoffmin(imaw) = this%input%minrate(n)
17541755
this%shutoffmax(imaw) = this%input%maxrate(n)
17551756
end if
17561757
!
17571758
! -- RATE_SCALING (compound group)
1758-
if (trim(str) == 'RATE_SCALING') then
1759+
if (trim(setting) == 'RATE_SCALING') then
17591760
this%pumpelev(imaw) = this%input%pump_elevation(n)
17601761
this%reduction_length(imaw) = this%input%scaling_length(n)
17611762
if (this%reduction_length(imaw) < DZERO) then
@@ -1766,7 +1767,7 @@ subroutine maw_rp(this)
17661767
!
17671768
! -- AUXILIARY (compound group)
17681769
if (this%naux > 0) then
1769-
if (trim(str) == 'PERIOD_AUXILIARY') then
1770+
if (trim(setting) == 'PERIOD_AUXILIARY') then
17701771
str = this%input%auxname(n)
17711772
do jj = 1, this%naux
17721773
if (trim(str) /= trim(this%auxname(jj))) cycle
@@ -1940,6 +1941,7 @@ end subroutine maw_rp
19401941
!> @brief Add package connection to matrix
19411942
!<
19421943
subroutine maw_ad(this)
1944+
use ConstantsModule, only: LENVARNAME
19431945
use TdisModule, only: kper, kstp
19441946
! -- dummy
19451947
class(MawType) :: this
@@ -1950,6 +1952,7 @@ subroutine maw_ad(this)
19501952
integer(I4B) :: ibnd
19511953
integer(I4B) :: imaw
19521954
character(len=LINELENGTH) :: str
1955+
character(len=LENVARNAME) :: setting
19531956
!
19541957
! -- sync PACKAGEDATA AUX from input context and advance observations
19551958
call this%BndExtType%bnd_ad()
@@ -1959,19 +1962,19 @@ subroutine maw_ad(this)
19591962
do n = 1, this%input%nbound
19601963
imaw = this%input%ifno(n)
19611964
if (imaw < 1 .or. imaw > this%nmawwells) cycle
1962-
str = this%input%setting(n)
1965+
setting = this%input%setting(n)
19631966
! RATE
1964-
if (trim(str) == 'RATE') then
1967+
if (trim(setting) == 'RATE') then
19651968
this%rate(imaw) = this%input%rate(n)
19661969
end if
19671970
! WELL_HEAD
1968-
if (trim(str) == 'WELL_HEAD') then
1971+
if (trim(setting) == 'WELL_HEAD') then
19691972
this%well_head(imaw) = this%input%well_head(n)
19701973
this%xnewpak(imaw) = this%well_head(imaw)
19711974
end if
19721975
! AUXILIARY (PERIOD override)
19731976
if (this%naux > 0) then
1974-
if (trim(str) == 'PERIOD_AUXILIARY') then
1977+
if (trim(setting) == 'PERIOD_AUXILIARY') then
19751978
str = this%input%auxname(n)
19761979
do jj = 1, this%naux
19771980
if (trim(str) /= trim(this%auxname(jj))) cycle

src/Utilities/Idm/mf6blockfile/Mf6FileKeystring.f90

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,14 @@ subroutine df(this)
145145
integer(I4B) :: n
146146
! init tsmanager (TDIS now available)
147147
call this%tsmanager%tsmanager_df()
148-
! link static TS strlocs; preserve for re-registration after reset()
148+
! register static TS links; mark as static so they survive reset
149149
do n = 1, this%static_loader%ts_sa_count()
150150
sa => this%static_loader%get_ts_sa(n)
151151
if (associated(sa)) then
152152
call sa%ts_update(this%tsmanager, &
153153
this%mf6_input%subcomponent_name, &
154154
this%ctx%iprpak, this%input_name, &
155-
clear_strlocs=.false.)
155+
is_static=.true.)
156156
end if
157157
end do
158158
end subroutine df
@@ -188,24 +188,9 @@ subroutine rp(this, parser)
188188
end subroutine rp
189189

190190
subroutine reset(this)
191-
use StructArrayModule, only: StructArrayType
192191
class(KeystringLoadType), intent(inout) :: this
193-
type(StructArrayType), pointer :: sa
194-
integer(I4B) :: n
195-
! clear TS links
192+
! clear period TS links; static links (isStatic=.true.) survive
196193
call this%tsmanager%reset(this%mf6_input%subcomponent_name)
197-
! re-register static TS links (strlocs preserved in df)
198-
if (this%ts_active) then
199-
do n = 1, this%static_loader%ts_sa_count()
200-
sa => this%static_loader%get_ts_sa(n)
201-
if (associated(sa)) then
202-
call sa%ts_update(this%tsmanager, &
203-
this%mf6_input%subcomponent_name, &
204-
this%ctx%iprpak, this%input_name, &
205-
clear_strlocs=.false.)
206-
end if
207-
end do
208-
end if
209194
end subroutine reset
210195

211196
subroutine destroy(this)

0 commit comments

Comments
 (0)