Note

This page was generated from a jupyter notebook.

Coupling a Landlab groundwater with a Mesa agent-based model

This notebook shows a toy example of how one might couple a simple groundwater model (Landlab’s GroundwaterDupuitPercolator, by Litwin et al. (2020)) with an agent-based model (ABM) written using the Mesa Agent-Based Modeling (ABM) package.

The purpose of this tutorial is to demonstrate the technical aspects of creating an integrated Landlab-Mesa model. The example is deliberately very simple in terms of the processes and interactions represented, and not meant to be a realistic portrayal of water-resources decision making. But the example does show how one might build a more sophisticated and interesting model using these basic ingredients.

(Greg Tucker, November 2021; created from earlier notebook example used in May 2020 workshop)

Running the groundwater model

The following section simply illustrates how to create a groundwater model using the GroundwaterDupuitPercolator component.

Imports:

[1]:
import matplotlib.pyplot as plt
from tqdm.notebook import trange

from landlab import RasterModelGrid
from landlab.components import GroundwaterDupuitPercolator
Matplotlib is building the font cache; this may take a moment.

Set parameters:

[2]:
base_depth = 22.0  # depth of aquifer base below ground level, m
initial_water_table_depth = 2.0  # starting depth to water table, m
dx = 100.0  # cell width, m
pumping_rate = 0.001  # pumping rate, m3/s
well_locations = [800, 1200]
K = 0.001  # hydraulic conductivity, (m/s)
n = 0.2  # porosity, (-)
dt = 3600.0  # time-step duration, s
background_recharge = 0.1 / (3600 * 24 * 365.25)  # recharge rate from infiltration, m/s

Create a grid and add fields:

[3]:
# Raster grid with closed boundaries
# boundaries = {'top': 'closed','bottom': 'closed','right':'closed','left':'closed'}
grid = RasterModelGrid((41, 41), xy_spacing=dx)  # , bc=boundaries)

# Topographic elevation field (meters)
elev = grid.add_zeros("topographic__elevation", at="node")

# Field for the elevation of the top of an impermeable geologic unit that forms
# the base of the aquifer (meters)
grid.at_node["aquifer_base__elevation"] = elev - base_depth

# Field for the elevation of the water table (meters)
grid.at_node["water_table__elevation"] = elev - initial_water_table_depth

# Field for the groundwater recharge rate (meters per second)
recharge = grid.add_full("recharge__rate", background_recharge, at="node")

# pumping rate, in terms of recharge
recharge[well_locations] -= pumping_rate / (dx * dx)

Instantiate the component (note use of an array/field instead of a scalar constant for recharge_rate):

[4]:
gdp = GroundwaterDupuitPercolator(
    grid,
    hydraulic_conductivity=K,
    porosity=n,
    recharge_rate=recharge,
    regularization_f=0.01,
)

Define a couple of handy functions to run the model for a day or a year:

[5]:
def run_for_one_day(gdp, dt):
    num_iter = int(3600.0 * 24 / dt)
    for _ in range(num_iter):
        gdp.run_one_step(dt)
[6]:
def run_for_one_year(gdp, dt):
    num_iter = int(365.25 * 3600.0 * 24 / dt)
    for _ in range(num_iter):
        gdp.run_one_step(dt)

Run for a year and plot the water table:

[7]:
for _ in trange(365):
    run_for_one_day(gdp, dt)
[8]:
grid.imshow("water_table__elevation", colorbar_label="Water table elevation (m)")
../../../_images/tutorials_agent_based_modeling_groundwater_landlab_mesa_groundwater_pumping_15_0.png

Aside: calculating a pumping rate in terms of recharge

The pumping rate at a particular grid cell (in volume per time, representing pumping from a well at that location) needs to be given in terms of a recharge rate (depth of water equivalent per time) in a given grid cell. Suppose for example you’re pumping 16 gallons/minute (horrible units of course). That equates to:

16 gal/min x 0.00378541 m3/gal x (1/60) min/sec =

[9]:
Qp = 16.0 * 0.00378541 / 60.0
print(Qp)
0.0010094426666666667

…equals about 0.001 m\(^3\)/s. That’s \(Q_p\). The corresponding negative recharge in a cell of dimensions \(\Delta x\) by \(\Delta x\) would be

\(R_p = Q_p / \Delta x^2\)

[10]:
Rp = Qp / (dx * dx)
print(Rp)
1.0094426666666667e-07

A very simple ABM with farmers who drill wells into the aquifer

For the sake of illustration, our ABM will be extremely simple. There are \(N\) farmers, at random locations, who each pump at a rate \(Q_p\) as long as the water table lies above the depth of their well, \(d_w\). Once the water table drops below their well, the well runs dry and they switch from crops to pasture.

Check that Mesa is installed

For the next step, we must verify that Mesa is available. If it is not, use one of the installation commands below to install, then re-start the kernel (Kernel => Restart) and continue.

[11]:
try:
    from mesa import Model
except ModuleNotFoundError:
    print(
        """
Mesa needs to be installed in order to run this notebook.

Normally Mesa should be pre-installed alongside the Landlab notebook collection.
But it appears that Mesa is not already installed on the system on which you are
running this notebook. You can install Mesa from a command prompt using either:

`conda install -c conda-forge mesa`

or

`pip install mesa`
    """
    )
    raise
Could not import SolaraViz. If you need it, install with 'pip install --pre mesa[viz]'

Defining the ABM

In Mesa, an ABM is created using a class for each Agent and a class for the Model. Here’s the Agent class (a Farmer). Farmers have a grid location and an attribute: whether they are actively pumping their well or not. They also have a well depth: the depth to the bottom of their well. Their action consists of checking whether their well is wet or dry; if wet, they will pump, and if dry, they will not.

[12]:
from mesa import Agent
from mesa.space import MultiGrid
from mesa.time import RandomActivation
[13]:
class FarmerAgent(Agent):
    """An agent who pumps from a well if it's not dry."""

    def __init__(self, unique_id, model, well_depth=5.0):
        # super().__init__(unique_id, model)
        super().__init__(model)
        self.pumping = True
        self.well_depth = well_depth

    def step(self):
        x, y = self.pos
        print(f"Farmer {self.unique_id}, ({x}, {y})")
        print(f"    Depth to the water table: {self.model.wt_depth_2d[x, y]}")
        print(f"    Depth to the bottom of the well: {self.well_depth}")
        if self.model.wt_depth_2d[x, y] >= self.well_depth:  # well is dry
            print("    Well is dry.")
            self.pumping = False
        else:
            print("    Well is pumping.")
            self.pumping = True

Next, define the model class. The model will take as a parameter a reference to a 2D array (with the same dimensions as the grid) that contains the depth to water table at each grid location. This allows the Farmer agents to check whether their well has run dry.

[14]:
import random


class FarmerModel(Model):
    """A model with several agents on a grid."""

    def __init__(self, N, width, height, well_depth, depth_to_water_table, seed=None):
        super().__init__()

        self.num_agents = N
        self.grid = MultiGrid(width, height, True)
        self.depth_to_water_table = depth_to_water_table
        self.schedule = RandomActivation(self)
        self.random = random.Random(seed)

        # Create agents
        for i in range(self.num_agents):
            a = FarmerAgent(i, self, well_depth)
            self.schedule.add(a)
            # Add the agent to a random grid cell (excluding the perimeter)
            x = self.random.randrange(self.grid.width - 2) + 1
            y = self.random.randrange(self.grid.width - 2) + 1
            self.grid.place_agent(a, (x, y))

    def step(self):
        self.wt_depth_2d = self.depth_to_water_table.reshape(
            (self.grid.width, self.grid.height)
        )
        self.schedule.step()

Setting up the Landlab grid, fields, and groundwater simulator

[15]:
base_depth = 22.0  # depth of aquifer base below ground level, m
initial_water_table_depth = 2.8  # starting depth to water table, m
dx = 100.0  # cell width, m
pumping_rate = 0.004  # pumping rate, m3/s
well_depth = 3  # well depth, m
background_recharge = 0.002 / (365.25 * 24 * 3600)  # recharge rate, m/s
K = 0.001  # hydraulic conductivity, (m/s)
n = 0.2  # porosity, (-)
dt = 3600.0  # time-step duration, s
num_agents = 12  # number of farmer agents
run_duration_yrs = 5  # run duration in years
[16]:
grid = RasterModelGrid((41, 41), xy_spacing=dx)

elev = grid.add_zeros("topographic__elevation", at="node")

grid.at_node["aquifer_base__elevation"] = elev - base_depth
grid.at_node["water_table__elevation"] = elev - initial_water_table_depth
grid.add_full("water_table__depth_below_ground", initial_water_table_depth, at="node")
recharge = grid.add_full("recharge__rate", background_recharge, at="node")

# pumping rate, in terms of recharge
recharge[well_locations] -= pumping_rate / (dx * dx)
[17]:
gdp = GroundwaterDupuitPercolator(
    grid,
    hydraulic_conductivity=K,
    porosity=n,
    recharge_rate=recharge,
    regularization_f=0.01,
)

Set up the Farmer model

[18]:
farmer_model = FarmerModel(
    num_agents,
    grid.number_of_node_columns,
    grid.number_of_node_rows,
    well_depth,
    # depth_to_wt.reshape(grid.shape),
    grid.at_node["water_table__depth_below_ground"].reshape(grid.shape),
    seed=1945,
)
/var/folders/2s/h6hvv9ps03xgz_krkkstvq_r0000gn/T/ipykernel_6669/2212081909.py:13: DeprecationWarning: The time module and all its Schedulers are deprecated and will be removed in Mesa 3.1. They can be replaced with AgentSet functionality. See the migration guide for details. https://mesa.readthedocs.io/latest/migration_guide.html#time-and-schedulers
  self.schedule = RandomActivation(self)

Check the spatial distribution of wells:

[19]:
import numpy as np


def get_well_count(model):
    well_count = np.zeros(grid.shape, dtype=int)
    pumping_well_count = np.zeros(grid.shape, dtype=int)
    for cell in model.grid.coord_iter():
        cell_content, (x, y) = cell
        well_count[x][y] = len(cell_content)
        for agent in cell_content:
            if agent.pumping:
                pumping_well_count[x][y] += 1
    return well_count, pumping_well_count


well_count, p_well_count = get_well_count(farmer_model)
grid.imshow(well_count)
../../../_images/tutorials_agent_based_modeling_groundwater_landlab_mesa_groundwater_pumping_35_0.png

Set the initial recharge field

[20]:
recharge[:] = -(pumping_rate / (dx * dx)) * p_well_count.flatten()
grid.imshow(-recharge * 3600 * 24, colorbar_label="Pumping rate (m/day)")
../../../_images/tutorials_agent_based_modeling_groundwater_landlab_mesa_groundwater_pumping_37_0.png

Run the model

[21]:
depth_to_wt = grid.at_node["water_table__depth_below_ground"]

for i in trange(run_duration_yrs):
    # Run the groundwater simulator for one year
    run_for_one_year(gdp, dt)

    # Update the depth to water table
    depth_to_wt[:] = (
        grid.at_node["topographic__elevation"] - grid.at_node["water_table__elevation"]
    )

    # Run the farmer model
    farmer_model.step()

    # Count the number of pumping wells
    well_count, pumping_well_count = get_well_count(farmer_model)
    total_pumping_wells = np.sum(pumping_well_count)
    print(f"In year {i + 1} there are {total_pumping_wells} pumping wells")
    print(f" and the greatest depth to water table is {np.amax(depth_to_wt)} meters.")

    # Update the recharge field according to current pumping rate
    recharge[:] = (
        background_recharge - (pumping_rate / (dx * dx)) * pumping_well_count.flatten()
    )
    print(f"Total recharge: {np.sum(recharge)}")
    print("")

    plt.figure()
    grid.imshow("water_table__elevation")
    plt.show()
Farmer 10, (36, 6)
    Depth to the water table: 2.9532171973868735
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 3, (19, 24)
    Depth to the water table: 3.125042438222149
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 1, (27, 20)
    Depth to the water table: 3.108003201164557
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 9, (17, 39)
    Depth to the water table: 2.887000015621961
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 11, (25, 12)
    Depth to the water table: 3.116970742578278
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 5, (14, 16)
    Depth to the water table: 3.1509947334725688
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 12, (36, 16)
    Depth to the water table: 2.9843739795064295
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 4, (25, 6)
    Depth to the water table: 3.037622566192745
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 6, (31, 37)
    Depth to the water table: 2.9320254253580877
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 7, (14, 9)
    Depth to the water table: 3.0812406798846723
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 2, (16, 20)
    Depth to the water table: 3.1586006697665425
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 8, (8, 13)
    Depth to the water table: 3.056278550336227
    Depth to the bottom of the well: 3
    Well is dry.
In year 1 there are 4 pumping wells
 and the greatest depth to water table is 3.1586006697665425 meters.
Total recharge: -1.4934646487692335e-06

../../../_images/tutorials_agent_based_modeling_groundwater_landlab_mesa_groundwater_pumping_39_2.png
Farmer 9, (17, 39)
    Depth to the water table: 2.8769211378838335
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 11, (25, 12)
    Depth to the water table: 2.824266525495407
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 2, (16, 20)
    Depth to the water table: 2.815141441130109
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 4, (25, 6)
    Depth to the water table: 2.8153339866056015
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 6, (31, 37)
    Depth to the water table: 2.9138457953766945
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 10, (36, 6)
    Depth to the water table: 2.9231182726368097
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 8, (8, 13)
    Depth to the water table: 2.805415000400476
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 7, (14, 9)
    Depth to the water table: 2.8079596555990136
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 3, (19, 24)
    Depth to the water table: 2.818472450469848
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 1, (27, 20)
    Depth to the water table: 2.8282651014507287
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 5, (14, 16)
    Depth to the water table: 2.811962766912295
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 12, (36, 16)
    Depth to the water table: 2.929668376739542
    Depth to the bottom of the well: 3
    Well is pumping.
In year 2 there are 12 pumping wells
 and the greatest depth to water table is 2.929668376739542 meters.
Total recharge: -4.693464648769233e-06

../../../_images/tutorials_agent_based_modeling_groundwater_landlab_mesa_groundwater_pumping_39_4.png
Farmer 11, (25, 12)
    Depth to the water table: 3.1143755941466367
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 7, (14, 9)
    Depth to the water table: 3.0790560260349658
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 1, (27, 20)
    Depth to the water table: 3.1051900454472623
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 4, (25, 6)
    Depth to the water table: 3.0359216454975524
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 5, (14, 16)
    Depth to the water table: 3.1481775464319455
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 9, (17, 39)
    Depth to the water table: 2.886638832506346
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 12, (36, 16)
    Depth to the water table: 2.9831172831320423
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 8, (8, 13)
    Depth to the water table: 3.054297728905631
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 10, (36, 6)
    Depth to the water table: 2.952407294852545
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 3, (19, 24)
    Depth to the water table: 3.1220281602523556
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 6, (31, 37)
    Depth to the water table: 2.9312215240038277
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 2, (16, 20)
    Depth to the water table: 3.155575445940869
    Depth to the bottom of the well: 3
    Well is dry.
In year 3 there are 4 pumping wells
 and the greatest depth to water table is 3.155575445940869 meters.
Total recharge: -1.4934646487692335e-06

../../../_images/tutorials_agent_based_modeling_groundwater_landlab_mesa_groundwater_pumping_39_6.png
Farmer 5, (14, 16)
    Depth to the water table: 2.811890486026062
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 10, (36, 6)
    Depth to the water table: 2.9231062348845462
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 3, (19, 24)
    Depth to the water table: 2.818391536671431
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 12, (36, 16)
    Depth to the water table: 2.929643146222773
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 6, (31, 37)
    Depth to the water table: 2.9138327971284497
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 2, (16, 20)
    Depth to the water table: 2.8150602977777908
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 8, (8, 13)
    Depth to the water table: 2.8053722758515605
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 9, (17, 39)
    Depth to the water table: 2.876914619030277
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 7, (14, 9)
    Depth to the water table: 2.807910311977299
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 11, (25, 12)
    Depth to the water table: 2.8242027101142426
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 4, (25, 6)
    Depth to the water table: 2.81529819732798
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 1, (27, 20)
    Depth to the water table: 2.8281922819535943
    Depth to the bottom of the well: 3
    Well is pumping.
In year 4 there are 12 pumping wells
 and the greatest depth to water table is 2.929643146222773 meters.
Total recharge: -4.693464648769233e-06

../../../_images/tutorials_agent_based_modeling_groundwater_landlab_mesa_groundwater_pumping_39_8.png
Farmer 2, (16, 20)
    Depth to the water table: 3.1555734391319206
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 8, (8, 13)
    Depth to the water table: 3.0542966780127614
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 1, (27, 20)
    Depth to the water table: 3.1051882524011702
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 6, (31, 37)
    Depth to the water table: 2.93122120939519
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 7, (14, 9)
    Depth to the water table: 3.0790548104511437
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 12, (36, 16)
    Depth to the water table: 2.9831166700784735
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 4, (25, 6)
    Depth to the water table: 3.0359207669681787
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 11, (25, 12)
    Depth to the water table: 3.114374020719641
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 10, (36, 6)
    Depth to the water table: 2.9524070028017135
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 5, (14, 16)
    Depth to the water table: 3.14817575877451
    Depth to the bottom of the well: 3
    Well is dry.
Farmer 9, (17, 39)
    Depth to the water table: 2.8866386746886192
    Depth to the bottom of the well: 3
    Well is pumping.
Farmer 3, (19, 24)
    Depth to the water table: 3.12202616458503
    Depth to the bottom of the well: 3
    Well is dry.
In year 5 there are 4 pumping wells
 and the greatest depth to water table is 3.1555734391319206 meters.
Total recharge: -1.4934646487692335e-06

../../../_images/tutorials_agent_based_modeling_groundwater_landlab_mesa_groundwater_pumping_39_10.png
[22]:
# Display the area of water table that lies below the well depth
too_deep = (
    grid.at_node["topographic__elevation"] - grid.at_node["water_table__elevation"]
    > well_depth
)

grid.imshow(too_deep)
../../../_images/tutorials_agent_based_modeling_groundwater_landlab_mesa_groundwater_pumping_40_0.png

Generated by nbsphinx from a Jupyter notebook.