A groundwater model for Schoonhoven

In this notebook we build a transient model for the area around Schoonhoven. Surface water is added to the model using a winter and a summer stage using the drain package. For the river Lek, we build a river package with a fixed stage of NAP+0.0 m.

import os

import flopy
import geopandas as gpd
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from shapely.geometry import LineString, Point

import nlmod
from nlmod.plot import DatasetCrossSection
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 settings

We define some model settings, like the name, the directory of the model files, the model extent and the time

model_name = "Schoonhoven"
model_ws = "09_schoonhoven"
figdir, cachedir = nlmod.util.get_model_dirs(model_ws)
extent = [116_500, 120_000, 439_000, 442_000]
time = pd.date_range("2020", "2023", freq="MS")  # monthly timestep

Download data

layer ‘waterdeel’ from bgt

The location of the surface water bodies is obtained from the GeoDataFrame that was created in the the surface water notebook. We saved this data as a geosjon file and load it here.

fname_bgt = os.path.join("..", "data_sources", "02_surface_water", "cache", "bgt.gpkg")
if not os.path.isfile(fname_bgt):
    raise (
        Exception(
            f"{fname_bgt} not found. Please run notebook 02_surface_water.ipynb in the 'data_sources' directory first"
        )
    )
bgt = gpd.read_file(fname_bgt)

Plot summer stage of surface water bodies

We can plot the summer stage. There are some surface water bodies without a summer stage, because the ‘bronhouder’ is not a water board. The main one is the river Lek, but there are also some surface water bodies without a summer stage in the north of the model area.

f, ax = nlmod.plot.get_map(extent)
norm = matplotlib.colors.Normalize(vmin=-3, vmax=1)
cmap = "viridis"
bgt.plot("summer_stage", ax=ax, norm=norm, cmap=cmap)
nlmod.plot.colorbar_inside(norm=norm, cmap=cmap);
../_images/61f4466bae4bd141ec7b794c2c51c031849ad5886ec4ad965d4e225a8901f44e.png

If no information about the stage is available, a constant stage is set to the minimal height of the digital terrain model (AHN) near the surface water body. We can plot these values as well:

f, ax = nlmod.plot.get_map(extent)
bgt.plot("ahn_min", ax=ax, norm=norm, cmap=cmap)
nlmod.plot.colorbar_inside(norm=norm, cmap=cmap);
../_images/e63aeb5457189ac9dabb5c7a5d68e8f7cd3f6dee1f61ca1d83eb1af5d1dd76fb.png

KNMI

KNMI precipitation and evaporation data is used to estimate the recharge.

# knmi data
oc_knmi = nlmod.read.knmi.download_knmi(extent=extent, delr=100., delc=100., start='2000-1-1', end='2025-1-1')
INFO:nlmod.dims.base.to_model_ds:resample layer model data to structured modelgrid
INFO:hydropandas.io.knmi.get_knmi_obs:get data from station 434 and variable RD from 2000-01-01 to 2025-01-01
INFO:hydropandas.io.knmi.get_knmi_obs:get data from station 348 and variable EV24 from 2000-01-01 to 2025-01-01

REGIS

For the schematisation of the subsurface we use REGIS. Let’s download this data for the required extent.

layer_model = nlmod.read.regis.get_combined_layer_models(
    extent,
    use_regis=True,
    use_geotop=False,
    cachedir=cachedir,
    cachename="layer_model.nc",
)
layer_model
WARNING:nlmod.dims.layers.remove_layer_dim_from_top:Botm of layer is not equal to top of deeper layer in 3 cells
INFO:nlmod.cache.wrapper:caching data -> layer_model.nc
<xarray.Dataset> Size: 371kB
Dimensions:      (y: 30, x: 35, layer: 29)
Coordinates:
  * y            (y) float64 240B 4.42e+05 4.418e+05 ... 4.392e+05 4.39e+05
  * x            (x) float64 280B 1.166e+05 1.166e+05 ... 1.198e+05 1.2e+05
  * layer        (layer) <U8 928B 'HLc' 'KRWYk1' 'KRz2' ... 'OOz2' 'OOc' 'BRk1'
    spatial_ref  int64 8B 0
Data variables:
    top          (y, x) float32 4kB -1.28 -1.22 -1.25 ... -4.05 -3.76 -4.21
    botm         (layer, y, x) float32 122kB -12.26 -12.11 ... -593.9 -595.5
    kh           (layer, y, x) float32 122kB nan nan nan nan ... nan nan nan nan
    kv           (layer, y, x) float32 122kB nan nan nan ... 0.002 0.002 0.002
Attributes: (12/41)
    references:                    https://www.dinoloket.nl/regis-ii-het-hydr...
    Conventions:                   CF-1.7
    creator_url:                   https://www.dinoloket.nl
    keywords_vocabulary:           NASA/GCMD Earth Science Keywords. Version 6.0
    acknowledgment:                https://www.dinoloket.nl
    project:                       REGIS v02r2s3
    ...                            ...
    geospatial_vertical_min:       -1235.92
    geospatial_vertical_max:       322.75
    geospatial_vertical_units:     m-NAP
    geospatial_vertical_positive:  up
    gridtype:                      structured
    extent:                        [116500, 120000, 439000, 442000]

We then create a regular grid, add necessary variables and fill NaN’s. For example, REGIS does not contain information about the hydraulic conductivity of the first layer (‘HLc’). These NaN’s are replaced by a default hydraulic conductivity (kh) of 1 m/d. This probably is not a good representation of the conductivity, but at least the model will run.

ds = nlmod.to_model_ds(layer_model, model_name, model_ws, delr=100.0, delc=100.0)
ds
INFO:nlmod.dims.base.to_model_ds:resample layer model data to structured modelgrid
INFO:nlmod.dims.layers.get_kh_kv:kv and kh both undefined in layer HLc
INFO:nlmod.dims.layers._fill_var:Filling 7594 values in active cells of kh by multipying kv with an anisotropy of 10
INFO:nlmod.dims.layers._fill_var:Filling 16762 values in active cells of kv by dividing kh by an anisotropy of 10
INFO:nlmod.dims.layers._fill_var:Filling 1050 values in active cells of kh with a value of 1.0 m/day
INFO:nlmod.dims.layers._fill_var:Filling 1050 values in active cells of kv with a value of 0.1 m/day
<xarray.Dataset> Size: 379kB
Dimensions:      (y: 30, x: 35, layer: 29)
Coordinates:
  * y            (y) float64 240B 4.42e+05 4.418e+05 ... 4.392e+05 4.39e+05
  * x            (x) float64 280B 1.166e+05 1.166e+05 ... 1.198e+05 1.2e+05
  * layer        (layer) <U8 928B 'HLc' 'KRWYk1' 'KRz2' ... 'OOz2' 'OOc' 'BRk1'
    spatial_ref  int64 8B 0
Data variables:
    top          (y, x) float32 4kB -1.28 -1.22 -1.25 ... -4.05 -3.76 -4.21
    botm         (layer, y, x) float32 122kB -12.26 -12.11 ... -593.9 -595.5
    kh           (layer, y, x) float32 122kB 1.0 1.0 1.0 1.0 ... 0.02 0.02 0.02
    kv           (layer, y, x) float32 122kB 0.1 0.1 0.1 ... 0.002 0.002 0.002
    area         (y, x) float64 8kB 1e+04 1e+04 1e+04 ... 1e+04 1e+04 1e+04
Attributes:
    extent:      [116500, 120000, 439000, 442000]
    gridtype:    structured
    model_name:  Schoonhoven
    mfversion:   mf6
    created_on:  20260513_15:12:47
    exe_name:    /home/docs/checkouts/readthedocs.org/user_builds/nlmod/envs/...
    model_ws:    09_schoonhoven
    figdir:      09_schoonhoven/figure
    cachedir:    09_schoonhoven/cache
    transport:   0

Add grid refinement

With the refine method, we can add grid refinement. The model will then use the disv-package instead of the dis-package. We can also test if the disv-package gives the same results as the dis-package by not specifying refinement_features: ds = nlmod.grid.refine(ds).

This notebook can be run with or without running the cell below.

refinement_features = [(bgt[bgt["bronhouder"] == "L0002"].dissolve().boundary, 2)]
ds = nlmod.grid.refine(ds, refinement_features=refinement_features)
INFO:nlmod.dims.grid.refine:create vertex grid using gridgen
INFO:nlmod.dims.grid.ds_to_gridprops:resample model Dataset to vertex modelgrid

Add information about time

ds = nlmod.time.set_ds_time(ds, time=time, start=3652)

Add knmi recharge to the model dataset

knmi_ds = nlmod.read.knmi.discretize_knmi(ds, oc_knmi, cachedir=cachedir, cachename="recharge")
ds.update(knmi_ds)
WARNING:nlmod.read.knmi.discretize_knmi:The default of hourly_precision=False will be changed to True in a future version of nlmod. Pass hourly_precision=False to retain current behavior or hourly_precision=True to adopt the future default and silence this warning.
INFO:nlmod.cache.wrapper:caching data -> recharge.nc

Create a groundwater flow model

Using the data from the xarray Dataset ds we generate a groundwater flow model.

# create simulation
sim = nlmod.sim.sim(ds)

# create time discretisation
tdis = nlmod.sim.tdis(ds, sim)

# create ims
ims = nlmod.sim.ims(sim)

# create groundwater flow model
gwf = nlmod.gwf.gwf(ds, sim)

# Create discretization
dis = nlmod.gwf.dis(ds, gwf)

# create node property flow
npf = nlmod.gwf.npf(ds, gwf, save_flows=True)

# Create the initial conditions package
ic = nlmod.gwf.ic(ds, gwf, starting_head=0.0)

# Create the output control package
oc = nlmod.gwf.oc(ds, gwf)

# create storagee package
sto = nlmod.gwf.sto(ds, gwf)
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._disv:creating mf6 DISV
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
INFO:nlmod.gwf.gwf.sto:creating mf6 STO

Process surface water

We intersect the surface water bodies with the grid, set a default bed resistance of 1 day, and seperate the large river ‘Lek’ form the other surface water bodies.

bed_resistance = 1.0

bgt_grid = nlmod.grid.gdf_to_grid(bgt, ds).set_index("cellid")

bgt_grid["cond"] = bgt_grid.area / bed_resistance

# handle the lek as a river
mask = bgt_grid["bronhouder"] == "L0002"
lek = bgt_grid[mask]
bgt_grid = bgt_grid[~mask]

# handle grote gracht and oude haven to model as a lake
ids_grote_gracht = [
    "W0656.774b12049d9a4252bd61c4ea442b5158",
    "W0656.59ab56cf0b2d4f15894c24369f0748df",
]
ids_oude_haven = [
    "W0656.a6013e26cd9442de86eac2295eb0012b",
    "W0656.2053970c192b4fe48bba882842e53eb5",
    "W0656.540780b5c9944b51b53d8a98445b315a",
    "W0656.a7c39fcaabe149c3b9eb4823f76db024",
    "W0656.cb3c3a25de4141d18c573b561f02e84a",
]

mask = bgt_grid["identificatie"].isin(ids_grote_gracht + ids_oude_haven)
lakes = bgt_grid[mask].copy()
lakes["name"] = ""
lakes.loc[lakes["identificatie"].isin(ids_grote_gracht), "name"] = "grotegracht"
lakes.loc[lakes["identificatie"].isin(ids_oude_haven), "name"] = "oudehaven"
bgt_grid = bgt_grid[~mask]

# cut rainfall and evaporation from model dataset
lak_rainfall, lak_evaporation = nlmod.gwf.lake.copy_meteorological_data_from_ds(
    lakes, ds, boundname_column="name"
)
Intersecting with grid:   0%|          | 0/1470 [00:00<?, ?it/s]
Intersecting with grid: 100%|██████████| 1470/1470 [00:05<00:00, 264.38it/s]

Lek as river

Model the river Lek as a river with a fixed stage of 0.5 m NAP

lek["stage"] = 0.0
lek["rbot"] = -3.0
spd = nlmod.gwf.surface_water.build_spd(lek, "RIV", ds)
riv = flopy.mf6.ModflowGwfriv(gwf, stress_period_data={0: spd})
Building stress period data RIV:   0%|          | 0/999 [00:00<?, ?it/s]
Building stress period data RIV: 100%|██████████| 999/999 [00:00<00:00, 7653.59it/s]

Other surface water as drains

Model the other surface water using the drain package, with a summer stage and a winter stage

drn = nlmod.gwf.surface_water.gdf_to_seasonal_pkg(bgt_grid, gwf, ds)
INFO:nlmod.gwf.surface_water.gdf_to_seasonal_pkg:Filling 4147 NaN's in rbot using a water depth of 0.5 meter.
Building stress period data for winter DRN:   0%|          | 0/4147 [00:00<?, ?it/s]
Building stress period data for winter DRN: 100%|██████████| 4147/4147 [00:00<00:00, 7738.11it/s]
Building stress period data for summer DRN:   0%|          | 0/4147 [00:00<?, ?it/s]
Building stress period data for summer DRN: 100%|██████████| 4147/4147 [00:00<00:00, 7776.59it/s]

Add lake

Model de “grote gracht” and “Oude Haven” as lakes. Let the grote gracht overflow into the oude Haven.

# add general properties to the lake gdf
summer_months = (4, 5, 6, 7, 8, 9)
if pd.to_datetime(ds.time.start).month in summer_months:
    lakes["strt"] = lakes["summer_stage"]
else:
    lakes["strt"] = lakes["winter_stage"]
lakes["clake"] = 100

# add inflow to Oude Haven
# ds['inflow_lake'] = xr.DataArray(100, dims=["time"], coords=dict(time=ds.time))
# lakes.loc[lakes['identificatie'].isin(ids_oude_haven), 'INFLOW'] = 'inflow_lake'

# add outlet to Oude Haven, water flows from Oude Haven to Grote Gracht.
lakes.loc[lakes["name"] == "oudehaven", "lakeout"] = "grotegracht"
lakes.loc[lakes["name"] == "oudehaven", "outlet_invert"] = 1.0  # overstort hoogte

# add lake to groundwaterflow model
lak = nlmod.gwf.lake_from_gdf(
    gwf,
    lakes,
    ds,
    boundname_column="name",
    rainfall=lak_rainfall,
    evaporation=lak_evaporation,
)
# create recharge package
rch = nlmod.gwf.rch(ds, gwf)
INFO:nlmod.gwf.gwf.rch:creating mf6 RCH

Run the model

nlmod.sim.write_and_run(sim, ds)
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
writing simulation...
  writing simulation name file...
  writing simulation tdis package...
  writing solution package ims...
  writing model Schoonhoven...
    writing model name file...
    writing package disv...
    writing package npf...
    writing package ic...
    writing package oc...
    writing package sto...
    writing package riv_0...
INFORMATION: maxbound in ('', 'riv', 'dimensions') changed to 999 based on size of stress_period_data
    writing package drn_0...
INFORMATION: maxbound in ('', 'drn', 'dimensions') changed to 8294 based on size of stress_period_data
    writing package obs_0...
    writing package ts_0...
    writing package lak...
    writing package obs_1...
    writing package rch...
    writing package ts_1...
INFO:nlmod.sim.sim.write_and_run:run model
FloPy is using the following executable to run the model: ../../../../../envs/latest/lib/python3.11/site-packages/nlmod/bin/mf6
                                   MODFLOW 6
                U.S. GEOLOGICAL SURVEY MODULAR HYDROLOGIC MODEL
                            VERSION 6.6.3 09/29/2025

   MODFLOW 6 compiled Oct 07 2025 22:51:46 with Intel(R) Fortran Intel(R) 64
   Compiler Classic for applications running on Intel(R) 64, Version 2021.7.0
                             Build 20220726_000000

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): 2026/05/13 15:13:00
 
 Writing simulation list file: mfsim.lst
 Using Simulation name file: mfsim.nam
    Solving:  Stress period:     1    Time step:     1
    Solving:  Stress period:     2    Time step:     1
    Solving:  Stress period:     3    Time step:     1
    Solving:  Stress period:     4    Time step:     1
    Solving:  Stress period:     5    Time step:     1
    Solving:  Stress period:     6    Time step:     1
    Solving:  Stress period:     7    Time step:     1
    Solving:  Stress period:     8    Time step:     1
    Solving:  Stress period:     9    Time step:     1
    Solving:  Stress period:    10    Time step:     1
    Solving:  Stress period:    11    Time step:     1
    Solving:  Stress period:    12    Time step:     1
    Solving:  Stress period:    13    Time step:     1
    Solving:  Stress period:    14    Time step:     1
    Solving:  Stress period:    15    Time step:     1
    Solving:  Stress period:    16    Time step:     1
    Solving:  Stress period:    17    Time step:     1
    Solving:  Stress period:    18    Time step:     1
    Solving:  Stress period:    19    Time step:     1
    Solving:  Stress period:    20    Time step:     1
    Solving:  Stress period:    21    Time step:     1
    Solving:  Stress period:    22    Time step:     1
    Solving:  Stress period:    23    Time step:     1
    Solving:  Stress period:    24    Time step:     1
    Solving:  Stress period:    25    Time step:     1
    Solving:  Stress period:    26    Time step:     1
    Solving:  Stress period:    27    Time step:     1
    Solving:  Stress period:    28    Time step:     1
    Solving:  Stress period:    29    Time step:     1
    Solving:  Stress period:    30    Time step:     1
    Solving:  Stress period:    31    Time step:     1
    Solving:  Stress period:    32    Time step:     1
    Solving:  Stress period:    33    Time step:     1
    Solving:  Stress period:    34    Time step:     1
    Solving:  Stress period:    35    Time step:     1
    Solving:  Stress period:    36    Time step:     1
    Solving:  Stress period:    37    Time step:     1
 
 Run end date and time (yyyy/mm/dd hh:mm:ss): 2026/05/13 15:13:08
 Elapsed run time:  8.132 Seconds
 
 Normal termination of simulation.

Post-processing

Get the simulated head

head = nlmod.gwf.get_heads_da(ds)

Plot the average head in the first layer on a map

norm = matplotlib.colors.Normalize(-2.5, 0.0)


pc = nlmod.plot.map_array(
    head.sel(layer="HLc").mean("time"),
    ds,
    norm=norm,
    colorbar=True,
    colorbar_label="head [m NAP]",
    title="mean head",
)
bgt.dissolve().plot(ax=pc.axes, edgecolor="k", facecolor="none");
../_images/69c7e109b30fa9a536a14acb5c59fd857ae95a52e4b84b799034f04d66a88131.png

Plot the average head in a cross-section, from north to south

x = 118228.0
line = [(x, 439000), (x, 442000)]

f, ax = plt.subplots(figsize=(10, 6), layout="constrained")
dcs = DatasetCrossSection(ds, line, ax=ax, zmin=-100.0, zmax=10.0)
pc = dcs.plot_array(head.mean("time"), norm=norm, head=head.mean("time"))

# add labels with layer names
cbar = nlmod.plot.colorbar_inside(pc)
dcs.plot_grid()
dcs.plot_layers(colors="none", min_label_area=1000)
[<matplotlib.patches.Polygon at 0x7457b85aa550>,
 <matplotlib.patches.Polygon at 0x7457b85a9b50>,
 <matplotlib.patches.Polygon at 0x7457adf06990>,
 <matplotlib.patches.Polygon at 0x7457ae736350>,
 <matplotlib.patches.Polygon at 0x7457adfc5450>,
 <matplotlib.patches.Polygon at 0x7457aef69990>,
 <matplotlib.patches.Polygon at 0x7457ac79bf10>,
 <matplotlib.patches.Polygon at 0x7457ad35c650>,
 <matplotlib.patches.Polygon at 0x7457adb0dc90>,
 <matplotlib.patches.Polygon at 0x7457ad35db10>,
 <matplotlib.patches.Polygon at 0x7457aefa65d0>,
 <matplotlib.patches.Polygon at 0x7457aefa4c50>,
 <matplotlib.patches.Polygon at 0x7457aefa4410>,
 <matplotlib.patches.Polygon at 0x7457ae795c50>,
 <matplotlib.patches.Polygon at 0x7457ae716a90>,
 <matplotlib.patches.Polygon at 0x7457aefec9d0>]
../_images/4c77c93cfa9d27b9fa56904c6db04ef1a7464e9fd98975b046feafde75010518.png

plot a time series at a certain location

x = 118228
y = 439870
head_point = nlmod.gwf.get_head_at_point(head, x=x, y=y, ds=ds)
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
handles = head_point.plot.line(ax=ax, hue="layer")
ax.set_ylabel("head [m NAP]");
../_images/7732fbee7bd88697a44d14447625299a00aebcdadc9cf222b7be383b5eaae273.png

plot the lake stages

df = pd.read_csv(os.path.join(model_ws, "lak_STAGE.csv"), index_col=0)
df.index = ds.time.values
ax = df.plot(figsize=(10, 3))
../_images/4b46cbd40ee41dd4c806cbbbccbba1733bb70ef2425aae9f7259e2e2e5468ef7.png

Compare with BRO measurements

Plot some properties of the first layer

We can plot some properties of the first layer, called HLc. As REGIS does not contain data about hydraulic conductivities for this layer, default values of 1 m/d for kh and 0.1 m/d for hv are used, which can be seen in the graphs below.

layer = "HLc"

f, axes = nlmod.plot.get_map(extent, nrows=2, ncols=2)
variables = ["top", "kh", "botm", "kv"]
for i, variable in enumerate(variables):
    ax = axes.ravel()[i]
    if variable == "top":
        if layer == ds.layer[0]:
            da = ds["top"]
        else:
            da = ds["botm"][np.where(ds.layer == layer)[0][0] - 1]
    else:
        da = ds[variable].sel(layer=layer)
    pc = nlmod.plot.data_array(da, ds=ds, ax=ax)
    nlmod.plot.colorbar_inside(pc, ax=ax)
    ax.text(
        0.5,
        0.98,
        f"{variable} in layer {layer}",
        ha="center",
        va="top",
        transform=ax.transAxes,
    )
../_images/465878f2e480f39f02974ae9065f0313c0f5b800fea8e08818bea48e28e6486e.png

Add pathlines

We create a modpath model which calculates the pathlines. We calculate the pathlines that start in the center of the modflow cells with a river boundary condition (the cells in the “Lek” river).

# create a modpath model
mpf = nlmod.modpath.mpf(gwf)

# create the basic modpath package
_mpfbas = nlmod.modpath.bas(mpf)

# get the nodes from a package
nodes = nlmod.modpath.package_to_nodes(gwf, "RIV_0", ibound=mpf.ib)

# create a particle tracking group from cell centers
pg = nlmod.modpath.pg_from_pd(nodes, localx=0.5, localy=0.5, localz=0.5)

# create the modpath simulation file
mpsim = nlmod.modpath.sim(mpf, pg, "forward", gwf=gwf)
adding Package:  MPBAS
adding Package:  MPSIM
# run modpath model
nlmod.modpath.write_and_run(mpf)
INFO:nlmod.modpath.modpath.write_and_run:write modpath files to model workspace

Writing packages:
   Package:  MPBAS
   Package:  MPSIM
 
INFO:nlmod.modpath.modpath.write_and_run:run modpath model
FloPy is using the following executable to run the model: ../../../../../../envs/latest/lib/python3.11/site-packages/nlmod/bin/mp7_2_002_provisional

MODPATH Version 7.2.002 PROVISIONAL 
Program compiled Oct 16 2023 02:57:43 with IFORT compiler (ver. 20.21.7)
 
Run particle tracking simulation ...
Processing Time Step     1 Period     1.  Time =  3.65200E+03  Steady-state flow
Processing Time Step     1 Period     2.  Time =  3.68300E+03  Steady-state flow                                                    
Processing Time Step     1 Period     3.  Time =  3.71200E+03  Steady-state flow                                                    
Processing Time Step     1 Period     4.  Time =  3.74300E+03  Steady-state flow                                                    
Processing Time Step     1 Period     5.  Time =  3.77300E+03  Steady-state flow                                                    
Processing Time Step     1 Period     6.  Time =  3.80400E+03  Steady-state flow
Processing Time Step     1 Period     7.  Time =  3.83400E+03  Steady-state flow                                                    
Processing Time Step     1 Period     8.  Time =  3.86500E+03  Steady-state flow                                                    
Processing Time Step     1 Period     9.  Time =  3.89600E+03  Steady-state flow                                                    
Processing Time Step     1 Period    10.  Time =  3.92600E+03  Steady-state flow                                                    
Processing Time Step     1 Period    11.  Time =  3.95700E+03  Steady-state flow
Processing Time Step     1 Period    12.  Time =  3.98700E+03  Steady-state flow                                                    
Processing Time Step     1 Period    13.  Time =  4.01800E+03  Steady-state flow                                                    
Processing Time Step     1 Period    14.  Time =  4.04900E+03  Steady-state flow                                                    
Processing Time Step     1 Period    15.  Time =  4.07700E+03  Steady-state flow                                                    
Processing Time Step     1 Period    16.  Time =  4.10800E+03  Steady-state flow
Processing Time Step     1 Period    17.  Time =  4.13800E+03  Steady-state flow                                                    
Processing Time Step     1 Period    18.  Time =  4.16900E+03  Steady-state flow                                                    
Processing Time Step     1 Period    19.  Time =  4.19900E+03  Steady-state flow                                                    
Processing Time Step     1 Period    20.  Time =  4.23000E+03  Steady-state flow                                                    
Processing Time Step     1 Period    21.  Time =  4.26100E+03  Steady-state flow
Processing Time Step     1 Period    22.  Time =  4.29100E+03  Steady-state flow                                                    
Processing Time Step     1 Period    23.  Time =  4.32200E+03  Steady-state flow                                                    
Processing Time Step     1 Period    24.  Time =  4.35200E+03  Steady-state flow                                                    
Processing Time Step     1 Period    25.  Time =  4.38300E+03  Steady-state flow                                                    
Processing Time Step     1 Period    26.  Time =  4.41400E+03  Steady-state flow
Processing Time Step     1 Period    27.  Time =  4.44200E+03  Steady-state flow                                                    
Processing Time Step     1 Period    28.  Time =  4.47300E+03  Steady-state flow                                                    
Processing Time Step     1 Period    29.  Time =  4.50300E+03  Steady-state flow                                                    
Processing Time Step     1 Period    30.  Time =  4.53400E+03  Steady-state flow                                                    
Processing Time Step     1 Period    31.  Time =  4.56400E+03  Steady-state flow
Processing Time Step     1 Period    32.  Time =  4.59500E+03  Steady-state flow                                                    
Processing Time Step     1 Period    33.  Time =  4.62600E+03  Steady-state flow                                                    
Processing Time Step     1 Period    34.  Time =  4.65600E+03  Steady-state flow                                                    
Processing Time Step     1 Period    35.  Time =  4.68700E+03  Steady-state flow                                                    
Processing Time Step     1 Period    36.  Time =  4.71700E+03  Steady-state flow
Processing Time Step     1 Period    37.  Time =  4.74800E+03  Steady-state flow

Particle Summary:
         0 particles are pending release.
         0 particles remain active.
         0 particles terminated at boundary faces.
         0 particles terminated at weak sink cells.
         0 particles terminated at weak source cells.
       999 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.
pdata = nlmod.modpath.load_pathline_data(mpf)
def get_segments(x, y, segments=None):
    # split each flow path into multiple line segments
    return [np.column_stack([x[i : i + 2], y[i : i + 2]]) for i in range(len(x) - 1)]


def get_array(time, to_year=True):
    # for each line-segment use the average time as the color
    array = (time[:-1] + time[1:]) / 2
    if to_year:
        array = array / 365.25
    return array


cmap = plt.get_cmap("turbo")
norm = matplotlib.colors.BoundaryNorm(
    [0, 1, 2, 5, 10, 25, 50, 100, 200, 500], cmap.N, extend="max"
)

# get line segments and color values
segments = []
array = []
for pid in np.unique(pdata["particleid"]):
    pf = pdata[pdata["particleid"] == pid]
    segments.extend(get_segments(pf["x"], pf["y"]))
    array.extend(get_array(pf["time"]))

f, ax = nlmod.plot.get_map(extent)
lc = matplotlib.collections.LineCollection(
    segments, cmap=cmap, norm=norm, array=array, linewidth=1.0
)
line = ax.add_collection(lc)
nlmod.plot.colorbar_inside(line, label="Travel time (years)")

bgt.dissolve().plot(ax=ax, edgecolor="k", facecolor="none");
../_images/445e44a156ecc57ff24d2d6428b827db9180aa042e8d19d1cd7fc19e04e8d28b.png
x = 118228.0
line = LineString([(x, 439000), (x, 442000)])

# get line segments and color values
segments = []
array = []
for pid in np.unique(pdata["particleid"]):
    pf = pdata[pdata["particleid"] == pid]
    d = line.distance(Point(pf["x"][0], pf["y"][0]))
    if d < 200.0:
        x = [line.project(Point(x, y)) for x, y in zip(pf["x"], pf["y"])]
        segments.extend(get_segments(x, pf["z"]))
        array.extend(get_array(pf["time"]))

f, ax = plt.subplots(figsize=(10, 6), layout="constrained")
ax.grid()
dcs = DatasetCrossSection(ds, line, ax=ax, zmin=-100.0, zmax=10.0)
lc = matplotlib.collections.LineCollection(
    segments, cmap=cmap, norm=norm, array=array, linewidth=1.0
)
line = ax.add_collection(lc)
nlmod.plot.colorbar_inside(line, label="Travel time (years)")
# add grid
dcs.plot_grid()
# add labels with layer names
dcs.plot_layers(alpha=0.0, min_label_area=1000)
[<matplotlib.patches.Polygon at 0x745796835e90>,
 <matplotlib.patches.Polygon at 0x745796834e90>,
 <matplotlib.patches.Polygon at 0x7457ad7cdf10>,
 <matplotlib.patches.Polygon at 0x7457ad7cd450>,
 <matplotlib.patches.Polygon at 0x745796811350>,
 <matplotlib.patches.Polygon at 0x7457a93ab350>,
 <matplotlib.patches.Polygon at 0x7457a49b2390>,
 <matplotlib.patches.Polygon at 0x7457a49b2110>,
 <matplotlib.patches.Polygon at 0x7457a49ab1d0>,
 <matplotlib.patches.Polygon at 0x7457a49b0990>,
 <matplotlib.patches.Polygon at 0x7457a4989850>,
 <matplotlib.patches.Polygon at 0x7457a49a1250>,
 <matplotlib.patches.Polygon at 0x7457a49a3e50>,
 <matplotlib.patches.Polygon at 0x7457a4a6d410>,
 <matplotlib.patches.Polygon at 0x7457a4a6d010>,
 <matplotlib.patches.Polygon at 0x7457a4a6fa50>]
../_images/156b2dd1fe0312adf10eea3940ee69f3414bb0193777ee19297361f2fddb5da3.png