pastastore 1.3.0__py3-none-any.whl → 1.5.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/store.py CHANGED
@@ -1,7 +1,9 @@
1
+ """Module containing the PastaStore object for managing time series and models."""
2
+
1
3
  import json
2
4
  import os
3
5
  import warnings
4
- from typing import List, Optional, Tuple, Union
6
+ from typing import List, Literal, Optional, Tuple, Union
5
7
 
6
8
  import numpy as np
7
9
  import pandas as pd
@@ -14,6 +16,7 @@ from pastastore.base import BaseConnector
14
16
  from pastastore.connectors import DictConnector
15
17
  from pastastore.plotting import Maps, Plots
16
18
  from pastastore.util import _custom_warning
19
+ from pastastore.version import PASTAS_GEQ_150
17
20
  from pastastore.yaml_interface import PastastoreYAML
18
21
 
19
22
  FrameorSeriesUnion = Union[pd.DataFrame, pd.Series]
@@ -72,7 +75,7 @@ class PastaStore:
72
75
  self.yaml = PastastoreYAML(self)
73
76
 
74
77
  def _register_connector_methods(self):
75
- """Internal method for registering connector methods."""
78
+ """Register connector methods (internal method)."""
76
79
  methods = [
77
80
  func
78
81
  for func in dir(self.conn)
@@ -83,26 +86,70 @@ class PastaStore:
83
86
 
84
87
  @property
85
88
  def oseries(self):
89
+ """
90
+ Returns the oseries metadata as dataframe.
91
+
92
+ Returns
93
+ -------
94
+ oseries
95
+ oseries metadata as dataframe
96
+ """
86
97
  return self.conn.oseries
87
98
 
88
99
  @property
89
100
  def stresses(self):
101
+ """
102
+ Returns the stresses metadata as dataframe.
103
+
104
+ Returns
105
+ -------
106
+ stresses
107
+ stresses metadata as dataframe
108
+ """
90
109
  return self.conn.stresses
91
110
 
92
111
  @property
93
112
  def models(self):
113
+ """Return list of model names.
114
+
115
+ Returns
116
+ -------
117
+ list
118
+ list of model names
119
+ """
94
120
  return self.conn.models
95
121
 
96
122
  @property
97
123
  def oseries_names(self):
124
+ """Return list of oseries names.
125
+
126
+ Returns
127
+ -------
128
+ list
129
+ list of oseries names
130
+ """
98
131
  return self.conn.oseries_names
99
132
 
100
133
  @property
101
134
  def stresses_names(self):
135
+ """Return list of streses names.
136
+
137
+ Returns
138
+ -------
139
+ list
140
+ list of stresses names
141
+ """
102
142
  return self.conn.stresses_names
103
143
 
104
144
  @property
105
145
  def model_names(self):
146
+ """Return list of model names.
147
+
148
+ Returns
149
+ -------
150
+ list
151
+ list of model names
152
+ """
106
153
  return self.conn.model_names
107
154
 
108
155
  @property
@@ -111,22 +158,57 @@ class PastaStore:
111
158
 
112
159
  @property
113
160
  def n_oseries(self):
161
+ """Return number of oseries.
162
+
163
+ Returns
164
+ -------
165
+ int
166
+ number of oseries
167
+ """
114
168
  return self.conn.n_oseries
115
169
 
116
170
  @property
117
171
  def n_stresses(self):
172
+ """Return number of stresses.
173
+
174
+ Returns
175
+ -------
176
+ int
177
+ number of stresses
178
+ """
118
179
  return self.conn.n_stresses
119
180
 
120
181
  @property
121
182
  def n_models(self):
183
+ """Return number of models.
184
+
185
+ Returns
186
+ -------
187
+ int
188
+ number of models
189
+ """
122
190
  return self.conn.n_models
123
191
 
124
192
  @property
125
193
  def oseries_models(self):
194
+ """Return dictionary of models per oseries.
195
+
196
+ Returns
197
+ -------
198
+ dict
199
+ dictionary containing list of models (values) for each oseries (keys).
200
+ """
126
201
  return self.conn.oseries_models
127
202
 
128
203
  @property
129
204
  def oseries_with_models(self):
205
+ """Return list of oseries for which models are contained in the database.
206
+
207
+ Returns
208
+ -------
209
+ list
210
+ list of oseries names for which models are contained in the database.
211
+ """
130
212
  return self.conn.oseries_with_models
131
213
 
132
214
  def __repr__(self):
@@ -136,7 +218,7 @@ class PastaStore:
136
218
  def get_oseries_distances(
137
219
  self, names: Optional[Union[list, str]] = None
138
220
  ) -> FrameorSeriesUnion:
139
- """Method to obtain the distances in meters between the oseries.
221
+ """Get the distances in meters between the oseries.
140
222
 
141
223
  Parameters
142
224
  ----------
@@ -175,7 +257,7 @@ class PastaStore:
175
257
  n: int = 1,
176
258
  maxdist: Optional[float] = None,
177
259
  ) -> FrameorSeriesUnion:
178
- """Method to obtain the nearest (n) oseries.
260
+ """Get the nearest (n) oseries.
179
261
 
180
262
  Parameters
181
263
  ----------
@@ -191,7 +273,6 @@ class PastaStore:
191
273
  oseries:
192
274
  list with the names of the oseries.
193
275
  """
194
-
195
276
  distances = self.get_oseries_distances(names)
196
277
  if maxdist is not None:
197
278
  distances = distances.where(distances <= maxdist, np.nan)
@@ -214,8 +295,7 @@ class PastaStore:
214
295
  stresses: Optional[Union[list, str]] = None,
215
296
  kind: Optional[Union[str, List[str]]] = None,
216
297
  ) -> FrameorSeriesUnion:
217
- """Method to obtain the distances in meters between the oseries and
218
- stresses.
298
+ """Get the distances in meters between the oseries and stresses.
219
299
 
220
300
  Parameters
221
301
  ----------
@@ -274,7 +354,7 @@ class PastaStore:
274
354
  n: int = 1,
275
355
  maxdist: Optional[float] = None,
276
356
  ) -> FrameorSeriesUnion:
277
- """Method to obtain the nearest (n) stresses of a specific kind.
357
+ """Get the nearest (n) stresses of a specific kind.
278
358
 
279
359
  Parameters
280
360
  ----------
@@ -295,7 +375,6 @@ class PastaStore:
295
375
  stresses:
296
376
  list with the names of the stresses.
297
377
  """
298
-
299
378
  distances = self.get_distances(oseries, stresses, kind)
300
379
  if maxdist is not None:
301
380
  distances = distances.where(distances <= maxdist, np.nan)
@@ -317,8 +396,9 @@ class PastaStore:
317
396
  progressbar=False,
318
397
  ignore_errors=False,
319
398
  ):
320
- """Get groundwater signatures. NaN-values are returned when the
321
- signature could not be computed.
399
+ """Get groundwater signatures.
400
+
401
+ NaN-values are returned when the signature cannot be computed.
322
402
 
323
403
  Parameters
324
404
  ----------
@@ -380,11 +460,16 @@ class PastaStore:
380
460
  i_signatures.append(sign_val)
381
461
  else:
382
462
  raise e
383
- signatures_df.loc[name, signatures] = i_signatures
463
+ signatures_df.loc[name, signatures] = i_signatures.squeeze()
384
464
 
385
465
  return signatures_df
386
466
 
387
- def get_tmin_tmax(self, libname, names=None, progressbar=False):
467
+ def get_tmin_tmax(
468
+ self,
469
+ libname: Literal["oseries", "stresses", "models"],
470
+ names: Union[str, List[str], None] = None,
471
+ progressbar: bool = False,
472
+ ):
388
473
  """Get tmin and tmax for time series.
389
474
 
390
475
  Parameters
@@ -403,22 +488,48 @@ class PastaStore:
403
488
  tmintmax : pd.dataframe
404
489
  Dataframe containing tmin and tmax per time series
405
490
  """
406
-
407
491
  names = self.conn._parse_names(names, libname=libname)
408
492
  tmintmax = pd.DataFrame(
409
493
  index=names, columns=["tmin", "tmax"], dtype="datetime64[ns]"
410
494
  )
411
495
  desc = f"Get tmin/tmax {libname}"
412
496
  for n in tqdm(names, desc=desc) if progressbar else names:
413
- if libname == "oseries":
414
- s = self.conn.get_oseries(n)
497
+ if libname == "models":
498
+ mld = self.conn.get_models(
499
+ n,
500
+ return_dict=True,
501
+ )
502
+ tmintmax.loc[n, "tmin"] = mld["settings"]["tmin"]
503
+ tmintmax.loc[n, "tmax"] = mld["settings"]["tmax"]
415
504
  else:
416
- s = self.conn.get_stresses(n)
417
- tmintmax.loc[n, "tmin"] = s.first_valid_index()
418
- tmintmax.loc[n, "tmax"] = s.last_valid_index()
505
+ s = (
506
+ self.conn.get_oseries(n)
507
+ if libname == "oseries"
508
+ else self.conn.get_stresses(n)
509
+ )
510
+ tmintmax.loc[n, "tmin"] = s.first_valid_index()
511
+ tmintmax.loc[n, "tmax"] = s.last_valid_index()
512
+
419
513
  return tmintmax
420
514
 
421
515
  def get_extent(self, libname, names=None, buffer=0.0):
516
+ """Get extent [xmin, xmax, ymin, ymax] from library.
517
+
518
+ Parameters
519
+ ----------
520
+ libname : str
521
+ name of the library containing the time series
522
+ ('oseries', 'stresses', 'models')
523
+ names : str, list of str, or None, optional
524
+ list of names to include for computing the extent
525
+ buffer : float, optional
526
+ add this distance to the extent, by default 0.0
527
+
528
+ Returns
529
+ -------
530
+ extent : list
531
+ extent [xmin, xmax, ymin, ymax]
532
+ """
422
533
  names = self.conn._parse_names(names, libname=libname)
423
534
  if libname in ["oseries", "stresses"]:
424
535
  df = getattr(self, libname)
@@ -443,8 +554,10 @@ class PastaStore:
443
554
  progressbar: Optional[bool] = False,
444
555
  ignore_errors: Optional[bool] = False,
445
556
  ) -> FrameorSeriesUnion:
446
- """Get model parameters. NaN-values are returned when the parameters
447
- are not present in the model or the model is not optimized.
557
+ """Get model parameters.
558
+
559
+ NaN-values are returned when the parameters are not present in the model or the
560
+ model is not optimized.
448
561
 
449
562
  Parameters
450
563
  ----------
@@ -526,7 +639,6 @@ class PastaStore:
526
639
  -------
527
640
  s : pandas.DataFrame
528
641
  """
529
-
530
642
  modelnames = self.conn._parse_names(modelnames, libname="models")
531
643
 
532
644
  # if statistics is str
@@ -558,6 +670,7 @@ class PastaStore:
558
670
  name: str,
559
671
  modelname: str = None,
560
672
  add_recharge: bool = True,
673
+ add_ar_noisemodel: bool = False,
561
674
  recharge_name: str = "recharge",
562
675
  ) -> ps.Model:
563
676
  """Create a pastas Model.
@@ -572,6 +685,8 @@ class PastaStore:
572
685
  add recharge to the model by looking for the closest
573
686
  precipitation and evaporation time series in the stresses
574
687
  library, by default True
688
+ add_ar1_noisemodel : bool, optional
689
+ add AR(1) noise model to the model, by default False
575
690
  recharge_name : str
576
691
  name of the RechargeModel
577
692
 
@@ -598,6 +713,8 @@ class PastaStore:
598
713
  ml = ps.Model(ts, name=modelname, metadata=meta)
599
714
  if add_recharge:
600
715
  self.add_recharge(ml, recharge_name=recharge_name)
716
+ if add_ar_noisemodel and PASTAS_GEQ_150:
717
+ ml.add_noisemodel(ps.ArNoiseModel())
601
718
  return ml
602
719
  else:
603
720
  raise ValueError("Empty time series!")
@@ -709,9 +826,9 @@ class PastaStore:
709
826
  for var in ("prec", "evap"):
710
827
  try:
711
828
  name = self.get_nearest_stresses(ml.oseries.name, kind=var).iloc[0, 0]
712
- except AttributeError:
829
+ except AttributeError as e:
713
830
  msg = "No precipitation or evaporation time series found!"
714
- raise Exception(msg)
831
+ raise Exception(msg) from e
715
832
  if isinstance(name, float):
716
833
  if np.isnan(name):
717
834
  raise ValueError(
@@ -828,8 +945,8 @@ class PastaStore:
828
945
  """
829
946
  try:
830
947
  from art_tools import pastas_get_model_results
831
- except Exception:
832
- raise ModuleNotFoundError("You need 'art_tools' to use this method!")
948
+ except Exception as e:
949
+ raise ModuleNotFoundError("You need 'art_tools' to use this method!") from e
833
950
 
834
951
  if mls is None:
835
952
  mls = self.conn.models
@@ -870,7 +987,7 @@ class PastaStore:
870
987
  "File already exists! " "Use 'overwrite=True' to " "force writing file."
871
988
  )
872
989
  elif os.path.exists(fname):
873
- warnings.warn(f"Overwriting file '{os.path.basename(fname)}'")
990
+ warnings.warn(f"Overwriting file '{os.path.basename(fname)}'", stacklevel=1)
874
991
 
875
992
  with ZipFile(fname, "w", compression=ZIP_DEFLATED) as archive:
876
993
  # oseries
@@ -991,8 +1108,9 @@ class PastaStore:
991
1108
  libname: str,
992
1109
  s: Optional[Union[list, str]] = None,
993
1110
  case_sensitive: bool = True,
1111
+ sort=True,
994
1112
  ):
995
- """Search for names of time series or models starting with s.
1113
+ """Search for names of time series or models starting with `s`.
996
1114
 
997
1115
  Parameters
998
1116
  ----------
@@ -1002,19 +1120,20 @@ class PastaStore:
1002
1120
  find names with part of this string or strings in list
1003
1121
  case_sensitive : bool, optional
1004
1122
  whether search should be case sensitive, by default True
1123
+ sort : bool, optional
1124
+ sort list of names
1005
1125
 
1006
1126
  Returns
1007
1127
  -------
1008
1128
  matches : list
1009
1129
  list of names that match search result
1010
1130
  """
1011
-
1012
1131
  if libname == "models":
1013
- lib_names = getattr(self, "model_names")
1132
+ lib_names = self.model_names
1014
1133
  elif libname == "stresses":
1015
- lib_names = getattr(self, "stresses_names")
1134
+ lib_names = self.stresses_names
1016
1135
  elif libname == "oseries":
1017
- lib_names = getattr(self, "oseries_names")
1136
+ lib_names = self.oseries_names
1018
1137
  else:
1019
1138
  raise ValueError("Provide valid libname: 'models', 'stresses' or 'oseries'")
1020
1139
 
@@ -1031,7 +1150,8 @@ class PastaStore:
1031
1150
  else:
1032
1151
  m = np.append(m, [n for n in lib_names if sub.lower() in n.lower()])
1033
1152
  matches = list(np.unique(m))
1034
-
1153
+ if sort:
1154
+ matches.sort()
1035
1155
  return matches
1036
1156
 
1037
1157
  def get_model_timeseries_names(
@@ -1060,7 +1180,6 @@ class PastaStore:
1060
1180
  indicating whether a stress is contained within a time series
1061
1181
  model.
1062
1182
  """
1063
-
1064
1183
  model_names = self.conn._parse_names(modelnames, libname="models")
1065
1184
  structure = pd.DataFrame(
1066
1185
  index=model_names, columns=["oseries"] + self.stresses_names
@@ -1127,6 +1246,53 @@ class PastaStore:
1127
1246
  "'libname' must be one of ['oseries', 'stresses', 'models']!"
1128
1247
  )
1129
1248
  getter = getattr(self.conn, f"get_{libname}")
1130
- for n in tqdm(names) if progressbar else names:
1249
+ for n in (
1250
+ tqdm(names, desc=f"Applying {func.__name__}") if progressbar else names
1251
+ ):
1131
1252
  result[n] = func(getter(n))
1132
1253
  return result
1254
+
1255
+ def within(self, extent, names=None, libname="oseries"):
1256
+ """Get names of items within extent.
1257
+
1258
+ Parameters
1259
+ ----------
1260
+ extent : list
1261
+ list with [xmin, xmax, ymin, ymax]
1262
+ names : str, list of str, optional
1263
+ list of names to include, by default None
1264
+ libname : str, optional
1265
+ name of library, must be one of ('oseries', 'stresses', 'models'), by
1266
+ default "oseries"
1267
+
1268
+ Returns
1269
+ -------
1270
+ list
1271
+ list of items within extent
1272
+ """
1273
+ xmin, xmax, ymin, ymax = extent
1274
+ names = self.conn._parse_names(names, libname)
1275
+ if libname == "oseries":
1276
+ df = self.oseries.loc[names]
1277
+ elif libname == "stresses":
1278
+ df = self.stresses.loc[names]
1279
+ elif libname == "models":
1280
+ onames = np.unique(
1281
+ [
1282
+ self.get_models(modelname, return_dict=True)["oseries"]["name"]
1283
+ for modelname in names
1284
+ ]
1285
+ )
1286
+ df = self.oseries.loc[onames]
1287
+ else:
1288
+ raise ValueError(
1289
+ "libname must be one of ['oseries', 'stresses', 'models']"
1290
+ f", got '{libname}'"
1291
+ )
1292
+ mask = (
1293
+ (df["x"] <= xmax)
1294
+ & (df["x"] >= xmin)
1295
+ & (df["y"] >= ymin)
1296
+ & (df["y"] <= ymax)
1297
+ )
1298
+ return df.loc[mask].index.tolist()
pastastore/styling.py ADDED
@@ -0,0 +1,67 @@
1
+ """Module containing dataframe styling functions."""
2
+
3
+ import matplotlib as mpl
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
6
+
7
+
8
+ def float_styler(val, norm, cmap=None):
9
+ """Style float values in DataFrame.
10
+
11
+ Parameters
12
+ ----------
13
+ val : float
14
+ value in cell
15
+ norm : matplotlib.colors.Normalize
16
+ normalizer to map values to range(0, 1)
17
+ cmap : colormap, optional
18
+ colormap to use, by default None, which uses RdYlBu
19
+
20
+ Returns
21
+ -------
22
+ str
23
+ css value pairs for styling dataframe
24
+
25
+ Usage
26
+ -----
27
+ Given some dataframe
28
+
29
+ >>> df.map(float_styler, subset=["some column"], norm=norm, cmap=cmap)
30
+ """
31
+ if cmap is None:
32
+ cmap = plt.get_cmap("RdYlBu")
33
+ bg = cmap(norm(val))
34
+ color = mpl.colors.rgb2hex(bg)
35
+ c = "White" if np.mean(bg[:3]) < 0.4 else "Black"
36
+ return f"background-color: {color}; color: {c}"
37
+
38
+
39
+ def boolean_styler(b):
40
+ """Style boolean values in DataFrame.
41
+
42
+ Parameters
43
+ ----------
44
+ b : bool
45
+ value in cell
46
+
47
+ Returns
48
+ -------
49
+ str
50
+ css value pairs for styling dataframe
51
+
52
+ Usage
53
+ -----
54
+ Given some dataframe
55
+
56
+ >>> df.map(boolean_styler, subset=["some column"])
57
+ """
58
+ if b:
59
+ return (
60
+ f"background-color: {mpl.colors.rgb2hex((231/255, 255/255, 239/255))}; "
61
+ "color: darkgreen"
62
+ )
63
+ else:
64
+ return (
65
+ f"background-color: {mpl.colors.rgb2hex((255/255, 238/255, 238/255))}; "
66
+ "color: darkred"
67
+ )