This page was generated from ex-gwf-capture.py. It's also available as a notebook.

Capture Fraction Analysis

This problem is an example of a capture fraction analysis based on Leake and others (2010) using the model developed by Freyberg (1988) and the MODFLOW API. The MODFLOW API is used because the capture fraction for each cell can be calculated without regenerating the input files. The capture fraction perturbation flux is added to the second well (WEL) package model.

Initial setup

Initial setup

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

[1]:
import os
import pathlib as pl
import shutil
import sys

import flopy
import git
import matplotlib as mpl
import matplotlib.pyplot as plt
import modflowapi
import numpy as np
import pooch
from flopy.plot.styles import styles
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-gwf-capture"
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()
data_path = root / "data" / sim_name if root else pl.Path.cwd()

# 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 = "meters"
time_units = "seconds"

# Load the bottom, hydraulic conductivity, and idomain arrays
fname = "bottom.txt"
fpath = pooch.retrieve(
    url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/{fname}",
    fname=fname,
    path=data_path,
    known_hash="md5:201758a5b7febb0390b8b52e634be27f",
)
bottom = np.loadtxt(fpath)
fname = "hydraulic_conductivity.txt"
fpath = pooch.retrieve(
    url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/{fname}",
    fname=fname,
    path=data_path,
    known_hash="md5:6c78564ba92e850d7d51d6e957b8a3ff",
)
k11 = np.loadtxt(fpath)
fname = "idomain.txt"
fpath = pooch.retrieve(
    url=f"https://github.com/MODFLOW-USGS/modflow6-examples/raw/master/data/{sim_name}/{fname}",
    fname=fname,
    path=data_path,
    known_hash="md5:435d4490adff7a35d1d4928661e45d81",
)
idomain = np.loadtxt(fpath, dtype=np.int32)

# Model parameters
nper = 1  # Number of periods
nlay = 1  # Number of layers
nrow = 40  # Number of rows
ncol = 20  # Number of columns
delr = 250.0  # Column width ($m$)
delc = 250.0  # Row width ($m$)
top = 35.0  # Top of the model ($m$)
icelltype = 1  # Cell conversion type
strt = 45.0  # Starting head ($m$)
recharge = 1.60000000e-09  # Recharge rate ($m/s$)
cf_q = -1e-3  # Perturbation flux ($m/s$)

# Temporal discretization
tdis_ds = ((1.0, 1.0, 1),)

# Well boundary conditions
wel_spd = {
    0: [
        [0, 8, 15, -0.00820000],
        [0, 10, 12, -0.00410000],
        [0, 19, 13, -0.00390000],
        [0, 25, 9, -8.30000000e-04],
        [0, 28, 5, -7.20000000e-04],
        [0, 33, 11, -0.00430000],
    ]
}

# Constant head boundary conditions
chd_spd = {
    0: [
        [0, 39, 5, 16.90000000],
        [0, 39, 6, 16.40000000],
        [0, 39, 7, 16.10000000],
        [0, 39, 8, 15.60000000],
        [0, 39, 9, 15.10000000],
        [0, 39, 10, 14.00000000],
        [0, 39, 11, 13.00000000],
        [0, 39, 12, 12.50000000],
        [0, 39, 13, 12.00000000],
        [0, 39, 14, 11.40000000],
    ]
}

# River boundary conditions
rbot = np.linspace(20.0, 10.25, num=nrow)
rstage = np.linspace(20.1, 11.25, num=nrow)
riv_spd = []
for idx, (s, b) in enumerate(zip(rstage, rbot)):
    riv_spd.append([0, idx, 14, s, 0.05, b])
riv_spd = {0: riv_spd}

# Solver parameters
nouter = 100
ninner = 25
hclose = 1e-9
rclose = 1e-3

# Create mapping array for the capture zone analysis
imap = idomain.copy()
for _k, i, j, _h in chd_spd[0]:
    imap[i, j] = 0

Model setup

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

[3]:
def build_models():
    sim_ws = os.path.join(workspace, sim_name)
    sim = flopy.mf6.MFSimulation(
        sim_name=sim_name,
        sim_ws=sim_ws,
        exe_name="mf6",
    )
    flopy.mf6.ModflowTdis(sim, nper=nper, perioddata=tdis_ds, time_units=time_units)
    flopy.mf6.ModflowIms(
        sim,
        linear_acceleration="BICGSTAB",
        outer_maximum=nouter,
        outer_dvclose=hclose * 10.0,
        inner_maximum=ninner,
        inner_dvclose=hclose,
        rcloserecord=f"{rclose} strict",
    )
    gwf = flopy.mf6.ModflowGwf(
        sim,
        modelname=sim_name,
        newtonoptions="NEWTON UNDER_RELAXATION",
    )
    flopy.mf6.ModflowGwfdis(
        gwf,
        length_units=length_units,
        nlay=nlay,
        nrow=nrow,
        ncol=ncol,
        delr=delr,
        delc=delc,
        top=top,
        botm=bottom,
        idomain=idomain,
    )

    flopy.mf6.ModflowGwfnpf(
        gwf,
        icelltype=icelltype,
        k=k11,
    )
    flopy.mf6.ModflowGwfic(gwf, strt=strt)
    flopy.mf6.ModflowGwfriv(gwf, stress_period_data=riv_spd, pname="RIV-1")
    flopy.mf6.ModflowGwfwel(gwf, stress_period_data=wel_spd, pname="WEL-1")
    flopy.mf6.ModflowGwfrcha(gwf, recharge=recharge)
    flopy.mf6.ModflowGwfchd(gwf, stress_period_data=chd_spd)
    flopy.mf6.ModflowGwfwel(
        gwf,
        maxbound=1,
        pname="CF-1",
        filename=f"{sim_name}.cf.wel",
    )
    flopy.mf6.ModflowGwfoc(
        gwf,
        printrecord=[
            ("BUDGET", "ALL"),
        ],
    )
    return sim


def write_models(sim, silent=True):
    sim.write_simulation(silent=silent)


def capture_fraction_iteration(mobj, q, inode=None):
    mobj.initialize()
    # time loop
    current_time = mobj.get_current_time()
    end_time = mobj.get_end_time()
    if inode is not None:
        update_wel_pak(mobj, inode, q)
    while current_time < end_time:
        mobj.update()
        current_time = mobj.get_current_time()
    qriv = get_streamflow(mobj)
    mobj.finalize()
    return qriv


def update_wel_pak(mobj, inode, q):
    # set nodelist to inode
    tag = mobj.get_var_address("NODELIST", sim_name, "CF-1")
    nodelist = mobj.get_value(tag)
    nodelist[0] = inode + 1  # convert from zero-based to one-based node number
    mobj.set_value(tag, nodelist)
    # set nbound to 1
    tag = mobj.get_var_address("NBOUND", sim_name, "CF-1")
    nbound = mobj.get_value(tag)
    nbound[0] = 1
    mobj.set_value(tag, nbound)
    # set bound to q
    tag = mobj.get_var_address("Q", sim_name, "CF-1")
    bound = mobj.get_value(tag)
    bound[:] = q
    mobj.set_value(tag, bound)


def get_streamflow(mobj):
    tag = mobj.get_var_address("SIMVALS", sim_name, "RIV-1")
    return mobj.get_value(tag).sum()


@timed
def run_models():
    soext = ".so"
    if sys.platform.lower() == "win32":
        soext = ".dll"
    if sys.platform.lower() == "darwin":
        soext = ".dylib"
    libmf6_path = pl.Path(shutil.which("mf6")).parent / f"libmf6{soext}"
    sim_ws = os.path.join(workspace, sim_name)
    mf6 = modflowapi.ModflowApi(libmf6_path, working_directory=sim_ws)
    qbase = capture_fraction_iteration(mf6, cf_q)

    # create capture fraction array
    capture = np.zeros((nrow, ncol), dtype=float)

    # iterate through each active cell
    ireduced_node = -1
    for irow in range(nrow):
        for jcol in range(ncol):
            # skip inactive cells
            if imap[irow, jcol] < 1:
                continue

            # increment reduced node number
            ireduced_node += 1

            # calculate the perturbed river flow
            qriv = capture_fraction_iteration(mf6, cf_q, inode=ireduced_node)

            # add the value to the capture array
            capture[irow, jcol] = (qriv - qbase) / abs(cf_q)

    # save the capture fraction array
    fpth = os.path.join(sim_ws, "capture.npz")
    np.savez_compressed(fpth, capture=capture)

Plotting results

Define functions to plot model results.

[4]:
# Figure properties
figure_size = (6, 6)


def plot_results(silent=True):
    if not plot:
        return

    if silent:
        verbosity_level = 0
    else:
        verbosity_level = 1

    with styles.USGSMap():
        sim_ws = os.path.join(workspace, sim_name)
        sim = flopy.mf6.MFSimulation.load(
            sim_name=sim_name, sim_ws=sim_ws, verbosity_level=verbosity_level
        )
        gwf = sim.get_model(sim_name)
        wel = gwf.get_package("WEL-1")

        # load the capture fraction data
        fpth = os.path.join(sim_ws, "capture.npz")
        capture = np.load(fpth)["capture"]

        # plot grid
        fig = plt.figure(figsize=(4, 3.75), constrained_layout=True)
        gs = mpl.gridspec.GridSpec(
            2,
            2,
            figure=fig,
            width_ratios=(4, 1),
            height_ratios=(1, 6),
        )

        ax = fig.add_subplot(gs[:, 0])
        ax.set_aspect("equal")

        mm = flopy.plot.PlotMapView(model=gwf, ax=ax)
        cf = mm.plot_array(capture, vmin=0, vmax=1)
        mm.plot_grid(lw=0.5, color="0.5")
        mm.plot_bc(package=wel)
        ax.axvline(x=14.5 * delc, lw=1.25, color="cyan")
        mm.plot_bc("CHD", color="green")
        mm.plot_ibound()
        ax.set_ylabel("y-coordinate, in feet")
        ax.set_xlabel("x-coordinate, in feet")
        styles.remove_edge_ticks(ax)

        ax = fig.add_subplot(gs[0, 1])
        ax.set_xlim(0, 1)
        ax.set_ylim(0, 1)
        ax.set_xticks([])
        ax.set_yticks([])
        ax.spines["top"].set_color("none")
        ax.spines["bottom"].set_color("none")
        ax.spines["left"].set_color("none")
        ax.spines["right"].set_color("none")
        ax.patch.set_alpha(0.0)
        cbar = plt.colorbar(cf, ax=ax, orientation="horizontal")
        cbar.ax.set_xlabel("Streamflow capture fraction")
        ax.plot(
            -1000,
            -1000,
            "s",
            ms=5,
            color="green",
            mec="black",
            mew=0.5,
            label="Constant head",
        )
        ax.plot(
            -1000,
            -1000,
            color="cyan",
            lw=1.25,
            label="River",
        )
        ax.plot(
            -1000,
            -1000,
            "s",
            ms=5,
            color="red",
            mec="black",
            mew=0.5,
            label="Well",
        )
        ax.plot(
            -1000,
            -1000,
            "s",
            ms=5,
            color="black",
            mec="black",
            mew=0.5,
            label="Inactive cell",
        )
        styles.graph_legend(
            ax,
            ncol=1,
            frameon=False,
            loc="upper center",
        )

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

Running the example

Define and invoke a function to run the example scenario, then plot results.

[5]:
def scenario(silent=True):
    sim = build_models()
    if write:
        write_models(sim, silent=silent)
    if run:
        run_models()


scenario()

if plot:
    # Simulated streamflow capture fraction map for the Freyberg (1988) groundwater flow model.
    plot_results()
<flopy.mf6.data.mfstructure.MFDataItemStructure object at 0x7fe8d17be160>
run_models took 17628.62 ms
../_images/_notebooks_ex-gwf-capture_10_2.png