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.
- pastas_plugins/__init__.py +35 -0
- pastas_plugins/cross_correlation/__init__.py +8 -0
- pastas_plugins/cross_correlation/cross_correlation.py +188 -0
- pastas_plugins/cross_correlation/plots.py +245 -0
- pastas_plugins/cross_correlation/version.py +1 -0
- pastas_plugins/modflow/__init__.py +4 -0
- pastas_plugins/modflow/modflow.py +186 -0
- pastas_plugins/modflow/stressmodels.py +113 -0
- pastas_plugins/modflow/version.py +1 -0
- pastas_plugins/pest/__init__.py +7 -0
- pastas_plugins/pest/solver.py +1161 -0
- pastas_plugins/pest/version.py +1 -0
- pastas_plugins/reservoirs/__init__.py +4 -0
- pastas_plugins/reservoirs/reservoir.py +210 -0
- pastas_plugins/reservoirs/stressmodels.py +145 -0
- pastas_plugins/reservoirs/version.py +1 -0
- pastas_plugins/responses/__init__.py +3 -0
- pastas_plugins/responses/parameters.py +130 -0
- pastas_plugins/responses/rfunc.py +196 -0
- pastas_plugins/responses/version.py +1 -0
- pastas_plugins/version.py +1 -0
- pastas_plugins-0.3.0.dist-info/LICENSE +21 -0
- pastas_plugins-0.3.0.dist-info/METADATA +141 -0
- pastas_plugins-0.3.0.dist-info/RECORD +26 -0
- pastas_plugins-0.3.0.dist-info/WHEEL +5 -0
- pastas_plugins-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -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,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))
|