pastastore 1.7.0__py3-none-any.whl → 1.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,776 @@
1
+ """HydroPandas extension for PastaStore.
2
+
3
+ Features:
4
+
5
+ - Add `hpd.Obs` and `hpd.ObsCollection` to PastaStore.
6
+ - Download and store meteorological data from KNMI or groundwater observations from BRO.
7
+ - Update currently stored (KNMI or BRO) time series from last observation to tmax.
8
+ """
9
+
10
+ import logging
11
+ from typing import List, Optional, Union
12
+
13
+ import hydropandas as hpd
14
+ import numpy as np
15
+ from hydropandas.io.knmi import _check_latest_measurement_date_de_bilt, get_stations
16
+ from pandas import DataFrame, Series, Timedelta, Timestamp
17
+ from pastas.timeseries_utils import timestep_weighted_resample
18
+ from tqdm.auto import tqdm
19
+
20
+ from pastastore.extensions.accessor import register_pastastore_accessor
21
+
22
+ logger = logging.getLogger("hydropandas_extension")
23
+
24
+
25
+ TimeType = Optional[Union[str, Timestamp]]
26
+
27
+
28
+ @register_pastastore_accessor("hpd")
29
+ class HydroPandasExtension:
30
+ """HydroPandas extension for PastaStore.
31
+
32
+ Parameters
33
+ ----------
34
+ store: pastastore.store.PastaStore
35
+ PastaStore object to extend with HydroPandas functionality
36
+ """
37
+
38
+ def __init__(self, store):
39
+ """Initialize HydroPandasExtenstion.
40
+
41
+ Parameters
42
+ ----------
43
+ store : pasta.store.PastaStore
44
+ PastaStore object to extend with HydroPandas functionality
45
+ """
46
+ self._store = store
47
+
48
+ def add_obscollection(
49
+ self,
50
+ libname: str,
51
+ oc: hpd.ObsCollection,
52
+ kind: Optional[str] = None,
53
+ data_column: Optional[str] = None,
54
+ unit_multiplier: float = 1.0,
55
+ update: bool = False,
56
+ normalize_datetime_index: bool = False,
57
+ ):
58
+ """Add an ObsCollection to the PastaStore.
59
+
60
+ Parameters
61
+ ----------
62
+ libname : str
63
+ Name of the library to add the ObsCollection to ["oseries", "stresses"].
64
+ oc : hpd.ObsCollection
65
+ ObsCollection to add to the store.
66
+ kind : str, optional
67
+ kind identifier for observations, by default None. Required for adding
68
+ stresses.
69
+ data_column : str, optional
70
+ name of column containing observation values, by default None.
71
+ unit_multiplier : float, optional
72
+ multiply unit by this value before saving it in the store
73
+ update : bool, optional
74
+ if True, update currently stored time series with new data
75
+ normalize_datetime_index : bool, optional
76
+ if True, normalize the datetime so stress value at midnight represents
77
+ the daily total, by default True.
78
+ """
79
+ for name, row in oc.iterrows():
80
+ obs = row["obs"]
81
+ # metadata = row.drop("obs").to_dict()
82
+ self.add_observation(
83
+ libname,
84
+ obs,
85
+ name=name,
86
+ kind=kind,
87
+ data_column=data_column,
88
+ unit_multiplier=unit_multiplier,
89
+ update=update,
90
+ normalize_datetime_index=normalize_datetime_index,
91
+ )
92
+
93
+ def add_observation(
94
+ self,
95
+ libname: str,
96
+ obs: hpd.Obs,
97
+ name: Optional[str] = None,
98
+ kind: Optional[str] = None,
99
+ data_column: Optional[str] = None,
100
+ unit_multiplier: float = 1.0,
101
+ update: bool = False,
102
+ normalize_datetime_index: bool = False,
103
+ ):
104
+ """Add an hydropandas observation series to the PastaStore.
105
+
106
+ Parameters
107
+ ----------
108
+ libname : str
109
+ Name of the library to add the observation to ["oseries", "stresses"].
110
+ obs : hpd.Obs
111
+ hydroPandas observation series to add to the store.
112
+ name : str, optional
113
+ Name of the observation, by default None. If None, the name of the
114
+ observation is used.
115
+ kind : str, optional
116
+ kind identifier for observations, by default None. Required for adding
117
+ stresses.
118
+ data_column : str, optional
119
+ name of column containing observation values, by default None.
120
+ unit_multiplier : float, optional
121
+ multiply unit by this value before saving it in the store
122
+ update : bool, optional
123
+ if True, update currently stored time series with new data
124
+ normalize_datetime_index : bool, optional
125
+ if True, normalize the datetime so stress value at midnight represents
126
+ the daily total, by default True.
127
+ """
128
+ # if data_column is not None, use data_column
129
+ if data_column is not None:
130
+ if not obs.empty:
131
+ o = obs[[data_column]]
132
+ else:
133
+ o = Series()
134
+ elif isinstance(obs, Series):
135
+ o = obs
136
+ # else raise error
137
+ elif isinstance(obs, DataFrame) and (obs.columns.size > 1):
138
+ raise ValueError("No data_column specified and obs has multiple columns.")
139
+ else:
140
+ raise TypeError("obs must be a Series or DataFrame with a single column.")
141
+
142
+ # break if obs is empty
143
+ if o.empty:
144
+ logger.info("Observation '%s' is empty, not adding to store.", name)
145
+ return
146
+
147
+ if normalize_datetime_index and o.index.size > 1:
148
+ o = self._normalize_datetime_index(o).iloc[1:] # remove first nan
149
+ elif normalize_datetime_index and o.index.size <= 1:
150
+ raise ValueError(
151
+ "Must have minimum of 2 observations for timestep_weighted_resample."
152
+ )
153
+
154
+ # gather metadata from obs object
155
+ metadata = {key: getattr(obs, key) for key in obs._metadata}
156
+
157
+ # convert np dtypes to builtins
158
+ for k, v in metadata.items():
159
+ if isinstance(v, np.integer):
160
+ metadata[k] = int(v)
161
+ elif isinstance(v, np.floating):
162
+ metadata[k] = float(v)
163
+
164
+ metadata.pop("name", None)
165
+ metadata.pop("meta", None)
166
+ unit = metadata.get("unit", None)
167
+ if unit == "m" and np.allclose(unit_multiplier, 1e-3):
168
+ metadata["unit"] = "mm"
169
+ elif unit_multiplier != 1.0:
170
+ metadata["unit"] = f"{unit_multiplier:.1e}*{unit}"
171
+
172
+ source = metadata.get("source", "")
173
+ if len(source) > 0:
174
+ source = f"{source} "
175
+
176
+ if update:
177
+ action_msg = "updated in"
178
+ else:
179
+ action_msg = "added to"
180
+
181
+ if libname == "oseries":
182
+ self._store.upsert_oseries(o.squeeze(axis=1), name, metadata=metadata)
183
+ logger.info(
184
+ "%sobservation '%s' %s oseries library.", source, name, action_msg
185
+ )
186
+ elif libname == "stresses":
187
+ if kind is None:
188
+ raise ValueError("`kind` must be specified for stresses!")
189
+ self._store.upsert_stress(
190
+ (o * unit_multiplier).squeeze(axis=1), name, kind, metadata=metadata
191
+ )
192
+ logger.info(
193
+ "%sstress '%s' (kind='%s') %s stresses library.",
194
+ source,
195
+ name,
196
+ kind,
197
+ action_msg,
198
+ )
199
+ else:
200
+ raise ValueError("libname must be 'oseries' or 'stresses'.")
201
+
202
+ def _get_tmin_tmax(self, tmin, tmax, oseries=None):
203
+ """Get tmin and tmax from store if not specified.
204
+
205
+ Parameters
206
+ ----------
207
+ tmin : TimeType
208
+ start time
209
+ tmax : TimeType
210
+ end time
211
+ oseries : str, optional
212
+ name of the observation series to get tmin/tmax for, by default None
213
+
214
+ Returns
215
+ -------
216
+ tmin, tmax : TimeType, TimeType
217
+ tmin and tmax
218
+ """
219
+ # get tmin/tmax if not specified
220
+ if tmin is None or tmax is None:
221
+ tmintmax = self._store.get_tmin_tmax(
222
+ "oseries", names=[oseries] if oseries else None
223
+ )
224
+ if tmin is None:
225
+ tmin = tmintmax.loc[:, "tmin"].min() - Timedelta(days=10 * 365)
226
+ if tmax is None:
227
+ tmax = tmintmax.loc[:, "tmax"].max()
228
+ return tmin, tmax
229
+
230
+ @staticmethod
231
+ def _normalize_datetime_index(obs):
232
+ """Normalize observation datetime index (i.e. set observation time to midnight).
233
+
234
+ Parameters
235
+ ----------
236
+ obs : pandas.Series
237
+ observation series to normalize
238
+
239
+ Returns
240
+ -------
241
+ hpd.Obs
242
+ observation series with normalized datetime index
243
+ """
244
+ if isinstance(obs, hpd.Obs):
245
+ metadata = {k: getattr(obs, k) for k in obs._metadata}
246
+ else:
247
+ metadata = {}
248
+ return obs.__class__(
249
+ timestep_weighted_resample(
250
+ obs,
251
+ obs.index.normalize(),
252
+ ).rename(obs.name),
253
+ **metadata,
254
+ )
255
+
256
+ def download_knmi_precipitation(
257
+ self,
258
+ stns: Optional[list[int]] = None,
259
+ meteo_var: str = "RD",
260
+ tmin: TimeType = None,
261
+ tmax: TimeType = None,
262
+ unit_multiplier: float = 1e3,
263
+ fill_missing_obs: bool = True,
264
+ normalize_datetime_index: bool = True,
265
+ **kwargs,
266
+ ):
267
+ """Download precipitation data from KNMI and store in PastaStore.
268
+
269
+ Parameters
270
+ ----------
271
+ stns : list of int/str, optional
272
+ list of station numbers to download data for, by default None
273
+ meteo_var : str, optional
274
+ variable to download, by default "RD", valid options are ["RD", "RH"].
275
+ tmin : TimeType, optional
276
+ start time, by default None
277
+ tmax : TimeType, optional
278
+ end time, by default None
279
+ unit_multiplier : float, optional
280
+ multiply unit by this value before saving it in the store,
281
+ by default 1e3 to convert m to mm
282
+ """
283
+ self.download_knmi_meteo(
284
+ meteo_var=meteo_var,
285
+ kind="prec",
286
+ stns=stns,
287
+ tmin=tmin,
288
+ tmax=tmax,
289
+ unit_multiplier=unit_multiplier,
290
+ fill_missing_obs=fill_missing_obs,
291
+ normalize_datetime_index=normalize_datetime_index,
292
+ **kwargs,
293
+ )
294
+
295
+ def download_knmi_evaporation(
296
+ self,
297
+ stns: Optional[list[int]] = None,
298
+ meteo_var: str = "EV24",
299
+ tmin: TimeType = None,
300
+ tmax: TimeType = None,
301
+ unit_multiplier: float = 1e3,
302
+ fill_missing_obs: bool = True,
303
+ normalize_datetime_index: bool = True,
304
+ **kwargs,
305
+ ):
306
+ """Download evaporation data from KNMI and store in PastaStore.
307
+
308
+ Parameters
309
+ ----------
310
+ stns : list of int/str, optional
311
+ list of station numbers to download data for, by default None
312
+ meteo_var : str, optional
313
+ variable to download, by default "EV24"
314
+ tmin : TimeType, optional
315
+ start time, by default None
316
+ tmax : TimeType, optional
317
+ end time, by default None
318
+ unit_multiplier : float, optional
319
+ multiply unit by this value before saving it in the store,
320
+ by default 1e3 to convert m to mm
321
+ fill_missing_obs : bool, optional
322
+ if True, fill missing observations by getting observations from nearest
323
+ station with data.
324
+ normalize_datetime_index : bool, optional
325
+ if True, normalize the datetime so stress value at midnight represents
326
+ the daily total, by default True.
327
+ """
328
+ self.download_knmi_meteo(
329
+ meteo_var=meteo_var,
330
+ kind="evap",
331
+ stns=stns,
332
+ tmin=tmin,
333
+ tmax=tmax,
334
+ unit_multiplier=unit_multiplier,
335
+ fill_missing_obs=fill_missing_obs,
336
+ normalize_datetime_index=normalize_datetime_index,
337
+ **kwargs,
338
+ )
339
+
340
+ def download_knmi_meteo(
341
+ self,
342
+ meteo_var: str,
343
+ kind: str,
344
+ stns: Optional[list[int]] = None,
345
+ tmin: TimeType = None,
346
+ tmax: TimeType = None,
347
+ unit_multiplier: float = 1.0,
348
+ normalize_datetime_index: bool = True,
349
+ fill_missing_obs: bool = True,
350
+ **kwargs,
351
+ ):
352
+ """Download meteorological data from KNMI and store in PastaStore.
353
+
354
+ Parameters
355
+ ----------
356
+ meteo_var : str, optional
357
+ variable to download, by default "RH", valid options are
358
+ e.g. ["RD", "RH", "EV24", "T", "Q"].
359
+ kind : str
360
+ kind identifier for observations in pastastore, usually "prec" or "evap".
361
+ stns : list of int/str, optional
362
+ list of station numbers to download data for, by default None
363
+ tmin : TimeType, optional
364
+ start time, by default None
365
+ tmax : TimeType, optional
366
+ end time, by default None
367
+ unit_multiplier : float, optional
368
+ multiply unit by this value before saving it in the store,
369
+ by default 1.0 (no conversion)
370
+ fill_missing_obs : bool, optional
371
+ if True, fill missing observations by getting observations from nearest
372
+ station with data.
373
+ normalize_datetime_index : bool, optional
374
+ if True, normalize the datetime so stress value at midnight represents
375
+ the daily total, by default True.
376
+ """
377
+ tmin, tmax = self._get_tmin_tmax(tmin, tmax)
378
+
379
+ if stns is None:
380
+ locations = self._store.oseries.loc[:, ["x", "y"]]
381
+ else:
382
+ locations = None
383
+
384
+ # download data
385
+ knmi = hpd.read_knmi(
386
+ locations=locations,
387
+ stns=stns,
388
+ meteo_vars=[meteo_var],
389
+ starts=tmin,
390
+ ends=tmax,
391
+ fill_missing_obs=fill_missing_obs,
392
+ **kwargs,
393
+ )
394
+
395
+ # add to store
396
+ self.add_obscollection(
397
+ libname="stresses",
398
+ oc=knmi,
399
+ kind=kind,
400
+ data_column=meteo_var,
401
+ unit_multiplier=unit_multiplier,
402
+ update=False,
403
+ normalize_datetime_index=normalize_datetime_index,
404
+ )
405
+
406
+ def download_nearest_knmi_precipitation(
407
+ self,
408
+ oseries: str,
409
+ meteo_var: str = "RD",
410
+ tmin: Optional[TimeType] = None,
411
+ tmax: Optional[TimeType] = None,
412
+ unit_multiplier: float = 1e-3,
413
+ normalize_datetime_index: bool = True,
414
+ fill_missing_obs: bool = True,
415
+ **kwargs,
416
+ ):
417
+ """Download precipitation time series data from nearest KNMI station.
418
+
419
+ Parameters
420
+ ----------
421
+ oseries : str
422
+ download nearest precipitation information for this observation well
423
+ meteo_var : str, optional
424
+ variable to download, by default "RD", valid options are ["RD", "RH"].
425
+ tmin : TimeType
426
+ start time
427
+ tmax : TimeType
428
+ end time
429
+ unit_multiplier : float, optional
430
+ multiply unit by this value before saving it in the store,
431
+ by default 1.0 (no conversion)
432
+ fill_missing_obs : bool, optional
433
+ if True, fill missing observations by getting observations from nearest
434
+ station with data.
435
+ fill_missing_obs : bool, optional
436
+ if True, fill missing observations by getting observations from nearest
437
+ station with data.
438
+ """
439
+ self.download_nearest_knmi_meteo(
440
+ oseries=oseries,
441
+ meteo_var=meteo_var,
442
+ kind="prec",
443
+ tmin=tmin,
444
+ tmax=tmax,
445
+ unit_multiplier=unit_multiplier,
446
+ normalize_datetime_index=normalize_datetime_index,
447
+ fill_missing_obs=fill_missing_obs,
448
+ **kwargs,
449
+ )
450
+
451
+ def download_nearest_knmi_evaporation(
452
+ self,
453
+ oseries: str,
454
+ meteo_var: str = "EV24",
455
+ tmin: Optional[TimeType] = None,
456
+ tmax: Optional[TimeType] = None,
457
+ unit_multiplier: float = 1e-3,
458
+ normalize_datetime_index: bool = True,
459
+ fill_missing_obs: bool = True,
460
+ **kwargs,
461
+ ):
462
+ """Download evaporation time series data from nearest KNMI station.
463
+
464
+ Parameters
465
+ ----------
466
+ oseries : str
467
+ download nearest evaporation information for this observation well
468
+ meteo_var : str, optional
469
+ variable to download, by default "EV24", valid options are:
470
+ ["EV24", "penman", "hargreaves", "makkink"].
471
+ tmin : TimeType
472
+ start time
473
+ tmax : TimeType
474
+ end time
475
+ unit_multiplier : float, optional
476
+ multiply unit by this value before saving it in the store,
477
+ by default 1.0 (no conversion)
478
+ fill_missing_obs : bool, optional
479
+ if True, fill missing observations by getting observations from nearest
480
+ station with data.
481
+ fill_missing_obs : bool, optional
482
+ if True, fill missing observations by getting observations from nearest
483
+ station with data.
484
+ """
485
+ self.download_nearest_knmi_meteo(
486
+ oseries=oseries,
487
+ meteo_var=meteo_var,
488
+ kind="evap",
489
+ tmin=tmin,
490
+ tmax=tmax,
491
+ unit_multiplier=unit_multiplier,
492
+ normalize_datetime_index=normalize_datetime_index,
493
+ fill_missing_obs=fill_missing_obs,
494
+ **kwargs,
495
+ )
496
+
497
+ def download_nearest_knmi_meteo(
498
+ self,
499
+ oseries: str,
500
+ meteo_var: str,
501
+ kind: str,
502
+ tmin: Optional[TimeType] = None,
503
+ tmax: Optional[TimeType] = None,
504
+ unit_multiplier: float = 1.0,
505
+ normalize_datetime_index: bool = True,
506
+ fill_missing_obs: bool = True,
507
+ **kwargs,
508
+ ):
509
+ """Download meteorological data from nearest KNMI station.
510
+
511
+ Parameters
512
+ ----------
513
+ oseries : str
514
+ download nearest meteorological information for this observation well
515
+ meteo_var : str
516
+ meteorological variable to download, e.g. "RD", "RH", "EV24", "T", "Q"
517
+ kind : str
518
+ kind identifier for observations in pastastore, usually "prec" or "evap".
519
+ tmin : TimeType
520
+ start time
521
+ tmax : TimeType
522
+ end time
523
+ unit_multiplier : float, optional
524
+ multiply unit by this value before saving it in the store,
525
+ by default 1.0 (no conversion)
526
+ fill_missing_obs : bool, optional
527
+ if True, fill missing observations by getting observations from nearest
528
+ station with data.
529
+ fill_missing_obs : bool, optional
530
+ if True, fill missing observations by getting observations from nearest
531
+ station with data.
532
+ """
533
+ xy = self._store.oseries.loc[[oseries], ["x", "y"]].to_numpy()
534
+ # download data
535
+ tmin, tmax = self._get_tmin_tmax(tmin, tmax, oseries=oseries)
536
+ knmi = hpd.read_knmi(
537
+ xy=xy,
538
+ meteo_vars=[meteo_var],
539
+ starts=tmin,
540
+ ends=tmax,
541
+ fill_missing_obs=fill_missing_obs,
542
+ **kwargs,
543
+ )
544
+ # add to store
545
+ self.add_obscollection(
546
+ libname="stresses",
547
+ oc=knmi,
548
+ kind=kind,
549
+ data_column=meteo_var,
550
+ unit_multiplier=unit_multiplier,
551
+ update=False,
552
+ normalize_datetime_index=normalize_datetime_index,
553
+ )
554
+
555
+ def update_knmi_meteo(
556
+ self,
557
+ names: Optional[List[str]] = None,
558
+ tmin: TimeType = None,
559
+ tmax: TimeType = None,
560
+ fill_missing_obs: bool = True,
561
+ normalize_datetime_index: bool = True,
562
+ raise_on_error: bool = False,
563
+ **kwargs,
564
+ ):
565
+ """Update meteorological data from KNMI in PastaStore.
566
+
567
+ Parameters
568
+ ----------
569
+ names : list of str, optional
570
+ list of names of observations to update, by default None
571
+ tmin : TimeType, optional
572
+ start time, by default None, which uses current last observation timestamp
573
+ as tmin
574
+ tmax : TimeType, optional
575
+ end time, by default None, which defaults to today
576
+ fill_missing_obs : bool, optional
577
+ if True, fill missing observations by getting observations from nearest
578
+ station with data.
579
+ normalize_datetime_index : bool, optional
580
+ if True, normalize the datetime so stress value at midnight represents
581
+ the daily total, by default True.
582
+ raise_on_error : bool, optional
583
+ if True, raise error if an error occurs, by default False
584
+ **kwargs : dict, optional
585
+ Additional keyword arguments to pass to `hpd.read_knmi()`
586
+ """
587
+ if "source" not in self._store.stresses.columns:
588
+ msg = (
589
+ "Cannot update KNMI stresses! "
590
+ "KNMI stresses cannot be identified if 'source' column is not defined."
591
+ )
592
+ logger.error(msg)
593
+ if raise_on_error:
594
+ raise ValueError(msg)
595
+ else:
596
+ return
597
+
598
+ if names is None:
599
+ names = self._store.stresses.loc[
600
+ self._store.stresses["source"] == "KNMI"
601
+ ].index.tolist()
602
+
603
+ tmintmax = self._store.get_tmin_tmax("stresses", names=names)
604
+
605
+ if tmax is not None:
606
+ if tmintmax["tmax"].min() >= Timestamp(tmax):
607
+ logger.info(f"All KNMI stresses are up to date till {tmax}.")
608
+ return
609
+
610
+ try:
611
+ maxtmax_rd = _check_latest_measurement_date_de_bilt("RD")
612
+ maxtmax_ev24 = _check_latest_measurement_date_de_bilt("EV24")
613
+ except Exception as e:
614
+ # otherwise use maxtmax 28 days (4 weeks) prior to today
615
+ logger.warning(
616
+ "Could not check latest measurement date in De Bilt: %s" % str(e)
617
+ )
618
+ maxtmax_rd = maxtmax_ev24 = Timestamp.today() - Timedelta(days=28)
619
+ logger.info(
620
+ "Using 28 days (4 weeks) prior to today as maxtmax: %s."
621
+ % str(maxtmax_rd)
622
+ )
623
+
624
+ for name in tqdm(names, desc="Updating KNMI meteo stresses"):
625
+ meteo_var = self._store.stresses.loc[name, "meteo_var"]
626
+ if meteo_var == "RD":
627
+ maxtmax = maxtmax_rd
628
+ elif meteo_var == "EV24":
629
+ maxtmax = maxtmax_ev24
630
+ else:
631
+ maxtmax = maxtmax_rd
632
+
633
+ # 1 days extra to ensure computation of daily totals using
634
+ # timestep_weighted_resample
635
+ if tmin is None:
636
+ itmin = tmintmax.loc[name, "tmax"] - Timedelta(days=1)
637
+ else:
638
+ itmin = tmin - Timedelta(days=1)
639
+
640
+ # ensure 2 observations at least
641
+ if itmin >= (maxtmax - Timedelta(days=1)):
642
+ logger.debug("KNMI %s is already up to date." % name)
643
+ continue
644
+
645
+ if tmax is None:
646
+ itmax = maxtmax
647
+ else:
648
+ itmax = Timestamp(tmax)
649
+
650
+ # fix for duplicate station entry in metadata:
651
+ stress_station = (
652
+ self._store.stresses.at[name, "station"]
653
+ if "station" in self._store.stresses.columns
654
+ else None
655
+ )
656
+ if stress_station is not None and not isinstance(
657
+ stress_station, (int, np.integer)
658
+ ):
659
+ stress_station = stress_station.squeeze().unique().item()
660
+
661
+ unit = self._store.stresses.loc[name, "unit"]
662
+ kind = self._store.stresses.loc[name, "kind"]
663
+ if stress_station is not None:
664
+ stn = stress_station
665
+ else:
666
+ stns = get_stations(meteo_var)
667
+ stn_name = name.split("_")[-1].lower()
668
+ mask = stns["name"].str.lower().str.replace(" ", "-") == stn_name
669
+ if not mask.any():
670
+ logger.warning(
671
+ "Station '%s' not found in list of KNMI %s stations."
672
+ % (stn_name, meteo_var)
673
+ )
674
+ continue
675
+ stn = stns.loc[mask].index[0]
676
+
677
+ if unit == "mm":
678
+ unit_multiplier = 1e3
679
+ else:
680
+ unit_multiplier = 1.0
681
+
682
+ logger.debug("Updating KNMI %s from %s to %s" % (name, itmin, itmax))
683
+ knmi = hpd.read_knmi(
684
+ stns=[stn],
685
+ meteo_vars=[meteo_var],
686
+ starts=itmin,
687
+ ends=itmax,
688
+ fill_missing_obs=fill_missing_obs,
689
+ **kwargs,
690
+ )
691
+ obs = knmi["obs"].iloc[0]
692
+
693
+ try:
694
+ self.add_observation(
695
+ "stresses",
696
+ obs,
697
+ name=name,
698
+ kind=kind,
699
+ data_column=meteo_var,
700
+ unit_multiplier=unit_multiplier,
701
+ update=True,
702
+ normalize_datetime_index=normalize_datetime_index,
703
+ )
704
+ except ValueError as e:
705
+ logger.error("Error updating KNMI %s: %s" % (name, str(e)))
706
+ if raise_on_error:
707
+ raise e
708
+
709
+ def download_bro_gmw(
710
+ self,
711
+ extent: Optional[List[float]] = None,
712
+ tmin: TimeType = None,
713
+ tmax: TimeType = None,
714
+ update: bool = False,
715
+ **kwargs,
716
+ ):
717
+ """Download groundwater monitoring well observations from BRO.
718
+
719
+ Parameters
720
+ ----------
721
+ extent: tuple, optional
722
+ Extent of the area to download observations from.
723
+ tmin: pandas.Timestamp, optional
724
+ Start date of the observations to download.
725
+ tmax: pandas.Timestamp, optional
726
+ End date of the observations to download.
727
+ **kwargs: dict, optional
728
+ Additional keyword arguments to pass to `hpd.read_bro()`
729
+ """
730
+ bro = hpd.read_bro(
731
+ extent=extent,
732
+ tmin=tmin,
733
+ tmax=tmax,
734
+ **kwargs,
735
+ )
736
+ self.add_obscollection("oseries", bro, data_column="values", update=update)
737
+
738
+ def update_bro_gmw(
739
+ self,
740
+ names: Optional[List[str]] = None,
741
+ tmin: TimeType = None,
742
+ tmax: TimeType = None,
743
+ **kwargs,
744
+ ):
745
+ """Update groundwater monitoring well observations from BRO.
746
+
747
+ Parameters
748
+ ----------
749
+ names : list of str, optional
750
+ list of names of observations to update, by default None which updates all
751
+ stored oseries.
752
+ tmin : TimeType, optional
753
+ start time, by default None, which uses current last observation timestamp
754
+ as tmin
755
+ tmax : TimeType, optional
756
+ end time, by default None, which defaults to today
757
+ **kwargs : dict, optional
758
+ Additional keyword arguments to pass to `hpd.GroundwaterObs.from_bro()`
759
+ """
760
+ if names is None:
761
+ names = self._store.oseries.index.to_list()
762
+
763
+ tmintmax = self._store.get_tmin_tmax("oseries")
764
+
765
+ for obsnam in tqdm(names, desc="Updating BRO oseries"):
766
+ bro_id, tube_number = obsnam.split("_")
767
+
768
+ if tmin is None:
769
+ _, tmin = tmintmax.loc[obsnam] # tmin is stored tmax
770
+
771
+ obs = hpd.GroundwaterObs.from_bro(
772
+ bro_id, int(tube_number), tmin=tmin, tmax=tmax, **kwargs
773
+ )
774
+ self.add_observation(
775
+ "oseries", obs, name=obsnam, data_column="values", update=True
776
+ )