Using a Modflow DRN model as a stressmodel in Pastas

Please note that the showcased workflow is not how we intend to keep things. Major breaking changes in the next release.

import os

import flopy
import pandas as pd
import pastas as ps

import pastas_plugins.modflow as ppmf

bindir = "bin"
mf6_exe = os.path.join(bindir, "mf6.exe")
if not os.path.isfile(mf6_exe):
    if not os.path.isdir(bindir):
        os.makedirs(bindir)
    flopy.utils.get_modflow("bin", repo="modflow6")
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[1], line 7
      4 import pandas as pd
      5 import pastas as ps
----> 7 import pastas_plugins.modflow as ppmf
      9 bindir = "bin"
     10 mf6_exe = os.path.join(bindir, "mf6.exe")

File ~/checkouts/readthedocs.org/user_builds/pastas-plugins/envs/latest/lib/python3.11/site-packages/pastas_plugins/modflow/__init__.py:9
      1 # ruff: noqa: F401
      2 from pastas_plugins.modflow.modflow import (
      3     ModflowDrn,
      4     ModflowDrnSto,
   (...)      7     ModflowUzf,
      8 )
----> 9 from pastas_plugins.modflow.stressmodels import ModflowModel
     10 from pastas_plugins.modflow.version import __version__

File ~/checkouts/readthedocs.org/user_builds/pastas-plugins/envs/latest/lib/python3.11/site-packages/pastas_plugins/modflow/stressmodels.py:7
      5 from pastas.stressmodels import StressModelBase
      6 from pastas.timeseries import TimeSeries
----> 7 from pastas.typing import ArrayLike, TimestampType
      9 from .modflow import Modflow
     11 logger = getLogger(__name__)

ImportError: cannot import name 'TimestampType' from 'pastas.typing' (/home/docs/checkouts/readthedocs.org/user_builds/pastas-plugins/envs/latest/lib/python3.11/site-packages/pastas/typing/__init__.py)
df = pd.read_csv("data/aftopping.csv", index_col=0, parse_dates=True)
head = df["B28H1808_2"].dropna()
prec = df["Precipitation"]
evap = df["Evaporation"]

Try to model TARSO with a one cell MODFLOW model

ml_tarso = ps.Model(head, constant=False)
ml_tarso.add_stressmodel(
    ps.TarsoModel(
        prec=prec, evap=evap, rfunc=ps.Exponential(), name="exp", oseries=head
    )
)
ml_tarso.solve()
ml_tarso.plot();
Fit report B28H1808_2             Fit Statistics
================================================
nfev    16                     EVP         96.88
nobs    1261                   R2           0.97
noise   False                  RMSE         0.05
tmin    2012-06-07 00:00:00    AICc     -7360.14
tmax    2017-02-01 00:00:00    BIC      -7324.25
freq    D                      Obj          1.82
warmup  3650 days 00:00:00     ___              
solver  LeastSquares           Interp.        No

Parameters (7 optimized)
================================================
           optimal     initial  vary
exp_A0  661.881263  258.226873  True
exp_a0  117.485675   10.000000  True
exp_d0   19.664685   19.675000  True
exp_A1  368.738780  258.226873  True
exp_a1  138.281724   10.000000  True
exp_d1   20.000696   20.032500  True
exp_f    -1.060276   -1.000000  True
../_images/1717c57b5b7a52ab359ab0ed39de1cd4207cbca4d78bdb9c61bff725b8201a7e.png

We generate a model with ModflowDrnSto, which adds a drain at a certain threshold-value, and modifies the storage coefficient above this level. If we calculate the parameter-values from the optimized Pastas model with TarsoModel, we see this model returns the same simulation.

# Generate a Model with ModflowDrnSto
ml_mds = ps.Model(head, constant=False)
modflow = ppmf.ModflowDrnSto(
    exe_name=mf6_exe, sim_ws="mf_files/drn_gw", head=head, silent=True
)
mm = ppmf.ModflowModel(prec=prec, evap=evap, modflow=modflow, name="modflow")
ml_mds.add_stressmodel(mm)

# take parameters from the model with the TarsoModel
p_tarso = ml_tarso.parameters["optimal"]
ml_mds.set_parameter(f"{mm.name}_d", initial=p_tarso["exp_d0"])
ml_mds.set_parameter(f"{mm.name}_c", initial=p_tarso["exp_A0"])
s = p_tarso["exp_a0"] / p_tarso["exp_A0"]
ml_mds.set_parameter(f"{mm.name}_s", initial=s)
ml_mds.set_parameter(f"{mm.name}_f", initial=p_tarso["exp_f"])
h_drn = p_tarso["exp_d1"] - p_tarso["exp_d0"]
ml_mds.set_parameter(f"{mm.name}_h_drn", initial=h_drn)
ml_mds.set_parameter(f"{mm.name}_c_drn", initial=p_tarso["exp_A1"])
s_drn = p_tarso["exp_a1"] / p_tarso["exp_A1"]
ml_mds.set_parameter(f"{mm.name}_s_drn", initial=s_drn)

# initialize model and plot
ml_mds.initialize()
ax = ml_mds.plot()
Model is not optimized yet, initial parameters are used.
Model is not optimized yet, initial parameters are used.
../_images/7055bb6538a5c4417d506b176191e3571e2af9bcc41b2a11126acf58b089e4e7.png
ml_mds.parameters
initial pmin pmax vary name dist stderr optimal
modflow_d 19.664685 18.960 2.039000e+01 True modflow uniform NaN NaN
modflow_c 661.881263 10.000 1.000000e+08 True modflow uniform NaN NaN
modflow_s 0.177503 0.001 5.000000e-01 True modflow uniform NaN NaN
modflow_f -1.060276 -2.000 0.000000e+00 True modflow uniform NaN NaN
modflow_h_drn 0.336012 0.000 1.000000e+01 True modflow uniform NaN NaN
modflow_c_drn 368.738780 10.000 1.000000e+08 True modflow uniform NaN NaN
modflow_s_drn 0.375013 0.001 1.000000e+00 True modflow uniform NaN NaN

We can plot the difference between the two models. This difference is smaller than the closing criterium of the MODFLOW-model (0.01 meter).

# plot the difference between both models
h1 = ml_tarso.simulate()
h2 = ml_mds.simulate()
(h1 - h2).dropna().plot();
Model is not optimized yet, initial parameters are used.
../_images/c510d27eae07b7bade6ec703ab91bfd5057901c23bea08ed49ff52a9a0eb56c5.png

Try to model ThresholdTransform with a one cell MODFLOW model

ml_tt = ps.Model(head)
ml_tt.add_stressmodel(ps.RechargeModel(prec=prec, evap=evap, rfunc=ps.Exponential()))
ml_tt.add_transform(ps.ThresholdTransform())
ml_tt.solve()
ml_tt.plot();
Fit report B28H1808_2               Fit Statistics
==================================================
nfev    26                     EVP           96.82
nobs    1261                   R2             0.97
noise   False                  RMSE           0.05
tmin    2012-06-07 00:00:00    AICc       -7335.76
tmax    2017-02-01 00:00:00    BIC        -7304.99
freq    D                      Obj            1.86
warmup  3650 days 00:00:00     ___                
solver  LeastSquares           Interp.          No

Parameters (6 optimized)
==================================================
                         optimal     initial  vary
recharge_A            594.799905  258.226873  True
recharge_a            103.941136   10.000000  True
recharge_f             -1.060085   -1.000000  True
constant_d             19.658142   19.880246  True
ThresholdTransform_1   20.008782   20.032500  True
ThresholdTransform_2    0.441636    0.500000  True
../_images/eef8bcfadc828b6dde1bf4893dc175368be85b1735d46562967de66809cd70d4.png

We try to model this sytyem with ModflowSto. However, this does not give us the same simulation.

ml_ms = ps.Model(head, constant=False)
modflow = ppmf.ModflowSto(
    exe_name=mf6_exe, sim_ws="mf_files/drn_gw", head=head, silent=True
)
mm = ppmf.ModflowModel(prec=prec, evap=evap, modflow=modflow, name="modflow")
ml_ms.add_stressmodel(mm)

p_tt = ml_tt.parameters["optimal"]
ml_ms.set_parameter(f"{mm.name}_d", initial=p_tt["constant_d"])
ml_ms.set_parameter(f"{mm.name}_c", initial=p_tt["recharge_A"])
s = p_tt["recharge_a"] / p_tt["recharge_A"]
ml_ms.set_parameter(f"{mm.name}_s", initial=s)
ml_ms.set_parameter(f"{mm.name}_f", initial=p_tt["recharge_f"])
h_drn = p_tt["ThresholdTransform_1"] - p_tt["constant_d"]
ml_ms.set_parameter(f"{mm.name}_h_drn", initial=h_drn)
s_drn = s / p_tt["ThresholdTransform_2"]
ml_ms.set_parameter(f"{mm.name}_s_drn", initial=s_drn)

ml_ms.initialize()
ax = ml_ms.plot()
Model is not optimized yet, initial parameters are used.
Model is not optimized yet, initial parameters are used.
../_images/10c914d54d60211b313ef9b2292462020ca7bcb9a25e1e6cd971f29940ef1522.png

With ModflowDrnSto we come closer, but we do not get the same result as the Pastas Model with the ThresholdTransform. It is not possible to model the ThresholdTransform in a one-cell Modflow model. ThresholdTransform calculates the discharged water using the non-transformed simulation. However, in the Modflow model, the calculation of the discharge is calculated using the simulation that is affected by the larger storage coefficient.

# Generate a Model with ModflowDrnSto
ml_mds = ps.Model(head, constant=False)
modflow = ppmf.ModflowDrnSto(
    exe_name=mf6_exe, sim_ws="mf_files/drn_gw", head=head, silent=True
)
mm = ppmf.ModflowModel(prec=prec, evap=evap, modflow=modflow, name="modflow")
ml_mds.add_stressmodel(mm)

# take parameters from the model with the TarsoModel
p_tt = ml_tt.parameters["optimal"]
ml_mds.set_parameter(f"{mm.name}_d", initial=p_tt["constant_d"])
ml_mds.set_parameter(f"{mm.name}_c", initial=p_tt["recharge_A"])
s = p_tt["recharge_a"] / p_tt["recharge_A"]
ml_mds.set_parameter(f"{mm.name}_s", initial=s)
ml_mds.set_parameter(f"{mm.name}_f", initial=p_tt["recharge_f"])
h_drn = p_tt["ThresholdTransform_1"] - p_tt["constant_d"]
ml_mds.set_parameter(f"{mm.name}_h_drn", initial=h_drn)
c_drn = p_tt["recharge_A"] * p_tt["ThresholdTransform_2"]
ml_mds.set_parameter(f"{mm.name}_c_drn", initial=c_drn)
s_drn = s / p_tt["ThresholdTransform_2"]
ml_mds.set_parameter(f"{mm.name}_s_drn", initial=s_drn)

# initialize model and plot
ml_mds.initialize()
ax = ml_mds.plot()
Model is not optimized yet, initial parameters are used.
Model is not optimized yet, initial parameters are used.
../_images/317fba89345bd1e54a1bacfa3c0e1c2cef8166f3a879e031728098d30e3842aa.png