This page was generated from ex-prt-mp7-p04.py. It's also available as a notebook.

Backward Particle Tracking, Refined Grid, Lateral Flow Boundaries

Application of a MODFLOW 6 particle-tracking (PRT) model to solve example 4 from the MODPATH 7 documentation.

This notebook demonstrates a steady-state MODFLOW 6 simulation using a quadpatch DISV grid with an irregular domain and a large number of inactive cells. Particles are tracked backwards from terminating locations, including a pair of wells in a locally- refined region of the grid and constant-head cells along the grid’s right side, to release locations along the left border of the grid’s active region. Injection wells along the left-hand border are used to generate boundary flows.

Initial setup

Import dependencies, define the example name and workspace, and read settings from environment variables.

[1]:
import pathlib as pl
from pprint import pformat

import flopy
import flopy.utils.binaryfile as bf
import git
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from flopy.mf6 import MFSimulation
from flopy.plot.styles import styles
from flopy.utils.gridgen import Gridgen
from matplotlib.collections import LineCollection
from modflow_devtools.misc import get_env, timed

# Example name and workspace paths. If this example is running
# in the git repository, use the folder structure described in
# the README. Otherwise just use the current working directory.
sim_name = "ex-prt-mp7-p04"
# shorten model names so they fit in 16-char limit
gwf_name = sim_name.replace("ex-prt-", "") + "-gwf"
prt_name = sim_name.replace("ex-prt-", "") + "-prt"
mp7_name = sim_name.replace("ex-prt-", "") + "-mp7"
try:
    root = pl.Path(git.Repo(".", search_parent_directories=True).working_dir)
except:
    root = None
workspace = root / "examples" if root else pl.Path.cwd()
figs_path = root / "figures" if root else pl.Path.cwd()
sim_ws = workspace / sim_name
gwf_ws = sim_ws / "gwf"
prt_ws = sim_ws / "prt"
mp7_ws = sim_ws / "mp7"
gwf_ws.mkdir(exist_ok=True, parents=True)
prt_ws.mkdir(exist_ok=True, parents=True)
mp7_ws.mkdir(exist_ok=True, parents=True)

# Define output file names
headfile = f"{gwf_name}.hds"
headfile_bkwd = f"{gwf_name}_bkwd.hds"
budgetfile = f"{gwf_name}.cbb"
budgetfile_bkwd = f"{gwf_name}_bkwd.bud"
trackfile_prt = f"{prt_name}.trk"
trackcsvfile_prt = f"{prt_name}.trk.csv"
budgetfile_prt = f"{prt_name}.cbb"
pathlinefile_mp7 = f"{mp7_name}.mppth"

# Settings from environment variables
write = get_env("WRITE", True)
run = get_env("RUN", True)
plot = get_env("PLOT", True)
plot_show = get_env("PLOT_SHOW", True)
plot_save = get_env("PLOT_SAVE", True)

Define parameters

Define model units, parameters and other settings.

[2]:
# Model units
length_units = "feet"
time_units = "days"
[3]:
# Model parameters
nper = 1  # Number of periods
nlay = 1  # Numer of layers
nrow = 21  # Number of rows
ncol = 26  # Number of columns
delr = 500.0  # Column width ($ft$)
delc = 500.0  # Row width ($ft$)
top = 100.0  # Top of the model ($ft$)
botm = 0.0  # Layer bottom elevations ($ft$)
kh = 50.0  # Horizontal hydraulic conductivity ($ft/d$)
porosity = 0.1  # Porosity (unitless)
[4]:
# Time discretization
tdis_rc = [(10000, 1, 1.0)]
[5]:
# Bottom elevations
botm = np.zeros((nlay, nrow, ncol), dtype=np.float32)

GRIDGEN can be used to create a quadpatch grid with a refined region in the upper left quadrant.

We will create a grid with 3 refinement levels, all nearly but not perfectly rectangular: a 500x500 area is carved out of the corners of the rectangle for each level. To form each refinement region’s polygon we combine 5 rectangles.

First create the top-level grid discretization.

[6]:
ms = flopy.modflow.Modflow()
dis = flopy.modflow.ModflowDis(
    ms,
    nlay=nlay,
    nrow=nrow,
    ncol=ncol,
    delr=delr,
    delc=delc,
    top=top,
    botm=botm,
)

# create Gridgen workspace
gridgen_ws = sim_ws / "gridgen"
gridgen_ws.mkdir(parents=True, exist_ok=True)

# create Gridgen object
g = Gridgen(ms.modelgrid, model_ws=gridgen_ws, exe_name="gridgen")

# add polygon for each refinement level
outer_polygon = [
    [
        (2500, 6000),
        (2500, 9500),
        (3000, 9500),
        (3000, 10000),
        (6000, 10000),
        (6000, 9500),
        (6500, 9500),
        (6500, 6000),
        (6000, 6000),
        (6000, 5500),
        (3000, 5500),
        (3000, 6000),
        (2500, 6000),
    ]
]
g.add_refinement_features([outer_polygon], "polygon", 1, range(nlay))
refshp0 = gridgen_ws / "rf0"

middle_polygon = [
    [
        (3000, 6500),
        (3000, 9000),
        (3500, 9000),
        (3500, 9500),
        (5500, 9500),
        (5500, 9000),
        (6000, 9000),
        (6000, 6500),
        (5500, 6500),
        (5500, 6000),
        (3500, 6000),
        (3500, 6500),
        (3000, 6500),
    ]
]
g.add_refinement_features([middle_polygon], "polygon", 2, range(nlay))
refshp1 = gridgen_ws / "rf1"

inner_polygon = [
    [
        (3500, 7000),
        (3500, 8500),
        (4000, 8500),
        (4000, 9000),
        (5000, 9000),
        (5000, 8500),
        (5500, 8500),
        (5500, 7000),
        (5000, 7000),
        (5000, 6500),
        (4000, 6500),
        (4000, 7000),
        (3500, 7000),
    ]
]
g.add_refinement_features([inner_polygon], "polygon", 3, range(nlay))
refshp2 = gridgen_ws / "rf2"

Build the grid and plot it with refinement levels superimposed.

[7]:
g.build(verbose=False)
grid = flopy.discretization.VertexGrid(**g.get_gridprops_vertexgrid())

fig = plt.figure(figsize=(15, 15))
ax = fig.add_subplot(1, 1, 1, aspect="equal")
mm = flopy.plot.PlotMapView(model=ms)
grid.plot(ax=ax)

flopy.plot.plot_shapefile(refshp0, ax=ax, facecolor="green", alpha=0.3)
flopy.plot.plot_shapefile(refshp1, ax=ax, facecolor="green", alpha=0.5)
flopy.plot.plot_shapefile(str(refshp2), ax=ax, facecolor="green", alpha=0.7)
[7]:
<matplotlib.collections.PatchCollection at 0x7f44138083d0>
../_images/_notebooks_ex-prt-mp7-p04_10_1.png

We are now ready to set up groundwater flow and particle tracking models.

Define shared variables, including discretization parameters, idomain, and porosity.

[8]:
# fmt: off
idomain = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
    0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
    0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0
]
# fmt: on
disv_props = g.get_gridprops_disv()

# from pprint import pprint
# pprint(idomain, compact=True)

Define well locations and flows.

[9]:
wells = [
    # negative q: discharge
    (0, 861, -30000.0, 0, -1),
    (0, 891, -30000.0, 0, -1),
    # positive q: injection
    (0, 1959, 10000.0, 1, 4),
    (0, 1932, 10000.0, 3, 3),
    (0, 1931, 10000.0, 3, 3),
    (0, 1930, 5000.0, 1, 4),
    (0, 1930, 5000.0, 3, 3),
    (0, 1903, 5000.0, 1, 4),
    (0, 1903, 5000.0, 3, 3),
    (0, 1876, 10000.0, 3, 3),
    (0, 1875, 10000.0, 3, 3),
    (0, 1874, 5000.0, 1, 4),
    (0, 1874, 5000.0, 3, 3),
    (0, 1847, 10000.0, 3, 3),
    (0, 1846, 10000.0, 3, 3),
    (0, 1845, 5000.0, 1, 4),
    (0, 1845, 5000.0, 3, 3),
    (0, 1818, 5000.0, 1, 4),
    (0, 1818, 5000.0, 3, 3),
    (0, 1792, 10000.0, 1, 4),
    (0, 1766, 10000.0, 1, 4),
    (0, 1740, 5000.0, 1, 4),
    (0, 1740, 5000.0, 4, 1),
    (0, 1715, 5000.0, 1, 4),
    (0, 1715, 5000.0, 4, 1),
    (0, 1690, 10000.0, 1, 4),
    (0, 1646, 5000.0, 1, 4),
    (0, 1646, 5000.0, 4, 1),
    (0, 1549, 5000.0, 1, 4),
    (0, 1549, 5000.0, 4, 1),
    (0, 1332, 5000.0, 4, 1),
    (0, 1332, 5000.0, 1, 5),
    (0, 1021, 2500.0, 1, 4),
    (0, 1021, 2500.0, 4, 1),
    (0, 1020, 5000.0, 1, 5),
    (0, 708, 2500.0, 1, 5),
    (0, 708, 2500.0, 4, 1),
    (0, 711, 625.0, 1, 4),
    (0, 711, 625.0, 4, 1),
    (0, 710, 625.0, 1, 4),
    (0, 710, 625.0, 4, 1),
    (0, 409, 1250.0, 1, 4),
    (0, 407, 625.0, 1, 4),
    (0, 407, 625.0, 4, 1),
    (0, 402, 625.0, 1, 5),
    (0, 402, 625.0, 4, 1),
    (0, 413, 1250.0, 1, 4),
    (0, 411, 1250.0, 1, 4),
    (0, 203, 1250.0, 1, 5),
    (0, 203, 1250.0, 4, 1),
    (0, 202, 1250.0, 1, 4),
    (0, 202, 1250.0, 4, 1),
    (0, 199, 2500.0, 1, 4),
    (0, 197, 1250.0, 1, 4),
    (0, 197, 1250.0, 4, 1),
    (0, 96, 2500.0, 1, 4),
    (0, 96, 2500.0, 4, 1),
    (0, 98, 1250.0, 1, 4),
    (0, 101, 1250.0, 1, 4),
    (0, 101, 1250.0, 4, 1),
    (0, 100, 1250.0, 1, 4),
    (0, 100, 1250.0, 4, 1),
    (0, 43, 2500.0, 1, 5),
    (0, 43, 2500.0, 4, 1),
    (0, 44, 2500.0, 1, 4),
    (0, 44, 2500.0, 4, 1),
    (0, 45, 5000.0, 4, 1),
    (0, 10, 10000.0, 1, 5),
]

assert len(wells) == 68

# Define particle release locations, initially in the
# representation for MODPATH 7 particle input style 1.
particles = [
    # MODPATH 7 input style 1
    # (node number, localx, localy, localz)
    (1327, 0.000, 0.125, 0.500),
    (1327, 0.000, 0.375, 0.500),
    (1327, 0.000, 0.625, 0.500),
    (1327, 0.000, 0.875, 0.500),
    (1545, 0.000, 0.125, 0.500),
    (1545, 0.000, 0.375, 0.500),
    (1545, 0.000, 0.625, 0.500),
    (1545, 0.000, 0.875, 0.500),
    (1643, 0.000, 0.125, 0.500),
    (1643, 0.000, 0.375, 0.500),
    (1643, 0.000, 0.625, 0.500),
    (1643, 0.000, 0.875, 0.500),
    (1687, 0.000, 0.125, 0.500),
    (1687, 0.000, 0.375, 0.500),
    (1687, 0.000, 0.625, 0.500),
    (1687, 0.000, 0.875, 0.500),
    (1713, 0.000, 0.125, 0.500),
    (1713, 0.000, 0.375, 0.500),
    (1713, 0.000, 0.625, 0.500),
    (1713, 0.000, 0.875, 0.500),
    (861, 0.000, 0.125, 0.500),
    (861, 0.000, 0.375, 0.500),
    (861, 0.000, 0.625, 0.500),
    (861, 0.000, 0.875, 0.500),
    (861, 1.000, 0.125, 0.500),
    (861, 1.000, 0.375, 0.500),
    (861, 1.000, 0.625, 0.500),
    (861, 1.000, 0.875, 0.500),
    (861, 0.125, 0.000, 0.500),
    (861, 0.375, 0.000, 0.500),
    (861, 0.625, 0.000, 0.500),
    (861, 0.875, 0.000, 0.500),
    (861, 0.125, 1.000, 0.500),
    (861, 0.375, 1.000, 0.500),
    (861, 0.625, 1.000, 0.500),
    (861, 0.875, 1.000, 0.500),
    (891, 0.000, 0.125, 0.500),
    (891, 0.000, 0.375, 0.500),
    (891, 0.000, 0.625, 0.500),
    (891, 0.000, 0.875, 0.500),
    (891, 1.000, 0.125, 0.500),
    (891, 1.000, 0.375, 0.500),
    (891, 1.000, 0.625, 0.500),
    (891, 1.000, 0.875, 0.500),
    (891, 0.125, 0.000, 0.500),
    (891, 0.375, 0.000, 0.500),
    (891, 0.625, 0.000, 0.500),
    (891, 0.875, 0.000, 0.500),
    (891, 0.125, 1.000, 0.500),
    (891, 0.375, 1.000, 0.500),
    (891, 0.625, 1.000, 0.500),
    (891, 0.875, 1.000, 0.500),
]

# For vertex grids, the MODFLOW 6 PRT model's PRP (particle release point) package expects initial
# particle locations as tuples `(particle ID, (layer, cell ID), x coord, y coord, z coord)`. While
# MODPATH 7 input style 1 expects local coordinates, PRT expects global coordinates (the user must
# guarantee that each release point's coordinates fall within the cell with the given ID). FloPy's
# `ParticleData` class provides a `to_coords()` utility method to convert local coordinates to
# global coordinates for easier migration from MODPATH 7 to PRT.
partdata = flopy.modpath.ParticleData(
    partlocs=[p[0] for p in particles],
    structured=False,
    localx=[p[1] for p in particles],
    localy=[p[2] for p in particles],
    localz=[p[3] for p in particles],
    timeoffset=0,
    drape=0,
)
# coords = partdata.to_coords(grid)
# particles_prt = [
#     (i, (0, p[0]), c[0], c[1], c[2]) for i, (p, c) in enumerate(zip(particles, coords))
# ]
particles_prt = list(partdata.to_prp(grid))

Model setup

Define functions to build models, write input files, and run the simulation.

[10]:


def build_gwf(): print("Building GWF model") # simulation sim = flopy.mf6.MFSimulation( sim_name=gwf_name, sim_ws=gwf_ws, exe_name="mf6", version="mf6" ) # temporal discretization tdis = flopy.mf6.ModflowTdis( sim, time_units=time_units, nper=len(tdis_rc), perioddata=tdis_rc ) # iterative model solver ims = flopy.mf6.ModflowIms( sim, pname="ims", complexity="SIMPLE", outer_dvclose=1e-4, outer_maximum=100, inner_dvclose=1e-5, under_relaxation_theta=0, under_relaxation_kappa=0, under_relaxation_gamma=0, under_relaxation_momentum=0, linear_acceleration="BICGSTAB", relaxation_factor=0.99, number_orthogonalizations=2, ) # groundwater flow model gwf = flopy.mf6.ModflowGwf( sim, modelname=gwf_name, model_nam_file=f"{sim_name}.nam", save_flows=True ) # grid discretization disv = flopy.mf6.ModflowGwfdisv( gwf, length_units=length_units, idomain=idomain, **disv_props ) # initial conditions ic = flopy.mf6.ModflowGwfic(gwf, strt=150.0) flopy.mf6.modflow.mfgwfwel.ModflowGwfwel( gwf, maxbound=len(wells), auxiliary=["IFACE", "IFLOWFACE"], save_flows=True, stress_period_data={0: wells}, ) # node property flow npf = flopy.mf6.ModflowGwfnpf( gwf, xt3doptions=True, save_flows=True, save_specific_discharge=True, save_saturation=True, icelltype=[0], k=[kh], ) # constant head boundary (period, node number, head) chd_bound = [ (0, 1327, 150.0), (0, 1545, 150.0), (0, 1643, 150.0), (0, 1687, 150.0), (0, 1713, 150.0), ] chd = flopy.mf6.ModflowGwfchd( gwf, pname="chd", save_flows=True, stress_period_data=chd_bound, # auxiliary=["IFLOWFACE"] ) # output control budget_file = f"{gwf_name}.cbb" head_file = f"{gwf_name}.hds" oc = flopy.mf6.ModflowGwfoc( gwf, pname="oc", budget_filerecord=[budget_file], head_filerecord=[head_file], saverecord=[("HEAD", "ALL"), ("BUDGET", "ALL")], ) return sim def build_prt(): print("Building PRT model") simprt = flopy.mf6.MFSimulation( sim_name=prt_name, version="mf6", exe_name="mf6", sim_ws=prt_ws ) flopy.mf6.ModflowTdis( simprt, pname="tdis", time_units="DAYS", nper=len(tdis_rc), perioddata=tdis_rc ) # Instantiate the MODFLOW 6 prt model prt = flopy.mf6.ModflowPrt( simprt, modelname=prt_name, model_nam_file=f"{prt_name}.nam" ) # Instantiate the MODFLOW 6 DISV vertex grid discretization disv = flopy.mf6.ModflowGwfdisv(prt, idomain=idomain, **disv_props) # Instantiate the MODFLOW 6 prt model input package flopy.mf6.ModflowPrtmip(prt, pname="mip", porosity=porosity) # Instantiate the MODFLOW 6 prt particle release point (prp) package flopy.mf6.ModflowPrtprp( prt, pname="prp", filename=f"{prt_name}_4.prp", nreleasepts=len(particles_prt), packagedata=particles_prt, perioddata={0: ["FIRST"]}, exit_solve_tolerance=1e-5, extend_tracking=True, ) # Instantiate the MODFLOW 6 prt output control package budgetfile_prt = f"{prt_name}.cbb" budget_record = [budgetfile_prt] trackfile_prt = f"{prt_name}.trk" trackcsvfile_prt = f"{prt_name}.trk.csv" flopy.mf6.ModflowPrtoc( prt, pname="oc", budget_filerecord=budget_record, track_filerecord=trackfile_prt, trackcsv_filerecord=trackcsvfile_prt, saverecord=[("BUDGET", "ALL")], ) # Instantiate the MODFLOW 6 prt flow model interface flopy.mf6.ModflowPrtfmi( prt, packagedata=[ ("GWFHEAD", pl.Path(f"../{gwf_ws.name}/{headfile_bkwd}")), ("GWFBUDGET", pl.Path(f"../{gwf_ws.name}/{budgetfile_bkwd}")), ], ) # Create an explicit model solution (EMS) for the MODFLOW 6 prt model ems = flopy.mf6.ModflowEms( simprt, pname="ems", filename=f"{prt_name}.ems", ) simprt.register_solution_package(ems, [prt.name]) return simprt def build_mp7(gwf): print("Building MP7 model") pg = flopy.modpath.ParticleGroup( particlegroupname="G1", particledata=partdata, filename=f"{sim_name}.sloc" ) mp = flopy.modpath.Modpath7( modelname=mp7_name, flowmodel=gwf, exe_name="mp7", model_ws=mp7_ws, ) mpbas = flopy.modpath.Modpath7Bas(mp, porosity=porosity) mpsim = flopy.modpath.Modpath7Sim( mp, simulationtype="pathline", trackingdirection="backward", budgetoutputoption="summary", particlegroups=[pg], ) return mp def build_models(): gwfsim = build_gwf() gwf = gwfsim.get_model(gwf_name) prtsim = build_prt() mp7sim = build_mp7(gwf) return gwfsim, prtsim, mp7sim def reverse_budgetfile(fpth, rev_fpth, tdis): f = bf.CellBudgetFile(fpth, tdis=tdis) f.reverse(rev_fpth) def reverse_headfile(fpth, rev_fpth, tdis): f = bf.HeadFile(fpth, tdis=tdis) f.reverse(rev_fpth) def write_models(*sims, silent=False): for sim in sims: if isinstance(sim, MFSimulation): sim.write_simulation(silent=silent) else: sim.write_input() @timed def run_models(*sims, silent=False): for sim in sims: if isinstance(sim, MFSimulation): success, buff = sim.run_simulation(silent=silent, report=True) else: success, buff = sim.run_model(silent=silent, report=True) assert success, pformat(buff) if "gwf" in sim.name: # Reverse budget and head files for backward tracking reverse_budgetfile(gwf_ws / budgetfile, gwf_ws / budgetfile_bkwd, sim.tdis) reverse_headfile(gwf_ws / headfile, gwf_ws / headfile_bkwd, sim.tdis)

Plotting results

Define functions to plot the grid, boundary conditions, and model results.

Below we highlight the vertices of well-containing cells which lie on the boundary between coarser- and finer-grained refinement regions (there are 7 of these). Note the disagreement between values of MODFLOW 6 PRT’s IFLOWFACE and MODPATH 7’s IFACE for these cells. This is because the cells have 5 polygonal faces.

[11]:


def sort_square_verts(verts): """Sort 4 or more points on a square in clockwise order, starting with the top-left point""" # sort by y coordinate verts.sort(key=lambda v: v[1], reverse=True) # separate top and bottom rows y0 = verts[0][1] t = [v for v in verts if v[1] == y0] b = verts[len(t) :] # sort top and bottom rows by x coordinate t.sort(key=lambda v: v[0]) b.sort(key=lambda v: v[0]) # return vertices in clockwise order return t + list(reversed(b)) def plot_well_cell_ids(ax): xc, yc = grid.get_xcellcenters_for_layer(0), grid.get_ycellcenters_for_layer(0) for well in wells: nn = well[1] x, y = xc[nn], yc[nn] ax.annotate(str(nn), (x - 50, y - 50), color="purple") cells_on_refinement_boundary = [10, 43, 203, 402, 708, 1020, 1332] def plot_well_vertices_on_refinement_boundary(ax): for nn in cells_on_refinement_boundary: verts = list(set([tuple(grid.verts[v]) for v in grid.iverts[nn]])) verts = [ v for v in verts if len([vv for vv in verts if vv[0] == v[0]]) > 2 or len([vv for vv in verts if vv[1] == v[1]]) > 2 ] for v in verts: ax.plot(v[0], v[1], "go", ms=3) def plot_well_ifaces(ax): ifaces = [] for well in wells: nn = well[1] iverts = grid.iverts[nn] # sort vertices of well cell in clockwise order verts = [tuple(grid.verts[v]) for v in iverts] sorted_verts = sort_square_verts(list(set(verts.copy()))) # reduce vertices to 4 corners of square xmax, xmin = ( max([v[0] for v in sorted_verts]), min([v[0] for v in sorted_verts]), ) ymax, ymin = ( max([v[1] for v in sorted_verts]), min([v[1] for v in sorted_verts]), ) sorted_verts = [ v for v in sorted_verts if v[0] in [xmax, xmin] and v[1] in [ymax, ymin] ] # define the iface line segment iface = well[3] if iface == 1: p0 = sorted_verts[0] p1 = sorted_verts[-1] elif iface == 2: p0 = sorted_verts[1] p1 = sorted_verts[2] elif iface == 3: p0 = sorted_verts[2] p1 = sorted_verts[3] elif iface == 4: p0 = sorted_verts[0] p1 = sorted_verts[1] else: continue ifaces.append([p0, p1]) lc = LineCollection(ifaces, color="red", lw=4) ax.add_collection(lc) def plot_map_view(ax, gwf): # plot map view of grid mv = flopy.plot.PlotMapView(model=gwf, ax=ax) mv.plot_grid(alpha=0.3) mv.plot_ibound() # inactive cells mv.plot_bc("WEL", alpha=0.3) # wells (red) ax.add_patch( # constant head boundary (blue) mpl.patches.Rectangle( ((ncol - 1) * delc, (nrow - 6) * delr), 1000, -2500, linewidth=5, facecolor="blue", alpha=0.5, ) ) def plot_inset(ax, gwf): # create inset axins = ax.inset_axes([-0.75, 0.35, 0.6, 0.8]) # plot grid features plot_map_view(axins, gwf) plot_well_ifaces(axins) plot_well_vertices_on_refinement_boundary(axins) # zoom in on refined region of injection well boundary axins.set_xlim(2000, 6000) axins.set_ylim(6000, 10500) # add legend legend_elements = [ mpl.lines.Line2D( [0], [0], color="red", lw=4, label="Well cell faces assigned flow (IFLOWFACE)", ), mpl.lines.Line2D( [0], [0], marker="o", color="grey", label="Well cell vertices on refinement border", markerfacecolor="g", markersize=15, ), ] axins.legend(handles=legend_elements, bbox_to_anchor=(0.33, -0.2, 0.8, 0.1)) # add the inset ax.indicate_inset_zoom(axins) def plot_grid(gwf, title=None): with styles.USGSPlot(): # setup the plot fig = plt.figure(figsize=(7, 7)) ax = fig.add_subplot(1, 1, 1, aspect="equal") if title is not None: styles.heading(ax=ax, heading=title) # add plot features plot_map_view(ax, gwf) plot_well_ifaces(ax) plot_inset(ax, gwf) # add legend ax.legend( handles=[ mpl.patches.Patch(color="red", label="Wells", alpha=0.3), mpl.patches.Patch(color="blue", label="Constant-head boundary"), ], bbox_to_anchor=(-1.1, 0, 0.8, 0.1), ) plt.subplots_adjust(left=0.43) if plot_show: plt.show() if plot_save: fig.savefig(figs_path / f"{sim_name}-grid")
[12]:
def plot_pathlines(ax, grid, hd, pl, title=None):
    ax.set_aspect("equal")
    if title is not None:
        ax.set_title(title)
    mm = flopy.plot.PlotMapView(modelgrid=grid, ax=ax)
    mm.plot_grid(lw=0.5, alpha=0.5)
    mm.plot_ibound()
    pc = mm.plot_array(hd, edgecolor="black", alpha=0.5)
    cb = plt.colorbar(pc, shrink=0.25, pad=0.1)
    cb.ax.set_xlabel(r"Head ($ft$)")
    mm.plot_pathline(pl, layer="all", lw=0.3, colors=["black"])


def plot_all_pathlines(grid, heads, prtpl, mp7pl=None, title=None):
    with styles.USGSPlot():
        fig, ax = plt.subplots(ncols=1 if mp7pl is None else 2, nrows=1, figsize=(7, 7))
        if title is not None:
            styles.heading(ax=ax, heading=title)

        plot_pathlines(
            ax if mp7pl is None else ax[0],
            grid,
            heads,
            prtpl,
            None if mp7pl is None else "MODFLOW 6 PRT",
        )
        if mp7pl is not None:
            plot_pathlines(ax[1], grid, heads, mp7pl, "MODPATH 7")

        if plot_show:
            plt.show()
        if plot_save:
            fig.savefig(figs_path / f"{sim_name}-paths")


def plot_all(gwf):
    # extract grid
    grid = gwf.modelgrid

    # load mf6 gwf head results
    hf = flopy.utils.HeadFile(gwf_ws / headfile)
    hds = hf.get_data()

    # load mf6 prt pathline results
    prt_pl = pd.read_csv(prt_ws / trackcsvfile_prt)

    # load mp7 pathline results
    plf = flopy.utils.PathlineFile(mp7_ws / pathlinefile_mp7)
    mp7_pl = pd.DataFrame(
        plf.get_destination_pathline_data(range(grid.nnodes), to_recarray=True)
    )

    plot_grid(gwf, title="Model grid and boundary conditions")
    plot_all_pathlines(grid, hds, prt_pl, title="Head and pathlines")

Running the example

Define a function to run the example scenarios and plot results.

[13]:


def scenario(silent=False): gwfsim, prtsim, mp7sim = build_models() if write: write_models(gwfsim, prtsim, mp7sim, silent=silent) if run: run_models(gwfsim, prtsim, mp7sim, silent=silent) if plot: gwf = gwfsim.get_model(gwf_name) plot_all(gwf) # Now run the scenario for example problem 4. scenario(silent=False)
Building GWF model
Building PRT model
Building MP7 model
writing simulation...
  writing simulation name file...
  writing simulation tdis package...
  writing solution package ims...
  writing model mp7-p04-gwf...
    writing model name file...
    writing package disv...
    writing package ic...
    writing package wel_0...
    writing package npf...
    writing package chd...
INFORMATION: maxbound in ('gwf6', 'chd', 'dimensions') changed to 5 based on size of stress_period_data
    writing package oc...
writing simulation...
  writing simulation name file...
  writing simulation tdis package...
  writing solution package ems...
  writing model mp7-p04-prt...
    writing model name file...
    writing package disv...
    writing package mip...
    writing package prp...
    writing package oc...
    writing package fmi...
FloPy is using the following executable to run the model: ../../../../../../../.local/bin/modflow/mf6
                                   MODFLOW 6
                U.S. GEOLOGICAL SURVEY MODULAR HYDROLOGIC MODEL
                            VERSION 6.6.1 02/07/2025
                               ***DEVELOP MODE***

        MODFLOW 6 compiled Feb  8 2025 12:33:09 with GCC version 13.3.0

This software has been approved for release by the U.S. Geological
Survey (USGS). Although the software has been subjected to rigorous
review, the USGS reserves the right to update the software as needed
pursuant to further analysis and review. No warranty, expressed or
implied, is made by the USGS or the U.S. Government as to the
functionality of the software and related material nor shall the
fact of release constitute any such warranty. Furthermore, the
software is released on condition that neither the USGS nor the U.S.
Government shall be held liable for any damages resulting from its
authorized or unauthorized use. Also refer to the USGS Water
Resources Software User Rights Notice for complete use, copyright,
and distribution information.


 MODFLOW runs in SEQUENTIAL mode

 Run start date and time (yyyy/mm/dd hh:mm:ss): 2025/02/08 12:50:05

 Writing simulation list file: mfsim.lst
 Using Simulation name file: mfsim.nam

    Solving:  Stress period:     1    Time step:     1

 Run end date and time (yyyy/mm/dd hh:mm:ss): 2025/02/08 12:50:05
 Elapsed run time:  0.047 Seconds

 Normal termination of simulation.
FloPy is using the following executable to run the model: ../../../../../../../.local/bin/modflow/mf6
                                   MODFLOW 6
                U.S. GEOLOGICAL SURVEY MODULAR HYDROLOGIC MODEL
                            VERSION 6.6.1 02/07/2025
                               ***DEVELOP MODE***

        MODFLOW 6 compiled Feb  8 2025 12:33:09 with GCC version 13.3.0

This software has been approved for release by the U.S. Geological
Survey (USGS). Although the software has been subjected to rigorous
review, the USGS reserves the right to update the software as needed
pursuant to further analysis and review. No warranty, expressed or
implied, is made by the USGS or the U.S. Government as to the
functionality of the software and related material nor shall the
fact of release constitute any such warranty. Furthermore, the
software is released on condition that neither the USGS nor the U.S.
Government shall be held liable for any damages resulting from its
authorized or unauthorized use. Also refer to the USGS Water
Resources Software User Rights Notice for complete use, copyright,
and distribution information.


 MODFLOW runs in SEQUENTIAL mode

 Run start date and time (yyyy/mm/dd hh:mm:ss): 2025/02/08 12:50:05

 Writing simulation list file: mfsim.lst
 Using Simulation name file: mfsim.nam

    Solving:  Stress period:     1    Time step:     1

 Run end date and time (yyyy/mm/dd hh:mm:ss): 2025/02/08 12:50:05
 Elapsed run time:  0.038 Seconds

 Normal termination of simulation.
FloPy is using the following executable to run the model: ../../../../../../../.local/bin/modflow/mp7

MODPATH Version 7.2.001
Program compiled Dec 31 2024 17:11:39 with IFORT compiler (ver. 20.21.7)


Run particle tracking simulation ...
Processing Time Step     1 Period     1.  Time =  1.00000E+04  Steady-state flow

Particle Summary:
         0 particles are pending release.
         0 particles remain active.
        52 particles terminated at boundary faces.
         0 particles terminated at weak sink cells.
         0 particles terminated at weak source cells.
         0 particles terminated at strong source/sink cells.
         0 particles terminated in cells with a specified zone number.
         0 particles were stranded in inactive or dry cells.
         0 particles were unreleased.
         0 particles have an unknown status.

Normal termination.
run_models took 153.45 ms
../_images/_notebooks_ex-prt-mp7-p04_21_3.png
../_images/_notebooks_ex-prt-mp7-p04_21_4.png