plans 1.1.0.post1.dev0__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,540 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ #
3
+ # Copyright (C) 2025 The Project Authors
4
+ # See pyproject.toml for authors/maintainers.
5
+ # See LICENSE for license details.
6
+ """
7
+ Handle chronological (time series) datasets.
8
+
9
+ Overview
10
+ --------
11
+
12
+ # todo [major docstring improvement] -- overview
13
+ Mauris gravida ex quam, in porttitor lacus lobortis vitae.
14
+ In a lacinia nisl. Pellentesque habitant morbi tristique senectus
15
+ et netus et malesuada fames ac turpis egestas.
16
+
17
+ Example
18
+ -------
19
+
20
+ # todo [major docstring improvement] -- examples
21
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
22
+ Nulla mollis tincidunt erat eget iaculis. Mauris gravida ex quam,
23
+ in porttitor lacus lobortis vitae. In a lacinia nisl.
24
+
25
+ .. code-block:: python
26
+
27
+ import numpy as np
28
+ print("Hello World!")
29
+
30
+ Mauris gravida ex quam, in porttitor lacus lobortis vitae.
31
+ In a lacinia nisl. Mauris gravida ex quam, in porttitor lacus lobortis vitae.
32
+ In a lacinia nisl.
33
+
34
+ """
35
+ from types import NoneType
36
+
37
+ # IMPORTS
38
+ # ***********************************************************************
39
+ # import modules from other libs
40
+
41
+ # Native imports
42
+ # =======================================================================
43
+ # import {module}
44
+ # ... {develop}
45
+
46
+ # External imports
47
+ # =======================================================================
48
+ import matplotlib.pyplot as plt
49
+ import numpy as np
50
+ import pandas as pd
51
+
52
+ # ... {develop}
53
+
54
+ # Project-level imports
55
+ # =======================================================================
56
+ from plans.datasets.core import *
57
+
58
+ # ... {develop}
59
+
60
+
61
+ # CONSTANTS
62
+ # ***********************************************************************
63
+
64
+
65
+ # FUNCTIONS
66
+ # ***********************************************************************
67
+
68
+
69
+ # Classes
70
+ # ***********************************************************************
71
+
72
+
73
+ # TIME SERIES
74
+ # ======================================================================
75
+
76
+ # Water Balance Series
77
+ # ----------------------------------------------------------------------
78
+
79
+
80
+ class WaterBalanceSeries(TimeSeries):
81
+
82
+ def __init__(self, name="MyWaterBalanceSeries", alias=None):
83
+ # Use the superior initialization from the parent class (TimeSeries)
84
+ super().__init__(name, alias=alias)
85
+ self.varname = "Flow"
86
+ self.varalias = "Wb"
87
+ self.varfield = "wb"
88
+ self.units = "mm"
89
+ self.name_object = "Water Balance Time Series"
90
+ self.agg = "sum" # Aggregation method
91
+ self.outlier_min = 0
92
+ self.rawcolor = "blue"
93
+ self.datarange_max = 10000 # absurd yearly precipitation
94
+ self.datarange_min = 0
95
+
96
+ @staticmethod
97
+ def view_pq_plot(ts_rain, ts_flow, specs, show=True, return_fig=False):
98
+
99
+ # setup hard coded of Streamflow specs
100
+ ts_flow.view_specs["zorder_data"] = 2
101
+ ts_flow.view_specs["zorder_histh"] = 2
102
+ ts_flow.view_specs["zorder_cdf"] = 2
103
+
104
+ # setup hard coded Rain specs
105
+ ts_rain.view_specs["zorder_data"] = 1
106
+ ts_rain.view_specs["zorder_histh"] = 1
107
+ ts_rain.view_specs["zorder_cdf"] = 1
108
+ ts_rain.view_specs["fill"] = True
109
+ ts_rain.view_specs["color_fill"] = None
110
+ ts_rain.view_specs["fill_only"] = True
111
+
112
+ output = TimeSeries.view_compare_times_series(
113
+ ts_first=ts_flow,
114
+ ts_second=ts_rain,
115
+ specs=specs,
116
+ show=show,
117
+ return_fig=return_fig,
118
+ )
119
+
120
+ return output
121
+
122
+
123
+ class RainSeries(WaterBalanceSeries):
124
+ """
125
+ A class for representing and working with rainfall time series data.
126
+
127
+ **Notes**
128
+
129
+ todo notes
130
+
131
+ **Examples**
132
+
133
+ todo examples
134
+
135
+
136
+ """
137
+
138
+ def __init__(self, name="MyRainSeries", alias=None):
139
+ # Use the superior initialization from the parent class (TimeSeries)
140
+ super().__init__(name, alias=alias)
141
+ self.varname = "Rain"
142
+ self.varalias = "P"
143
+ self.varfield = "ppt"
144
+ # Overwrite attributes specific to RainSeries
145
+ self.name_object = "Rain Time Series"
146
+ self.gapsize = (
147
+ 7 * 72
148
+ ) # Maximum gap size of 1 week assuming measure device turns off when is not raining
149
+ self.rawcolor = "slateblue"
150
+
151
+ def interpolate_gaps(self, inplace=False, method=None):
152
+ # overwrite interpolation method with constant=0
153
+ super().interpolate_gaps(method="constant", constant=0, inplace=inplace)
154
+ return None
155
+
156
+ def _set_frequency(self):
157
+ super()._set_frequency()
158
+ # overwrite gapsize to 1 week
159
+ dict_gaps = {
160
+ "second": int(7 * 24 * 60 * 60),
161
+ "minute": int(7 * 24 * 3),
162
+ "hour": int(7 * 24),
163
+ "day": int(7),
164
+ "month": 1,
165
+ "year": 1,
166
+ }
167
+ self.gapsize = dict_gaps[self.dtres]
168
+ return None
169
+
170
+
171
+ class StreamflowSeries(WaterBalanceSeries):
172
+ """
173
+ A class for representing and working with streamflow data (specific flow).
174
+
175
+ **Notes**
176
+
177
+ todo notes
178
+
179
+ **Examples**
180
+
181
+ todo examples
182
+
183
+ """
184
+
185
+ def __init__(self, name="MyStreaFlowSeries", alias=None):
186
+ # Use the superior initialization from the parent class (TimeSeries)
187
+ super().__init__(name, alias=alias)
188
+ self.varname = "Streamflow"
189
+ self.varalias = "Q"
190
+ self.varfield = "q"
191
+ # Overwrite attributes specific
192
+ self.name_object = "Streamflow Time Series"
193
+ self.rawcolor = "blue"
194
+
195
+ def get_baseflow(self):
196
+ # todo docstring
197
+ df = StreamflowSeries.separate_baseflow(
198
+ df=self.data, dt_field=self.dtfield, var_field=self.varfield
199
+ )
200
+
201
+ return df
202
+
203
+ @staticmethod
204
+ def separate_baseflow(df, dt_field="datetime", var_field="q"):
205
+ # todo docstring
206
+
207
+ df = df[[dt_field, var_field]].copy()
208
+
209
+ # todo develop -- improve with method options
210
+ w_size = 1
211
+ v_rises = np.zeros(len(df))
212
+ for i in range(w_size, len(v_rises) - w_size):
213
+ lcl_v = df[var_field].values[i]
214
+ lcl_v_last = df[var_field].values[i - w_size]
215
+ lcl_v_next = df[var_field].values[i + w_size]
216
+ if lcl_v_next > lcl_v and lcl_v_last >= lcl_v:
217
+ v_rises[i] = lcl_v
218
+
219
+ df_qb = df.copy()
220
+ df_qb["qb"] = v_rises
221
+ df_qb["qb"] = df_qb["qb"].replace(0, np.nan)
222
+ df_qb.dropna(inplace=True)
223
+
224
+ ts = StreamflowSeries()
225
+ ts.varfield = "qb"
226
+
227
+ # todo develop -- handle gapsize better
228
+ ts.gapsize = 100
229
+
230
+ ts.set_data(input_df=df_qb, input_dtfield=dt_field, input_varfield="qb")
231
+ ts.standardize()
232
+ df_qb = ts.interpolate_gaps(method="linear", inplace=False)
233
+ df_qb = df_qb[[ts.dtfield, f"{ts.varfield}_interp"]].copy()
234
+ df_qb = df_qb.rename(
235
+ columns={f"{ts.varfield}_interp": ts.varfield, ts.dtfield: dt_field}
236
+ )
237
+ df = pd.merge(left=df_qb, right=df, on=dt_field, how="left")
238
+
239
+ # fix qb
240
+ vq = df[var_field].values
241
+ vqb = df["qb"].values
242
+ df["qb"] = np.where(vqb > vq, vq, vqb)
243
+
244
+ # get qq
245
+ df["qq"] = df[var_field] - df["qb"]
246
+
247
+ return df
248
+
249
+
250
+ class ETSeries(WaterBalanceSeries):
251
+ """
252
+ A class for representing and working with ET data.
253
+
254
+ **Notes**
255
+
256
+ todo notes
257
+
258
+ **Examples**
259
+
260
+ todo examples
261
+
262
+ """
263
+
264
+ def __init__(self, name="MyETSeries", alias=None):
265
+ # Use the superior initialization from the parent class
266
+ super().__init__(name, alias=alias)
267
+ self.varname = "ET"
268
+ self.varfield = "et"
269
+ # Overwrite attributes specific
270
+ self.name_object = "ET Time Series"
271
+ self.rawcolor = "red"
272
+
273
+
274
+ class PETSeries(WaterBalanceSeries):
275
+ """
276
+ A class for representing and working with PET data.
277
+
278
+ **Notes**
279
+
280
+ todo notes
281
+
282
+ **Examples**
283
+
284
+ todo examples
285
+
286
+ """
287
+
288
+ def __init__(self, name="MyPETSeries", alias=None):
289
+ # Use the superior initialization from the parent class (TimeSeries)
290
+ super().__init__(name, alias=alias)
291
+ self.varname = "PET"
292
+ self.varalias = "PET"
293
+ self.varfield = "pet"
294
+ # Overwrite attributes specific
295
+ self.name_object = "PET Time Series"
296
+ self.rawcolor = "darkred"
297
+
298
+
299
+ # Other
300
+ # ----------------------------------------------------------------------
301
+
302
+
303
+ class TemperatureSeries(TimeSeries):
304
+ """
305
+ A class for representing and working with temperature time series data.
306
+
307
+ **Notes**
308
+
309
+ todo notes
310
+
311
+ **Examples**
312
+
313
+ todo examples
314
+
315
+ """
316
+
317
+ def __init__(self, name="MyTemperatureSeries", alias=None):
318
+ # Use the superior initialization from the parent class (TimeSeries)
319
+ super().__init__(name, alias=alias)
320
+ self.varname = "Temperature"
321
+ self.varalias = "TAS"
322
+ self.varfield = "tas"
323
+ self.units = "Celcius"
324
+ # Overwrite attributes specific
325
+ self.name_object = "Temperature Time Series"
326
+ self.agg = "mean" # Aggregation method,
327
+ self.gapsize = 6 # Maximum gap size of 6 hours assuming hourly Temperature
328
+ self.datarange_max = 50
329
+ self.datarange_min = -20
330
+ self.rawcolor = "orange"
331
+
332
+
333
+ class StageSeries(TimeSeries):
334
+ """
335
+ A class for representing and working with river stage time series data.
336
+
337
+
338
+ **Notes**
339
+
340
+ todo notes
341
+
342
+ **Examples**
343
+
344
+ todo examples
345
+
346
+
347
+ """
348
+
349
+ def __init__(self, name="MyStageSeries", alias=None):
350
+ """
351
+ Initialize a ``StageSeries`` object.
352
+ """
353
+ # Use the superior initialization from the parent class (TimeSeries)
354
+ super().__init__(name, alias=alias)
355
+ self.varname = "Stage"
356
+ self.varalias = "H"
357
+ self.varfield = "H"
358
+ self.units = "cm"
359
+ # Overwrite attributes specific to StageSeries
360
+ self.name_object = "Stage Time Series"
361
+ self.agg = "mean" # Aggregation method, set to "mean" by default
362
+ self.gapsize = 10 # Maximum gap size allowed for data interpolation
363
+ # Extra attributes specific to StageSeries
364
+ self.upstream_area = None
365
+
366
+ def get_metadata(self):
367
+ # Get metadata from the base class (TimeSeries)
368
+ base_metadata = super().get_metadata()
369
+ # Additional TimeSeries-specific metadata
370
+ extra_metadata = {
371
+ "UpstreamArea": self.upstream_area,
372
+ }
373
+ # Combine both base and specific metadata
374
+ base_metadata.update(extra_metadata)
375
+ return base_metadata
376
+
377
+
378
+ class DischargeSeries(TimeSeries):
379
+ """
380
+ A class for representing and working with discharge time series data (volumentric flow).
381
+
382
+ **Notes**
383
+
384
+ todo notes
385
+
386
+ **Examples**
387
+
388
+ todo examples
389
+
390
+ """
391
+
392
+ def __init__(self, name="MyFlowSeries", alias=None):
393
+ # Use the superior initialization from the parent class (TimeSeries)
394
+ super().__init__(name, alias=alias)
395
+ self.varname = "Discharge"
396
+ self.varfield = "Q"
397
+ self.varalias = self.varfield
398
+ self.units = "m3/s"
399
+ # Overwrite attributes specific
400
+ self.name_object = "Discharge Time Series"
401
+ self.agg = "mean" # Aggregation method, set to "mean" by default
402
+ self.gapsize = 6 # Maximum gap size of 6 hours assuming hourly Flow
403
+ self.datarange_max = 300000 # Amazon discharge
404
+ self.datarange_min = 0
405
+ self.rawcolor = "navy"
406
+ # Specific attributes
407
+ self.upstream_area = None # in sq km
408
+
409
+ @staticmethod
410
+ def view_cfcs(freqs, specs=None, show=True, colors=None, labels=None):
411
+ import matplotlib.pyplot as plt
412
+
413
+ default_specs = {
414
+ "folder": "C:/data",
415
+ "filename": "cfcs",
416
+ "fig_format": "jpg",
417
+ "dpi": 300,
418
+ "width": 4,
419
+ "height": 6,
420
+ "xmin": 0,
421
+ "xmax": 100,
422
+ "ymin": 0,
423
+ "ymin_log": 1,
424
+ "ymax": None,
425
+ "log": True,
426
+ "title": "CFCs",
427
+ "ylabel": "m3/s",
428
+ "xlabel": "Exeed. Prob. (%)",
429
+ }
430
+ # get specs
431
+ if specs is not None:
432
+ default_specs.update(specs)
433
+ specs = default_specs.copy()
434
+
435
+ # --------------------- figure setup --------------------- #
436
+ fig = plt.figure(figsize=(specs["width"], specs["height"])) # Width, Height
437
+
438
+ # handle min max
439
+ if specs["ymax"] is None:
440
+ lst_max = [freq["Values"].max() for freq in freqs]
441
+ specs["ymax"] = max(lst_max)
442
+
443
+ if colors is None:
444
+ colors = ["navy" for freq in freqs]
445
+ if labels is None:
446
+ labels = [None for freq in freqs]
447
+
448
+ # --------------------- plotting --------------------- #
449
+ for i in range(len(freqs)):
450
+ plt.plot(
451
+ freqs[i]["Exceedance"],
452
+ freqs[i]["Values"],
453
+ color=colors[i],
454
+ label=labels[i],
455
+ )
456
+ if labels is not None:
457
+ plt.legend()
458
+
459
+ # --------------------- post-plotting --------------------- #
460
+ # set basic plotting stuff
461
+ plt.title(specs["title"])
462
+ plt.ylabel(specs["ylabel"])
463
+ plt.xlabel(specs["xlabel"])
464
+ plt.xlim(specs["xmin"], specs["xmax"])
465
+ if specs["log"]:
466
+ plt.ylim(specs["ymin_log"], 1.2 * specs["ymax"])
467
+ plt.yscale("log")
468
+ else:
469
+ plt.ylim(specs["ymin"], 1.2 * specs["ymax"])
470
+
471
+ # Get current axes
472
+ ax = plt.gca()
473
+ # Set the y-ticks more densely
474
+ ax.set_yticks([1, 2.5, 5, 10, 25, 50, 100, 250, 500])
475
+
476
+ # Adjust layout to prevent cutoff
477
+ plt.tight_layout()
478
+
479
+ # --------------------- end --------------------- #
480
+ # show or save
481
+ if show:
482
+ plt.show()
483
+ return None
484
+ else:
485
+ file_path = "{}/{}.{}".format(
486
+ specs["folder"], specs["filename"], specs["fig_format"]
487
+ )
488
+ plt.savefig(file_path, dpi=specs["dpi"])
489
+ plt.close(fig)
490
+ return file_path
491
+
492
+
493
+ # ------------- TIME SERIES COLLECTIONS ------------- #
494
+
495
+
496
+ class RainSeriesSamples(TimeSeriesSpatialSamples):
497
+ # todo docstring
498
+
499
+ def __init__(self, name="MyRSColection"):
500
+ # todo docstring
501
+ super().__init__(name=name, base_object=RainSeries)
502
+ # overwrite parent attributes
503
+ self.name_object = "Rainfall Series Samples"
504
+ self._set_view_specs()
505
+
506
+
507
+ class TemperatureSeriesSamples(TimeSeriesSpatialSamples):
508
+ # todo docstring
509
+
510
+ def __init__(self, name="MyTempSColection"):
511
+ # todo docstring
512
+ super().__init__(name=name, base_object=TemperatureSeries)
513
+ # overwrite parent attributes
514
+ self.name_object = "Temperature Series Sample"
515
+ self._set_view_specs()
516
+
517
+
518
+ class StageSeriesCollection(TimeSeriesCluster):
519
+ # todo docstring
520
+ def __init__(self, name="MySSColection"):
521
+ # todo docstring
522
+ super().__init__(name=name, base_object=StageSeries)
523
+ # overwrite parent attributes
524
+ self.name_object = "Stage Series Collection"
525
+ self._set_view_specs()
526
+
527
+ def set_data(self, df_info, src_dir=None, filter_dates=None):
528
+ # generic part
529
+ super().set_data(df_info=df_info, filter_dates=filter_dates)
530
+ # custom part
531
+ for i in range(len(df_info)):
532
+ name = df_info["Name"].values[i]
533
+ self.collection[name].upstream_area = df_info["UpstreamArea"].values[i]
534
+ self.update(details=True)
535
+ return None
536
+
537
+
538
+ if __name__ == "__main__":
539
+ print("Hello World")
540
+ # todo [move to testing]