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/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .HBV_model import HBVModel
|
HBV_Lab/calibration.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
class calibration:
|
|
2
|
+
|
|
3
|
+
def calibrate(self, method='SLSQP', objective='NSE', iterations=100,
|
|
4
|
+
verbose=True, plot_results=True):
|
|
5
|
+
"""
|
|
6
|
+
Calibrate an HBV model's parameters to optimize the objective function.
|
|
7
|
+
|
|
8
|
+
Parameters:
|
|
9
|
+
-----------
|
|
10
|
+
self : HBVmodel
|
|
11
|
+
The HBV model instance to calibrate
|
|
12
|
+
method : str, default 'SLSQP'
|
|
13
|
+
Optimization method to use (see scipy.optimize.minimize).
|
|
14
|
+
Options include 'SLSQP', 'L-BFGS-B', 'Nelder-Mead', etc.
|
|
15
|
+
objective : str, default 'NSE'
|
|
16
|
+
Objective function to maximize. 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
|
+
iterations : int, default 100
|
|
22
|
+
Maximum number of iterations for the optimizer
|
|
23
|
+
verbose : bool, default True
|
|
24
|
+
Whether to print progress information
|
|
25
|
+
plot_results : bool, default True
|
|
26
|
+
Whether to plot the final results after calibration
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
--------
|
|
30
|
+
dict
|
|
31
|
+
Dictionary containing optimized parameters and performance metrics
|
|
32
|
+
"""
|
|
33
|
+
import scipy.optimize as opt
|
|
34
|
+
import numpy as np
|
|
35
|
+
import time
|
|
36
|
+
import copy
|
|
37
|
+
|
|
38
|
+
if self.data is None:
|
|
39
|
+
raise ValueError("No data loaded. Use load_data() method first.")
|
|
40
|
+
|
|
41
|
+
# Check if observed discharge data is available
|
|
42
|
+
if (self.column_names['obs_q'] is None or
|
|
43
|
+
self.column_names['obs_q'] not in self.data.columns):
|
|
44
|
+
raise ValueError("Observed discharge data is required for calibration.")
|
|
45
|
+
# Store the initial states to use it later for reseting the model
|
|
46
|
+
initial_states = self.states
|
|
47
|
+
# Extract observed discharge data
|
|
48
|
+
obs_q = self.data[self.column_names['obs_q']].values
|
|
49
|
+
|
|
50
|
+
# Get valid indices (where obs_q is not NaN)
|
|
51
|
+
valid_idx = ~np.isnan(obs_q)
|
|
52
|
+
if np.sum(valid_idx) == 0:
|
|
53
|
+
raise ValueError("No valid observed discharge values found.")
|
|
54
|
+
|
|
55
|
+
# Initial parameter values (using default values)
|
|
56
|
+
initial_params = []
|
|
57
|
+
param_names = []
|
|
58
|
+
param_bounds = []
|
|
59
|
+
param_groups = []
|
|
60
|
+
|
|
61
|
+
# Create flat parameter list for optimization
|
|
62
|
+
for group_name, group in self.params.items():
|
|
63
|
+
for param_name, param_info in group.items():
|
|
64
|
+
initial_params.append(param_info['default'])
|
|
65
|
+
param_names.append(f"{group_name}_{param_name}")
|
|
66
|
+
param_bounds.append((param_info['min'], param_info['max']))
|
|
67
|
+
param_groups.append(group_name)
|
|
68
|
+
|
|
69
|
+
# Helper function to convert flat parameter array to nested dictionary
|
|
70
|
+
def create_param_dict(flat_params):
|
|
71
|
+
# Create a deep copy of current parameters structure
|
|
72
|
+
param_dict = {group: {} for group in set(param_groups)}
|
|
73
|
+
|
|
74
|
+
# Fill in the parameter values
|
|
75
|
+
for i, (name, group) in enumerate(zip(param_names, param_groups)):
|
|
76
|
+
param_name = name.split('_', 1)[1] # Extract parameter name
|
|
77
|
+
|
|
78
|
+
# Initialize if not exists
|
|
79
|
+
if param_name not in param_dict[group]:
|
|
80
|
+
param_dict[group][param_name] = {}
|
|
81
|
+
|
|
82
|
+
# Copy min/max from original params
|
|
83
|
+
orig_group = self.params[group]
|
|
84
|
+
param_dict[group][param_name]['min'] = orig_group[param_name]['min']
|
|
85
|
+
param_dict[group][param_name]['max'] = orig_group[param_name]['max']
|
|
86
|
+
|
|
87
|
+
# Set the default to the optimized value
|
|
88
|
+
param_dict[group][param_name]['default'] = flat_params[i]
|
|
89
|
+
|
|
90
|
+
return param_dict
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Define the objective function to minimize
|
|
95
|
+
def objective_function(params):
|
|
96
|
+
# Update parameters structure with flat parameter array
|
|
97
|
+
param_dict = create_param_dict(params)
|
|
98
|
+
|
|
99
|
+
# Store original parameters to restore later
|
|
100
|
+
original_params = copy.deepcopy(self.params)
|
|
101
|
+
|
|
102
|
+
# Update model parameters
|
|
103
|
+
self.params = param_dict
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Run the model
|
|
107
|
+
self.run(verbose)
|
|
108
|
+
|
|
109
|
+
# Get simulated discharge and valid observed discharge
|
|
110
|
+
sim_q = self.results['discharge'][valid_idx]
|
|
111
|
+
obs_q_valid = obs_q[valid_idx]
|
|
112
|
+
self.calculate_performance_metrics
|
|
113
|
+
# Calculate objective function value
|
|
114
|
+
if objective == 'NSE':
|
|
115
|
+
# Nash-Sutcliffe Efficiency (to be maximized)
|
|
116
|
+
# mean_obs = np.mean(obs_q_valid)
|
|
117
|
+
# nse_numerator = np.sum((obs_q_valid - sim_q) ** 2)
|
|
118
|
+
# nse_denominator = np.sum((obs_q_valid - mean_obs) ** 2)
|
|
119
|
+
# value = 1 - (nse_numerator / nse_denominator)
|
|
120
|
+
# # For minimization, return negative NSE
|
|
121
|
+
|
|
122
|
+
return - self.performance_metrics['NSE']
|
|
123
|
+
|
|
124
|
+
elif objective == 'KGE':
|
|
125
|
+
# # Kling-Gupta Efficiency (to be maximized)
|
|
126
|
+
# mean_sim = np.mean(sim_q)
|
|
127
|
+
# mean_obs = np.mean(obs_q_valid)
|
|
128
|
+
# std_sim = np.std(sim_q)
|
|
129
|
+
# std_obs = np.std(obs_q_valid)
|
|
130
|
+
|
|
131
|
+
# r = np.corrcoef(obs_q_valid, sim_q)[0, 1] # Correlation
|
|
132
|
+
# alpha = (std_sim/mean_sim) / (std_obs/mean_sim) # Relative variability
|
|
133
|
+
# beta = mean_sim / mean_obs # Bias
|
|
134
|
+
|
|
135
|
+
# kge = 1 - np.sqrt((r - 1) ** 2 + (alpha - 1) ** 2 + (beta - 1) ** 2)
|
|
136
|
+
# For minimization, return negative KGE
|
|
137
|
+
return - self.performance_metrics['KGE']
|
|
138
|
+
|
|
139
|
+
elif objective == 'RMSE':
|
|
140
|
+
# Root Mean Square Error (to be minimized)
|
|
141
|
+
# rmse = np.sqrt(np.mean((obs_q_valid - sim_q) ** 2))
|
|
142
|
+
return self.performance_metrics['RMSE']
|
|
143
|
+
|
|
144
|
+
elif objective == 'MAE':
|
|
145
|
+
# Mean Absolute Error (to be minimized)
|
|
146
|
+
# mae = np.mean(np.abs(obs_q_valid - sim_q))
|
|
147
|
+
return self.performance_metrics['MAE']
|
|
148
|
+
|
|
149
|
+
else:
|
|
150
|
+
raise ValueError(f"Unknown objective function: {objective}")
|
|
151
|
+
|
|
152
|
+
# Callback function to track progress
|
|
153
|
+
num_iter = [0]
|
|
154
|
+
best_value = [float('inf') if objective in ['RMSE', 'MAE'] else float('-inf')]
|
|
155
|
+
start_time = time.time()
|
|
156
|
+
|
|
157
|
+
def callback(params):
|
|
158
|
+
num_iter[0] += 1
|
|
159
|
+
current_value = objective_function(params)
|
|
160
|
+
|
|
161
|
+
# For NSE and KGE, we're minimizing the negative value
|
|
162
|
+
if objective in ['NSE', 'KGE']:
|
|
163
|
+
display_value = -current_value
|
|
164
|
+
is_better = current_value < best_value[0]
|
|
165
|
+
else:
|
|
166
|
+
display_value = current_value
|
|
167
|
+
is_better = current_value < best_value[0]
|
|
168
|
+
|
|
169
|
+
if is_better:
|
|
170
|
+
best_value[0] = current_value
|
|
171
|
+
|
|
172
|
+
if verbose and num_iter[0] % max(1, iterations // 10) == 0:
|
|
173
|
+
elapsed = time.time() - start_time
|
|
174
|
+
print(f"Iteration {num_iter[0]}/{iterations}, "
|
|
175
|
+
f"{objective}: {display_value:.4f}, "
|
|
176
|
+
f"Time: {elapsed:.1f}s")
|
|
177
|
+
|
|
178
|
+
if verbose:
|
|
179
|
+
print(f"Starting calibration using {method} method...")
|
|
180
|
+
print(f"Optimizing {objective} with {len(param_names)} parameters and {iterations} iterations")
|
|
181
|
+
|
|
182
|
+
# Store original parameters to restore if needed
|
|
183
|
+
original_params = copy.deepcopy(self.params)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
# Run optimization
|
|
187
|
+
result = opt.minimize(
|
|
188
|
+
objective_function,
|
|
189
|
+
initial_params,
|
|
190
|
+
method=method,
|
|
191
|
+
bounds=param_bounds,
|
|
192
|
+
callback=callback,
|
|
193
|
+
options={'maxiter': iterations}
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Get optimized parameters
|
|
197
|
+
opt_params = result.x
|
|
198
|
+
|
|
199
|
+
# Create parameter dictionary from optimized values
|
|
200
|
+
optimized_params = create_param_dict(opt_params)
|
|
201
|
+
|
|
202
|
+
# Update model with optimized parameters
|
|
203
|
+
self.params = optimized_params
|
|
204
|
+
|
|
205
|
+
# Run the model with optimized parameters
|
|
206
|
+
|
|
207
|
+
self.run(verbose)
|
|
208
|
+
|
|
209
|
+
# Calculate final performance metrics
|
|
210
|
+
self.calculate_performance_metrics(verbose)
|
|
211
|
+
|
|
212
|
+
# Display results
|
|
213
|
+
if verbose:
|
|
214
|
+
print("\nCalibration completed!")
|
|
215
|
+
print(f"Final {objective}: {-result.fun if objective in ['NSE', 'KGE'] else result.fun:.4f}")
|
|
216
|
+
print(f"Success: {result.success}, Message: {result.message}")
|
|
217
|
+
print("\nOptimized Parameters:")
|
|
218
|
+
|
|
219
|
+
for group_name, group in optimized_params.items():
|
|
220
|
+
print(f"\n{group_name.upper()} parameters:")
|
|
221
|
+
for param_name, param_info in group.items():
|
|
222
|
+
print(f" {param_name}: {param_info['default']:.4f} (range: {param_info['min']}-{param_info['max']})")
|
|
223
|
+
|
|
224
|
+
print("\nPerformance Metrics:")
|
|
225
|
+
for metric, value in self.performance_metrics.items():
|
|
226
|
+
print(f" {metric}: {value:.4f}")
|
|
227
|
+
|
|
228
|
+
# Plot results if requested
|
|
229
|
+
if plot_results:
|
|
230
|
+
self.plot_results(show_plots=True)
|
|
231
|
+
|
|
232
|
+
# Return optimized parameters and performance
|
|
233
|
+
return {
|
|
234
|
+
'parameters': optimized_params,
|
|
235
|
+
'performance': self.performance_metrics,
|
|
236
|
+
'optimization_result': result
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
# Restore original parameters on error
|
|
241
|
+
self.params = original_params
|
|
242
|
+
print(f"Calibration failed with error: {str(e)}")
|
|
243
|
+
raise
|
HBV_Lab/hbv_step.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from snow import snow_routine
|
|
2
|
+
from soil import soil_routine
|
|
3
|
+
from response import response_routine_two_tanks
|
|
4
|
+
|
|
5
|
+
def hbv_step(precipitation, temperature, potential_et, params, initial_conditions):
|
|
6
|
+
"""
|
|
7
|
+
HBV-light hydrological model (single timestep version).
|
|
8
|
+
|
|
9
|
+
Runs one timestep of the model using snow, soil, and groundwater response routines.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
precipitation (float): Precipitation at the timestep (mm).
|
|
13
|
+
temperature (float): Air temperature at the timestep (°C).
|
|
14
|
+
potential_et (float): Potential evapotranspiration at the timestep (mm).
|
|
15
|
+
params (dict): Dictionary with parameters and their ranges:
|
|
16
|
+
- 'snow': parameters for the snow routine
|
|
17
|
+
- 'soil': parameters for the soil routine
|
|
18
|
+
- 'response': parameters for the groundwater response routine
|
|
19
|
+
initial_conditions (dict): Dictionary with initial storages:
|
|
20
|
+
- 'snowpack': initial snowpack (mm)
|
|
21
|
+
- 'liquid_water': initial liquid water in snowpack (mm)
|
|
22
|
+
- 'soil_moisture': initial soil moisture (mm)
|
|
23
|
+
- 'upper_storage': initial upper groundwater storage (mm)
|
|
24
|
+
- 'lower_storage': initial lower groundwater storage (mm)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
new_states (dict): Updated storages after the timestep.
|
|
28
|
+
fluxes (dict): Fluxes generated during the timestep (runoff, ET, etc.).
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# Unpack initial conditions
|
|
32
|
+
snowpack = initial_conditions['snowpack']
|
|
33
|
+
liquid_water = initial_conditions['liquid_water']
|
|
34
|
+
soil_moisture = initial_conditions['soil_moisture']
|
|
35
|
+
upper_storage = initial_conditions['upper_storage']
|
|
36
|
+
lower_storage = initial_conditions['lower_storage']
|
|
37
|
+
|
|
38
|
+
# Step 1: Snow routine
|
|
39
|
+
snowpack, liquid_water, runoff_from_snow = snow_routine(
|
|
40
|
+
precipitation, temperature, snowpack, liquid_water, params['snow']
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Step 2: Soil routine
|
|
44
|
+
soil_moisture, out_to_response, recharge, runoff_soil, actual_et = soil_routine(
|
|
45
|
+
runoff_from_snow, temperature, potential_et, soil_moisture, params['soil']
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Step 3: Groundwater response routine
|
|
49
|
+
upper_storage, lower_storage, discharge, quick_flow, intermediate_flow, baseflow = response_routine_two_tanks(
|
|
50
|
+
out_to_response, upper_storage, lower_storage, params['response']
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Updated states
|
|
54
|
+
new_states = {
|
|
55
|
+
'snowpack': snowpack,
|
|
56
|
+
'liquid_water': liquid_water,
|
|
57
|
+
'soil_moisture': soil_moisture,
|
|
58
|
+
'upper_storage': upper_storage,
|
|
59
|
+
'lower_storage': lower_storage,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Outputs/Fluxes
|
|
63
|
+
fluxes = {
|
|
64
|
+
'discharge': discharge,
|
|
65
|
+
'quick_flow': quick_flow,
|
|
66
|
+
'intermediate_flow': intermediate_flow,
|
|
67
|
+
'baseflow': baseflow,
|
|
68
|
+
'actual_et': actual_et,
|
|
69
|
+
'recharge': recharge,
|
|
70
|
+
'runoff_soil': runoff_soil,
|
|
71
|
+
'runoff_from_snow': runoff_from_snow,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return new_states, fluxes
|
HBV_Lab/response.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
def response_routine_two_tanks(out_to_response, upper_storage, lower_storage, params):
|
|
2
|
+
"""
|
|
3
|
+
Groundwater response routine for HBV-light model with three flow components.
|
|
4
|
+
|
|
5
|
+
Based on the HBV structure
|
|
6
|
+
|
|
7
|
+
Parameters:
|
|
8
|
+
-----------
|
|
9
|
+
out_to_response : float
|
|
10
|
+
Water from soil routine (recharge + surface runoff) (mm/day).
|
|
11
|
+
upper_storage : float
|
|
12
|
+
Current storage in the upper zone reservoir (SUZ) (mm).
|
|
13
|
+
lower_storage : float
|
|
14
|
+
Current storage in the lower zone reservoir (SLZ) (mm).
|
|
15
|
+
params : dict
|
|
16
|
+
Model parameters:
|
|
17
|
+
- 'K0' : Recession coefficient for quick flow (upper zone above threshold) [day^-1].
|
|
18
|
+
- 'K1' : Recession coefficient for intermediate flow (upper zone) [day^-1].
|
|
19
|
+
- 'K2' : Recession coefficient for baseflow (lower zone) [day^-1].
|
|
20
|
+
- 'UZL' : Threshold parameter for upper zone [mm].
|
|
21
|
+
- 'PERC' : Percolation rate from upper to lower zone (mm/day).
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
--------
|
|
25
|
+
upper_storage : float
|
|
26
|
+
Updated upper zone storage (mm).
|
|
27
|
+
lower_storage : float
|
|
28
|
+
Updated lower zone storage (mm).
|
|
29
|
+
discharge : float
|
|
30
|
+
Total discharge (fast + intermediate + slow runoff) (mm/day).
|
|
31
|
+
quick_flow : float
|
|
32
|
+
Quick flow component from upper zone (above threshold).
|
|
33
|
+
intermediate_flow : float
|
|
34
|
+
Intermediate flow component from upper zone.
|
|
35
|
+
baseflow : float
|
|
36
|
+
Baseflow component from lower zone.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Extract parameters
|
|
40
|
+
K0 = params['K0'] ['default'] # Quick flow coefficient (above threshold)
|
|
41
|
+
K1 = params['K1'] ['default'] # Intermediate flow coefficient
|
|
42
|
+
K2 = params['K2'] ['default'] # Baseflow coefficient
|
|
43
|
+
UZL = params['UZL'] ['default'] # Upper zone threshold
|
|
44
|
+
PERC = params['PERC'] ['default'] # Percolation from upper to lower zone
|
|
45
|
+
|
|
46
|
+
# --- Step 1: Add incoming water to upper zone reservoir ---
|
|
47
|
+
upper_storage += out_to_response
|
|
48
|
+
|
|
49
|
+
# --- Step 2: Percolation from upper to lower zone ---
|
|
50
|
+
percolation = min(PERC, upper_storage)
|
|
51
|
+
upper_storage -= percolation
|
|
52
|
+
lower_storage += percolation
|
|
53
|
+
|
|
54
|
+
# --- Step 3: Calculate runoff components ---
|
|
55
|
+
# Quick flow (Q0): only occurs if upper zone storage exceeds threshold
|
|
56
|
+
if upper_storage > UZL:
|
|
57
|
+
quick_flow = K0 * (upper_storage - UZL)
|
|
58
|
+
else:
|
|
59
|
+
quick_flow = 0.0
|
|
60
|
+
|
|
61
|
+
# Intermediate flow (Q1): from all upper zone storage
|
|
62
|
+
intermediate_flow = K1 * upper_storage
|
|
63
|
+
|
|
64
|
+
# Baseflow (Q2): from lower zone storage
|
|
65
|
+
baseflow = K2 * lower_storage
|
|
66
|
+
|
|
67
|
+
# --- Step 4: Update storages after runoff ---
|
|
68
|
+
upper_storage -= (quick_flow + intermediate_flow)
|
|
69
|
+
lower_storage -= baseflow
|
|
70
|
+
|
|
71
|
+
# --- Step 5: Total discharge ---
|
|
72
|
+
discharge = quick_flow + intermediate_flow + baseflow
|
|
73
|
+
|
|
74
|
+
# --- Step 6: Prevent negative storages ---
|
|
75
|
+
upper_storage = max(upper_storage, 0.0)
|
|
76
|
+
lower_storage = max(lower_storage, 0.0)
|
|
77
|
+
|
|
78
|
+
return upper_storage, lower_storage, discharge, quick_flow, intermediate_flow, baseflow
|
HBV_Lab/routing.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.ndimage import convolve1d
|
|
3
|
+
|
|
4
|
+
def route_with_maxbas(runoff_series= None, maxbas=1):
|
|
5
|
+
"""
|
|
6
|
+
Correct linear interpolation routing as per HBV specifications
|
|
7
|
+
- MAXBAS=1: No routing (weights = [1.0])
|
|
8
|
+
- MAXBAS=2: Weights = [0.5, 0.5]
|
|
9
|
+
- MAXBAS=3: Weights = [0.25, 0.5, 0.25]
|
|
10
|
+
- MAXBAS=4: Weights = [0.125, 0.375, 0.375, 0.125]
|
|
11
|
+
- And so on...
|
|
12
|
+
"""
|
|
13
|
+
if maxbas < 1:
|
|
14
|
+
raise ValueError("MAXBAS must be ≥1")
|
|
15
|
+
|
|
16
|
+
# Special case - no routing
|
|
17
|
+
if maxbas == 1:
|
|
18
|
+
return runoff_series.copy()
|
|
19
|
+
|
|
20
|
+
# Create proper weights using linear interpolation
|
|
21
|
+
if maxbas % 2 == 0: # Even MAXBAS
|
|
22
|
+
n = maxbas // 2
|
|
23
|
+
x = np.linspace(0.5, n - 0.5, n)
|
|
24
|
+
weights = np.concatenate([x, x[::-1]]) / (n)
|
|
25
|
+
else: # Odd MAXBAS
|
|
26
|
+
n = (maxbas + 1) // 2
|
|
27
|
+
x = np.linspace(0.5, n - 1, n)
|
|
28
|
+
weights = np.concatenate([x, x[-2::-1]]) / (n - 0.5)
|
|
29
|
+
|
|
30
|
+
weights /= weights.sum() # Ensure exact sum=1
|
|
31
|
+
|
|
32
|
+
# Apply convolution
|
|
33
|
+
routed = convolve1d(
|
|
34
|
+
runoff_series,
|
|
35
|
+
weights,
|
|
36
|
+
origin=-(len(weights)//2),
|
|
37
|
+
mode='constant',
|
|
38
|
+
cval=0.0
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return routed[:len(runoff_series)]
|
HBV_Lab/snow.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
def snow_routine(precipitation, temperature, snowpack, liquid_water, params):
|
|
2
|
+
"""
|
|
3
|
+
Snow routine: calculates snow accumulation, melt, refreezing, and release of meltwater.
|
|
4
|
+
|
|
5
|
+
Args:
|
|
6
|
+
precipitation (float): Precipitation at timestep (mm).
|
|
7
|
+
temperature (float): Air temperature at timestep (°C).
|
|
8
|
+
snowpack (float): Current snow water equivalent (mm).
|
|
9
|
+
liquid_water (float): Liquid water stored in snowpack (mm).
|
|
10
|
+
params (dict): Model parameters:
|
|
11
|
+
- TT (float): Threshold temperature (°C).
|
|
12
|
+
- CFMAX (float): Degree-day factor (mm/d/°C).
|
|
13
|
+
- CFR (float): Refreezing factor (-).
|
|
14
|
+
- CWH (float): Water holding capacity of snowpack (-, e.g., 0.1).
|
|
15
|
+
- SFCF (float): Snowfall correction factor (-).
|
|
16
|
+
- PCF (float): Precipitation correction factor——to account for bias in precipitation (-).
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
new_snowpack (float): Updated snowpack (mm).
|
|
21
|
+
new_liquid_water (float): Updated liquid water in snowpack (mm).
|
|
22
|
+
runoff (float): Released meltwater to soil (mm).
|
|
23
|
+
"""
|
|
24
|
+
# Extract parameters
|
|
25
|
+
TT = params['TT']['default']
|
|
26
|
+
CFMAX = params['CFMAX']['default']
|
|
27
|
+
CFR = params['CFR']['default']
|
|
28
|
+
CWH = params['CWH']['default']
|
|
29
|
+
SFCF = params['SFCF']['default']
|
|
30
|
+
#PCF = params['PCF']['default']
|
|
31
|
+
|
|
32
|
+
# Give the model a room to account for any biases in the estimation of precipitation
|
|
33
|
+
|
|
34
|
+
precipitation= precipitation
|
|
35
|
+
|
|
36
|
+
# Initialize snowfall and rainfall
|
|
37
|
+
snowfall = 0.0
|
|
38
|
+
rainfall = 0.0
|
|
39
|
+
# Calculate melting or refreezing
|
|
40
|
+
melt = 0.0
|
|
41
|
+
refreeze = 0.0
|
|
42
|
+
|
|
43
|
+
# Determine if precipitation is snow or rain
|
|
44
|
+
if temperature < TT:
|
|
45
|
+
|
|
46
|
+
snowfall = precipitation * SFCF
|
|
47
|
+
snowpack += snowfall
|
|
48
|
+
|
|
49
|
+
refreeze = CFR * CFMAX * (TT - temperature)
|
|
50
|
+
refreeze = min(refreeze, liquid_water)
|
|
51
|
+
snowpack += refreeze
|
|
52
|
+
liquid_water -= refreeze
|
|
53
|
+
else:
|
|
54
|
+
|
|
55
|
+
rainfall = precipitation
|
|
56
|
+
melt = CFMAX * (temperature - TT)
|
|
57
|
+
melt = min(melt, snowpack) # Cannot melt more than available snow
|
|
58
|
+
snowpack -= melt
|
|
59
|
+
liquid_water += melt + rainfall
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Water release from snowpack if above holding capacity
|
|
63
|
+
holding_capacity = CWH * snowpack
|
|
64
|
+
if liquid_water > holding_capacity:
|
|
65
|
+
runoff = liquid_water - holding_capacity
|
|
66
|
+
liquid_water = holding_capacity
|
|
67
|
+
else:
|
|
68
|
+
runoff = 0.0
|
|
69
|
+
|
|
70
|
+
return snowpack, liquid_water, runoff
|
HBV_Lab/soil.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
def soil_routine(runoff_from_snow, temperature, potential_et, soil_moisture, params):
|
|
2
|
+
"""
|
|
3
|
+
Soil moisture routine for HBV-light model.
|
|
4
|
+
|
|
5
|
+
Simulates soil moisture dynamics, actual evapotranspiration (ET),
|
|
6
|
+
groundwater recharge, and surface runoff based on incoming water
|
|
7
|
+
and current soil moisture conditions.
|
|
8
|
+
|
|
9
|
+
Parameters:
|
|
10
|
+
-----------
|
|
11
|
+
runoff_from_snow : float
|
|
12
|
+
Incoming liquid wls
|
|
13
|
+
ater from snow routine (mm/day).
|
|
14
|
+
temperature : float
|
|
15
|
+
Daily average temperature (°C). (Currently unused but kept for possible extensions).
|
|
16
|
+
potential_et : float
|
|
17
|
+
Potential evapotranspiration (mm/day).
|
|
18
|
+
soil_moisture : float
|
|
19
|
+
Current soil moisture storage (mm).
|
|
20
|
+
params : dict
|
|
21
|
+
Model parameters:
|
|
22
|
+
- 'FC' : Maximum soil moisture storage (Field Capacity) [mm].
|
|
23
|
+
- 'BETA' : Shape parameter controlling recharge [-].
|
|
24
|
+
- 'LP' : Soil moisture threshold for full evapotranspiration [-].
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
--------
|
|
28
|
+
soil_moisture : float
|
|
29
|
+
Updated soil moisture storage (mm).
|
|
30
|
+
recharge : float
|
|
31
|
+
Water recharging to the groundwater (mm/day).
|
|
32
|
+
actual_et : float
|
|
33
|
+
Actual evapotranspiration (mm/day).
|
|
34
|
+
runoff : float
|
|
35
|
+
Surface runoff due to soil overflow (mm/day).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
FC = params['FC'] ['default'] # Maximum soil moisture capacity
|
|
39
|
+
BETA = params['BETA'] ['default'] # Recharge curve parameter
|
|
40
|
+
LP = params['LP'] ['default'] # Limit for potential ET to be fully achieved
|
|
41
|
+
|
|
42
|
+
# --- Step 1: Calculate actual evapotranspiration ---
|
|
43
|
+
if soil_moisture > LP * FC:
|
|
44
|
+
actual_et = potential_et # Full potential ET when soil is wet enough
|
|
45
|
+
else:
|
|
46
|
+
actual_et = potential_et * (soil_moisture / (LP * FC)) # Reduced ET if soil is dry
|
|
47
|
+
|
|
48
|
+
# Ensure we don't evaporate more than what's available
|
|
49
|
+
actual_et = min(actual_et, soil_moisture)
|
|
50
|
+
|
|
51
|
+
# --- Step 2: Calculate groundwater recharge ---
|
|
52
|
+
if soil_moisture > 0:
|
|
53
|
+
recharge = runoff_from_snow * (soil_moisture / FC) ** BETA
|
|
54
|
+
else:
|
|
55
|
+
recharge = 0.0
|
|
56
|
+
|
|
57
|
+
# --- Step 3: Update soil moisture balance ---
|
|
58
|
+
soil_moisture = soil_moisture + runoff_from_snow - actual_et - recharge
|
|
59
|
+
|
|
60
|
+
# --- Step 4: Handle surface runoff if soil moisture exceeds FC ---
|
|
61
|
+
if soil_moisture > FC:
|
|
62
|
+
runoff = soil_moisture - FC # Excess water becomes surface runoff
|
|
63
|
+
soil_moisture = FC # Cap soil moisture at field capacity
|
|
64
|
+
|
|
65
|
+
else:
|
|
66
|
+
runoff = 0.0 # No surface runoff
|
|
67
|
+
|
|
68
|
+
# --- Step 5: Prevent negative soil moisture ---
|
|
69
|
+
soil_moisture = max(soil_moisture, 0.0)
|
|
70
|
+
|
|
71
|
+
out_to_response = recharge + runoff
|
|
72
|
+
|
|
73
|
+
return soil_moisture, out_to_response, recharge, runoff, actual_et
|