hydrating 0.0.3__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.
- hydrating/__init__.py +2 -0
- hydrating/core/__init__.py +0 -0
- hydrating/core/hydrating.py +663 -0
- hydrating/models/__init__.py +3 -0
- hydrating/models/models.py +260 -0
- hydrating-0.0.3.dist-info/METADATA +26 -0
- hydrating-0.0.3.dist-info/RECORD +9 -0
- hydrating-0.0.3.dist-info/WHEEL +4 -0
- hydrating-0.0.3.dist-info/licenses/LICENSE +21 -0
hydrating/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
RatingCurve class.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import lmfit
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from hydrating.models import RatingModel
|
|
14
|
+
|
|
15
|
+
# from .grades import Grades
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RatingCurve:
|
|
19
|
+
"""
|
|
20
|
+
Represent and fit a hydrologic rating curve.
|
|
21
|
+
|
|
22
|
+
A rating curve describes the relationship between stage (water level)
|
|
23
|
+
and discharge (flow). This class wraps a rating-model implementation,
|
|
24
|
+
manages initial parameter values, optionally stores observed data in a
|
|
25
|
+
DataFrame, and fits the selected model using ``lmfit`` package.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
model : RatingModel
|
|
30
|
+
RatingModel class used to create the underlying model and
|
|
31
|
+
parameters. The class is instantiated internally.
|
|
32
|
+
initial_parameters : dict or lmfit.Parameters, optional
|
|
33
|
+
Initial parameter values for the model. If a dictionary is
|
|
34
|
+
provided, it is converted to ``lmfit.Parameters`` using the
|
|
35
|
+
selected rating model. If omitted, default parameters are created.
|
|
36
|
+
It is recommended to provide initial parameter values for the model,
|
|
37
|
+
as they are often needed for successful fitting.
|
|
38
|
+
rating_name : str, optional
|
|
39
|
+
Optional name identifier for the rating curve.
|
|
40
|
+
|
|
41
|
+
Attributes
|
|
42
|
+
----------
|
|
43
|
+
rating_name : str
|
|
44
|
+
User supplied name for the rating curve.
|
|
45
|
+
rating_model : RatingModel
|
|
46
|
+
Instantiated rating-model object used for fitting and prediction.
|
|
47
|
+
initial_parameters : lmfit.Parameters
|
|
48
|
+
Parameter set used as the starting point for fitting.
|
|
49
|
+
fit_result : lmfit.model.ModelResult or None
|
|
50
|
+
Result returned by the most recent fit.
|
|
51
|
+
stage_series : pandas.Series or numpy.ndarray or None
|
|
52
|
+
Optional stage time series used for prediction of discharge.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
model: RatingModel,
|
|
58
|
+
initial_parameters: dict | lmfit.Parameters | None = None,
|
|
59
|
+
rating_name: str = "",
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Initialize a rating curve.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
model : RatingModel
|
|
67
|
+
RatingModel class used to create the underlying model instance,
|
|
68
|
+
parameters, fitted values, and predictions. The class is
|
|
69
|
+
instantiated internally.
|
|
70
|
+
initial_parameters : dict or lmfit.Parameters, optional
|
|
71
|
+
Initial parameter values for the rating model. Dictionaries are
|
|
72
|
+
converted to ``lmfit.Parameters`` by the selected rating model. If
|
|
73
|
+
omitted, the rating model's default parameters are used. It is recommended
|
|
74
|
+
to provide initial parameter values for the rating model, as they are often
|
|
75
|
+
needed for successful fitting.
|
|
76
|
+
rating_name : str, optional
|
|
77
|
+
Name identifier for the rating curve.
|
|
78
|
+
|
|
79
|
+
Raises
|
|
80
|
+
------
|
|
81
|
+
ValueError
|
|
82
|
+
If ``initial_parameters`` is not a dictionary, ``lmfit.Parameters``,
|
|
83
|
+
or ``None``.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
self.rating_name = rating_name
|
|
87
|
+
self.rating_model = model # instance is created in the setter
|
|
88
|
+
self.initial_parameters = initial_parameters # done by model setter
|
|
89
|
+
|
|
90
|
+
# self.rc_grade = None # grade stage intervals with Enum grades
|
|
91
|
+
# def set_grades():
|
|
92
|
+
# ...# @setter/getter properties?
|
|
93
|
+
|
|
94
|
+
# self.rc_limit = None # tuple, min, max stage its applicable
|
|
95
|
+
# self.rc_period = None # tuple (datetime-like to from validity)
|
|
96
|
+
|
|
97
|
+
self.fit_result = None
|
|
98
|
+
|
|
99
|
+
self.stage_series = (
|
|
100
|
+
None # timeseries of stage, can be used to show extrapolation
|
|
101
|
+
)
|
|
102
|
+
# of the rating curve, when comparing rating curves
|
|
103
|
+
|
|
104
|
+
self._dataf = None # user can add a dataframe using add_data, but this is strictly not needed
|
|
105
|
+
# can also just call .fit() with two numpy arrays
|
|
106
|
+
self._user_df_added = False
|
|
107
|
+
|
|
108
|
+
# def set_limits():
|
|
109
|
+
# ... #@setter/getter properties?
|
|
110
|
+
# def set_valid_periods():
|
|
111
|
+
# ... #@setter/getter properties?
|
|
112
|
+
|
|
113
|
+
# or use property??
|
|
114
|
+
# rc.model = Callable
|
|
115
|
+
# also for parameter
|
|
116
|
+
# rc.parameter = dict # overwrites the default
|
|
117
|
+
@property
|
|
118
|
+
def rating_model(self): # numpydoc ignore=GL08
|
|
119
|
+
return self._rating_model
|
|
120
|
+
|
|
121
|
+
@rating_model.setter
|
|
122
|
+
def rating_model(self, model: RatingModel): # numpydoc ignore=GL08
|
|
123
|
+
self._rating_model = model()
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def initial_parameters(self): # numpydoc ignore=GL08
|
|
127
|
+
return self._initial_parameters
|
|
128
|
+
|
|
129
|
+
@initial_parameters.setter
|
|
130
|
+
def initial_parameters(self, params):
|
|
131
|
+
"""
|
|
132
|
+
Set the initial parameters for the rating model.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
params : dict or lmfit.Parameters or None
|
|
137
|
+
If a dictionary is provided, it is converted to ``lmfit.Parameters``
|
|
138
|
+
using the selected rating model. If omitted, default parameters are
|
|
139
|
+
created. It is recommended to provide initial parameter values for the
|
|
140
|
+
model, as they are often needed for successful fitting.
|
|
141
|
+
|
|
142
|
+
Raises
|
|
143
|
+
------
|
|
144
|
+
ValueError
|
|
145
|
+
If ``params`` is not a dictionary, ``lmfit.Parameters``, or ``None``.
|
|
146
|
+
"""
|
|
147
|
+
model = self.rating_model.create_model()
|
|
148
|
+
if isinstance(params, dict):
|
|
149
|
+
self._initial_parameters = self.rating_model.create_parameters(
|
|
150
|
+
model, params
|
|
151
|
+
)
|
|
152
|
+
elif isinstance(params, lmfit.Parameters):
|
|
153
|
+
self._initial_parameters = params
|
|
154
|
+
elif params is None:
|
|
155
|
+
self._initial_parameters = self.rating_model.create_parameters(model)
|
|
156
|
+
else:
|
|
157
|
+
raise ValueError()
|
|
158
|
+
|
|
159
|
+
# Check the parameters
|
|
160
|
+
|
|
161
|
+
# @property
|
|
162
|
+
# def grade(self):
|
|
163
|
+
# return self._grade
|
|
164
|
+
|
|
165
|
+
# @grade.setter
|
|
166
|
+
# def grade(self, value):
|
|
167
|
+
# # [{'from': 1.5,
|
|
168
|
+
# # 'to': 2.5,
|
|
169
|
+
# # 'grade': Grades.Good},] ?
|
|
170
|
+
# self._grade = value
|
|
171
|
+
|
|
172
|
+
def add_data(
|
|
173
|
+
self,
|
|
174
|
+
data: pd.DataFrame,
|
|
175
|
+
stage: str,
|
|
176
|
+
discharge: str,
|
|
177
|
+
datetime_start: Optional[str] = None, # optional, key in data df
|
|
178
|
+
datetime_end: Optional[str] = None, # optional, key in data df
|
|
179
|
+
method: Optional[str] = None, # optional, key in data df
|
|
180
|
+
party: Optional[str] = None, # optional, key in data df
|
|
181
|
+
agency: Optional[str] = None, # optional, key in data df
|
|
182
|
+
uncertainty: Optional[str] = None, # optional, key in data df
|
|
183
|
+
grade: Optional[str] = None, # optional, key in data df
|
|
184
|
+
enabled: Optional[str] = None, # optional, key in data df
|
|
185
|
+
note: Optional[str] = None, # optional, key in data df
|
|
186
|
+
identifier: Optional[str] = None,
|
|
187
|
+
): # optional, key in data df
|
|
188
|
+
"""
|
|
189
|
+
Add stage-discharge measurements to the rating curve.
|
|
190
|
+
|
|
191
|
+
Several keys for columns in the provided dataframe can be specified, but only
|
|
192
|
+
the stage and discharge columns are required. The other columns are optional
|
|
193
|
+
but can be used to store metadata. The ``enabled`` column is added automatically
|
|
194
|
+
if not provided, and flags if a measurement is used or not in the fitting.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
data : pandas.DataFrame
|
|
199
|
+
Dataframe containing observed stage, discharge, and optional
|
|
200
|
+
metadata columns. A copy is stored internally.
|
|
201
|
+
stage : str
|
|
202
|
+
Column name in ``data`` containing stage values.
|
|
203
|
+
discharge : str
|
|
204
|
+
Column name in ``data`` containing discharge values.
|
|
205
|
+
datetime_start : str, optional
|
|
206
|
+
Column name in ``data`` containing observation start datetimes.
|
|
207
|
+
datetime_end : str, optional
|
|
208
|
+
Column name in ``data`` containing observation end datetimes.
|
|
209
|
+
method : str, optional
|
|
210
|
+
Column name in ``data`` containing measurement methods.
|
|
211
|
+
party : str, optional
|
|
212
|
+
Column name in ``data`` containing measurement parties.
|
|
213
|
+
agency : str, optional
|
|
214
|
+
Column name in ``data`` containing agencies responsible for the
|
|
215
|
+
measurements.
|
|
216
|
+
uncertainty : str, optional
|
|
217
|
+
Column name in ``data`` containing measurement uncertainty values.
|
|
218
|
+
grade : str, optional
|
|
219
|
+
Column name in ``data`` containing observation grades.
|
|
220
|
+
enabled : str, optional
|
|
221
|
+
Column name in ``data`` containing flags for whether observations
|
|
222
|
+
are enabled. If omitted, an ``enabled`` column with ``True`` values
|
|
223
|
+
is added to the internally stored dataframe.
|
|
224
|
+
note : str, optional
|
|
225
|
+
Column name in ``data`` containing observation notes.
|
|
226
|
+
identifier : str, optional
|
|
227
|
+
Column name in ``data`` containing observation identifiers.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
self._k_discharge = discharge
|
|
231
|
+
self._k_stage = stage
|
|
232
|
+
self._k_datetime_start = datetime_start
|
|
233
|
+
self._k_datetime_end = datetime_end
|
|
234
|
+
self._k_method = method
|
|
235
|
+
self._k_party = party
|
|
236
|
+
self._k_agency = agency
|
|
237
|
+
self._k_uncertainty = uncertainty
|
|
238
|
+
self._k_grade = grade
|
|
239
|
+
self._k_enabled = enabled
|
|
240
|
+
self._k_note = note
|
|
241
|
+
self._k_identifer = identifier
|
|
242
|
+
|
|
243
|
+
# take the data DataFrame and make a copy
|
|
244
|
+
# keep all columns of DataFrame
|
|
245
|
+
# create a new columns "enabled" for True/False flag to turn on off
|
|
246
|
+
self._dataf = data.copy()
|
|
247
|
+
if self._k_enabled is None:
|
|
248
|
+
self._k_enabled = "enabled"
|
|
249
|
+
self._dataf["enabled"] = True
|
|
250
|
+
|
|
251
|
+
self._user_df_added = True
|
|
252
|
+
|
|
253
|
+
# TODO use a setter/getter for dataf in addition to this function to add_data?
|
|
254
|
+
|
|
255
|
+
def _user_data_from_fit(self, x, y):
|
|
256
|
+
"""
|
|
257
|
+
If data is passed to fit() call then create _dataf here if the user
|
|
258
|
+
has not done so themselves by calling add_data().
|
|
259
|
+
|
|
260
|
+
FIXME: this needs improvement, will not work if user has added data with add_data()
|
|
261
|
+
and then calls fit() with different data, it will overwrite the user added data.
|
|
262
|
+
Maybe should check if _dataf is not None and raise an error if user is trying
|
|
263
|
+
to add data through fit() when they have already added data with add_data()?
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
----------
|
|
267
|
+
x : np.ndarray or pd.Series
|
|
268
|
+
Stage data passed to fit() call.
|
|
269
|
+
y : np.ndarray or pd.Series
|
|
270
|
+
Discharge data passed to fit() call.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
self._k_stage = "stage"
|
|
274
|
+
self._k_discharge = "discharge"
|
|
275
|
+
self._dataf = pd.DataFrame(data={self._k_stage: x, self._k_discharge: y})
|
|
276
|
+
|
|
277
|
+
# fit uses the model and parameters
|
|
278
|
+
# FIXME need to handle this better, if user has added data with add_data() and then calls fit() with different data, it will overwrite the user added data. Maybe should check if _dataf is not None and raise an error if user is trying to add data through fit() when they have already added data with add_data()?
|
|
279
|
+
# data should be passed to fit() call, or when initialized maybe better
|
|
280
|
+
# try to adopt a scikit-learn like API?
|
|
281
|
+
# add_data should probably be removed, deprecate that
|
|
282
|
+
def fit(
|
|
283
|
+
self,
|
|
284
|
+
x: pd.Series = None, # FIXME np.ndarray, pd.Series or str for key in dataf
|
|
285
|
+
y: pd.Series = None,
|
|
286
|
+
engine: str = "lmfit",
|
|
287
|
+
weights: pd.Series = None,
|
|
288
|
+
**kwargs,
|
|
289
|
+
):
|
|
290
|
+
"""
|
|
291
|
+
Fit the rating curve to observed stage-discharge data.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
----------
|
|
295
|
+
x : pandas.Series or numpy.ndarray or str, optional
|
|
296
|
+
Stage values used for fitting. If a string is provided, it is used
|
|
297
|
+
as a column name in the dataframe added with :meth:`add_data`. If
|
|
298
|
+
omitted, the stage column configured by :meth:`add_data` is used.
|
|
299
|
+
y : pandas.Series or numpy.ndarray or str, optional
|
|
300
|
+
Discharge values used for fitting. If a string is provided, it is
|
|
301
|
+
used as a column name in the dataframe added with :meth:`add_data`.
|
|
302
|
+
If omitted, the discharge column configured by :meth:`add_data` is
|
|
303
|
+
used.
|
|
304
|
+
engine : {"lmfit"}, default: "lmfit"
|
|
305
|
+
Fitting engine to use. Currently only ``"lmfit"`` is supported.
|
|
306
|
+
weights : pandas.Series, optional
|
|
307
|
+
Observation weights passed to ``lmfit.Model.fit``. If omitted, all
|
|
308
|
+
observations are weighted equally.
|
|
309
|
+
**kwargs
|
|
310
|
+
Additional keyword arguments passed to ``lmfit.Model.fit``.
|
|
311
|
+
|
|
312
|
+
Raises
|
|
313
|
+
------
|
|
314
|
+
NotImplementedError
|
|
315
|
+
If ``engine`` is not ``"lmfit"``.
|
|
316
|
+
|
|
317
|
+
Notes
|
|
318
|
+
-----
|
|
319
|
+
The fitted result is stored in ``fit_result`` and the best-fit
|
|
320
|
+
parameter values are stored in ``fit_best_parameters``. Fit residuals
|
|
321
|
+
and percent errors are stored internally for plotting.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
# Note: cannot use self. in default arguments as they are eval'ed at
|
|
325
|
+
# creation time
|
|
326
|
+
if x is None:
|
|
327
|
+
# try:
|
|
328
|
+
xf = self._dataf[self._k_stage].to_numpy()
|
|
329
|
+
# except: # TODO what error is this if dataf is None?
|
|
330
|
+
# ... # message to add data or pass data to fit
|
|
331
|
+
elif isinstance(x, str):
|
|
332
|
+
xf = self._dataf[x].to_numpy()
|
|
333
|
+
else: # is np.ndarray or Series, check for that?
|
|
334
|
+
xf = x.copy()
|
|
335
|
+
|
|
336
|
+
if y is None:
|
|
337
|
+
yf = self._dataf[self._k_discharge].to_numpy()
|
|
338
|
+
elif isinstance(y, str):
|
|
339
|
+
yf = self._dataf[y].to_numpy()
|
|
340
|
+
else: # is np.ndarray or Series, check for that?
|
|
341
|
+
yf = y.copy()
|
|
342
|
+
|
|
343
|
+
if not self._user_df_added:
|
|
344
|
+
self._user_data_from_fit(xf, yf)
|
|
345
|
+
|
|
346
|
+
if engine != "lmfit":
|
|
347
|
+
raise NotImplementedError(
|
|
348
|
+
f"{engine} is not supported, only lmfit is currently supported"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# lmfit engine
|
|
352
|
+
lmfit_model = self.rating_model.create_model()
|
|
353
|
+
lmfit_init_pars = self.initial_parameters
|
|
354
|
+
lmfit_weights = np.ones(len(yf)) if weights is None else weights.to_numpy()
|
|
355
|
+
|
|
356
|
+
# constrain parameters as defined in RatingModel
|
|
357
|
+
lmfit_init_pars = self.rating_model.constrain_pars_with_obs(
|
|
358
|
+
xf, yf, lmfit_init_pars
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
result = lmfit_model.fit(
|
|
362
|
+
data=yf, params=lmfit_init_pars, weights=lmfit_weights, h=xf, **kwargs
|
|
363
|
+
) # test with using kwargs = {'method':'differential_evolution'}
|
|
364
|
+
|
|
365
|
+
self._fit_obs_data = pd.DataFrame(data=yf, index=xf, columns=["observed"])
|
|
366
|
+
|
|
367
|
+
self.fit_result = result
|
|
368
|
+
self.fit_best_parameters = result.best_values
|
|
369
|
+
|
|
370
|
+
self._calc_fit_residuals()
|
|
371
|
+
|
|
372
|
+
def _calc_fit_residuals(self): # numpydoc ignore=GL08
|
|
373
|
+
self._fit_obs_data["predicted"] = self.predict(stage=self._fit_obs_data.index)
|
|
374
|
+
self._fit_obs_data["residual"] = (
|
|
375
|
+
self._fit_obs_data["predicted"] - self._fit_obs_data["observed"]
|
|
376
|
+
)
|
|
377
|
+
self._fit_obs_data["percent_error"] = (
|
|
378
|
+
self._fit_obs_data["residual"] / self._fit_obs_data["observed"] * 100
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
def add_stage_series(self, stage_series): # numpydoc ignore=PR01
|
|
382
|
+
"""Set the stage series to be used for prediction and plotting."""
|
|
383
|
+
self.stage_series = stage_series.copy()
|
|
384
|
+
|
|
385
|
+
def predict(self, stage=None):
|
|
386
|
+
"""
|
|
387
|
+
Predict discharge at a given stage or array-like of stages using the fitted model.
|
|
388
|
+
|
|
389
|
+
Parameters
|
|
390
|
+
----------
|
|
391
|
+
stage : float or array-like, optional
|
|
392
|
+
Stage value(s) at which to predict discharge. If omitted, the stage series added
|
|
393
|
+
with :meth:`add_stage_series` is used.
|
|
394
|
+
If no stage series is available, ``None`` is returned.
|
|
395
|
+
|
|
396
|
+
Returns
|
|
397
|
+
-------
|
|
398
|
+
float or np.ndarray
|
|
399
|
+
Discharge values predicted at the given stage(s). If no stage series is available,
|
|
400
|
+
``None`` is returned.
|
|
401
|
+
"""
|
|
402
|
+
if stage is None and self.stage_series is not None:
|
|
403
|
+
return self.rating_model.func(self.stage_series, **self.fit_best_parameters)
|
|
404
|
+
if stage is not None:
|
|
405
|
+
return self.rating_model.func(stage, **self.fit_best_parameters)
|
|
406
|
+
# FIXME raise
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
def inverse(self, discharge, initial_guess=None):
|
|
410
|
+
"""
|
|
411
|
+
Inverse the rating curve to find the stage at a given discharge.
|
|
412
|
+
|
|
413
|
+
Parameters
|
|
414
|
+
----------
|
|
415
|
+
discharge : float or array-like
|
|
416
|
+
Discharge value(s) at which to predict stage.
|
|
417
|
+
initial_guess : float or array-like, optional
|
|
418
|
+
Initial guess for the stage value(s) corresponding to the given discharge value(s).
|
|
419
|
+
This is required for models that do not have an inverse method.
|
|
420
|
+
"""
|
|
421
|
+
raise NotImplementedError()
|
|
422
|
+
# if model has inverse attribute, use that, else inverse it with minimizing
|
|
423
|
+
# this requires an initial guess
|
|
424
|
+
self.rating_model.inverse(
|
|
425
|
+
discharge, initial_guess, **self.fit_result.best_values
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def fit_summary(self):
|
|
429
|
+
"""Print a summary of the fitted model parameters and results."""
|
|
430
|
+
print(self.fit_result.fit_report())
|
|
431
|
+
|
|
432
|
+
def plot_residuals(
|
|
433
|
+
self,
|
|
434
|
+
scale="linear",
|
|
435
|
+
label_discharge="Discharge",
|
|
436
|
+
label_stage="Stage",
|
|
437
|
+
stage_on_y=False,
|
|
438
|
+
point_labels=None,
|
|
439
|
+
):
|
|
440
|
+
"""
|
|
441
|
+
Plot residuals and model performance for a fitted rating curve.
|
|
442
|
+
|
|
443
|
+
This function generates a plot to visualize the observed and predicted data, as well
|
|
444
|
+
as the residuals and percentage errors for the rating curve model fit. It supports optional
|
|
445
|
+
custom scaling for the axes and labeling.
|
|
446
|
+
|
|
447
|
+
Parameters
|
|
448
|
+
----------
|
|
449
|
+
scale : str, optional
|
|
450
|
+
The scale type to be used for the y-axis of the observed/predicted plot and the x-axis
|
|
451
|
+
of the residual and percent error plots. Default is "linear".
|
|
452
|
+
label_discharge : str, optional
|
|
453
|
+
Label for the discharge axis on the plots. Default is "Discharge".
|
|
454
|
+
label_stage : str, optional
|
|
455
|
+
Label for the stage axis on the plots. Default is "Stage".
|
|
456
|
+
stage_on_y : bool, optional
|
|
457
|
+
If True, plots stage on the y-axis and discharge on the x-axis. Default is False.
|
|
458
|
+
point_labels : str, optional
|
|
459
|
+
Column name in the fitted data DataFrame to use for labeling points in the plots. If
|
|
460
|
+
provided, labels will be added to the observed data points in the first subplot.
|
|
461
|
+
|
|
462
|
+
Returns
|
|
463
|
+
-------
|
|
464
|
+
tuple
|
|
465
|
+
A tuple containing:
|
|
466
|
+
- fig: matplotlib.figure.Figure
|
|
467
|
+
The overall figure object for the plots.
|
|
468
|
+
- axes: numpy.ndarray of matplotlib.axes.Axes
|
|
469
|
+
An array of axes objects for the three subplots.
|
|
470
|
+
|
|
471
|
+
Raises
|
|
472
|
+
------
|
|
473
|
+
ValueError
|
|
474
|
+
If the rating curve is not fitted before calling this method (i.e., `fit_result` is None).
|
|
475
|
+
|
|
476
|
+
Notes
|
|
477
|
+
-----
|
|
478
|
+
- The first subplot displays the observed vs predicted data.
|
|
479
|
+
- The second subplot shows the absolute residuals.
|
|
480
|
+
- The third subplot illustrates the percent error.
|
|
481
|
+
- If `stage_series` is provided, it defines the span of the stage axis; otherwise, the
|
|
482
|
+
minimum and maximum observed stage values from the fitted data are used.
|
|
483
|
+
- When `stage_on_y` is False (default), stage is the x-axis and discharge is the y-axis
|
|
484
|
+
for the first subplot, while other subplots share the x-axis (stage).
|
|
485
|
+
- Adjusts y-limits of the residual and percent error subplots for symmetric bounds.
|
|
486
|
+
"""
|
|
487
|
+
if self.fit_result is None:
|
|
488
|
+
ValueError(
|
|
489
|
+
"Rating curve is not fitted, call .fit() first."
|
|
490
|
+
) # TODO custom exception
|
|
491
|
+
|
|
492
|
+
if self.stage_series is not None:
|
|
493
|
+
min_stage = self.stage_series.min()
|
|
494
|
+
max_stage = self.stage_series.max()
|
|
495
|
+
else:
|
|
496
|
+
min_stage = self._fit_obs_data.index.min()
|
|
497
|
+
max_stage = self._fit_obs_data.index.max()
|
|
498
|
+
|
|
499
|
+
stage_plt = np.linspace(min_stage, max_stage, num=100)
|
|
500
|
+
q_plt = self.predict(stage=stage_plt)
|
|
501
|
+
|
|
502
|
+
fig, axes = plt.subplots(3, 1, sharex=True)
|
|
503
|
+
|
|
504
|
+
if stage_on_y:
|
|
505
|
+
y_plt = stage_plt
|
|
506
|
+
x_plt = q_plt
|
|
507
|
+
|
|
508
|
+
y_pts = self._fit_obs_data.index.to_numpy()
|
|
509
|
+
x_pts = self._fit_obs_data["observed"]
|
|
510
|
+
|
|
511
|
+
y_label = label_stage
|
|
512
|
+
x_label = label_discharge
|
|
513
|
+
else:
|
|
514
|
+
y_plt = q_plt
|
|
515
|
+
x_plt = stage_plt
|
|
516
|
+
|
|
517
|
+
y_pts = self._fit_obs_data["observed"]
|
|
518
|
+
x_pts = self._fit_obs_data.index.to_numpy()
|
|
519
|
+
|
|
520
|
+
x_label = label_stage
|
|
521
|
+
y_label = label_discharge
|
|
522
|
+
|
|
523
|
+
axes[0].plot(x_pts, y_pts, "ko")
|
|
524
|
+
axes[0].plot(x_plt, y_plt, "k-")
|
|
525
|
+
|
|
526
|
+
axes[1].axhline(0, color="k", linewidth=0.5)
|
|
527
|
+
axes[1].plot(x_pts, self._fit_obs_data["residual"], "ko")
|
|
528
|
+
|
|
529
|
+
axes[2].axhline(0, color="k", linewidth=0.5)
|
|
530
|
+
axes[2].plot(x_pts, self._fit_obs_data["percent_error"], "ko")
|
|
531
|
+
|
|
532
|
+
axes[0].set_yscale(scale)
|
|
533
|
+
axes[2].set_xscale(scale)
|
|
534
|
+
|
|
535
|
+
axes[0].set_ylabel(y_label)
|
|
536
|
+
axes[1].set_ylabel("Absolute error")
|
|
537
|
+
axes[2].set_ylabel("Percent error")
|
|
538
|
+
axes[2].set_xlabel(x_label)
|
|
539
|
+
|
|
540
|
+
axes[1].set_ylim(
|
|
541
|
+
(-max(np.abs(axes[1].get_ylim())), max(np.abs(axes[1].get_ylim())))
|
|
542
|
+
)
|
|
543
|
+
axes[2].set_ylim(
|
|
544
|
+
(-max(np.abs(axes[2].get_ylim())), max(np.abs(axes[2].get_ylim())))
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
return fig, axes
|
|
548
|
+
|
|
549
|
+
def plot(
|
|
550
|
+
self,
|
|
551
|
+
scale="linear",
|
|
552
|
+
label_discharge="Discharge",
|
|
553
|
+
label_stage="Stage",
|
|
554
|
+
stage_on_y=False,
|
|
555
|
+
point_labels=None,
|
|
556
|
+
cross_section=False,
|
|
557
|
+
):
|
|
558
|
+
"""
|
|
559
|
+
Plot the fitted rating curve and observational data.
|
|
560
|
+
|
|
561
|
+
Parameters
|
|
562
|
+
----------
|
|
563
|
+
scale : str, optional
|
|
564
|
+
A string indicating the type of scaling for the axes. Default is "linear".
|
|
565
|
+
label_discharge : str, optional
|
|
566
|
+
Label for the discharge axis. Default is "Discharge".
|
|
567
|
+
label_stage : str, optional
|
|
568
|
+
Label for the stage axis. Default is "Stage".
|
|
569
|
+
stage_on_y : bool, optional
|
|
570
|
+
If True, stage is plotted along the y-axis; otherwise, discharge is plotted on
|
|
571
|
+
the y-axis. Default is False.
|
|
572
|
+
point_labels : str, optional
|
|
573
|
+
Labels for the individual data points on the plot. Default is None.
|
|
574
|
+
cross_section : bool, optional
|
|
575
|
+
Plots the cross-section profile if available. If True, forces stage to be plotted on the y-axis, overriding the `stage_on_y`
|
|
576
|
+
parameter. Default is False.
|
|
577
|
+
|
|
578
|
+
Returns
|
|
579
|
+
-------
|
|
580
|
+
tuple
|
|
581
|
+
Contains:
|
|
582
|
+
- fig : matplotlib.figure.Figure
|
|
583
|
+
The matplotlib figure object containing the plot.
|
|
584
|
+
- ax : matplotlib.axes._axes.Axes
|
|
585
|
+
The matplotlib axes object for the plot.
|
|
586
|
+
|
|
587
|
+
Raises
|
|
588
|
+
------
|
|
589
|
+
ValueError
|
|
590
|
+
If the rating curve has not been fitted using the `.fit()` method.
|
|
591
|
+
|
|
592
|
+
Notes
|
|
593
|
+
-----
|
|
594
|
+
This function assumes that the rating curve has been fitted prior to calling it.
|
|
595
|
+
It also calculates the range of the stage series for generating the plot points
|
|
596
|
+
if such data is available. Otherwise, it derives the range from the fitted
|
|
597
|
+
observational data.
|
|
598
|
+
"""
|
|
599
|
+
if self.fit_result is None:
|
|
600
|
+
ValueError(
|
|
601
|
+
"Rating curve is not fitted, call .fit() first."
|
|
602
|
+
) # TODO custom exception
|
|
603
|
+
|
|
604
|
+
if self.stage_series is not None:
|
|
605
|
+
min_stage = self.stage_series.min()
|
|
606
|
+
max_stage = self.stage_series.max()
|
|
607
|
+
else:
|
|
608
|
+
min_stage = self._fit_obs_data.index.min()
|
|
609
|
+
max_stage = self._fit_obs_data.index.max()
|
|
610
|
+
|
|
611
|
+
stage_plt = np.linspace(min_stage, max_stage, num=100)
|
|
612
|
+
q_plt = self.predict(stage=stage_plt)
|
|
613
|
+
|
|
614
|
+
fig, ax = plt.subplots()
|
|
615
|
+
|
|
616
|
+
if cross_section:
|
|
617
|
+
stage_on_y = True
|
|
618
|
+
|
|
619
|
+
if stage_on_y:
|
|
620
|
+
y_plt = stage_plt
|
|
621
|
+
x_plt = q_plt
|
|
622
|
+
|
|
623
|
+
y_pts = self._fit_obs_data.index.to_numpy()
|
|
624
|
+
x_pts = self._fit_obs_data["observed"]
|
|
625
|
+
|
|
626
|
+
y_label = label_stage
|
|
627
|
+
x_label = label_discharge
|
|
628
|
+
else:
|
|
629
|
+
y_plt = q_plt
|
|
630
|
+
x_plt = stage_plt
|
|
631
|
+
|
|
632
|
+
y_pts = self._fit_obs_data["observed"]
|
|
633
|
+
x_pts = self._fit_obs_data.index.to_numpy()
|
|
634
|
+
|
|
635
|
+
x_label = label_stage
|
|
636
|
+
y_label = label_discharge
|
|
637
|
+
|
|
638
|
+
ax.plot(x_pts, y_pts, "ko")
|
|
639
|
+
ax.plot(x_plt, y_plt, "k-")
|
|
640
|
+
|
|
641
|
+
ax.set_yscale(scale)
|
|
642
|
+
ax.set_xscale(scale)
|
|
643
|
+
|
|
644
|
+
ax.set_ylabel(y_label)
|
|
645
|
+
ax.set_xlabel(x_label)
|
|
646
|
+
|
|
647
|
+
return fig, ax
|
|
648
|
+
|
|
649
|
+
# TODO
|
|
650
|
+
# def plot_shift():
|
|
651
|
+
# ...
|
|
652
|
+
|
|
653
|
+
# TODO
|
|
654
|
+
# def compare(self, other):
|
|
655
|
+
# ...
|
|
656
|
+
|
|
657
|
+
# TODO
|
|
658
|
+
# def add_obs_point(self):
|
|
659
|
+
# ...
|
|
660
|
+
|
|
661
|
+
@staticmethod
|
|
662
|
+
def _dict_from_parameters(parameters): # numpydoc ignore=GL08, PR01
|
|
663
|
+
return parameters.valuesdict()
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Rating curve models.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Protocol, Union
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from lmfit import Model, Parameters
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RatingModel(Protocol):
|
|
13
|
+
"""
|
|
14
|
+
Protocol class for rating curve models.
|
|
15
|
+
|
|
16
|
+
Methods for RatingModel:
|
|
17
|
+
|
|
18
|
+
func: the rating curve equation, with arguments stage and **parameters, returning
|
|
19
|
+
discharge
|
|
20
|
+
create_model: function that returns the lmfit Model
|
|
21
|
+
create_parameters: function that return lmfit Parameters for the model
|
|
22
|
+
constrain_pars_with_obs: function that puts limits on Parameters based on observations
|
|
23
|
+
e.g. zero flow stage cannot exceed observed stage with flow
|
|
24
|
+
inverse: function that return stage for a given discharge, the inverse of func()
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def func(self, x: np.ndarray, **parameters: float) -> np.ndarray:
|
|
28
|
+
"""
|
|
29
|
+
The rating curve equation.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
x : np.ndarray
|
|
34
|
+
Stage values.
|
|
35
|
+
**parameters : float
|
|
36
|
+
Model parameters.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
float or np.ndarray
|
|
41
|
+
Discharge for the provided stage.
|
|
42
|
+
"""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def create_model(self) -> Model:
|
|
46
|
+
"""
|
|
47
|
+
Create the lmfit Model.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
Model
|
|
52
|
+
The lmfit Model for the rating curve.
|
|
53
|
+
"""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
def create_parameters(
|
|
57
|
+
self, model: Model, parameters: Union[None, Parameters, dict]
|
|
58
|
+
) -> Parameters:
|
|
59
|
+
"""
|
|
60
|
+
Create the lmfit Parameters for the rating curve model, and set initial values.
|
|
61
|
+
This function should provide some default values if parameters is None or a parameter is missing.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
model : Model
|
|
66
|
+
The lmfit Model for the rating curve.
|
|
67
|
+
parameters : Union[None, Parameters, dict]
|
|
68
|
+
Initial parameters for the model. Can be None, a dict, or lmfit Parameters.
|
|
69
|
+
"""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
def constrain_pars_with_obs(
|
|
73
|
+
self, x: np.ndarray, y: np.ndarray, parameters: Parameters
|
|
74
|
+
) -> Parameters:
|
|
75
|
+
"""
|
|
76
|
+
Constraining on the parameters based on x and y values.
|
|
77
|
+
This function should put limits on Parameters based on observations.
|
|
78
|
+
For example, zero flow stage cannot exceed observed stage with flow.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
x : np.ndarray
|
|
83
|
+
Stage values.
|
|
84
|
+
y : np.ndarray
|
|
85
|
+
Discharge values.
|
|
86
|
+
parameters : Parameters
|
|
87
|
+
The lmfit Parameters for the model.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
Parameters
|
|
92
|
+
The lmfit Parameters with limits set based on observations.
|
|
93
|
+
"""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
def inverse(
|
|
97
|
+
self, y: np.ndarray, initial_guess: float, **parameters: float
|
|
98
|
+
) -> np.ndarray:
|
|
99
|
+
"""
|
|
100
|
+
Inverse the rating curve to find the stage at a given discharge.
|
|
101
|
+
This method should return the stage corresponding to the given discharge.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
y : np.ndarray
|
|
106
|
+
Discharge values.
|
|
107
|
+
initial_guess : float
|
|
108
|
+
Initial guess for the stage corresponding to the given discharge, used for numerical methods if needed.
|
|
109
|
+
**parameters : float
|
|
110
|
+
Model parameters.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
np.ndarray
|
|
115
|
+
Stage values corresponding to the given discharge.
|
|
116
|
+
"""
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class PowerLaw:
|
|
121
|
+
"""
|
|
122
|
+
Power law rating curve model.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def func(cls, h: np.ndarray, a: float, h0: float, b: float) -> np.ndarray:
|
|
127
|
+
"""
|
|
128
|
+
The power law rating curve function.
|
|
129
|
+
|
|
130
|
+
.. math::
|
|
131
|
+
Q(h) = a \\times (h-h0)^b
|
|
132
|
+
|
|
133
|
+
where :math:`Q` is discharge, :math:`h` is stage, :math:`h0` is stage at
|
|
134
|
+
zero flow, and :math:`a` and :math:`b` are fitted parameters.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
h : float or np.ndarray
|
|
139
|
+
Stage.
|
|
140
|
+
a : float
|
|
141
|
+
Parameter a.
|
|
142
|
+
h0 : float
|
|
143
|
+
Stage at zero flow.
|
|
144
|
+
b : float
|
|
145
|
+
Parameter b.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
float or np.ndarray
|
|
150
|
+
Discharge for the provided stage.
|
|
151
|
+
"""
|
|
152
|
+
return a * (h - h0) ** b
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def create_model(cls):
|
|
156
|
+
"""
|
|
157
|
+
Create the lmfit Model.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
Model
|
|
162
|
+
The lmfit Model for the power law rating curve.
|
|
163
|
+
"""
|
|
164
|
+
return Model(cls.func)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def create_parameters(cls, model, parameters=None):
|
|
168
|
+
"""
|
|
169
|
+
Create the lmfit Parameters for the power law rating curve model.
|
|
170
|
+
This function sets default values for the parameters if they are missing.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
model : Model
|
|
175
|
+
The lmfit Model for the rating curve.
|
|
176
|
+
parameters : Union[None, Parameters, dict]
|
|
177
|
+
Initial parameters for the model. Can be None, a dict, or lmfit Parameters.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
Parameters
|
|
182
|
+
The lmfit Parameters for the model, with default values set for missing parameters.
|
|
183
|
+
"""
|
|
184
|
+
# default parameters, used if missing from parameters argument
|
|
185
|
+
params_default = model.make_params()
|
|
186
|
+
params_default["h0"].value = 0
|
|
187
|
+
params_default["b"].value = 2
|
|
188
|
+
params_default["a"].value = 1
|
|
189
|
+
if parameters is None:
|
|
190
|
+
return params_default
|
|
191
|
+
|
|
192
|
+
# for dict and Parameters add the missing pars, if any
|
|
193
|
+
for k in set(params_default.keys()) - set(parameters.keys()):
|
|
194
|
+
parameters[k] = params_default.copy()[k]
|
|
195
|
+
if isinstance(parameters, Parameters):
|
|
196
|
+
return parameters
|
|
197
|
+
if isinstance(parameters, dict):
|
|
198
|
+
return model.make_params(**parameters)
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def constrain_pars_with_obs(cls, x, y, parameters):
|
|
202
|
+
"""
|
|
203
|
+
Constraining on the parameters based on observed values.
|
|
204
|
+
This function sets the maximum value of h0 to the minimum value of x, i.e. stage.
|
|
205
|
+
This is to avoid fitting a rating curve with zero flow stage that is higher than the observed stage with flow, which is not physically possible.
|
|
206
|
+
Avoid using this constrain if the observed stage goes below zero flow (i.e. flow is zero in the timeseries).
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
x : np.ndarray
|
|
211
|
+
Stage values.
|
|
212
|
+
y : np.ndarray
|
|
213
|
+
Discharge values. Not used in this function.
|
|
214
|
+
parameters : Parameters
|
|
215
|
+
The lmfit Parameters for the model.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
Parameters
|
|
220
|
+
The lmfit Parameters with limits set for max h0.
|
|
221
|
+
"""
|
|
222
|
+
# check if h0 was already set to a max value that is lower than the
|
|
223
|
+
# limit based on observations
|
|
224
|
+
h0_ceiling = min(np.min(x) - 1e-10, parameters["h0"].max)
|
|
225
|
+
parameters["h0"].max = h0_ceiling
|
|
226
|
+
|
|
227
|
+
return parameters
|
|
228
|
+
|
|
229
|
+
@classmethod
|
|
230
|
+
def inverse(
|
|
231
|
+
cls, y: np.ndarray, initial_guess: float, a: float, h0: float, b: float
|
|
232
|
+
) -> np.ndarray:
|
|
233
|
+
"""
|
|
234
|
+
Inverse the rating curve to find the stage at a given discharge.
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
y : np.ndarray
|
|
239
|
+
Discharge values.
|
|
240
|
+
initial_guess : float
|
|
241
|
+
Initial guess not used in this function.
|
|
242
|
+
a : float
|
|
243
|
+
Parameter a.
|
|
244
|
+
h0 : float
|
|
245
|
+
Parameter h0.
|
|
246
|
+
b : float
|
|
247
|
+
Parameter b.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
np.ndarray
|
|
252
|
+
Stage values corresponding to the given discharge.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
# todo see what's best, pass parameters dict or individual?
|
|
256
|
+
# a = parameters['a']
|
|
257
|
+
# b = parameters['b']
|
|
258
|
+
# h0 = parameters['h0']
|
|
259
|
+
|
|
260
|
+
return (y / a) ** (1 / b) - h0 # FIXME NOT TESTED WITH h0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hydrating
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: fitting hydrological stage discharge rating curves
|
|
5
|
+
Project-URL: Changelog, https://github.com/rhkarls/hydrating/blob/main/CHANGELOG.md
|
|
6
|
+
Project-URL: Documentation, https://github.com/rhkarls/hydrating
|
|
7
|
+
Project-URL: Homepage, https://github.com/rhkarls/hydrating
|
|
8
|
+
Project-URL: Issues, https://github.com/rhkarls/hydrating/issues
|
|
9
|
+
Author-email: Reinert Huseby Karlsen <rhkarls@proton.me>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: lmfit>=1.3.4
|
|
23
|
+
Requires-Dist: matplotlib>=3.10.9
|
|
24
|
+
Requires-Dist: numpy>=2.4.4
|
|
25
|
+
Requires-Dist: pandas>=3.0.2
|
|
26
|
+
Requires-Dist: pyrefly>=0.60.0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
hydrating/__init__.py,sha256=Mf_6rpsrPSXMg7lXeHGJXGrW9XeEP9NRFMkMZi6kZIs,81
|
|
2
|
+
hydrating/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
hydrating/core/hydrating.py,sha256=-FerU384en74z6uYw9uRqTwuswlssGITzRYLAY0ueZs,25557
|
|
4
|
+
hydrating/models/__init__.py,sha256=t1UQSB3QDap7srRMFxWjEZhnz_Bf20FJBmvGi3-4nGk,131
|
|
5
|
+
hydrating/models/models.py,sha256=1H5nygkcbArwC8jUpx1WCiDYJrLkVIIRTbnPWwoxE_U,8030
|
|
6
|
+
hydrating-0.0.3.dist-info/METADATA,sha256=x6HXG42ZfHRLt3q1dvBe78U4ZFz1tJHhsdTwJ_6VJO0,1151
|
|
7
|
+
hydrating-0.0.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
hydrating-0.0.3.dist-info/licenses/LICENSE,sha256=tFmGZzAd-8yy4frO1irUx5o83awFhZvJ7PwGWdVOznE,1106
|
|
9
|
+
hydrating-0.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2026, Reinert Huseby Karlsen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|