Test notebook for Pastas parameter sensitivity analysis with PEST++ SEN Solver

from pathlib import Path

import matplotlib.pyplot as plt
import pandas as pd
import pastas as ps

import pastas_plugins.pest as psp
/home/docs/checkouts/readthedocs.org/user_builds/pastas-plugins/envs/latest/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
head = (
    pd.read_csv(
        "https://raw.githubusercontent.com/pastas/pastas/master/doc/examples/data/head_nb1.csv",
        index_col="date",
        parse_dates=True,
    ).squeeze()
).iloc[-300:]
prec = pd.read_csv(
    "https://raw.githubusercontent.com/pastas/pastas/master/doc/examples/data/rain_nb1.csv",
    index_col="date",
    parse_dates=True,
).squeeze()
evap = pd.read_csv(
    "https://raw.githubusercontent.com/pastas/pastas/master/doc/examples/data/evap_nb1.csv",
    index_col="date",
    parse_dates=True,
).squeeze()
pex = (prec - evap).dropna().rename("PrecipitationExcess")
ml = ps.Model(head)
sm = ps.StressModel(
    pex, ps.Exponential(), name="pex", settings=ps.rcParams["timeseries"]["evap"]
)

ml.add_stressmodel(sm)
ml_sen = ml.copy()
ml_sen.name = "Pestsen"
solver = psp.PestSenSolver(
    exe_name="bin/pestpp-sen",
    model_ws=Path("pestf_sen/model"),
    temp_ws=Path("pestf_sen/temp"),
    master_ws=Path("pestf_sen/master"),
    noptmax=1,
    port_number=4005,
)
ml_sen.solver = solver
ml_sen.solver.set_model(ml_sen)
ml_sen.solver.start()

Hide code cell output

2026-03-10 15:43:16.656424 starting: opening PstFrom.log for logging
2026-03-10 15:43:16.656612 starting PstFrom process
2026-03-10 15:43:16.656641 starting: setting up dirs
2026-03-10 15:43:16.656697 starting: copying original_d '/home/docs/checkouts/readthedocs.org/user_builds/pastas-plugins/checkouts/latest/docs/examples/pestf_sen/model' to new_d '/home/docs/checkouts/readthedocs.org/user_builds/pastas-plugins/checkouts/latest/docs/examples/pestf_sen/temp'
2026-03-10 15:43:16.656842 finished: copying original_d '/home/docs/checkouts/readthedocs.org/user_builds/pastas-plugins/checkouts/latest/docs/examples/pestf_sen/model' to new_d '/home/docs/checkouts/readthedocs.org/user_builds/pastas-plugins/checkouts/latest/docs/examples/pestf_sen/temp' took: 0:00:00.000145
2026-03-10 15:43:16.656961 finished: setting up dirs took: 0:00:00.000320
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[4], line 3
      1 ml_sen = ml.copy()
      2 ml_sen.name = "Pestsen"
----> 3 solver = psp.PestSenSolver(
      4     exe_name="bin/pestpp-sen",
      5     model_ws=Path("pestf_sen/model"),
      6     temp_ws=Path("pestf_sen/temp"),
      7     master_ws=Path("pestf_sen/master"),
      8     noptmax=1,
      9     port_number=4005,
     10 )
     11 ml_sen.solver = solver
     12 ml_sen.solver.set_model(ml_sen)

File ~/checkouts/readthedocs.org/user_builds/pastas-plugins/envs/latest/lib/python3.11/site-packages/pastas_plugins/pest/solver.py:1307, in PestSenSolver.__init__(self, exe_name, model_ws, temp_ws, master_ws, noptmax, control_data, pcov, nfev, port_number, num_workers, use_pypestworker, **kwargs)
   1256 def __init__(
   1257     self,
   1258     exe_name: str | Path = "pestpp-sen",
   (...)   1269     **kwargs,
   1270 ) -> None:
   1271     """
   1272     Initialize the PESTPP-SEN class. This class is used to run the
   1273     PESTPP-SEN analysis and is not really a solver.
   (...)   1305     None
   1306     """
-> 1307     PestSolver.__init__(
   1308         self,
   1309         exe_name=exe_name,
   1310         model_ws=model_ws,
   1311         temp_ws=temp_ws,
   1312         pcov=pcov,
   1313         nfev=nfev,
   1314         control_data=control_data,
   1315         port_number=port_number,
   1316         use_pypestworker=use_pypestworker,
   1317         **kwargs,
   1318     )
   1319     self.master_ws = temp_ws if self.use_pypestworker else master_ws
   1320     self.noptmax = noptmax

File ~/checkouts/readthedocs.org/user_builds/pastas-plugins/envs/latest/lib/python3.11/site-packages/pastas_plugins/pest/solver.py:165, in PestSolver.__init__(self, exe_name, model_ws, temp_ws, noptmax, control_data, pcov, nfev, long_names, port_number, use_pypestworker, **kwargs)
    158 self.exe_name = Path(exe_name)  # pest executable
    159 self.pf = pyemu.utils.PstFrom(
    160     original_d=self.model_ws,
    161     new_d=self.temp_ws,
    162     remove_existing=True,
    163     longnames=long_names,
    164 )
--> 165 copy_file(self.exe_name, self.temp_ws)  # copy pest executable
    166 self.noptmax: int = noptmax
    167 self.control_data: dict[str, Any] = control_data

File ~/.asdf/installs/python/3.11.12/lib/python3.11/shutil.py:431, in copy(src, dst, follow_symlinks)
    429 if os.path.isdir(dst):
    430     dst = os.path.join(dst, os.path.basename(src))
--> 431 copyfile(src, dst, follow_symlinks=follow_symlinks)
    432 copymode(src, dst, follow_symlinks=follow_symlinks)
    433 return dst

File ~/.asdf/installs/python/3.11.12/lib/python3.11/shutil.py:256, in copyfile(src, dst, follow_symlinks)
    254     os.symlink(os.readlink(src), dst)
    255 else:
--> 256     with open(src, 'rb') as fsrc:
    257         try:
    258             with open(dst, 'wb') as fdst:
    259                 # macOS

FileNotFoundError: [Errno 2] No such file or directory: 'bin/pestpp-sen'
df = pd.read_csv(
    ml_sen.solver.master_ws / "pest.msn", index_col="parameter_name"
).set_index(ml.parameters.index)
df.head()
n_samples sen_mean sen_mean_abs sen_std_dev
pex_A 4 -2125350.00 2125350.00 2686220.00
pex_a 4 -174948.00 174948.00 349897.00
constant_d 4 1158.04 1369.95 2072.42
df = df.loc[df.sen_mean_abs > 1e-6, :]
df.loc[:, ["sen_mean_abs", "sen_std_dev"]].plot(kind="bar", figsize=(7, 4))
# ax = plt.gca()
# ax.set_ylim(1,ax.get_ylim()[1]*1.1)
plt.yscale("log")
fig, ax = plt.subplots(1, 1, figsize=(7.0, 8))
tmp_df = df
ax.scatter(tmp_df.sen_mean_abs, tmp_df.sen_std_dev, marker="^", s=20, c="r")
tmp_df = tmp_df.iloc[:8]
for x, y, n in zip(tmp_df.sen_mean_abs, tmp_df.sen_std_dev, tmp_df.index):
    ax.text(x, y, n)
mx = max(ax.get_xlim()[1], ax.get_ylim()[1])
mn = min(ax.get_xlim()[0], ax.get_ylim()[0])
ax.plot([mn, mx], [mn, mx], "k--")
ax.set_ylim(mn, mx)
ax.set_xlim(mn, mx)
ax.grid()
ax.set_ylabel("$\\sigma$")
ax.set_xlabel("$\\mu^*$")
plt.tight_layout()
../_images/369a671fdec0634371e678e98a75f08ec4fc004a5f8e0ad8ca1029915bf224b9.png ../_images/7f83c18e4413a2320ee93ded5beb8f1c60882a7bc49491419f71263b42cb2857.png