Example: Head Controlled Well

This example demonstrates how pymf6 can be used to control the rate of an extraction well so that the well water level does fall below under a given water level.

Setting up a model with flopy

First, we create a simple model with flopy. You may use any GUI tool such as ModelMuse to create such a model. This is not specific to pymf6 but helps to better understand the problem. You may skip ahead to the section with use of pymf6.

Creating the input data

First, we import a few need libraries:

"""Create, run, and postprocess a MODFLOW 6 model with flopy.
"""

import flopy
from flopy.utils.postprocessing import get_specific_discharge
from matplotlib import pyplot as plt
import numpy as np

The function make_input creates our model with help pf flopy:

def _make_wel_stress_period(gwf, wel_q, wel_coords):
    """Create stress period data for the wel package."""
    period = flopy.mf6.ModflowGwfwel.stress_period_data.empty(
        gwf,
        maxbound=1,
    )
    period[0][0] = (wel_coords, wel_q)
    return period


def make_input(
    wel_q,
    wel_coords,
    model_path,
    name,
    exe_name='mf6',
    verbosity_level=0):
    """Create MODFLOW 6 input"""
    sim = flopy.mf6.MFSimulation(
        sim_name=name,
        sim_ws=model_path,
        exe_name=exe_name,
        verbosity_level=verbosity_level,
    )
    times = (10.0, 120, 1.0)
    tdis_rc = [(1.0, 1, 1.0), times, times, times]
    flopy.mf6.ModflowTdis(
        sim, pname="tdis",
        time_units="DAYS",
        nper=4,
        perioddata=tdis_rc,
    )
    flopy.mf6.ModflowIms(sim)
    gwf = flopy.mf6.ModflowGwf(sim, modelname=name, save_flows=True)
    flopy.mf6.ModflowGwfdis(gwf, nrow=10, ncol=10)
    flopy.mf6.ModflowGwfic(gwf)
    flopy.mf6.ModflowGwfnpf(
        gwf,
        save_flows=True,
        save_specific_discharge=True,
        icelltype=[0],
        k=[0.5],
        k33=[0.1],
    )
    sy = flopy.mf6.ModflowGwfsto.sy.empty(
        gwf,
        default_value=0.2
    )
    ss = flopy.mf6.ModflowGwfsto.ss.empty(
        gwf, default_value=0.000001
    )
    flopy.mf6.ModflowGwfsto(
        gwf,
        pname="sto",
        save_flows=True,
        save_specific_discharge=True,
        iconvert=1,
        ss=ss,
        sy=sy,
        transient={0: True},
        )

    stress_period_data = {
        1: _make_wel_stress_period(gwf, wel_q / 10, wel_coords)[0],
        2: _make_wel_stress_period(gwf, wel_q, wel_coords)[0],
        3: _make_wel_stress_period(gwf, wel_q / 10, wel_coords)[0]
    }
    flopy.mf6.ModflowGwfwel(
        gwf,
        stress_period_data=stress_period_data,
    )
    flopy.mf6.ModflowGwfchd(
        gwf,
        stress_period_data=[
            [(0, 0, 0), 1.],
            [(0, 9, 9), 1.]],
    )
    budget_file = name + '.bud'
    head_file = name + '.hds'
    flopy.mf6.ModflowGwfoc(
        gwf,
        budget_filerecord=budget_file,
        head_filerecord=head_file,
        saverecord=[('HEAD', 'ALL'), ('BUDGET', 'ALL')])
    sim.write_simulation()

We specify some data for the model:

model_path=r'models\mf6'
name='headconwell'
wel_coords=(0, 4, 4)
wel_q = -0.5

and create the input files.

make_input(
    wel_q=wel_q,
    wel_coords=wel_coords,
    model_path=model_path,
    name=name
)

Running the model

The function get_simulation retrieves a flopy simulation object:

def get_simulation(model_path, exe_name='mf6', verbosity_level=0):
    """Get simulation for a model."""
    sim = flopy.mf6.MFSimulation.load(
        sim_ws=model_path,
        exe_name=exe_name,
        verbosity_level=verbosity_level,
    )
    return sim

This function runs the model:

def run_simulation(model_path, verbosity_level=0):
    """Run a MODFLOW 6 model"""
    sim = get_simulation(
        model_path,
        verbosity_level=verbosity_level)
    sim.run_simulation()

We run the simulation:

run_simulation(model_path=model_path)

There is visible output from MODFLOW 6. Setting the verbosity_level to 1 or higher in run_simulation will show the simulations steps.

Looking at the model results

Let’s look at the model results. This function visualizes the head at the end of the last time step:

def show_heads(model_path, name):
    """Plot calculated heads along with flow vector."""
    sim = get_simulation(model_path, name)
    gwf = sim.get_model(name)

    head = gwf.output.head().get_data()
    bud = gwf.output.budget()

    spdis = bud.get_data(text='DATA-SPDIS')[-1]
    qx, qy, _ = get_specific_discharge(spdis, gwf)
    pmv = flopy.plot.PlotMapView(gwf)
    pmv.plot_array(head)
    pmv.plot_grid(colors='white')
    pmv.contour_array(
        head,
        levels=np.arange(0.2, 1.0, 0.02),
    )
    pmv.plot_vector(qx, qy, normalize=True, color="white")

We look at the heads:

show_heads(model_path=model_path, name=name)
../../_images/8906d40d2f33853ccd136dd63fc54feb0530bc5da53e811259123ea64835baf9.png

The well is roughly in the middle of the model. The yellow cells have a constant water level and “feed” the model. The well causes a cone of depression.

This function shows the head the well location over time:

def show_well_head(model_path, name, wel_coords):
    """Plot head at well over time."""
    sim = get_simulation(model_path, name)
    gwf = sim.get_model(name)
    heads = gwf.output.head().get_ts(wel_coords)
    _, ax = plt.subplots()
    ax.plot(heads[:, 0], heads[:, 1])

This is the head at the well over time:

show_well_head(model_path=model_path, name=name, wel_coords=wel_coords)
../../_images/f501fd7109f4918d0f2eebeb5d550537f91d9b9729eb4210f2b6a3758066454d.png

There are four stress periods. The first period is steady state. Periods two to four a transient with different extraction rate. Period three has the full rate and periods two and four have only a tenth of the rate. See generation of the input files for details.

Working with pymf6

Finally, we will use pymf6. The objective is the avoid a drop of the water level at the well below a given limit.

Creating new input files

We generate the input files again. There are no differences in the files to the model above. The only difference is the model directory:

model_path=r'models\pymf6'

Creating the input files:

make_input(
    wel_q=wel_q,
    wel_coords=wel_coords,
    model_path=model_path,
    name=name
)

Run the model with pymf6

Now run the model with pymf6:

from pymf6.mf6 import MF6

def run_model(nam_file):
    mf6 = MF6(nam_file=nam_file)
    head = mf6.vars['SLN_1/X']
    wel_index = 44
    tolerance = 0.01
    head_limit = 0.5
    upper_limit = head_limit + tolerance
    lower_limit = head_limit - tolerance
    wel = mf6.vars['HEADCONWELL/WEL_0/BOUND']
    been_below = False
    for step in mf6.steps():
        if step < 21:
            if head[wel_index] <= upper_limit:
                wel[:] = wel[:] * 0.9
                been_below = True
            elif been_below and head[wel_index] >= upper_limit:
                wel[:] = wel[:] * 1.1

The class MF6 provides the functionality of pymf6. The dictionary mf6.vars provides all MODFLOW 6 variables. The head has the name SLN_1/X and the values of the boundary condition WEL is HEADCONWELL/WEL_0/BOUND. The name will change depending on model name and the chosen name for the boundary condition.

mf6.steps() provides a generator that allows to iterate over all MODFLOW 6 time steps. step is the current time step in the unit specified in the input files, here DAYS. We don’t want influence the last, which starts at day 21. We adjust the extraction rate by reducing o increasing the rate by 10%, if the water level at the well, found at index 44, is not within the upper and lower limit.

Running the model:

run_model(r'models/pymf6/mfsim.nam')

Looking at the results

Now the water level stays just around 0.5:

show_well_head(model_path=model_path, name=name, wel_coords=wel_coords)
../../_images/e34136f57427734e20d1f584191a1497c9a86eaa1148088de29a47e3a9579c6b.png

There is some oscillation in the water level. These can be reduced by adding more time steps in the TDIS package.