pastastore 1.4.0__py3-none-any.whl → 1.6.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.
- pastastore/__init__.py +2 -4
- pastastore/base.py +206 -59
- pastastore/connectors.py +23 -420
- pastastore/datasets.py +5 -10
- pastastore/plotting.py +27 -14
- pastastore/store.py +522 -73
- pastastore/styling.py +2 -1
- pastastore/util.py +22 -108
- pastastore/version.py +33 -1
- pastastore/yaml_interface.py +33 -25
- {pastastore-1.4.0.dist-info → pastastore-1.6.0.dist-info}/METADATA +15 -14
- pastastore-1.6.0.dist-info/RECORD +15 -0
- {pastastore-1.4.0.dist-info → pastastore-1.6.0.dist-info}/WHEEL +1 -1
- pastastore-1.4.0.dist-info/RECORD +0 -15
- {pastastore-1.4.0.dist-info → pastastore-1.6.0.dist-info}/LICENSE +0 -0
- {pastastore-1.4.0.dist-info → pastastore-1.6.0.dist-info}/top_level.txt +0 -0
pastastore/store.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
"""Module containing the PastaStore object for managing time series and models."""
|
|
2
|
+
|
|
1
3
|
import json
|
|
4
|
+
import logging
|
|
2
5
|
import os
|
|
3
6
|
import warnings
|
|
4
|
-
from typing import List, Optional, Tuple, Union
|
|
7
|
+
from typing import Dict, List, Literal, Optional, Tuple, Union
|
|
5
8
|
|
|
6
9
|
import numpy as np
|
|
7
10
|
import pandas as pd
|
|
@@ -14,11 +17,14 @@ from pastastore.base import BaseConnector
|
|
|
14
17
|
from pastastore.connectors import DictConnector
|
|
15
18
|
from pastastore.plotting import Maps, Plots
|
|
16
19
|
from pastastore.util import _custom_warning
|
|
20
|
+
from pastastore.version import PASTAS_GEQ_150, PASTAS_LEQ_022
|
|
17
21
|
from pastastore.yaml_interface import PastastoreYAML
|
|
18
22
|
|
|
19
23
|
FrameorSeriesUnion = Union[pd.DataFrame, pd.Series]
|
|
20
24
|
warnings.showwarning = _custom_warning
|
|
21
25
|
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
22
28
|
|
|
23
29
|
class PastaStore:
|
|
24
30
|
"""PastaStore object for managing pastas time series and models.
|
|
@@ -27,9 +33,8 @@ class PastaStore:
|
|
|
27
33
|
the database. Different Connectors are available, e.g.:
|
|
28
34
|
|
|
29
35
|
- PasConnector for storing all data as .pas (JSON) files on disk (recommended)
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
- PystoreConnector for saving data to disk using the Pystore module
|
|
36
|
+
- ArcticDBConnector for saving data on disk using arcticdb package
|
|
37
|
+
- DictConnector for storing all data in dictionaries (in-memory)
|
|
33
38
|
|
|
34
39
|
Parameters
|
|
35
40
|
----------
|
|
@@ -40,6 +45,8 @@ class PastaStore:
|
|
|
40
45
|
name of the PastaStore, by default takes the name of the Connector object
|
|
41
46
|
"""
|
|
42
47
|
|
|
48
|
+
_accessors = set()
|
|
49
|
+
|
|
43
50
|
def __init__(
|
|
44
51
|
self,
|
|
45
52
|
connector: Optional[BaseConnector] = None,
|
|
@@ -72,7 +79,7 @@ class PastaStore:
|
|
|
72
79
|
self.yaml = PastastoreYAML(self)
|
|
73
80
|
|
|
74
81
|
def _register_connector_methods(self):
|
|
75
|
-
"""
|
|
82
|
+
"""Register connector methods (internal method)."""
|
|
76
83
|
methods = [
|
|
77
84
|
func
|
|
78
85
|
for func in dir(self.conn)
|
|
@@ -83,26 +90,70 @@ class PastaStore:
|
|
|
83
90
|
|
|
84
91
|
@property
|
|
85
92
|
def oseries(self):
|
|
93
|
+
"""
|
|
94
|
+
Returns the oseries metadata as dataframe.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
oseries
|
|
99
|
+
oseries metadata as dataframe
|
|
100
|
+
"""
|
|
86
101
|
return self.conn.oseries
|
|
87
102
|
|
|
88
103
|
@property
|
|
89
104
|
def stresses(self):
|
|
105
|
+
"""
|
|
106
|
+
Returns the stresses metadata as dataframe.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
stresses
|
|
111
|
+
stresses metadata as dataframe
|
|
112
|
+
"""
|
|
90
113
|
return self.conn.stresses
|
|
91
114
|
|
|
92
115
|
@property
|
|
93
116
|
def models(self):
|
|
117
|
+
"""Return list of model names.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
list
|
|
122
|
+
list of model names
|
|
123
|
+
"""
|
|
94
124
|
return self.conn.models
|
|
95
125
|
|
|
96
126
|
@property
|
|
97
127
|
def oseries_names(self):
|
|
128
|
+
"""Return list of oseries names.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
list
|
|
133
|
+
list of oseries names
|
|
134
|
+
"""
|
|
98
135
|
return self.conn.oseries_names
|
|
99
136
|
|
|
100
137
|
@property
|
|
101
138
|
def stresses_names(self):
|
|
139
|
+
"""Return list of streses names.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
list
|
|
144
|
+
list of stresses names
|
|
145
|
+
"""
|
|
102
146
|
return self.conn.stresses_names
|
|
103
147
|
|
|
104
148
|
@property
|
|
105
149
|
def model_names(self):
|
|
150
|
+
"""Return list of model names.
|
|
151
|
+
|
|
152
|
+
Returns
|
|
153
|
+
-------
|
|
154
|
+
list
|
|
155
|
+
list of model names
|
|
156
|
+
"""
|
|
106
157
|
return self.conn.model_names
|
|
107
158
|
|
|
108
159
|
@property
|
|
@@ -111,22 +162,57 @@ class PastaStore:
|
|
|
111
162
|
|
|
112
163
|
@property
|
|
113
164
|
def n_oseries(self):
|
|
165
|
+
"""Return number of oseries.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
int
|
|
170
|
+
number of oseries
|
|
171
|
+
"""
|
|
114
172
|
return self.conn.n_oseries
|
|
115
173
|
|
|
116
174
|
@property
|
|
117
175
|
def n_stresses(self):
|
|
176
|
+
"""Return number of stresses.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
int
|
|
181
|
+
number of stresses
|
|
182
|
+
"""
|
|
118
183
|
return self.conn.n_stresses
|
|
119
184
|
|
|
120
185
|
@property
|
|
121
186
|
def n_models(self):
|
|
187
|
+
"""Return number of models.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
int
|
|
192
|
+
number of models
|
|
193
|
+
"""
|
|
122
194
|
return self.conn.n_models
|
|
123
195
|
|
|
124
196
|
@property
|
|
125
197
|
def oseries_models(self):
|
|
198
|
+
"""Return dictionary of models per oseries.
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
dict
|
|
203
|
+
dictionary containing list of models (values) for each oseries (keys).
|
|
204
|
+
"""
|
|
126
205
|
return self.conn.oseries_models
|
|
127
206
|
|
|
128
207
|
@property
|
|
129
208
|
def oseries_with_models(self):
|
|
209
|
+
"""Return list of oseries for which models are contained in the database.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
list
|
|
214
|
+
list of oseries names for which models are contained in the database.
|
|
215
|
+
"""
|
|
130
216
|
return self.conn.oseries_with_models
|
|
131
217
|
|
|
132
218
|
def __repr__(self):
|
|
@@ -136,7 +222,7 @@ class PastaStore:
|
|
|
136
222
|
def get_oseries_distances(
|
|
137
223
|
self, names: Optional[Union[list, str]] = None
|
|
138
224
|
) -> FrameorSeriesUnion:
|
|
139
|
-
"""
|
|
225
|
+
"""Get the distances in meters between the oseries.
|
|
140
226
|
|
|
141
227
|
Parameters
|
|
142
228
|
----------
|
|
@@ -175,7 +261,7 @@ class PastaStore:
|
|
|
175
261
|
n: int = 1,
|
|
176
262
|
maxdist: Optional[float] = None,
|
|
177
263
|
) -> FrameorSeriesUnion:
|
|
178
|
-
"""
|
|
264
|
+
"""Get the nearest (n) oseries.
|
|
179
265
|
|
|
180
266
|
Parameters
|
|
181
267
|
----------
|
|
@@ -191,7 +277,6 @@ class PastaStore:
|
|
|
191
277
|
oseries:
|
|
192
278
|
list with the names of the oseries.
|
|
193
279
|
"""
|
|
194
|
-
|
|
195
280
|
distances = self.get_oseries_distances(names)
|
|
196
281
|
if maxdist is not None:
|
|
197
282
|
distances = distances.where(distances <= maxdist, np.nan)
|
|
@@ -214,8 +299,7 @@ class PastaStore:
|
|
|
214
299
|
stresses: Optional[Union[list, str]] = None,
|
|
215
300
|
kind: Optional[Union[str, List[str]]] = None,
|
|
216
301
|
) -> FrameorSeriesUnion:
|
|
217
|
-
"""
|
|
218
|
-
stresses.
|
|
302
|
+
"""Get the distances in meters between the oseries and stresses.
|
|
219
303
|
|
|
220
304
|
Parameters
|
|
221
305
|
----------
|
|
@@ -274,7 +358,7 @@ class PastaStore:
|
|
|
274
358
|
n: int = 1,
|
|
275
359
|
maxdist: Optional[float] = None,
|
|
276
360
|
) -> FrameorSeriesUnion:
|
|
277
|
-
"""
|
|
361
|
+
"""Get the nearest (n) stresses of a specific kind.
|
|
278
362
|
|
|
279
363
|
Parameters
|
|
280
364
|
----------
|
|
@@ -295,7 +379,6 @@ class PastaStore:
|
|
|
295
379
|
stresses:
|
|
296
380
|
list with the names of the stresses.
|
|
297
381
|
"""
|
|
298
|
-
|
|
299
382
|
distances = self.get_distances(oseries, stresses, kind)
|
|
300
383
|
if maxdist is not None:
|
|
301
384
|
distances = distances.where(distances <= maxdist, np.nan)
|
|
@@ -317,8 +400,9 @@ class PastaStore:
|
|
|
317
400
|
progressbar=False,
|
|
318
401
|
ignore_errors=False,
|
|
319
402
|
):
|
|
320
|
-
"""Get groundwater signatures.
|
|
321
|
-
|
|
403
|
+
"""Get groundwater signatures.
|
|
404
|
+
|
|
405
|
+
NaN-values are returned when the signature cannot be computed.
|
|
322
406
|
|
|
323
407
|
Parameters
|
|
324
408
|
----------
|
|
@@ -384,7 +468,12 @@ class PastaStore:
|
|
|
384
468
|
|
|
385
469
|
return signatures_df
|
|
386
470
|
|
|
387
|
-
def get_tmin_tmax(
|
|
471
|
+
def get_tmin_tmax(
|
|
472
|
+
self,
|
|
473
|
+
libname: Literal["oseries", "stresses", "models"],
|
|
474
|
+
names: Union[str, List[str], None] = None,
|
|
475
|
+
progressbar: bool = False,
|
|
476
|
+
):
|
|
388
477
|
"""Get tmin and tmax for time series.
|
|
389
478
|
|
|
390
479
|
Parameters
|
|
@@ -403,22 +492,48 @@ class PastaStore:
|
|
|
403
492
|
tmintmax : pd.dataframe
|
|
404
493
|
Dataframe containing tmin and tmax per time series
|
|
405
494
|
"""
|
|
406
|
-
|
|
407
495
|
names = self.conn._parse_names(names, libname=libname)
|
|
408
496
|
tmintmax = pd.DataFrame(
|
|
409
497
|
index=names, columns=["tmin", "tmax"], dtype="datetime64[ns]"
|
|
410
498
|
)
|
|
411
499
|
desc = f"Get tmin/tmax {libname}"
|
|
412
500
|
for n in tqdm(names, desc=desc) if progressbar else names:
|
|
413
|
-
if libname == "
|
|
414
|
-
|
|
501
|
+
if libname == "models":
|
|
502
|
+
mld = self.conn.get_models(
|
|
503
|
+
n,
|
|
504
|
+
return_dict=True,
|
|
505
|
+
)
|
|
506
|
+
tmintmax.loc[n, "tmin"] = mld["settings"]["tmin"]
|
|
507
|
+
tmintmax.loc[n, "tmax"] = mld["settings"]["tmax"]
|
|
415
508
|
else:
|
|
416
|
-
s =
|
|
417
|
-
|
|
418
|
-
|
|
509
|
+
s = (
|
|
510
|
+
self.conn.get_oseries(n)
|
|
511
|
+
if libname == "oseries"
|
|
512
|
+
else self.conn.get_stresses(n)
|
|
513
|
+
)
|
|
514
|
+
tmintmax.loc[n, "tmin"] = s.first_valid_index()
|
|
515
|
+
tmintmax.loc[n, "tmax"] = s.last_valid_index()
|
|
516
|
+
|
|
419
517
|
return tmintmax
|
|
420
518
|
|
|
421
519
|
def get_extent(self, libname, names=None, buffer=0.0):
|
|
520
|
+
"""Get extent [xmin, xmax, ymin, ymax] from library.
|
|
521
|
+
|
|
522
|
+
Parameters
|
|
523
|
+
----------
|
|
524
|
+
libname : str
|
|
525
|
+
name of the library containing the time series
|
|
526
|
+
('oseries', 'stresses', 'models')
|
|
527
|
+
names : str, list of str, or None, optional
|
|
528
|
+
list of names to include for computing the extent
|
|
529
|
+
buffer : float, optional
|
|
530
|
+
add this distance to the extent, by default 0.0
|
|
531
|
+
|
|
532
|
+
Returns
|
|
533
|
+
-------
|
|
534
|
+
extent : list
|
|
535
|
+
extent [xmin, xmax, ymin, ymax]
|
|
536
|
+
"""
|
|
422
537
|
names = self.conn._parse_names(names, libname=libname)
|
|
423
538
|
if libname in ["oseries", "stresses"]:
|
|
424
539
|
df = getattr(self, libname)
|
|
@@ -443,8 +558,10 @@ class PastaStore:
|
|
|
443
558
|
progressbar: Optional[bool] = False,
|
|
444
559
|
ignore_errors: Optional[bool] = False,
|
|
445
560
|
) -> FrameorSeriesUnion:
|
|
446
|
-
"""Get model parameters.
|
|
447
|
-
|
|
561
|
+
"""Get model parameters.
|
|
562
|
+
|
|
563
|
+
NaN-values are returned when the parameters are not present in the model or the
|
|
564
|
+
model is not optimized.
|
|
448
565
|
|
|
449
566
|
Parameters
|
|
450
567
|
----------
|
|
@@ -526,7 +643,6 @@ class PastaStore:
|
|
|
526
643
|
-------
|
|
527
644
|
s : pandas.DataFrame
|
|
528
645
|
"""
|
|
529
|
-
|
|
530
646
|
modelnames = self.conn._parse_names(modelnames, libname="models")
|
|
531
647
|
|
|
532
648
|
# if statistics is str
|
|
@@ -556,8 +672,9 @@ class PastaStore:
|
|
|
556
672
|
def create_model(
|
|
557
673
|
self,
|
|
558
674
|
name: str,
|
|
559
|
-
modelname: str = None,
|
|
675
|
+
modelname: Optional[str] = None,
|
|
560
676
|
add_recharge: bool = True,
|
|
677
|
+
add_ar_noisemodel: bool = False,
|
|
561
678
|
recharge_name: str = "recharge",
|
|
562
679
|
) -> ps.Model:
|
|
563
680
|
"""Create a pastas Model.
|
|
@@ -572,6 +689,8 @@ class PastaStore:
|
|
|
572
689
|
add recharge to the model by looking for the closest
|
|
573
690
|
precipitation and evaporation time series in the stresses
|
|
574
691
|
library, by default True
|
|
692
|
+
add_ar_noisemodel : bool, optional
|
|
693
|
+
add AR(1) noise model to the model, by default False
|
|
575
694
|
recharge_name : str
|
|
576
695
|
name of the RechargeModel
|
|
577
696
|
|
|
@@ -591,13 +710,15 @@ class PastaStore:
|
|
|
591
710
|
meta = self.conn.get_metadata("oseries", name, as_frame=False)
|
|
592
711
|
ts = self.conn.get_oseries(name)
|
|
593
712
|
|
|
594
|
-
# convert to
|
|
713
|
+
# convert to time series and create model
|
|
595
714
|
if not ts.dropna().empty:
|
|
596
715
|
if modelname is None:
|
|
597
716
|
modelname = name
|
|
598
717
|
ml = ps.Model(ts, name=modelname, metadata=meta)
|
|
599
718
|
if add_recharge:
|
|
600
719
|
self.add_recharge(ml, recharge_name=recharge_name)
|
|
720
|
+
if add_ar_noisemodel and PASTAS_GEQ_150:
|
|
721
|
+
ml.add_noisemodel(ps.ArNoiseModel())
|
|
601
722
|
return ml
|
|
602
723
|
else:
|
|
603
724
|
raise ValueError("Empty time series!")
|
|
@@ -694,50 +815,363 @@ class PastaStore:
|
|
|
694
815
|
recharge_name : str
|
|
695
816
|
name of the RechargeModel
|
|
696
817
|
"""
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
818
|
+
if recharge is None:
|
|
819
|
+
recharge = ps.rch.Linear()
|
|
820
|
+
if rfunc is None:
|
|
821
|
+
rfunc = ps.Exponential
|
|
822
|
+
|
|
823
|
+
self.add_stressmodel(
|
|
824
|
+
ml,
|
|
825
|
+
stresses={"prec": "nearest", "evap": "nearest"},
|
|
826
|
+
rfunc=rfunc,
|
|
827
|
+
stressmodel=ps.RechargeModel,
|
|
828
|
+
stressmodel_name=recharge_name,
|
|
829
|
+
recharge=recharge,
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
def _parse_stresses(
|
|
833
|
+
self,
|
|
834
|
+
stresses: Union[str, List[str], Dict[str, str]],
|
|
835
|
+
kind: Optional[str],
|
|
836
|
+
stressmodel,
|
|
837
|
+
oseries: Optional[str] = None,
|
|
838
|
+
):
|
|
839
|
+
# parse stresses for RechargeModel, allow list of len 2 or 3 and
|
|
840
|
+
# set correct kwarg names
|
|
841
|
+
if stressmodel._name == "RechargeModel":
|
|
842
|
+
if isinstance(stresses, list):
|
|
843
|
+
if len(stresses) == 2:
|
|
844
|
+
stresses = {
|
|
845
|
+
"prec": stresses[0],
|
|
846
|
+
"evap": stresses[1],
|
|
847
|
+
}
|
|
848
|
+
elif len(stresses) == 3:
|
|
849
|
+
stresses = {
|
|
850
|
+
"prec": stresses[0],
|
|
851
|
+
"evap": stresses[1],
|
|
852
|
+
"temp": stresses[2],
|
|
853
|
+
}
|
|
854
|
+
else:
|
|
717
855
|
raise ValueError(
|
|
718
|
-
|
|
719
|
-
"
|
|
856
|
+
"RechargeModel requires 2 or 3 stress names, "
|
|
857
|
+
f"got: {len(stresses)}!"
|
|
720
858
|
)
|
|
859
|
+
# if stresses is list, create dictionary normally
|
|
860
|
+
elif isinstance(stresses, list):
|
|
861
|
+
stresses = {"stress": stresses}
|
|
862
|
+
# if stresses is str, make it a list of len 1
|
|
863
|
+
elif isinstance(stresses, str):
|
|
864
|
+
stresses = {"stress": [stresses]}
|
|
865
|
+
|
|
866
|
+
# check if stresses is a dictionary, else raise TypeError
|
|
867
|
+
if not isinstance(stresses, dict):
|
|
868
|
+
raise TypeError("stresses must be a list, string or dictionary!")
|
|
869
|
+
|
|
870
|
+
# if no kind specified, set to well for WellModel
|
|
871
|
+
if stressmodel._name == "WellModel":
|
|
872
|
+
if kind is None:
|
|
873
|
+
kind = "well"
|
|
874
|
+
|
|
875
|
+
# store a copy of the user input for kind
|
|
876
|
+
if isinstance(kind, list):
|
|
877
|
+
_kind = kind.copy()
|
|
878
|
+
else:
|
|
879
|
+
_kind = kind
|
|
880
|
+
|
|
881
|
+
# create empty list for gathering metadata
|
|
882
|
+
metadata = []
|
|
883
|
+
# loop over stresses keys/values
|
|
884
|
+
for i, (k, v) in enumerate(stresses.items()):
|
|
885
|
+
# if entry in dictionary is str, make it list of len 1
|
|
886
|
+
if isinstance(v, str):
|
|
887
|
+
v = [v]
|
|
888
|
+
# parse value
|
|
889
|
+
if isinstance(v, list):
|
|
890
|
+
for item in v:
|
|
891
|
+
names = [] # empty list for names
|
|
892
|
+
# parse nearest
|
|
893
|
+
if item.startswith("nearest"):
|
|
894
|
+
# check oseries defined if nearest option is used
|
|
895
|
+
if not oseries:
|
|
896
|
+
raise ValueError(
|
|
897
|
+
"Getting nearest stress(es) requires oseries name!"
|
|
898
|
+
)
|
|
899
|
+
try:
|
|
900
|
+
if len(item.split()) == 3: # nearest <n> <kind>
|
|
901
|
+
n = int(item.split()[1])
|
|
902
|
+
kind = item.split()[2]
|
|
903
|
+
elif len(item.split()) == 2: # nearest <n> | <kind>
|
|
904
|
+
try:
|
|
905
|
+
n = int(item.split()[1]) # try converting to <n>
|
|
906
|
+
except ValueError:
|
|
907
|
+
n = 1
|
|
908
|
+
kind = item.split()[1] # interpret as <kind>
|
|
909
|
+
else: # nearest
|
|
910
|
+
n = 1
|
|
911
|
+
# if RechargeModel, we can infer kind
|
|
912
|
+
if (
|
|
913
|
+
_kind is None
|
|
914
|
+
and stressmodel._name == "RechargeModel"
|
|
915
|
+
):
|
|
916
|
+
kind = k
|
|
917
|
+
elif _kind is None: # catch no kind with bare nearest
|
|
918
|
+
raise ValueError(
|
|
919
|
+
"Bare 'nearest' found but no kind specified."
|
|
920
|
+
)
|
|
921
|
+
elif isinstance(_kind, list):
|
|
922
|
+
kind = _kind[i] # if multiple kind, select i-th
|
|
923
|
+
except Exception as e:
|
|
924
|
+
# raise if nearest parsing failed
|
|
925
|
+
raise ValueError(
|
|
926
|
+
f"Could not parse stresses: '{item}'! "
|
|
927
|
+
"When using option 'nearest', use 'nearest' and specify"
|
|
928
|
+
" kind, or 'nearest <kind>' or 'nearest <n> <kind>'!"
|
|
929
|
+
) from e
|
|
930
|
+
# check if kind exists at all
|
|
931
|
+
if kind not in self.stresses.kind.values:
|
|
932
|
+
raise ValueError(
|
|
933
|
+
f"Could not find stresses with kind='{kind}'!"
|
|
934
|
+
)
|
|
935
|
+
# get stress names of <n> nearest <kind> stresses
|
|
936
|
+
inames = self.get_nearest_stresses(
|
|
937
|
+
oseries, kind=kind, n=n
|
|
938
|
+
).iloc[0]
|
|
939
|
+
# check if any NaNs in result
|
|
940
|
+
if inames.isna().any():
|
|
941
|
+
nkind = (self.stresses.kind == kind).sum()
|
|
942
|
+
raise ValueError(
|
|
943
|
+
f"Could not find {n} nearest stress(es) for '{kind}'! "
|
|
944
|
+
f"There are only {nkind} '{kind}' stresses."
|
|
945
|
+
)
|
|
946
|
+
# append names
|
|
947
|
+
names += inames.tolist()
|
|
948
|
+
else:
|
|
949
|
+
# assume name is name of stress
|
|
950
|
+
names.append(item)
|
|
951
|
+
# get stresses and metadata
|
|
952
|
+
stress_series, imeta = self.get_stresses(
|
|
953
|
+
names, return_metadata=True, squeeze=True
|
|
954
|
+
)
|
|
955
|
+
# replace stress name(s) with time series
|
|
956
|
+
if len(names) > 1:
|
|
957
|
+
stresses[k] = list(stress_series.values())
|
|
958
|
+
else:
|
|
959
|
+
stresses[k] = stress_series
|
|
960
|
+
# gather metadata
|
|
961
|
+
if isinstance(imeta, list):
|
|
962
|
+
metadata += imeta
|
|
963
|
+
else:
|
|
964
|
+
metadata.append(imeta)
|
|
965
|
+
|
|
966
|
+
return stresses, metadata
|
|
967
|
+
|
|
968
|
+
def get_stressmodel(
|
|
969
|
+
self,
|
|
970
|
+
stresses: Union[str, List[str], Dict[str, str]],
|
|
971
|
+
stressmodel=ps.StressModel,
|
|
972
|
+
stressmodel_name: Optional[str] = None,
|
|
973
|
+
rfunc=ps.Exponential,
|
|
974
|
+
rfunc_kwargs: Optional[dict] = None,
|
|
975
|
+
kind: Optional[Union[List[str], str]] = None,
|
|
976
|
+
oseries: Optional[str] = None,
|
|
977
|
+
**kwargs,
|
|
978
|
+
):
|
|
979
|
+
"""Get a Pastas stressmodel from stresses time series in Pastastore.
|
|
980
|
+
|
|
981
|
+
Supports "nearest" selection. Any stress name can be replaced by
|
|
982
|
+
"nearest [<n>] <kind>" where <n> is optional and represents the number of
|
|
983
|
+
nearest stresses and <kind> and represents the kind of stress to
|
|
984
|
+
consider. <kind> can also be specified directly with the `kind` kwarg.
|
|
985
|
+
|
|
986
|
+
Note: the 'nearest' option requires the oseries name to be provided.
|
|
987
|
+
Additionally, 'x' and 'y' metadata must be stored for oseries and stresses.
|
|
988
|
+
|
|
989
|
+
Parameters
|
|
990
|
+
----------
|
|
991
|
+
stresses : str, list of str, or dict
|
|
992
|
+
name(s) of the time series to use for the stressmodel, or dictionary
|
|
993
|
+
with key(s) and value(s) as time series name(s). Options include:
|
|
994
|
+
- name of stress: `"prec_stn"`
|
|
995
|
+
- list of stress names: `["prec_stn", "evap_stn"]`
|
|
996
|
+
- dict for RechargeModel: `{"prec": "prec_stn", "evap": "evap_stn"}`
|
|
997
|
+
- dict for StressModel: `{"stress": "well1"}`
|
|
998
|
+
- nearest, specifying kind: `"nearest well"`
|
|
999
|
+
- nearest specifying number and kind: `"nearest 2 well"`
|
|
1000
|
+
stressmodel : str or class
|
|
1001
|
+
stressmodel class to use, by default ps.StressModel
|
|
1002
|
+
stressmodel_name : str, optional
|
|
1003
|
+
name of the stressmodel, by default None, which uses the stress name,
|
|
1004
|
+
if there is 1 stress otherwise the name of the stressmodel type. For
|
|
1005
|
+
RechargeModels, the name defaults to 'recharge'.
|
|
1006
|
+
rfunc : str or class
|
|
1007
|
+
response function class to use, by default ps.Exponential
|
|
1008
|
+
rfunc_kwargs : dict, optional
|
|
1009
|
+
keyword arguments to pass to the response function, by default None
|
|
1010
|
+
kind : str or list of str, optional
|
|
1011
|
+
specify kind of stress(es) to use, by default None, useful in combination
|
|
1012
|
+
with 'nearest' option for defining stresses
|
|
1013
|
+
oseries : str, optional
|
|
1014
|
+
name of the oseries to use for the stressmodel, by default None, used when
|
|
1015
|
+
'nearest' option is used for defining stresses.
|
|
1016
|
+
**kwargs
|
|
1017
|
+
additional keyword arguments to pass to the stressmodel
|
|
1018
|
+
|
|
1019
|
+
Returns
|
|
1020
|
+
-------
|
|
1021
|
+
stressmodel : pastas.StressModel
|
|
1022
|
+
pastas StressModel that can be added to pastas Model.
|
|
1023
|
+
"""
|
|
1024
|
+
# get stressmodel class, if str was provided
|
|
1025
|
+
if isinstance(stressmodel, str):
|
|
1026
|
+
stressmodel = getattr(ps, stressmodel)
|
|
1027
|
+
|
|
1028
|
+
# parse stresses names to get time series and metadata
|
|
1029
|
+
stresses, metadata = self._parse_stresses(
|
|
1030
|
+
stresses=stresses, stressmodel=stressmodel, kind=kind, oseries=oseries
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
# get stressmodel name if not provided
|
|
1034
|
+
if stressmodel_name is None:
|
|
1035
|
+
if stressmodel._name == "RechargeModel":
|
|
1036
|
+
stressmodel_name = "recharge"
|
|
1037
|
+
elif len(metadata) == 1:
|
|
1038
|
+
stressmodel_name = stresses["stress"].squeeze().name
|
|
1039
|
+
else:
|
|
1040
|
+
stressmodel_name = stressmodel._name
|
|
1041
|
+
|
|
1042
|
+
# check if metadata is list of len 1 and unpack
|
|
1043
|
+
if isinstance(metadata, list) and len(metadata) == 1:
|
|
1044
|
+
metadata = metadata[0]
|
|
1045
|
+
|
|
1046
|
+
# get stressmodel time series settings
|
|
1047
|
+
if kind and "settings" not in kwargs:
|
|
1048
|
+
# try using kind to get predefined settings options
|
|
1049
|
+
if isinstance(kind, str):
|
|
1050
|
+
kwargs["settings"] = ps.rcParams["timeseries"].get(kind, None)
|
|
721
1051
|
else:
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1052
|
+
kwargs["settings"] = [
|
|
1053
|
+
ps.rcParams["timeseries"].get(ikind, None) for ikind in kind
|
|
1054
|
+
]
|
|
1055
|
+
elif kind is None and "settings" not in kwargs:
|
|
1056
|
+
# try using kind stored in metadata to get predefined settings options
|
|
1057
|
+
if isinstance(metadata, list):
|
|
1058
|
+
kwargs["settings"] = [
|
|
1059
|
+
ps.rcParams["timeseries"].get(imeta.get("kind", None), None)
|
|
1060
|
+
for imeta in metadata
|
|
1061
|
+
]
|
|
1062
|
+
elif isinstance(metadata, dict):
|
|
1063
|
+
kwargs["settings"] = ps.rcParams["timeseries"].get(
|
|
1064
|
+
metadata.get("kind", None), None
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
# get rfunc class if str was provided
|
|
1068
|
+
if isinstance(rfunc, str):
|
|
1069
|
+
rfunc = getattr(ps, rfunc)
|
|
1070
|
+
|
|
1071
|
+
# create empty rfunc_kwargs if not provided
|
|
1072
|
+
if rfunc_kwargs is None:
|
|
1073
|
+
rfunc_kwargs = {}
|
|
1074
|
+
|
|
1075
|
+
# special for WellModels
|
|
1076
|
+
if stressmodel._name == "WellModel":
|
|
1077
|
+
names = [s.squeeze().name for s in stresses["stress"]]
|
|
1078
|
+
# check oseries is provided
|
|
1079
|
+
if oseries is None:
|
|
1080
|
+
raise ValueError("WellModel requires 'oseries' to compute distances!")
|
|
1081
|
+
# compute distances and add to kwargs
|
|
1082
|
+
distances = (
|
|
1083
|
+
self.get_distances(oseries=oseries, stresses=names).T.squeeze().values
|
|
1084
|
+
)
|
|
1085
|
+
kwargs["distances"] = distances
|
|
1086
|
+
# set settings to well
|
|
1087
|
+
if "settings" not in kwargs:
|
|
1088
|
+
kwargs["settings"] = "well"
|
|
1089
|
+
# override rfunc and set to HantushWellModel
|
|
1090
|
+
rfunc = ps.HantushWellModel
|
|
1091
|
+
|
|
1092
|
+
# do not add metadata for pastas 0.22 and WellModel
|
|
1093
|
+
if not PASTAS_LEQ_022 and (stressmodel._name != "WellModel"):
|
|
1094
|
+
kwargs["metadata"] = metadata
|
|
1095
|
+
|
|
1096
|
+
return stressmodel(
|
|
1097
|
+
**stresses,
|
|
1098
|
+
rfunc=rfunc(**rfunc_kwargs),
|
|
1099
|
+
name=stressmodel_name,
|
|
1100
|
+
**kwargs,
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
def add_stressmodel(
|
|
1104
|
+
self,
|
|
1105
|
+
ml: Union[ps.Model, str],
|
|
1106
|
+
stresses: Union[str, List[str], Dict[str, str]],
|
|
1107
|
+
stressmodel=ps.StressModel,
|
|
1108
|
+
stressmodel_name: Optional[str] = None,
|
|
1109
|
+
rfunc=ps.Exponential,
|
|
1110
|
+
rfunc_kwargs: Optional[dict] = None,
|
|
1111
|
+
kind: Optional[Union[List[str], str]] = None,
|
|
1112
|
+
**kwargs,
|
|
1113
|
+
):
|
|
1114
|
+
"""Add a pastas StressModel from stresses time series in Pastastore.
|
|
1115
|
+
|
|
1116
|
+
Supports "nearest" selection. Any stress name can be replaced by
|
|
1117
|
+
"nearest [<n>] <kind>" where <n> is optional and represents the number of
|
|
1118
|
+
nearest stresses and <kind> and represents the kind of stress to
|
|
1119
|
+
consider. <kind> can also be specified directly with the `kind` kwarg.
|
|
1120
|
+
|
|
1121
|
+
Note: the 'nearest' option requires the oseries name to be provided.
|
|
1122
|
+
Additionally, 'x' and 'y' metadata must be stored for oseries and stresses.
|
|
1123
|
+
|
|
1124
|
+
Parameters
|
|
1125
|
+
----------
|
|
1126
|
+
ml : pastas.Model or str
|
|
1127
|
+
pastas.Model object to add StressModel to, if passed as string,
|
|
1128
|
+
model is loaded from store, the stressmodel is added and then written
|
|
1129
|
+
back to the store.
|
|
1130
|
+
stresses : str, list of str, or dict
|
|
1131
|
+
name(s) of the time series to use for the stressmodel, or dictionary
|
|
1132
|
+
with key(s) and value(s) as time series name(s). Options include:
|
|
1133
|
+
- name of stress: `"prec_stn"`
|
|
1134
|
+
- list of stress names: `["prec_stn", "evap_stn"]`
|
|
1135
|
+
- dict for RechargeModel: `{"prec": "prec_stn", "evap": "evap_stn"}`
|
|
1136
|
+
- dict for StressModel: `{"stress": "well1"}`
|
|
1137
|
+
- nearest, specifying kind: `"nearest well"`
|
|
1138
|
+
- nearest specifying number and kind: `"nearest 2 well"`
|
|
1139
|
+
stressmodel : str or class
|
|
1140
|
+
stressmodel class to use, by default ps.StressModel
|
|
1141
|
+
stressmodel_name : str, optional
|
|
1142
|
+
name of the stressmodel, by default None, which uses the stress name,
|
|
1143
|
+
if there is 1 stress otherwise the name of the stressmodel type. For
|
|
1144
|
+
RechargeModels, the name defaults to 'recharge'.
|
|
1145
|
+
rfunc : str or class
|
|
1146
|
+
response function class to use, by default ps.Exponential
|
|
1147
|
+
rfunc_kwargs : dict, optional
|
|
1148
|
+
keyword arguments to pass to the response function, by default None
|
|
1149
|
+
kind : str or list of str, optional
|
|
1150
|
+
specify kind of stress(es) to use, by default None, useful in combination
|
|
1151
|
+
with 'nearest' option for defining stresses
|
|
1152
|
+
**kwargs
|
|
1153
|
+
additional keyword arguments to pass to the stressmodel
|
|
1154
|
+
"""
|
|
1155
|
+
sm = self.get_stressmodel(
|
|
1156
|
+
stresses=stresses,
|
|
1157
|
+
stressmodel=stressmodel,
|
|
1158
|
+
stressmodel_name=stressmodel_name,
|
|
734
1159
|
rfunc=rfunc,
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
1160
|
+
rfunc_kwargs=rfunc_kwargs,
|
|
1161
|
+
kind=kind,
|
|
1162
|
+
oseries=ml if isinstance(ml, str) else ml.oseries.name,
|
|
1163
|
+
**kwargs,
|
|
739
1164
|
)
|
|
740
|
-
ml
|
|
1165
|
+
if isinstance(ml, str):
|
|
1166
|
+
ml = self.get_model(ml)
|
|
1167
|
+
ml.add_stressmodel(sm)
|
|
1168
|
+
self.conn.add_model(ml, overwrite=True)
|
|
1169
|
+
logger.info(
|
|
1170
|
+
f"Stressmodel '{sm.name}' added to model '{ml.name}' "
|
|
1171
|
+
"and stored in database."
|
|
1172
|
+
)
|
|
1173
|
+
else:
|
|
1174
|
+
ml.add_stressmodel(sm)
|
|
741
1175
|
|
|
742
1176
|
def solve_models(
|
|
743
1177
|
self,
|
|
@@ -828,8 +1262,8 @@ class PastaStore:
|
|
|
828
1262
|
"""
|
|
829
1263
|
try:
|
|
830
1264
|
from art_tools import pastas_get_model_results
|
|
831
|
-
except Exception:
|
|
832
|
-
raise ModuleNotFoundError("You need 'art_tools' to use this method!")
|
|
1265
|
+
except Exception as e:
|
|
1266
|
+
raise ModuleNotFoundError("You need 'art_tools' to use this method!") from e
|
|
833
1267
|
|
|
834
1268
|
if mls is None:
|
|
835
1269
|
mls = self.conn.models
|
|
@@ -870,7 +1304,7 @@ class PastaStore:
|
|
|
870
1304
|
"File already exists! " "Use 'overwrite=True' to " "force writing file."
|
|
871
1305
|
)
|
|
872
1306
|
elif os.path.exists(fname):
|
|
873
|
-
warnings.warn(f"Overwriting file '{os.path.basename(fname)}'")
|
|
1307
|
+
warnings.warn(f"Overwriting file '{os.path.basename(fname)}'", stacklevel=1)
|
|
874
1308
|
|
|
875
1309
|
with ZipFile(fname, "w", compression=ZIP_DEFLATED) as archive:
|
|
876
1310
|
# oseries
|
|
@@ -1011,13 +1445,12 @@ class PastaStore:
|
|
|
1011
1445
|
matches : list
|
|
1012
1446
|
list of names that match search result
|
|
1013
1447
|
"""
|
|
1014
|
-
|
|
1015
1448
|
if libname == "models":
|
|
1016
|
-
lib_names =
|
|
1449
|
+
lib_names = self.model_names
|
|
1017
1450
|
elif libname == "stresses":
|
|
1018
|
-
lib_names =
|
|
1451
|
+
lib_names = self.stresses_names
|
|
1019
1452
|
elif libname == "oseries":
|
|
1020
|
-
lib_names =
|
|
1453
|
+
lib_names = self.oseries_names
|
|
1021
1454
|
else:
|
|
1022
1455
|
raise ValueError("Provide valid libname: 'models', 'stresses' or 'oseries'")
|
|
1023
1456
|
|
|
@@ -1064,7 +1497,6 @@ class PastaStore:
|
|
|
1064
1497
|
indicating whether a stress is contained within a time series
|
|
1065
1498
|
model.
|
|
1066
1499
|
"""
|
|
1067
|
-
|
|
1068
1500
|
model_names = self.conn._parse_names(modelnames, libname="models")
|
|
1069
1501
|
structure = pd.DataFrame(
|
|
1070
1502
|
index=model_names, columns=["oseries"] + self.stresses_names
|
|
@@ -1138,6 +1570,23 @@ class PastaStore:
|
|
|
1138
1570
|
return result
|
|
1139
1571
|
|
|
1140
1572
|
def within(self, extent, names=None, libname="oseries"):
|
|
1573
|
+
"""Get names of items within extent.
|
|
1574
|
+
|
|
1575
|
+
Parameters
|
|
1576
|
+
----------
|
|
1577
|
+
extent : list
|
|
1578
|
+
list with [xmin, xmax, ymin, ymax]
|
|
1579
|
+
names : str, list of str, optional
|
|
1580
|
+
list of names to include, by default None
|
|
1581
|
+
libname : str, optional
|
|
1582
|
+
name of library, must be one of ('oseries', 'stresses', 'models'), by
|
|
1583
|
+
default "oseries"
|
|
1584
|
+
|
|
1585
|
+
Returns
|
|
1586
|
+
-------
|
|
1587
|
+
list
|
|
1588
|
+
list of items within extent
|
|
1589
|
+
"""
|
|
1141
1590
|
xmin, xmax, ymin, ymax = extent
|
|
1142
1591
|
names = self.conn._parse_names(names, libname)
|
|
1143
1592
|
if libname == "oseries":
|