|
| 1 | +""" |
| 2 | +This reproduces a ternary method regression |
| 3 | +where the particle's local z coordinate was |
| 4 | +improperly set to the bottom of the cell in |
| 5 | +an attempt to clamp the z coordinate to the |
| 6 | +unit interval. |
| 7 | +
|
| 8 | +This is a DISV grid which reduces to a DIS |
| 9 | +grid; we run the PRT model twice, once with |
| 10 | +pollock's method and once with the ternary |
| 11 | +method, and check that the results are equal. |
| 12 | +""" |
| 13 | + |
| 14 | +from os import environ |
| 15 | + |
| 16 | +import flopy |
| 17 | +import matplotlib.cm as cm |
| 18 | +import matplotlib.pyplot as plt |
| 19 | +import numpy as np |
| 20 | +import pandas as pd |
| 21 | +import pytest |
| 22 | +from flopy.utils.binaryfile import HeadFile |
| 23 | +from flopy.utils.gridutil import get_disv_kwargs |
| 24 | +from framework import TestFramework |
| 25 | +from modflow_devtools.misc import is_in_ci |
| 26 | +from prt_test_utils import get_model_name, get_partdata |
| 27 | + |
| 28 | +simname = "prtdisvvert" |
| 29 | +cases = [simname] |
| 30 | + |
| 31 | + |
| 32 | +# model info |
| 33 | +nlay = 2 |
| 34 | +nrow = 10 |
| 35 | +ncol = 10 |
| 36 | +ncpl = nrow * ncol |
| 37 | +delr = 1.0 |
| 38 | +delc = 1.0 |
| 39 | +nper = 1 |
| 40 | +perlen = 10 |
| 41 | +nstp = 5 |
| 42 | +tsmult = 1.0 |
| 43 | +tdis_rc = [(perlen, nstp, tsmult)] |
| 44 | +top = 25.0 |
| 45 | +botm = [20.0, 15.0] |
| 46 | +strt = 20 |
| 47 | +nouter, ninner = 100, 300 |
| 48 | +hclose, rclose, relax = 1e-9, 1e-3, 0.97 |
| 49 | +porosity = 0.1 |
| 50 | +tracktimes = list(np.linspace(0, 19, 20)) |
| 51 | + |
| 52 | + |
| 53 | +# vertex grid properties |
| 54 | +disvkwargs = get_disv_kwargs(nlay, nrow, ncol, delr, delc, top, botm) |
| 55 | + |
| 56 | +# release points in mp7 format |
| 57 | +releasepts_mp7 = [ |
| 58 | + # node number, localx, localy, localz |
| 59 | + (i * 10, 0.5, 0.5, 0.5) |
| 60 | + for i in range(10) |
| 61 | +] |
| 62 | + |
| 63 | + |
| 64 | +def build_gwf_sim(name, ws, mf6): |
| 65 | + gwf_name = f"{name}_gwf" |
| 66 | + sim = flopy.mf6.MFSimulation( |
| 67 | + sim_name=gwf_name, version="mf6", exe_name=mf6, sim_ws=ws |
| 68 | + ) |
| 69 | + |
| 70 | + # create tdis package |
| 71 | + tdis = flopy.mf6.ModflowTdis(sim, time_units="DAYS", nper=nper, perioddata=tdis_rc) |
| 72 | + |
| 73 | + # create gwf model |
| 74 | + gwf = flopy.mf6.ModflowGwf( |
| 75 | + sim, modelname=gwf_name, newtonoptions="NEWTON", save_flows=True |
| 76 | + ) |
| 77 | + |
| 78 | + # create iterative model solution and register the gwf model with it |
| 79 | + ims = flopy.mf6.ModflowIms( |
| 80 | + sim, |
| 81 | + print_option="SUMMARY", |
| 82 | + complexity="MODERATE", |
| 83 | + outer_dvclose=hclose, |
| 84 | + outer_maximum=nouter, |
| 85 | + under_relaxation="DBD", |
| 86 | + inner_maximum=ninner, |
| 87 | + inner_dvclose=hclose, |
| 88 | + rcloserecord=rclose, |
| 89 | + linear_acceleration="BICGSTAB", |
| 90 | + scaling_method="NONE", |
| 91 | + reordering_method="NONE", |
| 92 | + relaxation_factor=relax, |
| 93 | + ) |
| 94 | + sim.register_ims_package(ims, [gwf.name]) |
| 95 | + |
| 96 | + disv = flopy.mf6.ModflowGwfdisv(gwf, **disvkwargs) |
| 97 | + |
| 98 | + # initial conditions |
| 99 | + ic = flopy.mf6.ModflowGwfic(gwf, strt=strt) |
| 100 | + |
| 101 | + # node property flow |
| 102 | + npf = flopy.mf6.ModflowGwfnpf( |
| 103 | + gwf, |
| 104 | + save_flows=True, |
| 105 | + save_specific_discharge=True, |
| 106 | + save_saturation=True, |
| 107 | + ) |
| 108 | + |
| 109 | + # constant head boundary |
| 110 | + spd = { |
| 111 | + 0: [[(0, 0), 1.0, 1.0], [(0, 99), 0.0, 0.0]], |
| 112 | + # 1: [[(0, 0, 0), 0.0, 0.0], [(0, 9, 9), 1.0, 2.0]], |
| 113 | + } |
| 114 | + chd = flopy.mf6.ModflowGwfchd( |
| 115 | + gwf, |
| 116 | + pname="CHD-1", |
| 117 | + stress_period_data=spd, |
| 118 | + auxiliary=["concentration"], |
| 119 | + ) |
| 120 | + |
| 121 | + # output control |
| 122 | + oc = flopy.mf6.ModflowGwfoc( |
| 123 | + gwf, |
| 124 | + budget_filerecord=f"{gwf_name}.cbc", |
| 125 | + head_filerecord=f"{gwf_name}.hds", |
| 126 | + headprintrecord=[("COLUMNS", 10, "WIDTH", 15, "DIGITS", 6, "GENERAL")], |
| 127 | + saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], |
| 128 | + printrecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], |
| 129 | + filename=f"{gwf_name}.oc", |
| 130 | + ) |
| 131 | + |
| 132 | + # Print human-readable heads |
| 133 | + obs_lst = [] |
| 134 | + for k in np.arange(0, 1, 1): |
| 135 | + for i in np.arange(40, 50, 1): |
| 136 | + obs_lst.append(["obs_" + str(i + 1), "head", (k, i)]) |
| 137 | + |
| 138 | + obs_dict = {f"{gwf_name}.obs.csv": obs_lst} |
| 139 | + obs = flopy.mf6.ModflowUtlobs(gwf, pname="head_obs", digits=20, continuous=obs_dict) |
| 140 | + |
| 141 | + return sim |
| 142 | + |
| 143 | + |
| 144 | +def build_prt_sim(name, gwf_ws, prt_ws, mf6): |
| 145 | + # create simulation |
| 146 | + sim = flopy.mf6.MFSimulation( |
| 147 | + sim_name=name, |
| 148 | + exe_name=mf6, |
| 149 | + version="mf6", |
| 150 | + sim_ws=prt_ws, |
| 151 | + ) |
| 152 | + |
| 153 | + # create tdis package |
| 154 | + tdis = flopy.mf6.ModflowTdis(sim, time_units="DAYS", nper=nper, perioddata=tdis_rc) |
| 155 | + |
| 156 | + # create prt model |
| 157 | + prt_name = f"{name}_prt" |
| 158 | + prt = flopy.mf6.ModflowPrt(sim, modelname=prt_name) |
| 159 | + |
| 160 | + # create prt discretization |
| 161 | + disv = flopy.mf6.ModflowGwfdisv(prt, **disvkwargs) |
| 162 | + |
| 163 | + # create mip package |
| 164 | + flopy.mf6.ModflowPrtmip(prt, pname="mip", porosity=porosity) |
| 165 | + |
| 166 | + # convert mp7 particledata to prt release points |
| 167 | + partdata = get_partdata(prt.modelgrid, releasepts_mp7) |
| 168 | + releasepts = list(partdata.to_prp(prt.modelgrid)) |
| 169 | + |
| 170 | + # create prp package |
| 171 | + for i in range(2): |
| 172 | + prp_track_file = f"{prt_name}{i}.prp.trk" |
| 173 | + prp_track_csv_file = f"{prt_name}{i}.prp.trk.csv" |
| 174 | + flopy.mf6.ModflowPrtprp( |
| 175 | + prt, |
| 176 | + pname=f"prp{i}", |
| 177 | + filename=f"{prt_name}{i}.prp", |
| 178 | + nreleasepts=len(releasepts), |
| 179 | + packagedata=releasepts, |
| 180 | + perioddata={0: ["FIRST"]}, |
| 181 | + track_filerecord=[prp_track_file], |
| 182 | + trackcsv_filerecord=[prp_track_csv_file], |
| 183 | + stop_at_weak_sink=False, |
| 184 | + boundnames=True, |
| 185 | + print_input=True, |
| 186 | + dev_forceternary=i == 1, |
| 187 | + exit_solve_tolerance=1e-10, |
| 188 | + extend_tracking=True, |
| 189 | + ) |
| 190 | + |
| 191 | + # create output control package |
| 192 | + prt_track_file = f"{prt_name}.trk" |
| 193 | + prt_track_csv_file = f"{prt_name}.trk.csv" |
| 194 | + flopy.mf6.ModflowPrtoc( |
| 195 | + prt, |
| 196 | + pname="oc", |
| 197 | + track_filerecord=[prt_track_file], |
| 198 | + trackcsv_filerecord=[prt_track_csv_file], |
| 199 | + ) |
| 200 | + |
| 201 | + # create the flow model interface |
| 202 | + gwf_name = f"{name}_gwf" |
| 203 | + gwf_budget_file = gwf_ws / f"{gwf_name}.cbc" |
| 204 | + gwf_head_file = gwf_ws / f"{gwf_name}.hds" |
| 205 | + flopy.mf6.ModflowPrtfmi( |
| 206 | + prt, |
| 207 | + packagedata=[ |
| 208 | + ("GWFHEAD", gwf_head_file), |
| 209 | + ("GWFBUDGET", gwf_budget_file), |
| 210 | + ], |
| 211 | + ) |
| 212 | + |
| 213 | + # add explicit model solution |
| 214 | + ems = flopy.mf6.ModflowEms( |
| 215 | + sim, |
| 216 | + pname="ems", |
| 217 | + filename=f"{prt_name}.ems", |
| 218 | + ) |
| 219 | + sim.register_solution_package(ems, [prt.name]) |
| 220 | + |
| 221 | + return sim |
| 222 | + |
| 223 | + |
| 224 | +def build_models(idx, test): |
| 225 | + gwf_sim = build_gwf_sim(test.name, test.workspace / "gwf", test.targets["mf6"]) |
| 226 | + prt_sim = build_prt_sim( |
| 227 | + test.name, test.workspace / "gwf", test.workspace / "prt", test.targets["mf6"] |
| 228 | + ) |
| 229 | + return gwf_sim, prt_sim |
| 230 | + |
| 231 | + |
| 232 | +def check_output(idx, test, snapshot): |
| 233 | + name = test.name |
| 234 | + gwf_ws = test.workspace / "gwf" |
| 235 | + prt_ws = test.workspace / "prt" |
| 236 | + gwf_name = get_model_name(name, "gwf") |
| 237 | + prt_name = get_model_name(name, "prt") |
| 238 | + gwf_sim = test.sims[0] |
| 239 | + gwf = gwf_sim.get_model(gwf_name) |
| 240 | + mg = gwf.modelgrid |
| 241 | + |
| 242 | + prt_track_file = f"{prt_name}.trk" |
| 243 | + prt_track_csv_file = f"{prt_name}.trk.csv" |
| 244 | + |
| 245 | + # load mf6 pathline results |
| 246 | + mf6_pls = pd.read_csv(prt_ws / prt_track_csv_file, na_filter=False) |
| 247 | + |
| 248 | + if is_in_ci() and "ifort" in environ.get("FC", ""): |
| 249 | + return |
| 250 | + |
| 251 | + assert snapshot == mf6_pls.drop("name", axis=1).round(2).to_records(index=False) |
| 252 | + |
| 253 | + |
| 254 | +def plot_output(idx, test): |
| 255 | + name = test.name |
| 256 | + gwf_ws = test.workspace / "gwf" |
| 257 | + prt_ws = test.workspace / "prt" |
| 258 | + gwf_name = get_model_name(name, "gwf") |
| 259 | + prt_name = get_model_name(name, "prt") |
| 260 | + gwf_sim = test.sims[0] |
| 261 | + gwf = gwf_sim.get_model(gwf_name) |
| 262 | + mg = gwf.modelgrid |
| 263 | + |
| 264 | + # check mf6 output files exist |
| 265 | + gwf_head_file = f"{gwf_name}.hds" |
| 266 | + prt_track_csv_file = f"{prt_name}.trk.csv" |
| 267 | + |
| 268 | + # extract head, budget, and specific discharge results from GWF model |
| 269 | + hds = HeadFile(gwf_ws / gwf_head_file).get_data() |
| 270 | + bud = gwf.output.budget() |
| 271 | + spdis = bud.get_data(text="DATA-SPDIS")[0] |
| 272 | + qx, qy, qz = flopy.utils.postprocessing.get_specific_discharge(spdis, gwf) |
| 273 | + |
| 274 | + # load mf6 pathline results |
| 275 | + mf6_pls = pd.read_csv(prt_ws / prt_track_csv_file, na_filter=False) |
| 276 | + |
| 277 | + # set up plot |
| 278 | + fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 10)) |
| 279 | + ax.set_aspect("equal") |
| 280 | + |
| 281 | + # plot mf6 pathlines in map view |
| 282 | + pmv = flopy.plot.PlotMapView(modelgrid=mg, ax=ax) |
| 283 | + pmv.plot_grid() |
| 284 | + pmv.plot_array(hds[0], alpha=0.1) |
| 285 | + pmv.plot_vector(qx, qy, normalize=True, color="white") |
| 286 | + mf6_plines = mf6_pls.groupby(["iprp", "irpt", "trelease"]) |
| 287 | + for ipl, ((iprp, irpt, trelease), pl) in enumerate(mf6_plines): |
| 288 | + pl.plot( |
| 289 | + title="MF6 pathlines", |
| 290 | + kind="line", |
| 291 | + x="x", |
| 292 | + y="y", |
| 293 | + ax=ax, |
| 294 | + legend=False, |
| 295 | + color=cm.plasma(ipl / len(mf6_plines)), |
| 296 | + ) |
| 297 | + |
| 298 | + # view/save plot |
| 299 | + plt.show() |
| 300 | + plt.savefig(prt_ws / f"{name}.png") |
| 301 | + |
| 302 | + import pyvista as pv |
| 303 | + |
| 304 | + pv.set_plot_theme("document") |
| 305 | + pv.global_theme.allow_empty_mesh = True |
| 306 | + |
| 307 | + from flopy.export.vtk import Vtk |
| 308 | + |
| 309 | + axes = pv.Axes(show_actor=False, actor_scale=2.0, line_width=5) |
| 310 | + vtk = Vtk(model=gwf, binary=False, smooth=False) |
| 311 | + vtk.add_model(gwf) |
| 312 | + gwf_mesh = vtk.to_pyvista() |
| 313 | + |
| 314 | + p = pv.Plotter( |
| 315 | + window_size=[700, 700], |
| 316 | + ) |
| 317 | + p.enable_anti_aliasing() |
| 318 | + p.add_mesh(gwf_mesh, opacity=0.1, style="wireframe") |
| 319 | + p.add_mesh( |
| 320 | + mf6_pls[mf6_pls.iprp == 1][["x", "y", "z"]].to_numpy(), |
| 321 | + color="red", |
| 322 | + label="pollock's method", |
| 323 | + point_size=15, |
| 324 | + ) |
| 325 | + p.add_mesh( |
| 326 | + mf6_pls[mf6_pls.iprp == 2][["x", "y", "z"]].to_numpy(), |
| 327 | + color="green", |
| 328 | + label="ternary method", |
| 329 | + point_size=15, |
| 330 | + ) |
| 331 | + p.show() |
| 332 | + |
| 333 | + |
| 334 | +@pytest.mark.parametrize("idx, name", enumerate(cases)) |
| 335 | +def test_mf6model(idx, name, function_tmpdir, targets, array_snapshot, plot): |
| 336 | + test = TestFramework( |
| 337 | + name=name, |
| 338 | + workspace=function_tmpdir, |
| 339 | + build=lambda t: build_models(idx, t), |
| 340 | + check=lambda t: check_output(idx, t, array_snapshot), |
| 341 | + plot=lambda t: plot_output(idx, t) if plot else None, |
| 342 | + targets=targets, |
| 343 | + compare=None, |
| 344 | + ) |
| 345 | + test.run() |
0 commit comments