pastastore 1.7.1__py3-none-any.whl → 1.8.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.
- docs/conf.py +215 -0
- pastastore/__init__.py +6 -1
- pastastore/base.py +72 -630
- pastastore/connectors.py +876 -18
- pastastore/extensions/hpd.py +220 -37
- pastastore/store.py +238 -110
- pastastore/util.py +1 -1
- pastastore/version.py +1 -1
- {pastastore-1.7.1.dist-info → pastastore-1.8.0.dist-info}/METADATA +40 -39
- pastastore-1.8.0.dist-info/RECORD +28 -0
- {pastastore-1.7.1.dist-info → pastastore-1.8.0.dist-info}/WHEEL +1 -1
- {pastastore-1.7.1.dist-info → pastastore-1.8.0.dist-info}/top_level.txt +2 -0
- tests/conftest.py +169 -0
- tests/test_001_import.py +8 -0
- tests/test_002_connectors.py +277 -0
- tests/test_003_pastastore.py +321 -0
- tests/test_004_yaml.py +135 -0
- tests/test_005_maps_plots.py +81 -0
- tests/test_006_benchmark.py +181 -0
- tests/test_007_hpdextension.py +87 -0
- tests/test_008_stressmodels.py +128 -0
- pastastore-1.7.1.dist-info/RECORD +0 -18
- {pastastore-1.7.1.dist-info → pastastore-1.8.0.dist-info}/LICENSE +0 -0
pastastore/base.py
CHANGED
|
@@ -2,20 +2,15 @@
|
|
|
2
2
|
"""Base classes for PastaStore Connectors."""
|
|
3
3
|
|
|
4
4
|
import functools
|
|
5
|
-
import json
|
|
6
5
|
import warnings
|
|
7
6
|
|
|
8
7
|
# import weakref
|
|
9
8
|
from abc import ABC, abstractmethod
|
|
10
|
-
from collections.abc import Iterable
|
|
11
9
|
from itertools import chain
|
|
12
|
-
from typing import Dict, List, Optional, Tuple, Union
|
|
10
|
+
from typing import Callable, Dict, List, Optional, Tuple, Union
|
|
13
11
|
|
|
14
12
|
import pandas as pd
|
|
15
13
|
import pastas as ps
|
|
16
|
-
from numpy import isin
|
|
17
|
-
from packaging.version import parse as parse_version
|
|
18
|
-
from pastas.io.pas import PastasEncoder
|
|
19
14
|
from tqdm.auto import tqdm
|
|
20
15
|
|
|
21
16
|
from pastastore.util import ItemInLibraryException, _custom_warning, validate_names
|
|
@@ -30,7 +25,7 @@ class BaseConnector(ABC):
|
|
|
30
25
|
|
|
31
26
|
Class holds base logic for dealing with time series and Pastas Models. Create your
|
|
32
27
|
own Connector to a data source by writing a a class that inherits from this
|
|
33
|
-
BaseConnector. Your class has to override each abstractmethod and
|
|
28
|
+
BaseConnector. Your class has to override each abstractmethod and property.
|
|
34
29
|
"""
|
|
35
30
|
|
|
36
31
|
_default_library_names = [
|
|
@@ -47,6 +42,10 @@ class BaseConnector(ABC):
|
|
|
47
42
|
# True for pastas>=0.23.0 and False for pastas<=0.22.0
|
|
48
43
|
USE_PASTAS_VALIDATE_SERIES = False if PASTAS_LEQ_022 else True
|
|
49
44
|
|
|
45
|
+
# set series equality comparison settings (using assert_series_equal)
|
|
46
|
+
SERIES_EQUALITY_ABSOLUTE_TOLERANCE = 1e-10
|
|
47
|
+
SERIES_EQUALITY_RELATIVE_TOLERANCE = 0.0
|
|
48
|
+
|
|
50
49
|
def __repr__(self):
|
|
51
50
|
"""Representation string of the object."""
|
|
52
51
|
return (
|
|
@@ -65,7 +64,7 @@ class BaseConnector(ABC):
|
|
|
65
64
|
def _get_library(self, libname: str):
|
|
66
65
|
"""Get library handle.
|
|
67
66
|
|
|
68
|
-
Must be
|
|
67
|
+
Must be overridden by subclass.
|
|
69
68
|
|
|
70
69
|
Parameters
|
|
71
70
|
----------
|
|
@@ -89,7 +88,7 @@ class BaseConnector(ABC):
|
|
|
89
88
|
) -> None:
|
|
90
89
|
"""Add item for both time series and pastas.Models (internal method).
|
|
91
90
|
|
|
92
|
-
Must be
|
|
91
|
+
Must be overridden by subclass.
|
|
93
92
|
|
|
94
93
|
Parameters
|
|
95
94
|
----------
|
|
@@ -107,7 +106,7 @@ class BaseConnector(ABC):
|
|
|
107
106
|
def _get_item(self, libname: str, name: str) -> Union[FrameorSeriesUnion, Dict]:
|
|
108
107
|
"""Get item (series or pastas.Models) (internal method).
|
|
109
108
|
|
|
110
|
-
Must be
|
|
109
|
+
Must be overridden by subclass.
|
|
111
110
|
|
|
112
111
|
Parameters
|
|
113
112
|
----------
|
|
@@ -126,7 +125,7 @@ class BaseConnector(ABC):
|
|
|
126
125
|
def _del_item(self, libname: str, name: str) -> None:
|
|
127
126
|
"""Delete items (series or models) (internal method).
|
|
128
127
|
|
|
129
|
-
Must be
|
|
128
|
+
Must be overridden by subclass.
|
|
130
129
|
|
|
131
130
|
Parameters
|
|
132
131
|
----------
|
|
@@ -140,7 +139,7 @@ class BaseConnector(ABC):
|
|
|
140
139
|
def _get_metadata(self, libname: str, name: str) -> Dict:
|
|
141
140
|
"""Get metadata (internal method).
|
|
142
141
|
|
|
143
|
-
Must be
|
|
142
|
+
Must be overridden by subclass.
|
|
144
143
|
|
|
145
144
|
Parameters
|
|
146
145
|
----------
|
|
@@ -160,7 +159,7 @@ class BaseConnector(ABC):
|
|
|
160
159
|
def oseries_names(self):
|
|
161
160
|
"""List of oseries names.
|
|
162
161
|
|
|
163
|
-
Property must be
|
|
162
|
+
Property must be overridden by subclass.
|
|
164
163
|
"""
|
|
165
164
|
|
|
166
165
|
@property
|
|
@@ -168,7 +167,7 @@ class BaseConnector(ABC):
|
|
|
168
167
|
def stresses_names(self):
|
|
169
168
|
"""List of stresses names.
|
|
170
169
|
|
|
171
|
-
Property must be
|
|
170
|
+
Property must be overridden by subclass.
|
|
172
171
|
"""
|
|
173
172
|
|
|
174
173
|
@property
|
|
@@ -176,7 +175,37 @@ class BaseConnector(ABC):
|
|
|
176
175
|
def model_names(self):
|
|
177
176
|
"""List of model names.
|
|
178
177
|
|
|
179
|
-
Property must be
|
|
178
|
+
Property must be overridden by subclass.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
@abstractmethod
|
|
182
|
+
def _parallel(
|
|
183
|
+
self,
|
|
184
|
+
func: Callable,
|
|
185
|
+
names: List[str],
|
|
186
|
+
progressbar: Optional[bool] = True,
|
|
187
|
+
max_workers: Optional[int] = None,
|
|
188
|
+
chunksize: Optional[int] = None,
|
|
189
|
+
desc: str = "",
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Parallel processing of function.
|
|
192
|
+
|
|
193
|
+
Must be overridden by subclass.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
func : function
|
|
198
|
+
function to apply in parallel
|
|
199
|
+
names : list
|
|
200
|
+
list of names to apply function to
|
|
201
|
+
progressbar : bool, optional
|
|
202
|
+
show progressbar, by default True
|
|
203
|
+
max_workers : int, optional
|
|
204
|
+
maximum number of workers, by default None
|
|
205
|
+
chunksize : int, optional
|
|
206
|
+
chunksize for parallel processing, by default None
|
|
207
|
+
desc : str, optional
|
|
208
|
+
description for progressbar, by default ""
|
|
180
209
|
"""
|
|
181
210
|
|
|
182
211
|
def set_check_model_series_values(self, b: bool):
|
|
@@ -670,22 +699,27 @@ class BaseConnector(ABC):
|
|
|
670
699
|
metadata["kind"] = kind
|
|
671
700
|
self._upsert_series("stresses", series, name, metadata=metadata)
|
|
672
701
|
|
|
673
|
-
def del_models(self, names: Union[list, str]) -> None:
|
|
702
|
+
def del_models(self, names: Union[list, str], verbose: bool = True) -> None:
|
|
674
703
|
"""Delete model(s) from the database.
|
|
675
704
|
|
|
676
705
|
Parameters
|
|
677
706
|
----------
|
|
678
707
|
names : str or list of str
|
|
679
708
|
name(s) of the model to delete
|
|
709
|
+
verbose : bool, optional
|
|
710
|
+
print information about deleted models, by default True
|
|
680
711
|
"""
|
|
681
|
-
|
|
712
|
+
names = self._parse_names(names, libname="models")
|
|
713
|
+
for n in names:
|
|
682
714
|
mldict = self.get_models(n, return_dict=True)
|
|
683
715
|
oname = mldict["oseries"]["name"]
|
|
684
716
|
self._del_item("models", n)
|
|
685
717
|
self._del_oseries_model_link(oname, n)
|
|
686
718
|
self._clear_cache("_modelnames_cache")
|
|
719
|
+
if verbose:
|
|
720
|
+
print(f"Deleted {len(names)} model(s) from database.")
|
|
687
721
|
|
|
688
|
-
def del_model(self, names: Union[list, str]) -> None:
|
|
722
|
+
def del_model(self, names: Union[list, str], verbose: bool = True) -> None:
|
|
689
723
|
"""Delete model(s) from the database.
|
|
690
724
|
|
|
691
725
|
Alias for del_models().
|
|
@@ -694,10 +728,14 @@ class BaseConnector(ABC):
|
|
|
694
728
|
----------
|
|
695
729
|
names : str or list of str
|
|
696
730
|
name(s) of the model to delete
|
|
731
|
+
verbose : bool, optional
|
|
732
|
+
print information about deleted models, by default True
|
|
697
733
|
"""
|
|
698
|
-
self.del_models(names=names)
|
|
734
|
+
self.del_models(names=names, verbose=verbose)
|
|
699
735
|
|
|
700
|
-
def del_oseries(
|
|
736
|
+
def del_oseries(
|
|
737
|
+
self, names: Union[list, str], remove_models: bool = False, verbose: bool = True
|
|
738
|
+
):
|
|
701
739
|
"""Delete oseries from the database.
|
|
702
740
|
|
|
703
741
|
Parameters
|
|
@@ -706,29 +744,38 @@ class BaseConnector(ABC):
|
|
|
706
744
|
name(s) of the oseries to delete
|
|
707
745
|
remove_models : bool, optional
|
|
708
746
|
also delete models for deleted oseries, default is False
|
|
747
|
+
verbose : bool, optional
|
|
748
|
+
print information about deleted oseries, by default True
|
|
709
749
|
"""
|
|
710
750
|
names = self._parse_names(names, libname="oseries")
|
|
711
751
|
for n in names:
|
|
712
752
|
self._del_item("oseries", n)
|
|
713
753
|
self._clear_cache("oseries")
|
|
754
|
+
if verbose:
|
|
755
|
+
print(f"Deleted {len(names)} oseries from database.")
|
|
714
756
|
# remove associated models from database
|
|
715
757
|
if remove_models:
|
|
716
758
|
modelnames = list(
|
|
717
759
|
chain.from_iterable([self.oseries_models.get(n, []) for n in names])
|
|
718
760
|
)
|
|
719
|
-
self.del_models(modelnames)
|
|
761
|
+
self.del_models(modelnames, verbose=verbose)
|
|
720
762
|
|
|
721
|
-
def del_stress(self, names: Union[list, str]):
|
|
763
|
+
def del_stress(self, names: Union[list, str], verbose: bool = True):
|
|
722
764
|
"""Delete stress from the database.
|
|
723
765
|
|
|
724
766
|
Parameters
|
|
725
767
|
----------
|
|
726
768
|
names : str or list of str
|
|
727
769
|
name(s) of the stress to delete
|
|
770
|
+
verbose : bool, optional
|
|
771
|
+
print information about deleted stresses, by default True
|
|
728
772
|
"""
|
|
729
|
-
|
|
773
|
+
names = self._parse_names(names, libname="stresses")
|
|
774
|
+
for n in names:
|
|
730
775
|
self._del_item("stresses", n)
|
|
731
776
|
self._clear_cache("stresses")
|
|
777
|
+
if verbose:
|
|
778
|
+
print(f"Deleted {len(names)} stress(es) from database.")
|
|
732
779
|
|
|
733
780
|
def _get_series(
|
|
734
781
|
self,
|
|
@@ -1088,8 +1135,8 @@ class BaseConnector(ABC):
|
|
|
1088
1135
|
time series contained in library
|
|
1089
1136
|
"""
|
|
1090
1137
|
names = self._parse_names(names, libname)
|
|
1091
|
-
for
|
|
1092
|
-
yield self._get_series(libname,
|
|
1138
|
+
for name in names:
|
|
1139
|
+
yield self._get_series(libname, name, progressbar=False)
|
|
1093
1140
|
|
|
1094
1141
|
def iter_oseries(self, names: Optional[List[str]] = None):
|
|
1095
1142
|
"""Iterate over oseries in library.
|
|
@@ -1315,611 +1362,6 @@ class BaseConnector(ABC):
|
|
|
1315
1362
|
return d
|
|
1316
1363
|
|
|
1317
1364
|
|
|
1318
|
-
class ConnectorUtil:
|
|
1319
|
-
"""Mix-in class for general Connector helper functions.
|
|
1320
|
-
|
|
1321
|
-
Only for internal methods, and not methods that are related to CRUD operations on
|
|
1322
|
-
database.
|
|
1323
|
-
"""
|
|
1324
|
-
|
|
1325
|
-
def _parse_names(
|
|
1326
|
-
self,
|
|
1327
|
-
names: Optional[Union[list, str]] = None,
|
|
1328
|
-
libname: Optional[str] = "oseries",
|
|
1329
|
-
) -> list:
|
|
1330
|
-
"""Parse names kwarg, returns iterable with name(s) (internal method).
|
|
1331
|
-
|
|
1332
|
-
Parameters
|
|
1333
|
-
----------
|
|
1334
|
-
names : Union[list, str], optional
|
|
1335
|
-
str or list of str or None or 'all' (last two options
|
|
1336
|
-
retrieves all names)
|
|
1337
|
-
libname : str, optional
|
|
1338
|
-
name of library, default is 'oseries'
|
|
1339
|
-
|
|
1340
|
-
Returns
|
|
1341
|
-
-------
|
|
1342
|
-
list
|
|
1343
|
-
list of names
|
|
1344
|
-
"""
|
|
1345
|
-
if not isinstance(names, str) and isinstance(names, Iterable):
|
|
1346
|
-
return names
|
|
1347
|
-
elif isinstance(names, str) and names != "all":
|
|
1348
|
-
return [names]
|
|
1349
|
-
elif names is None or names == "all":
|
|
1350
|
-
if libname == "oseries":
|
|
1351
|
-
return self.oseries_names
|
|
1352
|
-
elif libname == "stresses":
|
|
1353
|
-
return self.stresses_names
|
|
1354
|
-
elif libname == "models":
|
|
1355
|
-
return self.model_names
|
|
1356
|
-
elif libname == "oseries_models":
|
|
1357
|
-
return self.oseries_with_models
|
|
1358
|
-
else:
|
|
1359
|
-
raise ValueError(f"No library '{libname}'!")
|
|
1360
|
-
else:
|
|
1361
|
-
raise NotImplementedError(f"Cannot parse 'names': {names}")
|
|
1362
|
-
|
|
1363
|
-
@staticmethod
|
|
1364
|
-
def _meta_list_to_frame(metalist: list, names: list):
|
|
1365
|
-
"""Convert list of metadata dictionaries to DataFrame.
|
|
1366
|
-
|
|
1367
|
-
Parameters
|
|
1368
|
-
----------
|
|
1369
|
-
metalist : list
|
|
1370
|
-
list of metadata dictionaries
|
|
1371
|
-
names : list
|
|
1372
|
-
list of names corresponding to data in metalist
|
|
1373
|
-
|
|
1374
|
-
Returns
|
|
1375
|
-
-------
|
|
1376
|
-
pandas.DataFrame
|
|
1377
|
-
DataFrame containing overview of metadata
|
|
1378
|
-
"""
|
|
1379
|
-
# convert to dataframe
|
|
1380
|
-
if len(metalist) > 1:
|
|
1381
|
-
meta = pd.DataFrame(metalist)
|
|
1382
|
-
if len({"x", "y"}.difference(meta.columns)) == 0:
|
|
1383
|
-
meta["x"] = meta["x"].astype(float)
|
|
1384
|
-
meta["y"] = meta["y"].astype(float)
|
|
1385
|
-
elif len(metalist) == 1:
|
|
1386
|
-
meta = pd.DataFrame(metalist)
|
|
1387
|
-
elif len(metalist) == 0:
|
|
1388
|
-
meta = pd.DataFrame()
|
|
1389
|
-
|
|
1390
|
-
meta.index = names
|
|
1391
|
-
meta.index.name = "name"
|
|
1392
|
-
return meta
|
|
1393
|
-
|
|
1394
|
-
def _parse_model_dict(self, mdict: dict, update_ts_settings: bool = False):
|
|
1395
|
-
"""Parse dictionary describing pastas models (internal method).
|
|
1396
|
-
|
|
1397
|
-
Parameters
|
|
1398
|
-
----------
|
|
1399
|
-
mdict : dict
|
|
1400
|
-
dictionary describing pastas.Model
|
|
1401
|
-
update_ts_settings : bool, optional
|
|
1402
|
-
update stored tmin and tmax in time series settings
|
|
1403
|
-
based on time series loaded from store.
|
|
1404
|
-
|
|
1405
|
-
Returns
|
|
1406
|
-
-------
|
|
1407
|
-
ml : pastas.Model
|
|
1408
|
-
time series analysis model
|
|
1409
|
-
"""
|
|
1410
|
-
PASFILE_LEQ_022 = parse_version(
|
|
1411
|
-
mdict["file_info"]["pastas_version"]
|
|
1412
|
-
) <= parse_version("0.22.0")
|
|
1413
|
-
|
|
1414
|
-
# oseries
|
|
1415
|
-
if "series" not in mdict["oseries"]:
|
|
1416
|
-
name = str(mdict["oseries"]["name"])
|
|
1417
|
-
if name not in self.oseries.index:
|
|
1418
|
-
msg = "oseries '{}' not present in library".format(name)
|
|
1419
|
-
raise LookupError(msg)
|
|
1420
|
-
mdict["oseries"]["series"] = self.get_oseries(name).squeeze()
|
|
1421
|
-
# update tmin/tmax from time series
|
|
1422
|
-
if update_ts_settings:
|
|
1423
|
-
mdict["oseries"]["settings"]["tmin"] = mdict["oseries"]["series"].index[
|
|
1424
|
-
0
|
|
1425
|
-
]
|
|
1426
|
-
mdict["oseries"]["settings"]["tmax"] = mdict["oseries"]["series"].index[
|
|
1427
|
-
-1
|
|
1428
|
-
]
|
|
1429
|
-
|
|
1430
|
-
# StressModel, WellModel
|
|
1431
|
-
for ts in mdict["stressmodels"].values():
|
|
1432
|
-
if "stress" in ts.keys():
|
|
1433
|
-
# WellModel
|
|
1434
|
-
classkey = "stressmodel" if PASFILE_LEQ_022 else "class"
|
|
1435
|
-
if ts[classkey] == "WellModel":
|
|
1436
|
-
for stress in ts["stress"]:
|
|
1437
|
-
if "series" not in stress:
|
|
1438
|
-
name = str(stress["name"])
|
|
1439
|
-
if name in self.stresses.index:
|
|
1440
|
-
stress["series"] = self.get_stresses(name).squeeze()
|
|
1441
|
-
# update tmin/tmax from time series
|
|
1442
|
-
if update_ts_settings:
|
|
1443
|
-
stress["settings"]["tmin"] = stress["series"].index[
|
|
1444
|
-
0
|
|
1445
|
-
]
|
|
1446
|
-
stress["settings"]["tmax"] = stress["series"].index[
|
|
1447
|
-
-1
|
|
1448
|
-
]
|
|
1449
|
-
# StressModel
|
|
1450
|
-
else:
|
|
1451
|
-
for stress in ts["stress"] if PASFILE_LEQ_022 else [ts["stress"]]:
|
|
1452
|
-
if "series" not in stress:
|
|
1453
|
-
name = str(stress["name"])
|
|
1454
|
-
if name in self.stresses.index:
|
|
1455
|
-
stress["series"] = self.get_stresses(name).squeeze()
|
|
1456
|
-
# update tmin/tmax from time series
|
|
1457
|
-
if update_ts_settings:
|
|
1458
|
-
stress["settings"]["tmin"] = stress["series"].index[
|
|
1459
|
-
0
|
|
1460
|
-
]
|
|
1461
|
-
stress["settings"]["tmax"] = stress["series"].index[
|
|
1462
|
-
-1
|
|
1463
|
-
]
|
|
1464
|
-
|
|
1465
|
-
# RechargeModel, TarsoModel
|
|
1466
|
-
if ("prec" in ts.keys()) and ("evap" in ts.keys()):
|
|
1467
|
-
for stress in [ts["prec"], ts["evap"]]:
|
|
1468
|
-
if "series" not in stress:
|
|
1469
|
-
name = str(stress["name"])
|
|
1470
|
-
if name in self.stresses.index:
|
|
1471
|
-
stress["series"] = self.get_stresses(name).squeeze()
|
|
1472
|
-
# update tmin/tmax from time series
|
|
1473
|
-
if update_ts_settings:
|
|
1474
|
-
stress["settings"]["tmin"] = stress["series"].index[0]
|
|
1475
|
-
stress["settings"]["tmax"] = stress["series"].index[-1]
|
|
1476
|
-
else:
|
|
1477
|
-
msg = "stress '{}' not present in library".format(name)
|
|
1478
|
-
raise KeyError(msg)
|
|
1479
|
-
|
|
1480
|
-
# hack for pcov w dtype object (when filled with NaNs on store?)
|
|
1481
|
-
if "fit" in mdict:
|
|
1482
|
-
if "pcov" in mdict["fit"]:
|
|
1483
|
-
pcov = mdict["fit"]["pcov"]
|
|
1484
|
-
if pcov.dtypes.apply(lambda dtyp: isinstance(dtyp, object)).any():
|
|
1485
|
-
mdict["fit"]["pcov"] = pcov.astype(float)
|
|
1486
|
-
|
|
1487
|
-
# check pastas version vs pas-file version
|
|
1488
|
-
file_version = mdict["file_info"]["pastas_version"]
|
|
1489
|
-
|
|
1490
|
-
# check file version and pastas version
|
|
1491
|
-
# if file<0.23 and pastas>=1.0 --> error
|
|
1492
|
-
PASTAS_GT_023 = parse_version(ps.__version__) > parse_version("0.23.1")
|
|
1493
|
-
if PASFILE_LEQ_022 and PASTAS_GT_023:
|
|
1494
|
-
raise UserWarning(
|
|
1495
|
-
f"This file was created with Pastas v{file_version} "
|
|
1496
|
-
f"and cannot be loaded with Pastas v{ps.__version__} Please load and "
|
|
1497
|
-
"save the file with Pastas 0.23 first to update the file "
|
|
1498
|
-
"format."
|
|
1499
|
-
)
|
|
1500
|
-
|
|
1501
|
-
try:
|
|
1502
|
-
# pastas>=0.15.0
|
|
1503
|
-
ml = ps.io.base._load_model(mdict)
|
|
1504
|
-
except AttributeError:
|
|
1505
|
-
# pastas<0.15.0
|
|
1506
|
-
ml = ps.io.base.load_model(mdict)
|
|
1507
|
-
return ml
|
|
1508
|
-
|
|
1509
|
-
@staticmethod
|
|
1510
|
-
def _validate_input_series(series):
|
|
1511
|
-
"""Check if series is pandas.DataFrame or pandas.Series.
|
|
1512
|
-
|
|
1513
|
-
Parameters
|
|
1514
|
-
----------
|
|
1515
|
-
series : object
|
|
1516
|
-
object to validate
|
|
1517
|
-
|
|
1518
|
-
Raises
|
|
1519
|
-
------
|
|
1520
|
-
TypeError
|
|
1521
|
-
if object is not of type pandas.DataFrame or pandas.Series
|
|
1522
|
-
"""
|
|
1523
|
-
if not (isinstance(series, pd.DataFrame) or isinstance(series, pd.Series)):
|
|
1524
|
-
raise TypeError("Please provide pandas.DataFrame or pandas.Series!")
|
|
1525
|
-
if isinstance(series, pd.DataFrame):
|
|
1526
|
-
if series.columns.size > 1:
|
|
1527
|
-
raise ValueError("Only DataFrames with one column are supported!")
|
|
1528
|
-
|
|
1529
|
-
@staticmethod
|
|
1530
|
-
def _set_series_name(series, name):
|
|
1531
|
-
"""Set series name to match user defined name in store.
|
|
1532
|
-
|
|
1533
|
-
Parameters
|
|
1534
|
-
----------
|
|
1535
|
-
series : pandas.Series or pandas.DataFrame
|
|
1536
|
-
set name for this time series
|
|
1537
|
-
name : str
|
|
1538
|
-
name of the time series (used in the pastastore)
|
|
1539
|
-
"""
|
|
1540
|
-
if isinstance(series, pd.Series):
|
|
1541
|
-
series.name = name
|
|
1542
|
-
# empty string on index name causes trouble when reading
|
|
1543
|
-
# data from ArcticDB: TODO: check if still an issue?
|
|
1544
|
-
if series.index.name == "":
|
|
1545
|
-
series.index.name = None
|
|
1546
|
-
|
|
1547
|
-
if isinstance(series, pd.DataFrame):
|
|
1548
|
-
series.columns = [name]
|
|
1549
|
-
# check for hydropandas objects which are instances of DataFrame but
|
|
1550
|
-
# do have a name attribute
|
|
1551
|
-
if hasattr(series, "name"):
|
|
1552
|
-
series.name = name
|
|
1553
|
-
return series
|
|
1554
|
-
|
|
1555
|
-
@staticmethod
|
|
1556
|
-
def _check_stressmodels_supported(ml):
|
|
1557
|
-
supported_stressmodels = [
|
|
1558
|
-
"StressModel",
|
|
1559
|
-
"StressModel2",
|
|
1560
|
-
"RechargeModel",
|
|
1561
|
-
"WellModel",
|
|
1562
|
-
"TarsoModel",
|
|
1563
|
-
"Constant",
|
|
1564
|
-
"LinearTrend",
|
|
1565
|
-
"StepModel",
|
|
1566
|
-
]
|
|
1567
|
-
if isinstance(ml, ps.Model):
|
|
1568
|
-
smtyps = [sm._name for sm in ml.stressmodels.values()]
|
|
1569
|
-
elif isinstance(ml, dict):
|
|
1570
|
-
classkey = "stressmodel" if PASTAS_LEQ_022 else "class"
|
|
1571
|
-
smtyps = [sm[classkey] for sm in ml["stressmodels"].values()]
|
|
1572
|
-
check = isin(smtyps, supported_stressmodels)
|
|
1573
|
-
if not all(check):
|
|
1574
|
-
unsupported = set(smtyps) - set(supported_stressmodels)
|
|
1575
|
-
raise NotImplementedError(
|
|
1576
|
-
"PastaStore does not support storing models with the "
|
|
1577
|
-
f"following stressmodels: {unsupported}"
|
|
1578
|
-
)
|
|
1579
|
-
|
|
1580
|
-
@staticmethod
|
|
1581
|
-
def _check_model_series_names_for_store(ml):
|
|
1582
|
-
prec_evap_model = ["RechargeModel", "TarsoModel"]
|
|
1583
|
-
|
|
1584
|
-
if isinstance(ml, ps.Model):
|
|
1585
|
-
series_names = [
|
|
1586
|
-
istress.series.name
|
|
1587
|
-
for sm in ml.stressmodels.values()
|
|
1588
|
-
for istress in sm.stress
|
|
1589
|
-
]
|
|
1590
|
-
|
|
1591
|
-
elif isinstance(ml, dict):
|
|
1592
|
-
# non RechargeModel, Tarsomodel, WellModel stressmodels
|
|
1593
|
-
classkey = "stressmodel" if PASTAS_LEQ_022 else "class"
|
|
1594
|
-
if PASTAS_LEQ_022:
|
|
1595
|
-
series_names = [
|
|
1596
|
-
istress["name"]
|
|
1597
|
-
for sm in ml["stressmodels"].values()
|
|
1598
|
-
if sm[classkey] not in (prec_evap_model + ["WellModel"])
|
|
1599
|
-
for istress in sm["stress"]
|
|
1600
|
-
]
|
|
1601
|
-
else:
|
|
1602
|
-
series_names = [
|
|
1603
|
-
sm["stress"]["name"]
|
|
1604
|
-
for sm in ml["stressmodels"].values()
|
|
1605
|
-
if sm[classkey] not in (prec_evap_model + ["WellModel"])
|
|
1606
|
-
]
|
|
1607
|
-
|
|
1608
|
-
# WellModel
|
|
1609
|
-
if isin(
|
|
1610
|
-
["WellModel"],
|
|
1611
|
-
[i[classkey] for i in ml["stressmodels"].values()],
|
|
1612
|
-
).any():
|
|
1613
|
-
series_names += [
|
|
1614
|
-
istress["name"]
|
|
1615
|
-
for sm in ml["stressmodels"].values()
|
|
1616
|
-
if sm[classkey] in ["WellModel"]
|
|
1617
|
-
for istress in sm["stress"]
|
|
1618
|
-
]
|
|
1619
|
-
|
|
1620
|
-
# RechargeModel, TarsoModel
|
|
1621
|
-
if isin(
|
|
1622
|
-
prec_evap_model,
|
|
1623
|
-
[i[classkey] for i in ml["stressmodels"].values()],
|
|
1624
|
-
).any():
|
|
1625
|
-
series_names += [
|
|
1626
|
-
istress["name"]
|
|
1627
|
-
for sm in ml["stressmodels"].values()
|
|
1628
|
-
if sm[classkey] in prec_evap_model
|
|
1629
|
-
for istress in [sm["prec"], sm["evap"]]
|
|
1630
|
-
]
|
|
1631
|
-
|
|
1632
|
-
else:
|
|
1633
|
-
raise TypeError("Expected pastas.Model or dict!")
|
|
1634
|
-
if len(series_names) - len(set(series_names)) > 0:
|
|
1635
|
-
msg = (
|
|
1636
|
-
"There are multiple stresses series with the same name! "
|
|
1637
|
-
"Each series name must be unique for the PastaStore!"
|
|
1638
|
-
)
|
|
1639
|
-
raise ValueError(msg)
|
|
1640
|
-
|
|
1641
|
-
def _check_oseries_in_store(self, ml: Union[ps.Model, dict]):
|
|
1642
|
-
"""Check if Model oseries are contained in PastaStore (internal method).
|
|
1643
|
-
|
|
1644
|
-
Parameters
|
|
1645
|
-
----------
|
|
1646
|
-
ml : Union[ps.Model, dict]
|
|
1647
|
-
pastas Model
|
|
1648
|
-
"""
|
|
1649
|
-
if isinstance(ml, ps.Model):
|
|
1650
|
-
name = ml.oseries.name
|
|
1651
|
-
elif isinstance(ml, dict):
|
|
1652
|
-
name = str(ml["oseries"]["name"])
|
|
1653
|
-
else:
|
|
1654
|
-
raise TypeError("Expected pastas.Model or dict!")
|
|
1655
|
-
if name not in self.oseries.index:
|
|
1656
|
-
msg = (
|
|
1657
|
-
f"Cannot add model because oseries '{name}' "
|
|
1658
|
-
"is not contained in store."
|
|
1659
|
-
)
|
|
1660
|
-
raise LookupError(msg)
|
|
1661
|
-
# expensive check
|
|
1662
|
-
if self.CHECK_MODEL_SERIES_VALUES and isinstance(ml, ps.Model):
|
|
1663
|
-
s_org = self.get_oseries(name).squeeze().dropna()
|
|
1664
|
-
if PASTAS_LEQ_022:
|
|
1665
|
-
so = ml.oseries.series_original
|
|
1666
|
-
else:
|
|
1667
|
-
so = ml.oseries._series_original
|
|
1668
|
-
if not so.dropna().equals(s_org):
|
|
1669
|
-
raise ValueError(
|
|
1670
|
-
f"Cannot add model because model oseries '{name}'"
|
|
1671
|
-
" is different from stored oseries!"
|
|
1672
|
-
)
|
|
1673
|
-
|
|
1674
|
-
def _check_stresses_in_store(self, ml: Union[ps.Model, dict]):
|
|
1675
|
-
"""Check if stresses time series are contained in PastaStore (internal method).
|
|
1676
|
-
|
|
1677
|
-
Parameters
|
|
1678
|
-
----------
|
|
1679
|
-
ml : Union[ps.Model, dict]
|
|
1680
|
-
pastas Model
|
|
1681
|
-
"""
|
|
1682
|
-
prec_evap_model = ["RechargeModel", "TarsoModel"]
|
|
1683
|
-
if isinstance(ml, ps.Model):
|
|
1684
|
-
for sm in ml.stressmodels.values():
|
|
1685
|
-
if sm._name in prec_evap_model:
|
|
1686
|
-
stresses = [sm.prec, sm.evap]
|
|
1687
|
-
else:
|
|
1688
|
-
stresses = sm.stress
|
|
1689
|
-
for s in stresses:
|
|
1690
|
-
if str(s.name) not in self.stresses.index:
|
|
1691
|
-
msg = (
|
|
1692
|
-
f"Cannot add model because stress '{s.name}' "
|
|
1693
|
-
"is not contained in store."
|
|
1694
|
-
)
|
|
1695
|
-
raise LookupError(msg)
|
|
1696
|
-
if self.CHECK_MODEL_SERIES_VALUES:
|
|
1697
|
-
s_org = self.get_stresses(s.name).squeeze()
|
|
1698
|
-
if PASTAS_LEQ_022:
|
|
1699
|
-
so = s.series_original
|
|
1700
|
-
else:
|
|
1701
|
-
so = s._series_original
|
|
1702
|
-
if not so.equals(s_org):
|
|
1703
|
-
raise ValueError(
|
|
1704
|
-
f"Cannot add model because model stress "
|
|
1705
|
-
f"'{s.name}' is different from stored stress!"
|
|
1706
|
-
)
|
|
1707
|
-
elif isinstance(ml, dict):
|
|
1708
|
-
for sm in ml["stressmodels"].values():
|
|
1709
|
-
classkey = "stressmodel" if PASTAS_LEQ_022 else "class"
|
|
1710
|
-
if sm[classkey] in prec_evap_model:
|
|
1711
|
-
stresses = [sm["prec"], sm["evap"]]
|
|
1712
|
-
elif sm[classkey] in ["WellModel"]:
|
|
1713
|
-
stresses = sm["stress"]
|
|
1714
|
-
else:
|
|
1715
|
-
stresses = sm["stress"] if PASTAS_LEQ_022 else [sm["stress"]]
|
|
1716
|
-
for s in stresses:
|
|
1717
|
-
if str(s["name"]) not in self.stresses.index:
|
|
1718
|
-
msg = (
|
|
1719
|
-
f"Cannot add model because stress '{s['name']}' "
|
|
1720
|
-
"is not contained in store."
|
|
1721
|
-
)
|
|
1722
|
-
raise LookupError(msg)
|
|
1723
|
-
else:
|
|
1724
|
-
raise TypeError("Expected pastas.Model or dict!")
|
|
1725
|
-
|
|
1726
|
-
def _stored_series_to_json(
|
|
1727
|
-
self,
|
|
1728
|
-
libname: str,
|
|
1729
|
-
names: Optional[Union[list, str]] = None,
|
|
1730
|
-
squeeze: bool = True,
|
|
1731
|
-
progressbar: bool = False,
|
|
1732
|
-
):
|
|
1733
|
-
"""Write stored series to JSON.
|
|
1734
|
-
|
|
1735
|
-
Parameters
|
|
1736
|
-
----------
|
|
1737
|
-
libname : str
|
|
1738
|
-
library name
|
|
1739
|
-
names : Optional[Union[list, str]], optional
|
|
1740
|
-
names of series, by default None
|
|
1741
|
-
squeeze : bool, optional
|
|
1742
|
-
return single entry as json string instead
|
|
1743
|
-
of list, by default True
|
|
1744
|
-
progressbar : bool, optional
|
|
1745
|
-
show progressbar, by default False
|
|
1746
|
-
|
|
1747
|
-
Returns
|
|
1748
|
-
-------
|
|
1749
|
-
files : list or str
|
|
1750
|
-
list of series converted to JSON string or single string
|
|
1751
|
-
if single entry is returned and squeeze is True
|
|
1752
|
-
"""
|
|
1753
|
-
names = self._parse_names(names, libname=libname)
|
|
1754
|
-
files = []
|
|
1755
|
-
for n in tqdm(names, desc=libname) if progressbar else names:
|
|
1756
|
-
s = self._get_series(libname, n, progressbar=False)
|
|
1757
|
-
if isinstance(s, pd.Series):
|
|
1758
|
-
s = s.to_frame()
|
|
1759
|
-
try:
|
|
1760
|
-
sjson = s.to_json(orient="columns")
|
|
1761
|
-
except ValueError as e:
|
|
1762
|
-
msg = (
|
|
1763
|
-
f"DatetimeIndex of '{n}' probably contains NaT "
|
|
1764
|
-
"or duplicate timestamps!"
|
|
1765
|
-
)
|
|
1766
|
-
raise ValueError(msg) from e
|
|
1767
|
-
files.append(sjson)
|
|
1768
|
-
if len(files) == 1 and squeeze:
|
|
1769
|
-
return files[0]
|
|
1770
|
-
else:
|
|
1771
|
-
return files
|
|
1772
|
-
|
|
1773
|
-
def _stored_metadata_to_json(
|
|
1774
|
-
self,
|
|
1775
|
-
libname: str,
|
|
1776
|
-
names: Optional[Union[list, str]] = None,
|
|
1777
|
-
squeeze: bool = True,
|
|
1778
|
-
progressbar: bool = False,
|
|
1779
|
-
):
|
|
1780
|
-
"""Write metadata from stored series to JSON.
|
|
1781
|
-
|
|
1782
|
-
Parameters
|
|
1783
|
-
----------
|
|
1784
|
-
libname : str
|
|
1785
|
-
library containing series
|
|
1786
|
-
names : Optional[Union[list, str]], optional
|
|
1787
|
-
names to parse, by default None
|
|
1788
|
-
squeeze : bool, optional
|
|
1789
|
-
return single entry as json string instead of list, by default True
|
|
1790
|
-
progressbar : bool, optional
|
|
1791
|
-
show progressbar, by default False
|
|
1792
|
-
|
|
1793
|
-
Returns
|
|
1794
|
-
-------
|
|
1795
|
-
files : list or str
|
|
1796
|
-
list of json string
|
|
1797
|
-
"""
|
|
1798
|
-
names = self._parse_names(names, libname=libname)
|
|
1799
|
-
files = []
|
|
1800
|
-
for n in tqdm(names, desc=libname) if progressbar else names:
|
|
1801
|
-
meta = self.get_metadata(libname, n, as_frame=False)
|
|
1802
|
-
meta_json = json.dumps(meta, cls=PastasEncoder, indent=4)
|
|
1803
|
-
files.append(meta_json)
|
|
1804
|
-
if len(files) == 1 and squeeze:
|
|
1805
|
-
return files[0]
|
|
1806
|
-
else:
|
|
1807
|
-
return files
|
|
1808
|
-
|
|
1809
|
-
def _series_to_archive(
|
|
1810
|
-
self,
|
|
1811
|
-
archive,
|
|
1812
|
-
libname: str,
|
|
1813
|
-
names: Optional[Union[list, str]] = None,
|
|
1814
|
-
progressbar: bool = True,
|
|
1815
|
-
):
|
|
1816
|
-
"""Write DataFrame or Series to zipfile (internal method).
|
|
1817
|
-
|
|
1818
|
-
Parameters
|
|
1819
|
-
----------
|
|
1820
|
-
archive : zipfile.ZipFile
|
|
1821
|
-
reference to an archive to write data to
|
|
1822
|
-
libname : str
|
|
1823
|
-
name of the library to write to zipfile
|
|
1824
|
-
names : str or list of str, optional
|
|
1825
|
-
names of the time series to write to archive, by default None,
|
|
1826
|
-
which writes all time series to archive
|
|
1827
|
-
progressbar : bool, optional
|
|
1828
|
-
show progressbar, by default True
|
|
1829
|
-
"""
|
|
1830
|
-
names = self._parse_names(names, libname=libname)
|
|
1831
|
-
for n in tqdm(names, desc=libname) if progressbar else names:
|
|
1832
|
-
sjson = self._stored_series_to_json(
|
|
1833
|
-
libname, names=n, progressbar=False, squeeze=True
|
|
1834
|
-
)
|
|
1835
|
-
meta_json = self._stored_metadata_to_json(
|
|
1836
|
-
libname, names=n, progressbar=False, squeeze=True
|
|
1837
|
-
)
|
|
1838
|
-
archive.writestr(f"{libname}/{n}.json", sjson)
|
|
1839
|
-
archive.writestr(f"{libname}/{n}_meta.json", meta_json)
|
|
1840
|
-
|
|
1841
|
-
def _models_to_archive(self, archive, names=None, progressbar=True):
|
|
1842
|
-
"""Write pastas.Model to zipfile (internal method).
|
|
1843
|
-
|
|
1844
|
-
Parameters
|
|
1845
|
-
----------
|
|
1846
|
-
archive : zipfile.ZipFile
|
|
1847
|
-
reference to an archive to write data to
|
|
1848
|
-
names : str or list of str, optional
|
|
1849
|
-
names of the models to write to archive, by default None,
|
|
1850
|
-
which writes all models to archive
|
|
1851
|
-
progressbar : bool, optional
|
|
1852
|
-
show progressbar, by default True
|
|
1853
|
-
"""
|
|
1854
|
-
names = self._parse_names(names, libname="models")
|
|
1855
|
-
for n in tqdm(names, desc="models") if progressbar else names:
|
|
1856
|
-
m = self.get_models(n, return_dict=True)
|
|
1857
|
-
jsondict = json.dumps(m, cls=PastasEncoder, indent=4)
|
|
1858
|
-
archive.writestr(f"models/{n}.pas", jsondict)
|
|
1859
|
-
|
|
1860
|
-
@staticmethod
|
|
1861
|
-
def _series_from_json(fjson: str, squeeze: bool = True):
|
|
1862
|
-
"""Load time series from JSON.
|
|
1863
|
-
|
|
1864
|
-
Parameters
|
|
1865
|
-
----------
|
|
1866
|
-
fjson : str
|
|
1867
|
-
path to file
|
|
1868
|
-
squeeze : bool, optional
|
|
1869
|
-
squeeze time series object to obtain pandas Series
|
|
1870
|
-
|
|
1871
|
-
Returns
|
|
1872
|
-
-------
|
|
1873
|
-
s : pd.DataFrame
|
|
1874
|
-
DataFrame containing time series
|
|
1875
|
-
"""
|
|
1876
|
-
s = pd.read_json(fjson, orient="columns", precise_float=True, dtype=False)
|
|
1877
|
-
if not isinstance(s.index, pd.DatetimeIndex):
|
|
1878
|
-
s.index = pd.to_datetime(s.index, unit="ms")
|
|
1879
|
-
s = s.sort_index() # needed for some reason ...
|
|
1880
|
-
if squeeze:
|
|
1881
|
-
return s.squeeze()
|
|
1882
|
-
return s
|
|
1883
|
-
|
|
1884
|
-
@staticmethod
|
|
1885
|
-
def _metadata_from_json(fjson: str):
|
|
1886
|
-
"""Load metadata dictionary from JSON.
|
|
1887
|
-
|
|
1888
|
-
Parameters
|
|
1889
|
-
----------
|
|
1890
|
-
fjson : str
|
|
1891
|
-
path to file
|
|
1892
|
-
|
|
1893
|
-
Returns
|
|
1894
|
-
-------
|
|
1895
|
-
meta : dict
|
|
1896
|
-
dictionary containing metadata
|
|
1897
|
-
"""
|
|
1898
|
-
with open(fjson, "r") as f:
|
|
1899
|
-
meta = json.load(f)
|
|
1900
|
-
return meta
|
|
1901
|
-
|
|
1902
|
-
def _get_model_orphans(self):
|
|
1903
|
-
"""Get models whose oseries no longer exist in database.
|
|
1904
|
-
|
|
1905
|
-
Returns
|
|
1906
|
-
-------
|
|
1907
|
-
dict
|
|
1908
|
-
dictionary with oseries names as keys and lists of model names
|
|
1909
|
-
as values
|
|
1910
|
-
"""
|
|
1911
|
-
d = {}
|
|
1912
|
-
for mlnam in tqdm(self.model_names, desc="Identifying model orphans"):
|
|
1913
|
-
mdict = self.get_models(mlnam, return_dict=True)
|
|
1914
|
-
onam = mdict["oseries"]["name"]
|
|
1915
|
-
if onam not in self.oseries_names:
|
|
1916
|
-
if onam in d:
|
|
1917
|
-
d[onam] = d[onam].append(mlnam)
|
|
1918
|
-
else:
|
|
1919
|
-
d[onam] = [mlnam]
|
|
1920
|
-
return d
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
1365
|
class ModelAccessor:
|
|
1924
1366
|
"""Object for managing access to stored models.
|
|
1925
1367
|
|