HBV-Lab 0.2.3__tar.gz → 1.1.0__tar.gz

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.
@@ -6,7 +6,7 @@ HBV-like hydrological model. It handles parameter management, data reading, mode
6
6
  execution, and output visualization.
7
7
 
8
8
  Usage:
9
- from hbv_model import HBVModel
9
+ from HBV_Lab import HBVModel
10
10
  model = HBVModel()
11
11
  model.load_data("path/to/data.csv")
12
12
  model.set_parameters(params)
@@ -24,8 +24,6 @@ import datetime
24
24
  from types import MethodType
25
25
  from .uncertainty import uncertainty
26
26
  from .calibration import calibration
27
- ## from calibration import calibrate_hbv_model
28
- ## from uncertainty import evaluate_uncertainty
29
27
  from .hbv_step import hbv_step
30
28
  from .routing import route_with_maxbas
31
29
 
@@ -71,7 +69,7 @@ class HBVModel(uncertainty, calibration):
71
69
  'lower_storage': 20.0 # Lower zone storage (mm)
72
70
  }
73
71
 
74
- # Initialize time tracking
72
+ ## Initialize time tracking
75
73
  self.start_date = None
76
74
  self.end_date = None
77
75
  self.time_step = 'D' # Default: daily
@@ -424,25 +422,42 @@ class HBVModel(uncertainty, calibration):
424
422
  sim_q_valid = sim_q[valid_idx]
425
423
  obs_q_valid = obs_q[valid_idx]
426
424
 
427
- # Calculate Nash-Sutcliffe Efficiency (NSE)
425
+ # Calculate Nash-Sutcliffe Efficiency (NSE).
426
+ # NSE is undefined when the observations have zero variance (constant
427
+ # series); guard the division so we return NaN instead of raising a
428
+ # RuntimeWarning / producing +/-inf.
428
429
  mean_obs = np.mean(obs_q_valid)
429
430
  nse_numerator = np.sum((obs_q_valid - sim_q_valid) ** 2)
430
431
  nse_denominator = np.sum((obs_q_valid - mean_obs) ** 2)
431
- nse = 1 - (nse_numerator / nse_denominator)
432
-
433
- # Calculate Kling-Gupta Efficiency (KGE)
432
+ nse = 1 - (nse_numerator / nse_denominator) if nse_denominator > 0 else np.nan
433
+
434
+ # Calculate Kling-Gupta Efficiency (KGE).
435
+ # This uses the modified KGE' (Kling et al., 2012) in which the
436
+ # variability ratio is based on the coefficient of variation
437
+ # (CV_sim / CV_obs) rather than the plain standard-deviation ratio.
434
438
  mean_sim = np.mean(sim_q_valid)
435
439
  std_obs = np.std(obs_q_valid)
436
440
  std_sim = np.std(sim_q_valid)
437
-
438
- r = np.corrcoef(obs_q_valid, sim_q_valid)[0, 1] # Correlation coefficient
439
- alpha = (std_sim/mean_sim) / (std_obs/mean_obs) # Relative variability
440
- beta = mean_sim / mean_obs # Bias
441
-
442
- kge = 1 - np.sqrt((r - 1) ** 2 + (alpha - 1) ** 2 + (beta - 1) ** 2)
443
-
444
- # Calculate percent bias
445
- pbias = 100 * (np.sum(sim_q_valid - obs_q_valid) / np.sum(obs_q_valid))
441
+
442
+ # Correlation is undefined if either series is constant.
443
+ if std_obs == 0 or std_sim == 0:
444
+ r = np.nan
445
+ else:
446
+ r = np.corrcoef(obs_q_valid, sim_q_valid)[0, 1]
447
+
448
+ if mean_obs == 0 or mean_sim == 0 or std_obs == 0:
449
+ # KGE components cannot be formed (division by zero); flag as NaN.
450
+ alpha = np.nan
451
+ beta = np.nan
452
+ kge = np.nan
453
+ else:
454
+ alpha = (std_sim / mean_sim) / (std_obs / mean_obs) # variability ratio
455
+ beta = mean_sim / mean_obs # bias ratio
456
+ kge = 1 - np.sqrt((r - 1) ** 2 + (alpha - 1) ** 2 + (beta - 1) ** 2)
457
+
458
+ # Calculate percent bias (undefined if observations sum to zero).
459
+ sum_obs = np.sum(obs_q_valid)
460
+ pbias = 100 * (np.sum(sim_q_valid - obs_q_valid) / sum_obs) if sum_obs != 0 else np.nan
446
461
 
447
462
  # Calculate RMSE and MAE
448
463
  rmse = np.sqrt(np.mean((sim_q_valid - obs_q_valid) ** 2))
@@ -766,23 +781,27 @@ class HBVModel(uncertainty, calibration):
766
781
 
767
782
  return None
768
783
 
784
+ @staticmethod
769
785
  def load_model(model_path):
770
-
771
786
  """
772
787
  Load a saved HBV model from a file.
773
-
788
+
789
+ Can be called either on the class or on an instance::
790
+
791
+ model = HBVModel.load_model("path/to/model")
792
+
774
793
  Parameters:
775
794
  -----------
776
795
  model_path : str
777
796
  Path to the saved model file.
778
-
797
+
779
798
  Returns:
780
799
  --------
781
800
  HBVModel
782
801
  The loaded model instance.
783
802
  """
784
803
  import pickle
785
-
804
+
786
805
  with open(model_path, 'rb') as f:
787
806
  model = pickle.load(f)
788
807
 
@@ -0,0 +1,36 @@
1
+ """
2
+ HBV_Lab — an intuitive, object-oriented Python implementation of a lumped
3
+ conceptual HBV hydrological model for education and research.
4
+
5
+ Typical usage::
6
+
7
+ from HBV_Lab import HBVModel
8
+ model = HBVModel()
9
+ model.load_data(data=df, ...)
10
+ model.run()
11
+ model.calibrate()
12
+ model.evaluate_uncertainty()
13
+
14
+ The individual process routines are also importable for advanced use::
15
+
16
+ from HBV_Lab import snow_routine, soil_routine, response_routine_two_tanks
17
+ from HBV_Lab import hbv_step, route_with_maxbas
18
+ """
19
+
20
+ from .HBV_model import HBVModel
21
+ from .hbv_step import hbv_step
22
+ from .snow import snow_routine
23
+ from .soil import soil_routine
24
+ from .response import response_routine_two_tanks
25
+ from .routing import route_with_maxbas
26
+
27
+ __version__ = "1.1.0"
28
+
29
+ __all__ = [
30
+ "HBVModel",
31
+ "hbv_step",
32
+ "snow_routine",
33
+ "soil_routine",
34
+ "response_routine_two_tanks",
35
+ "route_with_maxbas",
36
+ ]
@@ -1,17 +1,22 @@
1
1
  class calibration:
2
2
 
3
- def calibrate(self, method='SLSQP', objective='NSE', iterations=100,
3
+ def calibrate(self, method='Nelder-Mead', objective='NSE', iterations=100,
4
4
  verbose=True, plot_results=True):
5
5
  """
6
6
  Calibrate an HBV model's parameters to optimize the objective function.
7
-
7
+
8
8
  Parameters:
9
9
  -----------
10
10
  self : HBVmodel
11
11
  The HBV model instance to calibrate
12
- method : str, default 'SLSQP'
12
+ method : str, default 'Nelder-Mead'
13
13
  Optimization method to use (see scipy.optimize.minimize).
14
- Options include 'SLSQP', 'L-BFGS-B', 'Nelder-Mead', etc.
14
+ A gradient-free method is used by default because the HBV objective
15
+ surface is piecewise-constant (many min/max thresholds), which makes
16
+ finite-difference gradients ~0 and causes gradient-based methods such
17
+ as 'SLSQP' or 'L-BFGS-B' to terminate immediately without improving
18
+ on the starting values. Gradient-free options that respect the
19
+ parameter bounds include 'Nelder-Mead' and 'Powell'.
15
20
  objective : str, default 'NSE'
16
21
  Objective function to maximize. Options are:
17
22
  - 'NSE': Nash-Sutcliffe Efficiency (higher is better)
@@ -103,13 +108,13 @@ class calibration:
103
108
  self.params = param_dict
104
109
 
105
110
 
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
111
+ # Run the model (run() computes performance metrics internally when
112
+ # observed discharge is available, honouring the warmup period)
113
+ self.run(verbose=False)
114
+
115
+ # Ensure performance metrics reflect the current parameter set
116
+ self.calculate_performance_metrics(verbose=False)
117
+
113
118
  # Calculate objective function value
114
119
  if objective == 'NSE':
115
120
  # Nash-Sutcliffe Efficiency (to be maximized)
@@ -13,8 +13,6 @@ def snow_routine(precipitation, temperature, snowpack, liquid_water, params):
13
13
  - CFR (float): Refreezing factor (-).
14
14
  - CWH (float): Water holding capacity of snowpack (-, e.g., 0.1).
15
15
  - SFCF (float): Snowfall correction factor (-).
16
- - PCF (float): Precipitation correction factor——to account for bias in precipitation (-).
17
-
18
16
 
19
17
  Returns:
20
18
  new_snowpack (float): Updated snowpack (mm).
@@ -27,11 +25,6 @@ def snow_routine(precipitation, temperature, snowpack, liquid_water, params):
27
25
  CFR = params['CFR']['default']
28
26
  CWH = params['CWH']['default']
29
27
  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
28
 
36
29
  # Initialize snowfall and rainfall
37
30
  snowfall = 0.0
@@ -9,8 +9,7 @@ def soil_routine(runoff_from_snow, temperature, potential_et, soil_moisture, par
9
9
  Parameters:
10
10
  -----------
11
11
  runoff_from_snow : float
12
- Incoming liquid wls
13
- ater from snow routine (mm/day).
12
+ Incoming liquid water from the snow routine (mm/day).
14
13
  temperature : float
15
14
  Daily average temperature (°C). (Currently unused but kept for possible extensions).
16
15
  potential_et : float
@@ -27,12 +26,14 @@ def soil_routine(runoff_from_snow, temperature, potential_et, soil_moisture, par
27
26
  --------
28
27
  soil_moisture : float
29
28
  Updated soil moisture storage (mm).
29
+ out_to_response : float
30
+ Total water passed to the response routine (recharge + surface runoff) (mm/day).
30
31
  recharge : float
31
32
  Water recharging to the groundwater (mm/day).
32
- actual_et : float
33
- Actual evapotranspiration (mm/day).
34
33
  runoff : float
35
34
  Surface runoff due to soil overflow (mm/day).
35
+ actual_et : float
36
+ Actual evapotranspiration (mm/day).
36
37
  """
37
38
 
38
39
  FC = params['FC'] ['default'] # Maximum soil moisture capacity
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HBV_Lab
3
- Version: 0.2.3
3
+ Version: 1.1.0
4
4
  Summary: An intuitive, object-oriented and user-friendly Python implementation of a lumped conceptual HBV hydrological model for educational and research purposes.
5
5
  Home-page: https://github.com/abdallaox/HBV_python_implementation
6
6
  Author: Abdalla Mohammed
@@ -20,6 +20,9 @@ Requires-Dist: pandas
20
20
  Requires-Dist: matplotlib
21
21
  Requires-Dist: scipy
22
22
  Requires-Dist: tqdm
23
+ Requires-Dist: openpyxl
24
+ Provides-Extra: playground
25
+ Requires-Dist: bokeh>=2.4.0; extra == "playground"
23
26
  Dynamic: author
24
27
  Dynamic: author-email
25
28
  Dynamic: classifier
@@ -29,6 +32,7 @@ Dynamic: home-page
29
32
  Dynamic: keywords
30
33
  Dynamic: license
31
34
  Dynamic: license-file
35
+ Dynamic: provides-extra
32
36
  Dynamic: requires-dist
33
37
  Dynamic: requires-python
34
38
  Dynamic: summary
@@ -39,14 +43,29 @@ HBV is a simple conceptual hydrological model that simulates the main hydrologic
39
43
 
40
44
  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
45
 
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.
46
+ This version implements the snow, soil, response and routing routines—controlled by 14 calibratable parameters as shown below—in addition to calibration and uncertainty analysis modules. See the [documentation for one step of the model.](https://lucid.app/publicSegments/view/a0edb3b6-8eba-4db5-9984-bfd23cc004ef/image.png)
47
+
48
+ Conceptually the parameters belong to four routines:
49
+
43
50
  ```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
- }
51
+ parameters = {
52
+ 'snow': ['TT', 'CFMAX', 'SFCF', 'CFR', 'CWH'],
53
+ 'soil': ['FC', 'LP', 'BETA'],
54
+ 'response': ['K0', 'K1', 'K2', 'UZL', 'PERC'],
55
+ 'routing': ['MAXBAS'],
56
+ }
57
+ ```
58
+
59
+ In the model object the parameters are stored in three groups—`snow`, `soil`, and
60
+ `response`—and the routing parameter `MAXBAS` lives inside the `response` group.
61
+ Each parameter is a dict with `min` / `max` / `default` values, so a custom update
62
+ passed to `set_parameters` looks like:
63
+
64
+ ```python
65
+ model.set_parameters({
66
+ 'soil': {'FC': {'min': 50, 'max': 500, 'default': 250}},
67
+ 'response': {'MAXBAS': {'default': 4}},
68
+ })
50
69
  ```
51
70
 
52
71
 
@@ -55,29 +74,56 @@ This can be flexibly used for different modelling tasks, but can also be used in
55
74
  ## Get Started
56
75
 
57
76
  ### Install the Package
58
- ```python
59
- pip install HBV_Lab or
60
- ! pip install HBV_Lab
77
+ From PyPI:
78
+ ```bash
79
+ pip install HBV_Lab
80
+ ```
81
+ Or, inside a notebook:
82
+ ```python
83
+ !pip install HBV_Lab
84
+ ```
85
+ To work from the source / latest version:
86
+ ```bash
87
+ git clone https://github.com/abdallaox/HBV_python_implementation.git
88
+ cd HBV_python_implementation
89
+ pip install -e .
61
90
  ```
91
+
62
92
  ### How to Use
63
93
  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.)
64
94
  ```python
95
+ import pandas as pd
65
96
  from HBV_Lab import HBVModel
97
+
98
+ # 1. Load forcing data (a DataFrame with date, precipitation, temperature,
99
+ # potential ET and—optionally—observed discharge columns)
100
+ df = pd.read_excel("data/test_data_2.xlsx")
66
101
  model = HBVModel()
67
- model.load_data("pandas dataframe")
68
- model.set_parameters(params)
102
+ model.load_data(
103
+ data=df,
104
+ date_column="Date", precip_column="P", temp_column="T",
105
+ pet_column="PET", obs_q_column="Q",
106
+ date_format="%Y%m%d", warmup_end="19811231",
107
+ )
108
+
109
+ # 2. (Optional) override default parameter ranges/values
110
+ # model.set_parameters({'soil': {'FC': {'default': 250}}})
111
+
112
+ # 3. Run, calibrate and analyse
69
113
  model.run()
70
- model.calibrate()
71
- model.evaluate_uncertainity()
114
+ model.calibrate() # gradient-free optimisation by default
115
+ model.evaluate_uncertainty() # Monte-Carlo uncertainty analysis
72
116
  model.plot_results()
73
- model.save_results()
74
- model.save_model("path")
75
- model.load_model("path")
117
+
118
+ # 4. Persist results and the model itself
119
+ model.save_results("results/run.csv")
120
+ model.save_model("models/my_model")
121
+ model = HBVModel.load_model("models/my_model")
76
122
  ```
77
123
  ### Tutorial
78
124
  Start by following a simple case study in the notebook: [**quick_start_guide.ipynb**](https://github.com/abdallaox/HBV_python_implementation/blob/main/quick_start_guide.ipynb)
79
125
  ### Play with HBV
80
- Get a feeling of how the model works and the role of the different parameters in [**HBVLAB**](https://hbv-playground.onrender.com/HBV_playground)—a playground that uses a model developed with this implementation.
126
+ Get a feeling of how the model works and the role of the different parameters in [**HBVLAB**](https://hbvpythonimplementation-production.up.railway.app/HBV_playground)—a playground that uses a model developed with this implementation.
81
127
  ### References
82
128
 
83
129
  **[1]** Bergström, S., & Forsman, A. (1973). Development of a conceptual deterministic rainfall-runoff model. *Hydrology Research*, *4*, 147-170.
@@ -14,4 +14,9 @@ HBV_Lab.egg-info/PKG-INFO
14
14
  HBV_Lab.egg-info/SOURCES.txt
15
15
  HBV_Lab.egg-info/dependency_links.txt
16
16
  HBV_Lab.egg-info/requires.txt
17
- HBV_Lab.egg-info/top_level.txt
17
+ HBV_Lab.egg-info/top_level.txt
18
+ tests/test_hbv_model.py
19
+ tests/test_hbv_step.py
20
+ tests/test_response.py
21
+ tests/test_snow.py
22
+ tests/test_soil.py
@@ -0,0 +1,9 @@
1
+ numpy
2
+ pandas
3
+ matplotlib
4
+ scipy
5
+ tqdm
6
+ openpyxl
7
+
8
+ [playground]
9
+ bokeh>=2.4.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HBV_Lab
3
- Version: 0.2.3
3
+ Version: 1.1.0
4
4
  Summary: An intuitive, object-oriented and user-friendly Python implementation of a lumped conceptual HBV hydrological model for educational and research purposes.
5
5
  Home-page: https://github.com/abdallaox/HBV_python_implementation
6
6
  Author: Abdalla Mohammed
@@ -20,6 +20,9 @@ Requires-Dist: pandas
20
20
  Requires-Dist: matplotlib
21
21
  Requires-Dist: scipy
22
22
  Requires-Dist: tqdm
23
+ Requires-Dist: openpyxl
24
+ Provides-Extra: playground
25
+ Requires-Dist: bokeh>=2.4.0; extra == "playground"
23
26
  Dynamic: author
24
27
  Dynamic: author-email
25
28
  Dynamic: classifier
@@ -29,6 +32,7 @@ Dynamic: home-page
29
32
  Dynamic: keywords
30
33
  Dynamic: license
31
34
  Dynamic: license-file
35
+ Dynamic: provides-extra
32
36
  Dynamic: requires-dist
33
37
  Dynamic: requires-python
34
38
  Dynamic: summary
@@ -39,14 +43,29 @@ HBV is a simple conceptual hydrological model that simulates the main hydrologic
39
43
 
40
44
  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
45
 
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.
46
+ This version implements the snow, soil, response and routing routines—controlled by 14 calibratable parameters as shown below—in addition to calibration and uncertainty analysis modules. See the [documentation for one step of the model.](https://lucid.app/publicSegments/view/a0edb3b6-8eba-4db5-9984-bfd23cc004ef/image.png)
47
+
48
+ Conceptually the parameters belong to four routines:
49
+
43
50
  ```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
- }
51
+ parameters = {
52
+ 'snow': ['TT', 'CFMAX', 'SFCF', 'CFR', 'CWH'],
53
+ 'soil': ['FC', 'LP', 'BETA'],
54
+ 'response': ['K0', 'K1', 'K2', 'UZL', 'PERC'],
55
+ 'routing': ['MAXBAS'],
56
+ }
57
+ ```
58
+
59
+ In the model object the parameters are stored in three groups—`snow`, `soil`, and
60
+ `response`—and the routing parameter `MAXBAS` lives inside the `response` group.
61
+ Each parameter is a dict with `min` / `max` / `default` values, so a custom update
62
+ passed to `set_parameters` looks like:
63
+
64
+ ```python
65
+ model.set_parameters({
66
+ 'soil': {'FC': {'min': 50, 'max': 500, 'default': 250}},
67
+ 'response': {'MAXBAS': {'default': 4}},
68
+ })
50
69
  ```
51
70
 
52
71
 
@@ -55,29 +74,56 @@ This can be flexibly used for different modelling tasks, but can also be used in
55
74
  ## Get Started
56
75
 
57
76
  ### Install the Package
58
- ```python
59
- pip install HBV_Lab or
60
- ! pip install HBV_Lab
77
+ From PyPI:
78
+ ```bash
79
+ pip install HBV_Lab
80
+ ```
81
+ Or, inside a notebook:
82
+ ```python
83
+ !pip install HBV_Lab
84
+ ```
85
+ To work from the source / latest version:
86
+ ```bash
87
+ git clone https://github.com/abdallaox/HBV_python_implementation.git
88
+ cd HBV_python_implementation
89
+ pip install -e .
61
90
  ```
91
+
62
92
  ### How to Use
63
93
  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.)
64
94
  ```python
95
+ import pandas as pd
65
96
  from HBV_Lab import HBVModel
97
+
98
+ # 1. Load forcing data (a DataFrame with date, precipitation, temperature,
99
+ # potential ET and—optionally—observed discharge columns)
100
+ df = pd.read_excel("data/test_data_2.xlsx")
66
101
  model = HBVModel()
67
- model.load_data("pandas dataframe")
68
- model.set_parameters(params)
102
+ model.load_data(
103
+ data=df,
104
+ date_column="Date", precip_column="P", temp_column="T",
105
+ pet_column="PET", obs_q_column="Q",
106
+ date_format="%Y%m%d", warmup_end="19811231",
107
+ )
108
+
109
+ # 2. (Optional) override default parameter ranges/values
110
+ # model.set_parameters({'soil': {'FC': {'default': 250}}})
111
+
112
+ # 3. Run, calibrate and analyse
69
113
  model.run()
70
- model.calibrate()
71
- model.evaluate_uncertainity()
114
+ model.calibrate() # gradient-free optimisation by default
115
+ model.evaluate_uncertainty() # Monte-Carlo uncertainty analysis
72
116
  model.plot_results()
73
- model.save_results()
74
- model.save_model("path")
75
- model.load_model("path")
117
+
118
+ # 4. Persist results and the model itself
119
+ model.save_results("results/run.csv")
120
+ model.save_model("models/my_model")
121
+ model = HBVModel.load_model("models/my_model")
76
122
  ```
77
123
  ### Tutorial
78
124
  Start by following a simple case study in the notebook: [**quick_start_guide.ipynb**](https://github.com/abdallaox/HBV_python_implementation/blob/main/quick_start_guide.ipynb)
79
125
  ### Play with HBV
80
- Get a feeling of how the model works and the role of the different parameters in [**HBVLAB**](https://hbv-playground.onrender.com/HBV_playground)—a playground that uses a model developed with this implementation.
126
+ Get a feeling of how the model works and the role of the different parameters in [**HBVLAB**](https://hbvpythonimplementation-production.up.railway.app/HBV_playground)—a playground that uses a model developed with this implementation.
81
127
  ### References
82
128
 
83
129
  **[1]** Bergström, S., & Forsman, A. (1973). Development of a conceptual deterministic rainfall-runoff model. *Hydrology Research*, *4*, 147-170.
@@ -0,0 +1,95 @@
1
+ # HBV_Lab (Python implementation of a lumped conceptual HBV model)
2
+
3
+ HBV is a simple conceptual hydrological model that simulates the main hydrological processes related to snow, soil, groundwater, and routing [[1]](https://iwaponline.com/hr/article/4/3/147/1357/DEVELOPMENT-OF-A-CONCEPTUAL-DETERMINISTIC-RAINFALL). 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/).
4
+
5
+ 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.
6
+
7
+ This version implements the snow, soil, response and routing routines—controlled by 14 calibratable parameters as shown below—in addition to calibration and uncertainty analysis modules. See the [documentation for one step of the model.](https://lucid.app/publicSegments/view/a0edb3b6-8eba-4db5-9984-bfd23cc004ef/image.png)
8
+
9
+ Conceptually the parameters belong to four routines:
10
+
11
+ ```python
12
+ parameters = {
13
+ 'snow': ['TT', 'CFMAX', 'SFCF', 'CFR', 'CWH'],
14
+ 'soil': ['FC', 'LP', 'BETA'],
15
+ 'response': ['K0', 'K1', 'K2', 'UZL', 'PERC'],
16
+ 'routing': ['MAXBAS'],
17
+ }
18
+ ```
19
+
20
+ In the model object the parameters are stored in three groups—`snow`, `soil`, and
21
+ `response`—and the routing parameter `MAXBAS` lives inside the `response` group.
22
+ Each parameter is a dict with `min` / `max` / `default` values, so a custom update
23
+ passed to `set_parameters` looks like:
24
+
25
+ ```python
26
+ model.set_parameters({
27
+ 'soil': {'FC': {'min': 50, 'max': 500, 'default': 250}},
28
+ 'response': {'MAXBAS': {'default': 4}},
29
+ })
30
+ ```
31
+
32
+
33
+ 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.).
34
+
35
+ ## Get Started
36
+
37
+ ### Install the Package
38
+ From PyPI:
39
+ ```bash
40
+ pip install HBV_Lab
41
+ ```
42
+ Or, inside a notebook:
43
+ ```python
44
+ !pip install HBV_Lab
45
+ ```
46
+ To work from the source / latest version:
47
+ ```bash
48
+ git clone https://github.com/abdallaox/HBV_python_implementation.git
49
+ cd HBV_python_implementation
50
+ pip install -e .
51
+ ```
52
+
53
+ ### How to Use
54
+ 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.)
55
+ ```python
56
+ import pandas as pd
57
+ from HBV_Lab import HBVModel
58
+
59
+ # 1. Load forcing data (a DataFrame with date, precipitation, temperature,
60
+ # potential ET and—optionally—observed discharge columns)
61
+ df = pd.read_excel("data/test_data_2.xlsx")
62
+ model = HBVModel()
63
+ model.load_data(
64
+ data=df,
65
+ date_column="Date", precip_column="P", temp_column="T",
66
+ pet_column="PET", obs_q_column="Q",
67
+ date_format="%Y%m%d", warmup_end="19811231",
68
+ )
69
+
70
+ # 2. (Optional) override default parameter ranges/values
71
+ # model.set_parameters({'soil': {'FC': {'default': 250}}})
72
+
73
+ # 3. Run, calibrate and analyse
74
+ model.run()
75
+ model.calibrate() # gradient-free optimisation by default
76
+ model.evaluate_uncertainty() # Monte-Carlo uncertainty analysis
77
+ model.plot_results()
78
+
79
+ # 4. Persist results and the model itself
80
+ model.save_results("results/run.csv")
81
+ model.save_model("models/my_model")
82
+ model = HBVModel.load_model("models/my_model")
83
+ ```
84
+ ### Tutorial
85
+ Start by following a simple case study in the notebook: [**quick_start_guide.ipynb**](https://github.com/abdallaox/HBV_python_implementation/blob/main/quick_start_guide.ipynb)
86
+ ### Play with HBV
87
+ Get a feeling of how the model works and the role of the different parameters in [**HBVLAB**](https://hbvpythonimplementation-production.up.railway.app/HBV_playground)—a playground that uses a model developed with this implementation.
88
+ ### References
89
+
90
+ **[1]** Bergström, S., & Forsman, A. (1973). Development of a conceptual deterministic rainfall-runoff model. *Hydrology Research*, *4*, 147-170.
91
+
92
+ **[2]** Seibert, J., & Vis, M. J. P. (2012). Teaching hydrological modeling with a user-friendly catchment-runoff-model software package. *Hydrology and Earth System Sciences*, *16*(9), 3315-3325. [doi:10.5194/hess-16-3315-2012](https://doi.org/10.5194/hess-16-3315-2012)
93
+
94
+ **[3]** AghaKouchak, A., Nakhjiri, N., & Habib, E. (2013). An educational model for ensemble streamflow simulation and uncertainty analysis. *Hydrology and Earth System Sciences*, *17*(2), 445-452. [doi:10.5194/hess-17-445-2013](https://doi.org/10.5194/hess-17-445-2013)
95
+
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as f:
5
5
 
6
6
  setup(
7
7
  name='HBV_Lab',
8
- version='0.2.3',
8
+ version='1.1.0',
9
9
  packages=find_packages(include=['HBV_Lab', 'HBV_Lab.*']),
10
10
  install_requires=[
11
11
  'numpy',
@@ -13,7 +13,12 @@ setup(
13
13
  'matplotlib',
14
14
  'scipy',
15
15
  'tqdm',
16
+ 'openpyxl', # required by pandas to read the .xlsx example data
16
17
  ],
18
+ extras_require={
19
+ # Optional dependencies for the interactive Bokeh playground
20
+ 'playground': ['bokeh>=2.4.0'],
21
+ },
17
22
  author='Abdalla Mohammed',
18
23
  author_email='abdalla.mohammed.ox@gmail.com',
19
24
  description='An intuitive, object-oriented and user-friendly Python implementation of a lumped conceptual HBV hydrological model for educational and research purposes.',
@@ -0,0 +1,75 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ import os
4
+ import tempfile
5
+ import pytest
6
+ from HBV_Lab import HBVModel
7
+
8
+
9
+ def make_sample_df(n_days=10):
10
+ dates = pd.date_range(start='2000-01-01', periods=n_days, freq='D')
11
+ df = pd.DataFrame({
12
+ 'Date': dates.strftime('%Y%m%d'),
13
+ 'Precipitation': np.ones(n_days) * 1.0,
14
+ 'Temperature': np.ones(n_days) * 2.0,
15
+ 'PotentialET': np.ones(n_days) * 0.5,
16
+ 'ObservedQ': np.ones(n_days) * 0.2
17
+ })
18
+ return df
19
+
20
+
21
+ def test_load_data_and_time_step_and_pet_expansion():
22
+ model = HBVModel()
23
+ df = make_sample_df(n_days=20)
24
+
25
+ model.load_data(data=df, date_column='Date', date_format='%Y%m%d', precip_column='Precipitation', temp_column='Temperature', pet_column='PotentialET', obs_q_column='ObservedQ')
26
+
27
+ assert model.data is not None
28
+ assert model.start_date is not None
29
+ assert model.time_step == 'D'
30
+
31
+
32
+ def test_set_parameters_validation():
33
+ model = HBVModel()
34
+ with pytest.raises(ValueError):
35
+ model.set_parameters(None)
36
+
37
+ # valid update
38
+ custom = {'soil': {'FC': {'default': 200.0}}}
39
+ model.set_parameters(custom)
40
+ assert model.params['soil']['FC']['default'] == 200.0
41
+
42
+
43
+ def test_set_initial_conditions_and_run(tmp_path):
44
+ model = HBVModel()
45
+ df = make_sample_df(n_days=15)
46
+ model.load_data(data=df, date_column='Date', date_format='%Y%m%d', precip_column='Precipitation', temp_column='Temperature', pet_column='PotentialET', obs_q_column='ObservedQ')
47
+
48
+ model.set_initial_conditions(snowpack=5.0, soil_moisture=40.0)
49
+ results = model.run(verbose=False)
50
+
51
+ assert results['discharge'].shape[0] == 15
52
+ assert np.all(results['discharge'] >= 0.0)
53
+
54
+ # Test save_results
55
+ out_file = tmp_path / "results_test.csv"
56
+ model.results = results
57
+ model.save_results(str(out_file))
58
+ assert os.path.exists(out_file)
59
+
60
+
61
+ def test_performance_metrics_calculation():
62
+ model = HBVModel()
63
+ df = make_sample_df(n_days=12)
64
+ model.load_data(data=df, date_column='Date', date_format='%Y%m%d', precip_column='Precipitation', temp_column='Temperature', pet_column='PotentialET', obs_q_column='ObservedQ')
65
+
66
+ model.set_initial_conditions()
67
+ results = model.run(verbose=False)
68
+ # attach observed into results
69
+ results['observed_q'] = np.ones(len(results['discharge'])) * 0.2
70
+ model.results = results
71
+
72
+ model.calculate_performance_metrics(verbose=False)
73
+ assert hasattr(model, 'performance_metrics')
74
+ keys = {'NSE','KGE','PBIAS','RMSE','MAE'}
75
+ assert keys.issubset(set(model.performance_metrics.keys()))
@@ -0,0 +1,18 @@
1
+ from HBV_Lab import hbv_step
2
+
3
+
4
+ def test_hbv_step_returns_expected_keys():
5
+ params = {
6
+ 'snow': {'TT': {'default': 0.0}, 'CFMAX': {'default': 2.0}, 'CFR': {'default': 0.0}, 'CWH': {'default': 0.1}, 'SFCF': {'default': 1.0}},
7
+ 'soil': {'FC': {'default': 100.0}, 'BETA': {'default': 2.0}, 'LP': {'default': 0.5}},
8
+ 'response': {'K0': {'default': 0.5}, 'K1': {'default': 0.2}, 'K2': {'default': 0.01}, 'UZL': {'default': 20.0}, 'PERC': {'default': 1.0}}
9
+ }
10
+
11
+ initial_conditions = {'snowpack': 0.0, 'liquid_water': 0.0, 'soil_moisture': 50.0, 'upper_storage': 10.0, 'lower_storage': 20.0}
12
+
13
+ new_states, fluxes = hbv_step(precipitation=5.0, temperature=5.0, potential_et=2.0, params=params, initial_conditions=initial_conditions)
14
+
15
+ # new_states contains required keys
16
+ assert set(new_states.keys()) == {'snowpack','liquid_water','soil_moisture','upper_storage','lower_storage'}
17
+ # fluxes contain discharge and flow components
18
+ assert 'discharge' in fluxes and 'quick_flow' in fluxes and 'baseflow' in fluxes
@@ -0,0 +1,23 @@
1
+ from HBV_Lab import response_routine_two_tanks
2
+
3
+
4
+ def test_response_basic_mass_balance():
5
+ params = {'K0': {'default': 0.5}, 'K1': {'default': 0.2}, 'K2': {'default': 0.01}, 'UZL': {'default': 20.0}, 'PERC': {'default': 1.0}}
6
+
7
+ upper, lower, discharge, q0, q1, q2 = response_routine_two_tanks(out_to_response=5.0, upper_storage=10.0, lower_storage=20.0, params=params)
8
+
9
+ # discharge is sum of flow components
10
+ assert discharge == q0 + q1 + q2
11
+ assert upper >= 0.0
12
+ assert lower >= 0.0
13
+
14
+
15
+ def test_response_percolation_and_threshold():
16
+ params = {'K0': {'default': 0.5}, 'K1': {'default': 0.2}, 'K2': {'default': 0.01}, 'UZL': {'default': 0.0}, 'PERC': {'default': 100.0}}
17
+
18
+ # Large PERC means most incoming water should percolate to lower zone
19
+ upper, lower, discharge, q0, q1, q2 = response_routine_two_tanks(out_to_response=3.0, upper_storage=1.0, lower_storage=0.0, params=params)
20
+
21
+ # Ensure percolation did not make storages negative and produced baseflow
22
+ assert lower >= 0.0
23
+ assert q2 >= 0.0
@@ -0,0 +1,38 @@
1
+ from HBV_Lab import snow_routine
2
+
3
+
4
+ def test_snow_accumulates_and_runs_off():
5
+ params = {
6
+ 'TT': {'default': 0.0},
7
+ 'CFMAX': {'default': 2.0},
8
+ 'CFR': {'default': 0.0},
9
+ 'CWH': {'default': 0.1},
10
+ 'SFCF': {'default': 1.0}
11
+ }
12
+
13
+ # Cold day: precipitation becomes snowfall and increases snowpack
14
+ snowpack, liquid, runoff = snow_routine(precipitation=10.0, temperature=-5.0,
15
+ snowpack=0.0, liquid_water=0.0, params=params)
16
+
17
+ assert snowpack > 0.0
18
+ assert liquid >= 0.0
19
+ assert runoff == 0.0 # no holding capacity exceeded immediately
20
+
21
+
22
+ def test_snow_melts_when_warm():
23
+ params = {
24
+ 'TT': {'default': 0.0},
25
+ 'CFMAX': {'default': 5.0},
26
+ 'CFR': {'default': 0.0},
27
+ 'CWH': {'default': 0.2},
28
+ 'SFCF': {'default': 1.0}
29
+ }
30
+
31
+ # Warm day with existing snowpack should generate melt and liquid water
32
+ snowpack, liquid, runoff = snow_routine(precipitation=0.0, temperature=5.0,
33
+ snowpack=20.0, liquid_water=0.0, params=params)
34
+
35
+ assert snowpack < 20.0
36
+ assert liquid >= 0.0
37
+ # since holding capacity = CWH * snowpack after melt, liquid might be <= holding capacity
38
+ assert runoff >= 0.0
@@ -0,0 +1,28 @@
1
+ import pytest
2
+ from HBV_Lab import soil_routine
3
+
4
+
5
+ def test_soil_et_and_recharge():
6
+ params = {'FC': {'default': 100.0}, 'BETA': {'default': 2.0}, 'LP': {'default': 0.5}}
7
+
8
+ # With moderate soil moisture and incoming runoff, we expect some recharge and ET
9
+ soil_moisture, out_to_response, recharge, runoff, actual_et = soil_routine(
10
+ runoff_from_snow=10.0, temperature=5.0, potential_et=2.0, soil_moisture=50.0, params=params
11
+ )
12
+
13
+ assert soil_moisture >= 0.0
14
+ assert actual_et <= 2.0
15
+ assert recharge >= 0.0
16
+ assert out_to_response == pytest.approx(recharge + runoff)
17
+
18
+
19
+ def test_soil_overflow_produces_runoff():
20
+ params = {'FC': {'default': 20.0}, 'BETA': {'default': 1.0}, 'LP': {'default': 0.5}}
21
+
22
+ # Large incoming water should cause soil to exceed FC and produce runoff
23
+ soil_moisture, out_to_response, recharge, runoff, actual_et = soil_routine(
24
+ runoff_from_snow=100.0, temperature=5.0, potential_et=0.0, soil_moisture=10.0, params=params
25
+ )
26
+
27
+ assert runoff > 0.0
28
+ assert soil_moisture <= params['FC']['default']
@@ -1,5 +0,0 @@
1
- from .HBV_model import HBVModel
2
- from .uncertainty import uncertainty
3
- from .calibration import calibration
4
- from .hbv_step import hbv_step
5
- from .routing import route_with_maxbas
@@ -1,5 +0,0 @@
1
- numpy
2
- pandas
3
- matplotlib
4
- scipy
5
- tqdm
hbv_lab-0.2.3/README.md DELETED
@@ -1,53 +0,0 @@
1
- # HBV_Lab (Python implementation of a lumped conceptual HBV model)
2
-
3
- HBV is a simple conceptual hydrological model that simulates the main hydrological processes related to snow, soil, groundwater, and routing [[1]](https://iwaponline.com/hr/article/4/3/147/1357/DEVELOPMENT-OF-A-CONCEPTUAL-DETERMINISTIC-RAINFALL). 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/).
4
-
5
- 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.
6
-
7
- 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.
8
- ```python
9
- parameters = {
10
- 'snow': ['TT', 'CFMAX', 'SFCF', 'CFR', 'CWH'],
11
- 'soil': ['FC', 'LP', 'BETA'],
12
- 'response': ['K0', 'K1', 'K2', 'UZL', 'PERC']
13
- 'routing' : [ 'MAXBAS'],
14
- }
15
- ```
16
-
17
-
18
- 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.).
19
-
20
- ## Get Started
21
-
22
- ### Install the Package
23
- ```python
24
- pip install HBV_Lab or
25
- ! pip install HBV_Lab
26
- ```
27
- ### How to Use
28
- 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.)
29
- ```python
30
- from HBV_Lab import HBVModel
31
- model = HBVModel()
32
- model.load_data("pandas dataframe")
33
- model.set_parameters(params)
34
- model.run()
35
- model.calibrate()
36
- model.evaluate_uncertainity()
37
- model.plot_results()
38
- model.save_results()
39
- model.save_model("path")
40
- model.load_model("path")
41
- ```
42
- ### Tutorial
43
- Start by following a simple case study in the notebook: [**quick_start_guide.ipynb**](https://github.com/abdallaox/HBV_python_implementation/blob/main/quick_start_guide.ipynb)
44
- ### Play with HBV
45
- Get a feeling of how the model works and the role of the different parameters in [**HBVLAB**](https://hbv-playground.onrender.com/HBV_playground)—a playground that uses a model developed with this implementation.
46
- ### References
47
-
48
- **[1]** Bergström, S., & Forsman, A. (1973). Development of a conceptual deterministic rainfall-runoff model. *Hydrology Research*, *4*, 147-170.
49
-
50
- **[2]** Seibert, J., & Vis, M. J. P. (2012). Teaching hydrological modeling with a user-friendly catchment-runoff-model software package. *Hydrology and Earth System Sciences*, *16*(9), 3315-3325. [doi:10.5194/hess-16-3315-2012](https://doi.org/10.5194/hess-16-3315-2012)
51
-
52
- **[3]** AghaKouchak, A., Nakhjiri, N., & Habib, E. (2013). An educational model for ensemble streamflow simulation and uncertainty analysis. *Hydrology and Earth System Sciences*, *17*(2), 445-452. [doi:10.5194/hess-17-445-2013](https://doi.org/10.5194/hess-17-445-2013)
53
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes