pastas-plugins 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ # ruff : noqa: F401
2
+ import pathlib
3
+ from importlib import import_module
4
+
5
+ # from platform import python_version
6
+ # from packaging.version import parse as parse_version
7
+ from pastas_plugins.version import __version__
8
+
9
+
10
+ def list_plugins():
11
+ plugins = pathlib.Path(__file__).parent.iterdir()
12
+ plugins = [
13
+ plugin.stem
14
+ for plugin in plugins
15
+ if plugin.is_dir() and not plugin.stem.startswith("_")
16
+ ]
17
+ plugins.sort()
18
+ return plugins
19
+
20
+
21
+ def show_plugin_versions():
22
+ showtip = False
23
+ plugins = list_plugins()
24
+ msg = f"pastas_plugins version : {__version__}\n"
25
+ for plugin in plugins:
26
+ try:
27
+ module = import_module(f"pastas_plugins.{plugin}.version")
28
+ version = module.__version__
29
+ except ModuleNotFoundError:
30
+ showtip = True
31
+ version = "not available (check dependencies)"
32
+ msg += f"- {(plugin + ' version'):25s} : {version}\n"
33
+ if showtip:
34
+ msg += "\nNote: To install missing dependencies use `pip install pastas-plugins[<plugin-name>]`"
35
+ print(msg)
@@ -0,0 +1,8 @@
1
+ # ruff: noqa: F401
2
+ from pastas_plugins.cross_correlation.cross_correlation import (
3
+ ccf,
4
+ fit_response,
5
+ prewhiten,
6
+ )
7
+ from pastas_plugins.cross_correlation.plots import plot_ccf_overview, plot_corr
8
+ from pastas_plugins.cross_correlation.version import __version__
@@ -0,0 +1,188 @@
1
+ from typing import Tuple, Union
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ import pastas as ps
6
+ import scipy as sc
7
+ from statsmodels.tsa.ar_model import AutoReg
8
+ from statsmodels.tsa.arima.model import ARIMA
9
+ from statsmodels.tsa.filters.filtertools import convolution_filter
10
+
11
+
12
+ def ccf(
13
+ x: pd.Series,
14
+ y: pd.Series,
15
+ nlags: Union[int, None] = None,
16
+ adjusted: bool = True,
17
+ alpha: Union[float, None] = None,
18
+ ) -> Union[pd.Series, pd.DataFrame]:
19
+ """Cross-correlation of two time series.
20
+
21
+ Parameters
22
+ ----------
23
+ x : pd.Series
24
+ Time series
25
+ y : pd.Series
26
+ Time series, len(y) should be equal to len(x).
27
+ nlags : int or None, optional
28
+ Number of lags to return cross-correlations for, by default None which
29
+ uses nlags equal to len(x).
30
+ adjusted : bool, optional
31
+ If True, denominators for cross-correlation are len(x)-k, otherwise
32
+ len(x), by default True
33
+ alpha : float or None, optional
34
+ If a float between 0 and 1 is given, the confidence intervals for the
35
+ given level are returned in a DataFrame. For instance if alpha=0.05,
36
+ 95% confidence intervals are returned where the standard deviation is
37
+ computed according to 1/sqrt(len(x)).
38
+
39
+ Returns
40
+ -------
41
+ pandas Series or DataFrame
42
+ """
43
+ # check if lengths are equal
44
+ assert len(x) == len(y), "Length of series x and y should be equal"
45
+ # check if series are equidistant
46
+ for series in (x, y):
47
+ if pd.infer_freq(series.index) is None:
48
+ msg = (
49
+ "The frequency of the index of time series %s could not be "
50
+ "inferred. Please provide a time series with a equidistant time step."
51
+ )
52
+ raise ValueError(msg % series.name)
53
+
54
+ n = len(x)
55
+
56
+ xbar = x - x.mean()
57
+ ybar = y - y.mean()
58
+
59
+ d = np.arange(n, 0, -1) if adjusted else n
60
+ cc = sc.signal.correlate(xbar, ybar, mode="full", method="fft")
61
+ cvf = cc[n - 1 :] / (np.std(x) * np.std(y) * d)
62
+
63
+ nlags = n if nlags is None else nlags
64
+ index = pd.Index(np.arange(nlags), name="Lags")
65
+ ret = cvf[:nlags]
66
+
67
+ if alpha is not None:
68
+ interval = sc.stats.norm.ppf(1.0 - alpha / 2.0) / np.sqrt(n)
69
+ crosscorr = pd.DataFrame(
70
+ data=np.vstack([ret, ret - interval, ret + interval]).T,
71
+ index=index,
72
+ columns=["Cross-correlation", f"CI {alpha / 2}", f"CI {1 - alpha / 2}"],
73
+ )
74
+ else:
75
+ crosscorr = pd.Series(
76
+ data=ret,
77
+ index=index,
78
+ name="Cross-correlation",
79
+ )
80
+ return crosscorr
81
+
82
+
83
+ def prewhiten(
84
+ x: pd.Series, y: Union[pd.Series, None] = None, ar: int = 20, arima: bool = False
85
+ ) -> Union[pd.Series, Tuple[pd.Series]]:
86
+ """Prewhiten time series using AR(ar) model.
87
+
88
+ An AR(ar) model is fitted on time series x. The goal is to obtain residuals that
89
+ adhere to a white noise process. Next, the AR(ar) model is applied to time series Y.
90
+
91
+ Note
92
+ ----
93
+ If prewhitened time series for x still shows significant autocorrelation or partial
94
+ autocorrelation, try increasing the number of autoregressive parameters.
95
+
96
+ Parameters
97
+ ----------
98
+ x : pd.Series
99
+ time series on which AR(ar) model will be fitted
100
+ y : pd.Series, optional
101
+ time series that will be filtered using the AR(ar) model fitted on x
102
+ ar : int, optional
103
+ number of autoregressive parameters (sometimes called `p`), by default 20
104
+ arima: bool, optional
105
+ use an ARIMA(ar,0,0) model instead of an AR(ar) model, by default False
106
+ which causes a significant speedup at the cost of a very small accuracy
107
+ penalty
108
+
109
+ Returns
110
+ -------
111
+ pwx : pd.Series
112
+ prewhitened time series for x (should no longer show significant
113
+ autocorrelation or partial autocorrelation)
114
+ pwy : pd.Series, optional
115
+ prewhitened time series for y, if y is provided
116
+ """
117
+
118
+ # fit AR model on x
119
+ if arima:
120
+ ml = ARIMA(x.values, order=(ar, 0, 0), trend="c").fit()
121
+ else:
122
+ ml = AutoReg(x.values, lags=ar, trend="c").fit()
123
+
124
+ # get model filtered model residuals
125
+ residuals = ml.resid[ar:] if arima else ml.resid
126
+ pwx = pd.Series(residuals, index=x.index[ar:])
127
+
128
+ if y is not None:
129
+ # apply same filter on y
130
+ arparams = ml.arparams if arima else ml.params[1:]
131
+ filt = np.append(1.0, -arparams)
132
+ pwy = convolution_filter(y.values, filt=filt, nsides=1)
133
+ pwy = pd.Series(data=pwy[ar:], index=y.index[ar:])
134
+ return pwx, pwy
135
+ else:
136
+ return pwx
137
+
138
+
139
+ def fit_response(
140
+ ccf: pd.Series,
141
+ rfunc: ps.typing.RFunc,
142
+ scale_factor: float = 1.0,
143
+ dt: float = 1.0,
144
+ ) -> np.ndarray[float]:
145
+ """Fit the response function to the cross-correlation function using least
146
+ squares optimization.
147
+
148
+ Parameters:
149
+ -----------
150
+ ccf : pd.Series
151
+ The cross-correlation function.
152
+ rfunc : ps.typing.RFunc
153
+ The response function to fit on the impulse response.
154
+ scale_factor : float, optional
155
+ Scale factor applied to the cross-correlation function to obtain the
156
+ impulse response, by default 1.0.
157
+ dt : float, optional
158
+ Time step of the response function, by default 1.0.
159
+
160
+ Returns:
161
+ --------
162
+ np.ndarray[float]
163
+ The optimized parameters of the response function.
164
+
165
+ """
166
+
167
+ def obj_func(p):
168
+ """Objective function for least squares optimization."""
169
+ impulse_response = (ccf * scale_factor).values
170
+ blockr = rfunc.block(p, dt=dt, cutoff=rfunc.cutoff)
171
+
172
+ # make sure length of residuals is constant
173
+ if len(blockr) > len(impulse_response):
174
+ blockr = blockr[: len(impulse_response)]
175
+ elif len(blockr) < len(impulse_response):
176
+ blockr = np.append(blockr, np.zeros(len(impulse_response) - len(blockr)))
177
+
178
+ return impulse_response - blockr
179
+
180
+ params = rfunc.get_init_parameters(rfunc._name)
181
+ pini = params["initial"].values
182
+ bounds = (
183
+ params["pmin"].fillna(-np.inf).values,
184
+ params["pmax"].fillna(np.inf).values,
185
+ )
186
+
187
+ res = sc.optimize.least_squares(obj_func, x0=pini, bounds=bounds)
188
+ return res.x
@@ -0,0 +1,245 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+ import pandas as pd
4
+ from statsmodels.graphics.tsaplots import _plot_corr, plot_acf, plot_pacf
5
+
6
+ from pastas_plugins.cross_correlation.cross_correlation import ccf
7
+
8
+
9
+ def plot_corr(
10
+ corr: pd.Series | pd.DataFrame,
11
+ ax: plt.Axes | None = None,
12
+ vlines_kwargs: dict | None = None,
13
+ **kwargs,
14
+ ):
15
+ """Helper function for the statsmodels _plot_corr function.
16
+
17
+ Parameters
18
+ ----------
19
+ corr : pd.Series or pd.DataFrame
20
+ the correlation result to plot
21
+ ax : plt.Axes, optional
22
+ axes to plot on, by default None
23
+ vlines_kwargs : dict, optional
24
+ keyword arguments for the vlines function, by default None
25
+
26
+ Returns
27
+ -------
28
+ plt.Axes
29
+ axes with the plot
30
+ """
31
+
32
+ if ax is None:
33
+ _, ax = plt.subplots(**kwargs)
34
+
35
+ acf_x = corr.values if isinstance(corr, pd.Series) else corr.iloc[:, 0].values
36
+ confint = corr.iloc[:, 1:3].values if isinstance(corr, pd.DataFrame) else None
37
+ lags = corr.index.values
38
+ vlines_kwargs = {} if vlines_kwargs is None else vlines_kwargs
39
+ _plot_corr(
40
+ ax=ax,
41
+ title="",
42
+ acf_x=acf_x,
43
+ confint=confint,
44
+ lags=lags,
45
+ irregular=False,
46
+ use_vlines=True,
47
+ vlines_kwargs=vlines_kwargs,
48
+ )
49
+ return ax
50
+
51
+
52
+ def plot_ccf_overview(x, y, nlags=None, tmin=None, tmax=None, axes=None):
53
+ """Plot an overview of the cross-correlation between two time series.
54
+
55
+ Parameters
56
+ ----------
57
+ x : pd.Series
58
+ Time series 1
59
+ y : pd.Series
60
+ Time series 2
61
+ nlags : int, optional
62
+ number of lags to return cross-correlations for, by default None which
63
+ uses number of lags equal to len(x).
64
+ tmin : str or pd.Timestamp, optional
65
+ tmin for both time series, by default None
66
+ tmax : str or pd.Timestamp, optional
67
+ tmax for both time series, by default None
68
+ axes : Axes mosaic, optional
69
+ if provided, use axes from previous plot
70
+
71
+ Returns
72
+ -------
73
+ axes : Axes mosaic
74
+ return axes of subplots mosaic
75
+ """
76
+ if tmin is None:
77
+ tmin = np.min([x.index[0], y.index[0]])
78
+ if tmax is None:
79
+ tmax = np.max([x.index[-1], y.index[-1]])
80
+
81
+ x = x.loc[tmin:tmax]
82
+ y = y.loc[tmin:tmax]
83
+
84
+ if axes is None:
85
+ mosaic = [
86
+ ["x", "x", "norm", "norm"],
87
+ ["y", "y", "norm", "norm"],
88
+ ["x-acf", "y-acf", "ccf", "ccf"],
89
+ ["x-pacf", "y-pacf", "ccf", "ccf"],
90
+ ]
91
+
92
+ fig, axes = plt.subplot_mosaic(mosaic, figsize=(16, 8))
93
+ rescale_axes = False
94
+ newaxes = True
95
+ color1 = "C0"
96
+ color2 = "C1"
97
+ else:
98
+ fig = axes["x"].figure
99
+ rescale_axes = True
100
+ newaxes = False
101
+ color1 = "C2"
102
+ color2 = "C3"
103
+
104
+ # set names if not provided
105
+ if x.name is None:
106
+ x.name = "x"
107
+ if y.name is None:
108
+ y.name = "y"
109
+
110
+ # plot time series
111
+ axes["x"].plot(x.index, x, label=x.name, color=color1)
112
+ axes["x"].legend(loc=(0, 1), frameon=False)
113
+ axes["x"].set_ylabel("x")
114
+ axes["x"].set_xlim(pd.Timestamp(tmin), pd.Timestamp(tmax))
115
+ axes["y"].plot(y.index, y, label=y.name, c=color2)
116
+ axes["y"].legend(loc=(0, 1), frameon=False)
117
+ axes["y"].set_ylabel("y")
118
+ axes["y"].set_xlim(pd.Timestamp(tmin), pd.Timestamp(tmax))
119
+
120
+ # plot normalized series
121
+ xnorm = (x - x.mean()) / x.std()
122
+ ynorm = (y - y.mean()) / y.std()
123
+ axes["norm"].plot(
124
+ xnorm.index, xnorm, label=x.name + " (normalized)", alpha=0.7, color=color1
125
+ )
126
+ axes["norm"].plot(
127
+ ynorm.index, ynorm, label=y.name + " (normalized)", alpha=0.7, color=color2
128
+ )
129
+ axes["norm"].legend(loc=(0, 1), frameon=False, ncol=2)
130
+ axes["norm"].set_ylabel("normalized [-]")
131
+ axes["norm"].set_xlim(pd.Timestamp(tmin), pd.Timestamp(tmax))
132
+ handles, _ = axes["norm"].get_legend_handles_labels()
133
+
134
+ # plot acf, pacf
135
+ plot_acf(
136
+ xnorm,
137
+ ax=axes["x-acf"],
138
+ color=color1,
139
+ alpha=0.05,
140
+ title="",
141
+ zero=False,
142
+ auto_ylims=True,
143
+ vlines_kwargs={"color": "k"},
144
+ )
145
+ plot_acf(
146
+ ynorm,
147
+ ax=axes["y-acf"],
148
+ color=color2,
149
+ alpha=0.05,
150
+ title="",
151
+ zero=False,
152
+ auto_ylims=True,
153
+ vlines_kwargs={"color": "k"},
154
+ )
155
+ axes["x-acf"].set_xlim(left=0.0)
156
+ axes["x-acf"].set_ylabel("ACF [-]")
157
+ (p1,) = axes["x-acf"].plot([], [], marker="o", ls="none", color=color1)
158
+ if not newaxes:
159
+ leg = axes["x-acf"].get_legend()
160
+ handles = leg.legend_handles
161
+ labels = [t.get_text() for t in leg.get_texts()]
162
+ handles += [p1]
163
+ labels += [x.name]
164
+ else:
165
+ handles = [p1]
166
+ labels = [x.name]
167
+
168
+ axes["x-acf"].legend(handles, labels, loc=(0, 1), frameon=False)
169
+ axes["y-acf"].set_xlim(left=0.0)
170
+ axes["y-acf"].get_children()[3].set_facecolor(color2)
171
+ (p2,) = axes["y-acf"].plot([], [], marker="o", ls="none", color=color2)
172
+ if not newaxes:
173
+ leg = axes["y-acf"].get_legend()
174
+ handles = leg.legend_handles
175
+ labels = [t.get_text() for t in leg.get_texts()]
176
+ handles += [p2]
177
+ labels += [y.name]
178
+ else:
179
+ handles = [p2]
180
+ labels = [y.name]
181
+ axes["y-acf"].legend(handles, labels, loc=(0, 1), frameon=False)
182
+
183
+ plot_pacf(
184
+ xnorm,
185
+ method="ywm",
186
+ ax=axes["x-pacf"],
187
+ color=color1,
188
+ alpha=0.05,
189
+ title="",
190
+ zero=False,
191
+ auto_ylims=True,
192
+ vlines_kwargs={"color": "k"},
193
+ )
194
+ plot_pacf(
195
+ ynorm,
196
+ method="ywm",
197
+ ax=axes["y-pacf"],
198
+ color=color2,
199
+ alpha=0.05,
200
+ title="",
201
+ zero=False,
202
+ auto_ylims=True,
203
+ vlines_kwargs={"color": "k"},
204
+ )
205
+ axes["x-pacf"].set_xlim(left=0.0)
206
+ axes["x-pacf"].set_ylabel("PACF [-]")
207
+ axes["x-pacf"].set_xlabel("Lags")
208
+
209
+ axes["y-pacf"].set_xlim(left=0.0)
210
+ axes["y-pacf"].get_children()[3].set_facecolor("C1")
211
+ axes["y-pacf"].set_xlabel("Lags")
212
+
213
+ # ccf
214
+ cc = ccf(x, y, nlags=nlags)
215
+ axes["ccf"].bar(
216
+ cc.index,
217
+ cc,
218
+ width=1.0,
219
+ linewidth=0.5,
220
+ alpha=0.5,
221
+ label=f"CCF ({x.name}|{y.name})",
222
+ )
223
+ axes["ccf"].set_ylabel("CCF [-]")
224
+ axes["ccf"].set_xlabel("Lags")
225
+ axes["ccf"].legend(loc=(0, 1), frameon=False)
226
+ axes["ccf"].set_xlim(left=0.0)
227
+
228
+ share_x = [axes["x"], axes["y"], axes["norm"]]
229
+ for i, iax in enumerate(share_x):
230
+ if i < (len(share_x) - 1):
231
+ iax.sharex(share_x[-1])
232
+
233
+ # share_x = [axes["x-acf"], axes["x-pacf"], axes["y-acf"], axes["y-pacf"]]
234
+ # for i, iax in enumerate(share_x):
235
+ # if i < (len(share_x) - 1):
236
+ # iax.sharex(share_x[-1])
237
+
238
+ fig.tight_layout()
239
+ fig.align_ylabels()
240
+
241
+ if rescale_axes:
242
+ for iax in axes.values():
243
+ iax.autoscale()
244
+
245
+ return axes
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ # ruff: noqa: F401
2
+ from pastas_plugins.modflow.modflow import ModflowRch
3
+ from pastas_plugins.modflow.stressmodels import ModflowModel
4
+ from pastas_plugins.modflow.version import __version__
@@ -0,0 +1,186 @@
1
+ import functools
2
+ import logging
3
+ from typing import List, Protocol
4
+
5
+ import flopy
6
+ import numpy as np
7
+ from pandas import DataFrame, Series
8
+ from pastas.typing import ArrayLike
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class Modflow(Protocol):
14
+ def __init__(self) -> None: ...
15
+
16
+ def get_init_parameters(self) -> DataFrame: ...
17
+
18
+ def create_model(self) -> None: ...
19
+
20
+ def simulate(self) -> ArrayLike: ...
21
+
22
+
23
+ class ModflowRch:
24
+ def __init__(
25
+ self, exe_name: str, sim_ws: str, raise_on_modflow_error: bool = False
26
+ ):
27
+ self.exe_name = exe_name
28
+ self.sim_ws = sim_ws
29
+ self._name = "mf_rch"
30
+ self._stress = None
31
+ self._simulation = None
32
+ self._gwf = None
33
+ self._changing_packages = (
34
+ "STO",
35
+ "GHB",
36
+ "RCH",
37
+ )
38
+ self.raise_on_modflow_error = raise_on_modflow_error
39
+
40
+ def get_init_parameters(self, name: str) -> DataFrame:
41
+ parameters = DataFrame(columns=["initial", "pmin", "pmax", "vary", "name"])
42
+ parameters.loc[name + "_sy"] = (0.05, 0.001, 0.5, True, name)
43
+ parameters.loc[name + "_c"] = (220, 1e1, 1e8, True, name)
44
+ parameters.loc[name + "_f"] = (-1.0, -2.0, 0.0, True, name)
45
+ return parameters
46
+
47
+ def create_model(self) -> None:
48
+ sim = flopy.mf6.MFSimulation(
49
+ sim_name=self._name,
50
+ version="mf6",
51
+ exe_name=self.exe_name,
52
+ sim_ws=self.sim_ws,
53
+ lazy_io=True,
54
+ )
55
+
56
+ _ = flopy.mf6.ModflowTdis(
57
+ sim,
58
+ time_units="DAYS",
59
+ nper=self._nper,
60
+ perioddata=[(1, 1, 1) for _ in range(self._nper)],
61
+ )
62
+
63
+ gwf = flopy.mf6.ModflowGwf(
64
+ sim,
65
+ modelname=self._name,
66
+ )
67
+
68
+ _ = flopy.mf6.ModflowIms(
69
+ sim,
70
+ complexity="SIMPLE",
71
+ outer_dvclose=1e-2,
72
+ inner_dvclose=1e-2,
73
+ rcloserecord=1e-1,
74
+ linear_acceleration="BICGSTAB",
75
+ pname=None,
76
+ )
77
+ # sim.register_ims_package(imsgwf, [self._name])
78
+
79
+ _ = flopy.mf6.ModflowGwfdis(
80
+ gwf,
81
+ length_units="METERS",
82
+ nlay=1,
83
+ nrow=1,
84
+ ncol=1,
85
+ delr=1,
86
+ delc=1,
87
+ top=1.0,
88
+ botm=0.0,
89
+ idomain=1,
90
+ pname=None,
91
+ )
92
+
93
+ _ = flopy.mf6.ModflowGwfnpf(
94
+ gwf, save_flows=False, icelltype=0, k=1.0, pname="npf"
95
+ )
96
+
97
+ _ = flopy.mf6.ModflowGwfic(gwf, strt=0.0, pname="ic")
98
+
99
+ _ = flopy.mf6.ModflowGwfoc(
100
+ gwf,
101
+ head_filerecord=f"{gwf.name}.hds",
102
+ saverecord=[("HEAD", "ALL")],
103
+ pname=None,
104
+ )
105
+
106
+ sim.write_simulation(silent=True)
107
+ self._simulation = sim
108
+ self._gwf = gwf
109
+
110
+ def update_model(self, p: ArrayLike):
111
+ sy, c, f = p[0:3]
112
+
113
+ d = 0.0
114
+ r = self._stress[0] + f * self._stress[1]
115
+
116
+ # remove existing packages
117
+ if all(
118
+ [True for x in self._gwf.get_package_list() if x in self._changing_packages]
119
+ ):
120
+ [self._gwf.remove_package(x) for x in self._changing_packages]
121
+
122
+ haq = (self._gwf.dis.top.array - self._gwf.dis.botm.array)[0]
123
+ sto = flopy.mf6.ModflowGwfsto(
124
+ self._gwf,
125
+ save_flows=False,
126
+ iconvert=0,
127
+ ss=sy / haq,
128
+ transient=True,
129
+ pname="sto",
130
+ )
131
+ sto.write()
132
+
133
+ # ghb
134
+ ghb = flopy.mf6.ModflowGwfghb(
135
+ self._gwf,
136
+ maxbound=1,
137
+ stress_period_data={0: [[(0, 0, 0), d, 1.0 / c]]},
138
+ pname="ghb",
139
+ )
140
+ ghb.write()
141
+
142
+ rts = [(i, x) for i, x in zip(range(self._nper + 1), np.append(r, 0.0))]
143
+
144
+ ts_dict = {
145
+ "filename": "recharge.ts",
146
+ "timeseries": rts,
147
+ "time_series_namerecord": ["recharge"],
148
+ "interpolation_methodrecord": ["stepwise"],
149
+ }
150
+
151
+ rch = flopy.mf6.ModflowGwfrch(
152
+ self._gwf,
153
+ maxbound=1,
154
+ pname="rch",
155
+ stress_period_data={0: [[(0, 0, 0), "recharge"]]},
156
+ timeseries=ts_dict,
157
+ )
158
+ rch.write()
159
+ rch.ts.write()
160
+
161
+ self._gwf.name_file.write()
162
+
163
+ @functools.lru_cache(maxsize=5)
164
+ def _get_head(self, p):
165
+ self.update_model(p=p)
166
+ success, _ = self._simulation.run_simulation(silent=True)
167
+ if success:
168
+ return self._gwf.output.head().get_ts((0, 0, 0))[:, 1]
169
+ else:
170
+ logger.error(
171
+ "ModflowError: model run failed with parameters: "
172
+ f"sy={p[0]}, c={p[1]}, f={p[2]}"
173
+ )
174
+ if self.raise_on_modflow_error:
175
+ raise Exception(
176
+ "Modflow run failed. Check the LIST file for more information."
177
+ )
178
+ else:
179
+ return np.zeros(self._nper)
180
+
181
+ def simulate(self, p: ArrayLike, stress: List[Series]) -> ArrayLike:
182
+ if self._simulation is None:
183
+ self._stress = stress
184
+ self._nper = len(self._stress[0])
185
+ self.create_model()
186
+ return self._get_head(tuple(p))