Building a groundwater model from scratch

This notebook shows how to build a basic model from scratch using nlmod.

import flopy as fp
import pandas as pd

import nlmod
nlmod.util.get_color_logger("INFO")
nlmod.show_versions()
Python version     : 3.11.14
NumPy version      : 2.4.4
Xarray version     : 2026.4.0
Matplotlib version : 3.10.9
Flopy version      : 3.10.0

nlmod version      : 0.11.3dev

Model parameters

extent = [-500, 500, -500, 500]

top = 0.0
botm = [-10, -15, -30]

kh = [10, 0.1, 20]
kv = [0.5 * k for k in kh]

dx = 10.0
dy = 10.0

Create model dataset

ds = nlmod.get_ds(
    extent,
    delr=dx,
    delc=dy,
    top=top,
    botm=botm,
    kh=kh,
    kv=kv,
    model_ws="00_model_from_scratch",
    model_name="from_scratch",
)
ds
INFO:nlmod.dims.base.to_model_ds:resample layer model data to structured modelgrid
<xarray.Dataset> Size: 882kB
Dimensions:      (y: 100, x: 100, layer: 3)
Coordinates:
  * y            (y) float64 800B 495.0 485.0 475.0 ... -475.0 -485.0 -495.0
  * x            (x) float64 800B -495.0 -485.0 -475.0 ... 475.0 485.0 495.0
  * layer        (layer) int64 24B 0 1 2
    spatial_ref  int64 8B 0
Data variables:
    top          (y, x) float64 80kB 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0
    botm         (layer, y, x) float64 240kB -10.0 -10.0 -10.0 ... -30.0 -30.0
    kh           (layer, y, x) float64 240kB 10.0 10.0 10.0 ... 20.0 20.0 20.0
    kv           (layer, y, x) float64 240kB 5.0 5.0 5.0 5.0 ... 10.0 10.0 10.0
    area         (y, x) float64 80kB 100.0 100.0 100.0 ... 100.0 100.0 100.0
Attributes:
    extent:      [-500, 500, -500, 500]
    gridtype:    structured
    model_name:  from_scratch
    mfversion:   mf6
    created_on:  20260513_15:10:28
    exe_name:    /home/docs/checkouts/readthedocs.org/user_builds/nlmod/envs/...
    model_ws:    00_model_from_scratch
    figdir:      00_model_from_scratch/figure
    cachedir:    00_model_from_scratch/cache
    transport:   0

Set time discretisation

ds = nlmod.time.set_ds_time(ds, time=pd.Timestamp.today(), start=1)

Start building model

sim = nlmod.sim.sim(ds)
tdis = nlmod.sim.tdis(ds, sim)
ims = nlmod.sim.ims(sim, complexity="SIMPLE")
gwf = nlmod.gwf.gwf(ds, sim)
dis = nlmod.gwf.dis(ds, gwf)
npf = nlmod.gwf.npf(
    ds, gwf, save_flows=True, save_specific_discharge=True, save_saturation=True
)
ic = nlmod.gwf.ic(ds, gwf, starting_head=1.0)
oc = nlmod.gwf.oc(ds, gwf, save_head=True)
INFO:nlmod.sim.sim.sim:creating mf6 SIM
INFO:nlmod.sim.sim.tdis:creating mf6 TDIS
INFO:nlmod.sim.sim.ims:creating mf6 IMS
INFO:nlmod.gwf.gwf.gwf:creating mf6 GWF
INFO:nlmod.gwf.gwf._dis:creating mf6 DIS
INFO:nlmod.gwf.gwf.npf:creating mf6 NPF
INFO:nlmod.gwf.gwf.ic:creating mf6 IC
INFO:nlmod.gwf.gwf.ic:adding 'starting_head' data array to ds
INFO:nlmod.gwf.gwf.oc:creating mf6 OC

Add wells

wells = pd.DataFrame(columns=["x", "y", "top", "botm", "Q"], index=range(2))
wells.index.name = "well no."
wells.loc[0] = 100, -50, -5, -10, -100.0
wells.loc[1] = 200, 150, -20, -30, -300.0
wells
x y top botm Q
well no.
0 100 -50 -5 -10 -100.0
1 200 150 -20 -30 -300.0
wel = nlmod.gwf.wells.wel_from_df(wells, gwf)
Intersecting with grid:   0%|          | 0/2 [00:00<?, ?it/s]
Intersecting with grid: 100%|██████████| 2/2 [00:00<00:00, 191.68it/s]

Add river

xyriv = [
    (250, -500),
    (300, -300),
    (275, 0),
    (200, 250),
    (175, 500),
]

riv_layer = 0  # add to first layer

bed_resistance = 0.1  # days
riv_cond = dx * dy / bed_resistance  # conductance
riv_stage = 1.0  # m NAP
riv_botm = -3.0  # m NAP
riv_data = nlmod.gwf.surface_water.rivdata_from_xylist(
    gwf, xyriv, riv_layer, riv_stage, riv_cond, riv_botm
)

riv = fp.mf6.ModflowGwfriv(gwf, stress_period_data={0: riv_data})

Write and run Simulation

nlmod.sim.write_and_run(sim, ds, silent=True)
INFO:nlmod.sim.sim.write_and_run:write model dataset to cache
INFO:nlmod.sim.sim.write_and_run:write modflow files to model workspace
INFO:nlmod.sim.sim.write_and_run:run model

Load heads

head = nlmod.gwf.get_heads_da(ds)

Plot heads

# using nlmod plotting methods
ax = nlmod.plot.map_array(
    head.sel(layer=0).isel(time=0),
    ds,
    cmap="RdYlBu",
    colorbar_label="head [m NAP]",
    xlabel="x [km]",
    ylabel="y [km]",
    title="head first layer",
    plot_grid=True,
)
../_images/b5ebd04858d698dadef8f34bfeef404e458be912f76243b299465d2c28565317.png

Plot heads in all layers

# using xarray plotting methods
fg = head.plot(
    x="x",
    y="y",
    col="layer",
    col_wrap=3,
    cmap="RdYlBu",
    subplot_kws={"aspect": "equal"},
    cbar_kwargs={"label": "head [m NAP]"},
)

for iax in fg.axs.flat:
    nlmod.plot.modelgrid(ds, ax=iax, alpha=0.5, lw=0.5)
../_images/c46855de29f644782f00a1aabc06b5413e75dca5158d737c05e0ebf1eed782c5.png