HBV-Lab 0.1.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.
- HBV_Lab/HBV_model.py +792 -0
- HBV_Lab/__init__.py +1 -0
- HBV_Lab/calibration.py +243 -0
- HBV_Lab/hbv_step.py +74 -0
- HBV_Lab/response.py +78 -0
- HBV_Lab/routing.py +41 -0
- HBV_Lab/snow.py +70 -0
- HBV_Lab/soil.py +73 -0
- HBV_Lab/uncertainty.py +364 -0
- hbv_lab-0.1.0.dist-info/METADATA +86 -0
- hbv_lab-0.1.0.dist-info/RECORD +14 -0
- hbv_lab-0.1.0.dist-info/WHEEL +5 -0
- hbv_lab-0.1.0.dist-info/licenses/LICENSE +21 -0
- hbv_lab-0.1.0.dist-info/top_level.txt +1 -0
HBV_Lab/uncertainty.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
class uncertainty:
|
|
2
|
+
|
|
3
|
+
def evaluate_uncertainty(self, n_runs=10000, objective='NSE', save_best=10,
|
|
4
|
+
plot_results=True, verbose=True, seed=None, narrow_percent=None,
|
|
5
|
+
exclude_warmup_in_plots=True):
|
|
6
|
+
"""
|
|
7
|
+
Perform uncertainty analysis on an HBV model by sampling from parameter ranges.
|
|
8
|
+
|
|
9
|
+
Parameters:
|
|
10
|
+
-----------
|
|
11
|
+
self : HBVmodel
|
|
12
|
+
The HBV model instance to analyze (typically after calibration)
|
|
13
|
+
n_runs : int, default 10000
|
|
14
|
+
Number of model runs with different parameter sets
|
|
15
|
+
objective : str, default 'NSE'
|
|
16
|
+
Objective function to evaluate model performance. Options are:
|
|
17
|
+
- 'NSE': Nash-Sutcliffe Efficiency (higher is better)
|
|
18
|
+
- 'KGE': Kling-Gupta Efficiency (higher is better)
|
|
19
|
+
- 'RMSE': Root Mean Square Error (lower is better)
|
|
20
|
+
- 'MAE': Mean Absolute Error (lower is better)
|
|
21
|
+
save_best : int, default 10
|
|
22
|
+
Number of best parameter sets to save
|
|
23
|
+
plot_results : bool, default True
|
|
24
|
+
Whether to plot the results after analysis
|
|
25
|
+
verbose : bool, default True
|
|
26
|
+
Whether to print progress information
|
|
27
|
+
seed : int, optional
|
|
28
|
+
Random seed for reproducibility
|
|
29
|
+
narrow_percent : float, optional
|
|
30
|
+
If provided, narrows the parameter search space to this percentage around the default values
|
|
31
|
+
exclude_warmup_in_plots : bool, default True
|
|
32
|
+
Whether to exclude the warmup period from plots
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
--------
|
|
36
|
+
dict
|
|
37
|
+
Dictionary containing the best parameter sets, their performance metrics,
|
|
38
|
+
and uncertainty statistics
|
|
39
|
+
"""
|
|
40
|
+
import numpy as np
|
|
41
|
+
import pandas as pd
|
|
42
|
+
import matplotlib.pyplot as plt
|
|
43
|
+
import time
|
|
44
|
+
import copy
|
|
45
|
+
from tqdm.auto import tqdm
|
|
46
|
+
|
|
47
|
+
if self.data is None:
|
|
48
|
+
raise ValueError("No data loaded. Use load_data() method first.")
|
|
49
|
+
|
|
50
|
+
# Check if observed discharge data is available
|
|
51
|
+
if (self.column_names['obs_q'] is None or
|
|
52
|
+
self.column_names['obs_q'] not in self.data.columns):
|
|
53
|
+
raise ValueError("Observed discharge data is required for uncertainty analysis.")
|
|
54
|
+
|
|
55
|
+
# Set random seed for reproducibility if provided
|
|
56
|
+
if seed is not None:
|
|
57
|
+
np.random.seed(seed)
|
|
58
|
+
|
|
59
|
+
# Store the initial states and parameters to restore later
|
|
60
|
+
initial_states = copy.deepcopy(self.states)
|
|
61
|
+
original_params = copy.deepcopy(self.params)
|
|
62
|
+
|
|
63
|
+
# Extract observed discharge data
|
|
64
|
+
obs_q = self.data[self.column_names['obs_q']].values
|
|
65
|
+
|
|
66
|
+
# Get valid indices (where obs_q is not NaN)
|
|
67
|
+
valid_idx = ~np.isnan(obs_q)
|
|
68
|
+
if np.sum(valid_idx) == 0:
|
|
69
|
+
raise ValueError("No valid observed discharge values found.")
|
|
70
|
+
|
|
71
|
+
# Create flat parameter list for sampling
|
|
72
|
+
param_info = []
|
|
73
|
+
for group_name, group in self.params.items():
|
|
74
|
+
for param_name, param_data in group.items():
|
|
75
|
+
param_info.append({
|
|
76
|
+
'group': group_name,
|
|
77
|
+
'name': param_name,
|
|
78
|
+
'min': param_data['min'],
|
|
79
|
+
'max': param_data['max'],
|
|
80
|
+
'default': param_data['default']
|
|
81
|
+
})
|
|
82
|
+
# Narrow ranges around the defaults (best fit) if narrow_percent is specified
|
|
83
|
+
if narrow_percent is not None:
|
|
84
|
+
for p in param_info:
|
|
85
|
+
best_val = p['default']
|
|
86
|
+
full_range = p['max'] - p['min']
|
|
87
|
+
delta = full_range * narrow_percent
|
|
88
|
+
|
|
89
|
+
new_min = max(p['min'], best_val - delta)
|
|
90
|
+
new_max = min(p['max'], best_val + delta)
|
|
91
|
+
|
|
92
|
+
p['min'] = new_min
|
|
93
|
+
p['max'] = new_max
|
|
94
|
+
if verbose:
|
|
95
|
+
print(f"Narrowed range {p['group']}_{p['name']}: {new_min:.4f} to {new_max:.4f}")
|
|
96
|
+
|
|
97
|
+
# Helper function to create parameter dictionary from sampled values
|
|
98
|
+
def create_param_dict(flat_params):
|
|
99
|
+
param_dict = {group_name: {} for group_name in set(p['group'] for p in param_info)}
|
|
100
|
+
|
|
101
|
+
for i, p in enumerate(param_info):
|
|
102
|
+
if p['name'] not in param_dict[p['group']]:
|
|
103
|
+
param_dict[p['group']][p['name']] = {}
|
|
104
|
+
|
|
105
|
+
param_dict[p['group']][p['name']]['min'] = p['min']
|
|
106
|
+
param_dict[p['group']][p['name']]['max'] = p['max']
|
|
107
|
+
param_dict[p['group']][p['name']]['default'] = flat_params[i]
|
|
108
|
+
|
|
109
|
+
return param_dict
|
|
110
|
+
|
|
111
|
+
# Evaluate model with a given parameter set
|
|
112
|
+
def evaluate_model(params):
|
|
113
|
+
# Create parameter dictionary
|
|
114
|
+
param_dict = create_param_dict(params)
|
|
115
|
+
|
|
116
|
+
# Update model parameters
|
|
117
|
+
self.params = param_dict
|
|
118
|
+
|
|
119
|
+
# Reset states
|
|
120
|
+
self.states = copy.deepcopy(initial_states)
|
|
121
|
+
|
|
122
|
+
# Run the model
|
|
123
|
+
self.run(verbose=False) # Setting verbose=False to reduce output clutter
|
|
124
|
+
|
|
125
|
+
# Calculate performance metrics
|
|
126
|
+
self.calculate_performance_metrics(verbose=False) # Setting verbose=False to reduce output clutter
|
|
127
|
+
|
|
128
|
+
# Return the specified objective
|
|
129
|
+
if objective == 'NSE':
|
|
130
|
+
return self.performance_metrics['NSE']
|
|
131
|
+
elif objective == 'KGE':
|
|
132
|
+
return self.performance_metrics['KGE']
|
|
133
|
+
elif objective == 'RMSE':
|
|
134
|
+
return -self.performance_metrics['RMSE'] # Negative for minimization
|
|
135
|
+
elif objective == 'MAE':
|
|
136
|
+
return -self.performance_metrics['MAE'] # Negative for minimization
|
|
137
|
+
else:
|
|
138
|
+
raise ValueError(f"Unknown objective function: {objective}")
|
|
139
|
+
|
|
140
|
+
# Prepare to store results
|
|
141
|
+
n_params = len(param_info)
|
|
142
|
+
results = {
|
|
143
|
+
'parameters': np.zeros((n_runs, n_params)),
|
|
144
|
+
'performance': np.zeros(n_runs)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Start timing
|
|
148
|
+
start_time = time.time()
|
|
149
|
+
|
|
150
|
+
if verbose:
|
|
151
|
+
print(f"Starting uncertainty analysis with {n_runs} runs...")
|
|
152
|
+
print(f"Sampling {n_params} parameters uniformly across their ranges")
|
|
153
|
+
print(f"Evaluating with {objective} as the objective function")
|
|
154
|
+
|
|
155
|
+
# Run Monte Carlo simulations
|
|
156
|
+
for i in tqdm(range(n_runs)):
|
|
157
|
+
# Sample parameters uniformly from their ranges
|
|
158
|
+
sampled_params = np.array([np.random.uniform(p['min'], p['max']) for p in param_info])
|
|
159
|
+
|
|
160
|
+
# Store parameters
|
|
161
|
+
results['parameters'][i, :] = sampled_params
|
|
162
|
+
|
|
163
|
+
# Evaluate model and store performance
|
|
164
|
+
results['performance'][i] = evaluate_model(sampled_params)
|
|
165
|
+
|
|
166
|
+
# Sort results by performance (descending order)
|
|
167
|
+
sort_indices = np.argsort(-results['performance'])
|
|
168
|
+
sorted_performance = results['performance'][sort_indices]
|
|
169
|
+
sorted_parameters = results['parameters'][sort_indices]
|
|
170
|
+
|
|
171
|
+
# Get the best parameter sets
|
|
172
|
+
best_indices = sort_indices[:save_best]
|
|
173
|
+
best_performance = results['performance'][best_indices]
|
|
174
|
+
best_parameters = results['parameters'][best_indices]
|
|
175
|
+
|
|
176
|
+
# Save best parameter sets
|
|
177
|
+
best_param_sets = []
|
|
178
|
+
for i in range(save_best):
|
|
179
|
+
param_set = create_param_dict(best_parameters[i])
|
|
180
|
+
best_param_sets.append({
|
|
181
|
+
'parameters': param_set,
|
|
182
|
+
'performance': best_performance[i] if objective in ['NSE', 'KGE'] else -best_performance[i]
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
# Store performance with original (calibrated) parameters
|
|
186
|
+
self.params = original_params
|
|
187
|
+
self.states = copy.deepcopy(initial_states)
|
|
188
|
+
self.run(verbose=False)
|
|
189
|
+
self.calculate_performance_metrics(verbose=False)
|
|
190
|
+
original_performance = self.performance_metrics[objective]
|
|
191
|
+
if objective in ['RMSE', 'MAE']:
|
|
192
|
+
original_performance = -original_performance # Adjust sign for consistent comparison
|
|
193
|
+
|
|
194
|
+
# Compare best run with original (calibrated) run
|
|
195
|
+
best_vs_original = best_performance[0] - original_performance
|
|
196
|
+
|
|
197
|
+
# Generate prediction intervals
|
|
198
|
+
time_index = self.data.index
|
|
199
|
+
dates = self.results['dates'] # Get dates from model results
|
|
200
|
+
obs_q_valid = obs_q[valid_idx]
|
|
201
|
+
|
|
202
|
+
# Create a dataframe to store all the best model runs
|
|
203
|
+
best_runs_df = pd.DataFrame(index=time_index)
|
|
204
|
+
best_runs_df['dates'] = dates # Add dates column
|
|
205
|
+
best_runs_df['observed'] = self.data[self.column_names['obs_q']]
|
|
206
|
+
|
|
207
|
+
# Run model with best parameter sets and store results
|
|
208
|
+
for i in range(save_best):
|
|
209
|
+
self.params = create_param_dict(best_parameters[i])
|
|
210
|
+
self.states = copy.deepcopy(initial_states)
|
|
211
|
+
self.run(verbose=False)
|
|
212
|
+
best_runs_df[f'best_{i+1}'] = self.results['discharge']
|
|
213
|
+
|
|
214
|
+
# Run model with original parameters to get baseline
|
|
215
|
+
self.params = original_params
|
|
216
|
+
self.states = copy.deepcopy(initial_states)
|
|
217
|
+
self.run(verbose=False)
|
|
218
|
+
best_runs_df['original'] = self.results['discharge']
|
|
219
|
+
|
|
220
|
+
# Calculate 95% prediction interval from best runs
|
|
221
|
+
best_runs_df['q5'] = best_runs_df.filter(like='best_').quantile(0.025, axis=1)
|
|
222
|
+
best_runs_df['q95'] = best_runs_df.filter(like='best_').quantile(0.975, axis=1)
|
|
223
|
+
|
|
224
|
+
# Determine warmup period index for plotting
|
|
225
|
+
warmup_idx = 0
|
|
226
|
+
if exclude_warmup_in_plots and hasattr(self, 'warmup_end') and self.warmup_end is not None:
|
|
227
|
+
if 'dates' in best_runs_df.columns:
|
|
228
|
+
# Find the index of the first date after warmup_end
|
|
229
|
+
warmup_idx = np.sum(best_runs_df['dates'] <= self.warmup_end)
|
|
230
|
+
if verbose:
|
|
231
|
+
print(f"Excluding data up to {self.warmup_end} ({warmup_idx} timesteps) from plots as warmup period.")
|
|
232
|
+
else:
|
|
233
|
+
# Default to 10% if no dates available
|
|
234
|
+
warmup_idx = int(len(obs_q) * 0.1)
|
|
235
|
+
if verbose:
|
|
236
|
+
print(f"No dates found. Defaulting to exclude first {warmup_idx} timesteps (10% of data) from plots.")
|
|
237
|
+
elif exclude_warmup_in_plots:
|
|
238
|
+
# Default: exclude first 10% of the data
|
|
239
|
+
warmup_idx = int(len(obs_q) * 0.1)
|
|
240
|
+
if verbose:
|
|
241
|
+
print(f"No warmup_end specified. Excluding first {warmup_idx} timesteps (10% of data) from plots.")
|
|
242
|
+
|
|
243
|
+
# Plot results if requested
|
|
244
|
+
if plot_results:
|
|
245
|
+
plt.figure(figsize=(12, 6))
|
|
246
|
+
|
|
247
|
+
# Apply warmup exclusion for plotting
|
|
248
|
+
plot_df = best_runs_df.iloc[warmup_idx:].copy()
|
|
249
|
+
plot_dates = dates[warmup_idx:] if isinstance(dates, np.ndarray) else plot_df.index
|
|
250
|
+
|
|
251
|
+
# Plot uncertainty band
|
|
252
|
+
plt.fill_between(plot_dates, plot_df['q5'], plot_df['q95'],
|
|
253
|
+
color='lightgray', alpha=0.7, label='95% Prediction Interval')
|
|
254
|
+
|
|
255
|
+
# Plot best run
|
|
256
|
+
plt.plot(plot_dates, plot_df['best_1'], 'b-', linewidth=1, label='Best Run')
|
|
257
|
+
|
|
258
|
+
# Plot original (calibrated) run
|
|
259
|
+
plt.plot(plot_dates, plot_df['original'], 'r--', linewidth=1.5, label='Calibrated Run')
|
|
260
|
+
|
|
261
|
+
# Plot observed data
|
|
262
|
+
valid_obs = ~np.isnan(plot_df['observed'])
|
|
263
|
+
plt.plot(plot_dates[valid_obs], plot_df['observed'][valid_obs], 'k.',
|
|
264
|
+
markersize=3, label='Observed')
|
|
265
|
+
|
|
266
|
+
plt.title(f'Uncertainty Analysis Results (n={n_runs})')
|
|
267
|
+
plt.xlabel('Time')
|
|
268
|
+
plt.ylabel('Discharge (mm/day)')
|
|
269
|
+
|
|
270
|
+
# Set sensible y-axis limits
|
|
271
|
+
valid_data = np.concatenate([
|
|
272
|
+
plot_df['observed'][valid_obs],
|
|
273
|
+
plot_df['original'],
|
|
274
|
+
plot_df['best_1'],
|
|
275
|
+
plot_df['q95']
|
|
276
|
+
])
|
|
277
|
+
max_val = np.nanmax(valid_data)
|
|
278
|
+
leeway = 0.1 * max_val # 10% extra space
|
|
279
|
+
plt.ylim(0, max_val + leeway)
|
|
280
|
+
|
|
281
|
+
plt.legend()
|
|
282
|
+
plt.grid(True, alpha=0.3)
|
|
283
|
+
|
|
284
|
+
# Add annotation about performance
|
|
285
|
+
if objective in ['NSE', 'KGE']:
|
|
286
|
+
better_text = "better" if best_vs_original > 0 else "worse"
|
|
287
|
+
diff_text = f"Best run is {abs(best_vs_original):.4f} {better_text} than calibrated run"
|
|
288
|
+
else:
|
|
289
|
+
better_text = "better" if best_vs_original < 0 else "worse"
|
|
290
|
+
diff_text = f"Best run is {abs(best_vs_original):.4f} {better_text} than calibrated run"
|
|
291
|
+
|
|
292
|
+
plt.figtext(0.5, 0.002, diff_text, ha='center', fontsize=12)
|
|
293
|
+
|
|
294
|
+
plt.tight_layout()
|
|
295
|
+
plt.show()
|
|
296
|
+
|
|
297
|
+
# Plot parameter distributions for the top runs
|
|
298
|
+
n_params = len(param_info)
|
|
299
|
+
n_cols = min(3, n_params)
|
|
300
|
+
n_rows = (n_params + n_cols - 1) // n_cols
|
|
301
|
+
|
|
302
|
+
plt.figure(figsize=(15, n_rows * 3))
|
|
303
|
+
|
|
304
|
+
for i, p in enumerate(param_info):
|
|
305
|
+
plt.subplot(n_rows, n_cols, i + 1)
|
|
306
|
+
param_values = best_parameters[:save_best, i]
|
|
307
|
+
|
|
308
|
+
plt.hist(param_values, bins=min(10, save_best), alpha=0.7)
|
|
309
|
+
plt.axvline(p['default'], color='r', linestyle='--',
|
|
310
|
+
linewidth=2, label='Calibrated')
|
|
311
|
+
plt.axvline(best_parameters[0, i], color='b', linestyle='-',
|
|
312
|
+
linewidth=2, label='Best Run')
|
|
313
|
+
|
|
314
|
+
plt.title(f"{p['group']}_{p['name']}")
|
|
315
|
+
plt.xlabel('Parameter Value')
|
|
316
|
+
plt.ylabel('Frequency')
|
|
317
|
+
|
|
318
|
+
# Only show legend on the first subplot
|
|
319
|
+
if i == 0:
|
|
320
|
+
plt.legend()
|
|
321
|
+
|
|
322
|
+
plt.tight_layout()
|
|
323
|
+
plt.show()
|
|
324
|
+
|
|
325
|
+
# Compute elapsed time
|
|
326
|
+
elapsed_time = time.time() - start_time
|
|
327
|
+
|
|
328
|
+
if verbose:
|
|
329
|
+
print(f"\nUncertainty analysis completed in {elapsed_time:.2f} seconds")
|
|
330
|
+
print(f"Analyzed {n_runs} parameter sets")
|
|
331
|
+
|
|
332
|
+
print("\nTop Performance Values:")
|
|
333
|
+
for i in range(min(5, save_best)):
|
|
334
|
+
perf_value = best_performance[i] if objective in ['NSE', 'KGE'] else -best_performance[i]
|
|
335
|
+
print(f" Run {i+1}: {objective} = {perf_value:.4f}")
|
|
336
|
+
|
|
337
|
+
print(f"\nOriginal (Calibrated) Performance: {objective} = {original_performance:.4f}")
|
|
338
|
+
|
|
339
|
+
if objective in ['NSE', 'KGE']:
|
|
340
|
+
better_text = "better" if best_vs_original > 0 else "worse"
|
|
341
|
+
else:
|
|
342
|
+
better_text = "better" if best_vs_original < 0 else "worse"
|
|
343
|
+
|
|
344
|
+
print(f"Best run is {abs(best_vs_original):.4f} {better_text} than calibrated run")
|
|
345
|
+
|
|
346
|
+
# Restore original parameters and states
|
|
347
|
+
self.params = original_params
|
|
348
|
+
self.states = copy.deepcopy(initial_states)
|
|
349
|
+
|
|
350
|
+
# Return results
|
|
351
|
+
return {
|
|
352
|
+
'best_parameter_sets': best_param_sets,
|
|
353
|
+
'uncertainty_bounds': {
|
|
354
|
+
'lower': best_runs_df['q5'].tolist(),
|
|
355
|
+
'upper': best_runs_df['q95'].tolist()
|
|
356
|
+
},
|
|
357
|
+
'best_runs': best_runs_df,
|
|
358
|
+
'objective': objective,
|
|
359
|
+
'n_runs': n_runs,
|
|
360
|
+
'save_best': save_best,
|
|
361
|
+
'original_performance': original_performance,
|
|
362
|
+
'best_performance': best_performance[0] if objective in ['NSE', 'KGE'] else -best_performance[0],
|
|
363
|
+
#'warmup_idx': warmup_idx
|
|
364
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: HBV_Lab
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An intuitive, object-oriented and user-friendly Python implementation of a lumped conceptual HBV hydrological model for educational and research purposes.
|
|
5
|
+
Home-page: https://github.com/abdallaox/HBV_python_implementation
|
|
6
|
+
Author: Abdalla Mohammed
|
|
7
|
+
Author-email: abdalla.mohammed.ox@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: hydrology HBV-model rainfall-runoff hydrological-modelling
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Hydrology
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: numpy
|
|
19
|
+
Requires-Dist: pandas
|
|
20
|
+
Requires-Dist: matplotlib
|
|
21
|
+
Requires-Dist: scipy
|
|
22
|
+
Requires-Dist: tqdm
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: author-email
|
|
25
|
+
Dynamic: classifier
|
|
26
|
+
Dynamic: description
|
|
27
|
+
Dynamic: description-content-type
|
|
28
|
+
Dynamic: home-page
|
|
29
|
+
Dynamic: keywords
|
|
30
|
+
Dynamic: license
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
Dynamic: requires-dist
|
|
33
|
+
Dynamic: requires-python
|
|
34
|
+
Dynamic: summary
|
|
35
|
+
|
|
36
|
+
# HBV_Lab (Python implementation of a lumped conceptual HBV model)
|
|
37
|
+
|
|
38
|
+
HBV is a simple conceptual hydrological model that simulates the main hydrological processes related to snow, soil, groundwater, and routing [[1]](https://watermark.silverchair.com/147.pdf?token=AQECAHi208BE49Ooan9kkhW_Ercy7Dm3ZL_9Cf3qfKAc485ysgAAA_swggP3BgkqhkiG9w0BBwagggPoMIID5AIBADCCA90GCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMKaeJWQ7VWIq_mqLkAgEQgIIDrh5vsWeaZ0wY6F-ERiovjNb4KimkGt_o5Pj5ZbNlsoszUqDq-oFtdcns5O02KBex_JakkQDDkMBBlg_PFsx9vpuBqYn1kC7EnMp274TfQ2NxQC70hO3OXoYWvMcZRf-Zl3r9w2LYzlmGc9fk2PmF173MeCsuIKaaA79mD0QvqI_9hN8Dz6P43uY3ybzhZPAWUaOFtudQTFy6iC9DZ1jChvSOBK_LpqltPDB5kbyDXkB8OYQl1RQQ3bBtO_G3pwFuat3m2YyKINkI7kLl6alxTs0VQc9cEXgxs_QkBubg_VBtVIaD8mNa7BxQ3PrWpPyrn02TfcAq67SVekWeN9K94P4B8aBBYuyi1-W8vqlp22gzHzr6FrtiP7PEE-7ymBAWQgro7XwZ4bulncCHb1seCeoLbuStdR93KGvKto6hOyN3uxuQZ5e1iP7726MkrMI5MhmCuw8awj2qQeH4LL4g1UuublCNtCX0KNkTCtdj3OUJfxORlLqcCS6PMwM7xkZpw4ytgJ1ro77v3T-yn3V7TbgriNX7UB9BWU2HGVz6SsSkZQoEcop0GPb_EpoqMMkw3NwEn8alJBXTcSbjkrAzWPm6kgZ3agvzbAI53fvueVayzDiZ49ifMnImnn3ge6Dcp3tb-M1Yw6rj8et1UhgfhJMbE0VGc_05KvEdrQYTndorlZPPUPpRK-3-jHOTDwOjz2ME-orp_oAcwoRqaja-e1aeLAuNFImu7PKsRSyn1x3zTh_2lks1_xt6T8kQgXeRluGhJwth1hXCa2ld7qrd1wo6H-6x63UFwO35ygr5MuIOuUnf0W2Rfb7tQtiPY9Vn_snj3frwtqBPOsgQUYtS5jXkSL-FngaOnUedrBvtv0iybblBtdd-LnClDE3KRDdVSjAAa0VS3v5RoUBxs3c8p44N5D3S4NQvdc8PZy9aHeJ6Tl7VNg-gWneGm3_BrOgiW1ylifGMb3X0J6NoYHSDy0mzR51VcM3w-gSCg44ejKuMPqS1yI-yTkZrAfXEKI8ECNdjLz_A9OjaMHr_saFijNhrdHX_l9_bFlfQEzh1zq6ueHZR6Bx3bMriUQYyajTQ0zpNdaQQXm1m5Caq4A-Agkmd6m9SPQNRquMCSYkP10uEw1deYjz6nAhELvTJhG7T8e2ZZ_uYantsrPO54_MkPTPI--4Mx9ePzP9modf7Dc3c98Iu1230duLUQd5sfkkRCF1Ab7P-FlJpUYt1xA2bS7csXfYc_IToq4EcNXx_Zw). There are many software packages and off-the-shelf products that implement different versions of it [[2]](https://www.geo.uzh.ch/en/units/h2k/Services/HBV-Model.html) [[3]](https://hess.copernicus.org/articles/17/445/2013/).
|
|
39
|
+
|
|
40
|
+
I've been experimenting with the model lately and—in an endeavour to better understand the logic behind it—I decided to implement my own version—in Python, following an intuitive object-oriented programming approach.
|
|
41
|
+
|
|
42
|
+
This versioin implements the snow, soil, response and routing routines—controled by 14 calibratable parameters as shown below. In addition to calibration and uncertainty analysis modules.
|
|
43
|
+
```python
|
|
44
|
+
parameters = {
|
|
45
|
+
'snow': ['TT', 'CFMAX', 'SFCF', 'CFR', 'CWH'],
|
|
46
|
+
'soil': ['FC', 'LP', 'BETA'],
|
|
47
|
+
'response': ['K0', 'K1', 'K2', 'UZL', 'PERC']
|
|
48
|
+
'routing' : [ 'MAXBAS'],
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
This can be flexibly used for different modelling tasks, but can also be used in a classroom setup—to explain hydrological concepts (processes, calibration, uncertainty analysis, etc.).
|
|
54
|
+
|
|
55
|
+
## Get Started
|
|
56
|
+
|
|
57
|
+
### Install the Package
|
|
58
|
+
```bash
|
|
59
|
+
pip install HBV_Lab
|
|
60
|
+
```
|
|
61
|
+
### How to Use
|
|
62
|
+
It is very intuitive—you create a model like an object which has attributes (data, parameters, initial conditions, etc.) that you can assign and access. The object also performs functions (calibration, uncertainty estimation, save, load, etc.)
|
|
63
|
+
```python
|
|
64
|
+
from HBV_Lab import HBVModel
|
|
65
|
+
model = HBVModel()
|
|
66
|
+
model.load_data("pandas dataframe")
|
|
67
|
+
model.set_parameters(params)
|
|
68
|
+
model.run()
|
|
69
|
+
model.calibrate()
|
|
70
|
+
model.evaluate_uncertainity()
|
|
71
|
+
model.plot_results()
|
|
72
|
+
model.save_results()
|
|
73
|
+
model.save_model("path")
|
|
74
|
+
model.load_model("path")
|
|
75
|
+
```
|
|
76
|
+
### Tutorial
|
|
77
|
+
Start by following a simple case study in the notebook: [**quick_start_guide.ipynb**](quick_start_guide.ipynb)
|
|
78
|
+
### Play with HBV
|
|
79
|
+
Get a feeling of how HBV model work and the role of the different parameters in [**HBVLAB**](https://www.linkedin.com/in/abdallaimam/) (which uses a model developed with this HBV implementation).
|
|
80
|
+
### References
|
|
81
|
+
**[1]** Bergström, S., & Forsman, A. (1973). DEVELOPMENT OF A CONCEPTUAL DETERMINISTIC RAINFALL-RUNOFF MODEL. Hydrology Research, 4, 147-170.
|
|
82
|
+
|
|
83
|
+
**[2]** Seibert, J. and Vis, M. J. P.: Teaching hydrological modeling with a user-friendly catchment-runoff-model software package, Hydrol. Earth Syst. Sci., 16, 3315–3325, https://doi.org/10.5194/hess-16-3315-2012, 2012.
|
|
84
|
+
|
|
85
|
+
**[3]** AghaKouchak, A., Nakhjiri, N., and Habib, E.: An educational model for ensemble streamflow simulation and uncertainty analysis, Hydrol. Earth Syst. Sci., 17, 445–452, https://doi.org/10.5194/hess-17-445-2013, 2013.
|
|
86
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
HBV_Lab/HBV_model.py,sha256=qESzu_BtSeHioXjlWTvRZ75Z3JmcsZFv1251E6rtZ7w,34459
|
|
2
|
+
HBV_Lab/__init__.py,sha256=IVVuP29S05wRm2S6DPfv6T_HZLLdOBSHH1d5wvXUn3U,31
|
|
3
|
+
HBV_Lab/calibration.py,sha256=h7aXvYkrFB8x9XZF2Oq_yCKbJ4NrlKWUS87gzRboUOc,10596
|
|
4
|
+
HBV_Lab/hbv_step.py,sha256=bVuruQP-_-wPnBJPRmJAWTkgtxXG2yQWej37dyg9khY,2957
|
|
5
|
+
HBV_Lab/response.py,sha256=bmV_9M7JlX7DJgbLynb620t4_t-1f1OAF6ovcKmunDg,3123
|
|
6
|
+
HBV_Lab/routing.py,sha256=XG08CivFlGBNbpHY4jeyKRDDxuVIM826QPlloeWUxJU,1241
|
|
7
|
+
HBV_Lab/snow.py,sha256=3WLC0w7CugDElnu5OHtFveDV3qQNqKkuy96T37CA9wU,2532
|
|
8
|
+
HBV_Lab/soil.py,sha256=HwbAjusb0PstpkFKauCNWx5d8ad5qbcXurIuygVIPYY,2802
|
|
9
|
+
HBV_Lab/uncertainty.py,sha256=gqF8njeICYzOfG-03_DADBdeGz-RD-IL6Mn41n2ou2E,16436
|
|
10
|
+
hbv_lab-0.1.0.dist-info/licenses/LICENSE,sha256=Bfu0Cd9crnTcYQvU2HptthyOzhc7jBTUicQdMhKsDoU,1094
|
|
11
|
+
hbv_lab-0.1.0.dist-info/METADATA,sha256=22w42e2UVuNp10DSr5JFOwV8UfeuOgHew06AUD65pk0,5593
|
|
12
|
+
hbv_lab-0.1.0.dist-info/WHEEL,sha256=GHB6lJx2juba1wDgXDNlMTyM13ckjBMKf-OnwgKOCtA,91
|
|
13
|
+
hbv_lab-0.1.0.dist-info/top_level.txt,sha256=8PP6duueMX7y7CQk0_x7COQYhAfVDZBiMRp9j_sREkc,8
|
|
14
|
+
hbv_lab-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ABDALLA MOHAMMED
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
HBV_Lab
|