├── LICENSE ├── README.md ├── example_data ├── LA011201_basin.geojson ├── LA011201_forcings.csv └── LA011201_obs.csv ├── julia ├── README.md ├── calibrate_hymod.jl ├── run_hymod.jl └── src │ ├── Calibration.jl │ ├── Hymod.jl │ └── Utils.jl └── python ├── README.md ├── calibrate_hymod.py ├── run_hymod.py └── src ├── __init__.py ├── __pycache__ ├── __init__.cpython-36.pyc ├── __init__.cpython-37.pyc ├── calibration.cpython-36.pyc ├── hymod.cpython-36.pyc └── hymod.cpython-37.pyc ├── calibration.py └── hymod.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kel Markert 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hymod 2 | Implementation of the HYMOD rainfall-runoff model in Python and Julia 3 | 4 | [Hymod paper](https://www.proc-iahs.net/368/180/2015/piahs-368-180-2015.pdf) 5 | -------------------------------------------------------------------------------- /julia/README.md: -------------------------------------------------------------------------------- 1 | # Hymod.jl 2 | 3 | Julia package for calibrating and running the Hymod rainfall-runoff model. 4 | 5 | 6 | The example can be run using the following command: 7 | 8 | ``` 9 | $ julia run_hymod.jl 10 | ``` 11 | -------------------------------------------------------------------------------- /julia/calibrate_hymod.jl: -------------------------------------------------------------------------------- 1 | using CSV, DataFrames, Dates#, Plots 2 | include("src/Hymod.jl") 3 | 4 | obsPath = "../example_data/LA011201_obs.csv" 5 | obs = CSV.read(obsPath) 6 | # obs.isodate = Hymod.Utils.parseDates(obs, format="m/d/Y",dateCol=:date) 7 | 8 | forcingPath = "../example_data/LA011201_forcings.csv" 9 | forcings = CSV.read(forcingPath) 10 | 11 | forcings.pet = Hymod.hargreaves(forcings,tminCol=:tmin,tmaxCol=:tmax,dtCol=:isodate) 12 | 13 | paramSpace = Dict{Symbol,Dict}( 14 | :cmax => Dict{Symbol,Float64}(:lower => 1.0, :upper => 100), 15 | :bexp => Dict{Symbol,Float64}(:lower => 0.0, :upper => 2.0), 16 | :alpha => Dict{Symbol,Float64}(:lower => 0.2, :upper => 0.99), 17 | :ks => Dict{Symbol,Float64}(:lower => 0.01, :upper => 0.5), 18 | :kq => Dict{Symbol,Float64}(:lower => 0.5, :upper => 1.2) 19 | ) 20 | 21 | calStart = Date(1986,1,1) 22 | calEnd = Date(1995,12,31) 23 | 24 | calForcings = filter(row -> row[:isodate] >= calStart && row[:isodate] <= calEnd, forcings) 25 | obsSel = filter(row -> row[:datetime] >= calStart && row[:datetime] <= calEnd, obs) 26 | 27 | nIters = 5 28 | 29 | calQ, calPars, loss = Hymod.calibrate(calForcings,obsSel.discharge,paramSpace,nIters) 30 | print(calPars,loss) 31 | 32 | # # do some plotting to show the results 33 | # obsSel[:calibrated] = calQ 34 | # obsSel = filter(row -> row[:datetime] >= Date(1987,1,1), obsSel) 35 | 36 | # theme(:bright) 37 | 38 | # plot(obsSel[:datetime],[obsSel[:discharge] obsSel[:calibrated]], 39 | # label=["Observed" "Calibrated"], 40 | # xlabel="Date", 41 | # xrotation=40, 42 | # ylabel="Discharge m³/s", 43 | # dpi=200 44 | # ) 45 | # savefig("calibrated_discharge.png") -------------------------------------------------------------------------------- /julia/run_hymod.jl: -------------------------------------------------------------------------------- 1 | using CSV, DataFrames, Dates, Plots 2 | include("src/Hymod.jl") 3 | 4 | @time begin 5 | obsPath = "../example_data/LA011201_obs.csv" 6 | obs = CSV.read(obsPath) 7 | # obs.isodate = Hymod.Utils.parseDates(obs, format="m/d/Y",dateCol=:date) 8 | 9 | forcingPath = "../example_data/LA011201_forcings.csv" 10 | forcings = CSV.read(forcingPath) 11 | 12 | forcings.pet = Hymod.hargreaves(forcings,tminCol=:tmin,tmaxCol=:tmax,dtCol=:isodate) 13 | 14 | pars = Hymod.get_random_params() 15 | q = Hymod.simulate(forcings,precipCol=:precip, petCol=:pet; pars...) 16 | end 17 | -------------------------------------------------------------------------------- /julia/src/Calibration.jl: -------------------------------------------------------------------------------- 1 | module Calibration 2 | 3 | using Distributions 4 | 5 | function monteCarlo(paramSpace;samples=100000) 6 | params = Dict{Symbol,Array}() 7 | 8 | keyList = collect(keys(paramSpace)) 9 | 10 | for k in keyList 11 | p = paramSpace[k] 12 | minV = p[:lower] 13 | maxV = p[:upper] 14 | params[k] = collect(rand(Uniform(minV,maxV), samples)) 15 | end 16 | 17 | return params 18 | 19 | end 20 | 21 | function latinHypercube(func,forcing,obs,param_bounds;samples=10000) 22 | 23 | 24 | end 25 | 26 | # end module 27 | end 28 | -------------------------------------------------------------------------------- /julia/src/Hymod.jl: -------------------------------------------------------------------------------- 1 | # 2 | module Hymod 3 | 4 | using DataFrames, Dates, Distributions, TOML 5 | 6 | include("Utils.jl") 7 | include("Calibration.jl") 8 | 9 | function get_random_params() 10 | param_bounds = Dict{Symbol,Dict}( 11 | :cmax => Dict{Symbol,Float64}(:lower => 1.0, :upper => 100), 12 | :bexp => Dict{Symbol,Float64}(:lower => 0.0, :upper => 2.0), 13 | :alpha => Dict{Symbol,Float64}(:lower => 0.2, :upper => 0.99), 14 | :ks => Dict{Symbol,Float64}(:lower => 0.01, :upper => 0.5), 15 | :kq => Dict{Symbol,Float64}(:lower => 0.5, :upper => 1.2) 16 | ) 17 | 18 | out = Dict() 19 | 20 | for k in keys(param_bounds) 21 | minV = param_bounds[k][:lower] 22 | maxV = param_bounds[k][:upper] 23 | p = rand(Uniform(minV,maxV), 1) 24 | out[k] = p[1] 25 | end 26 | 27 | return out 28 | end 29 | 30 | function _power(x,y) 31 | x=abs(x) # Needed to capture invalid overflow with netgative values 32 | return x^y 33 | end 34 | 35 | function _excess(x_loss,cmax,bexp,prc,pet) 36 | xn_prev = x_loss 37 | ct_prev = cmax * (1 - _power((1 - ((bexp + 1) * (xn_prev) / cmax)), (1 / (bexp + 1)))) 38 | # Calculate Effective rainfall 1 39 | ER1 = max((prc - cmax + ct_prev), 0.0) 40 | prc = prc- ER1 41 | dummy = min(((ct_prev + prc) / cmax), 1) 42 | xn = (cmax / (bexp + 1)) * (1 - _power((1 - dummy), (bexp + 1))) 43 | 44 | # Calculate Effective rainfall 2 45 | ER2 = max(prc - (xn - xn_prev), 0) 46 | 47 | # Alternative approach 48 | evap = (1 - (((cmax / (bexp + 1)) - xn) / (cmax / (bexp + 1)))) * pet # actual ET is linearly related to the soil moisture state 49 | xn = max(xn - evap, 0) # update state 50 | 51 | return ER1,ER2,xn 52 | end 53 | 54 | function linearReservoir(x_slow,inflow,Rs) 55 | # Linear reservoir routing 56 | x_slow = (1 - Rs) * x_slow + (1 - Rs) * inflow 57 | outflow = (Rs / (1 - Rs)) * x_slow 58 | return x_slow,outflow 59 | end 60 | 61 | function hargreaves(forcings; tminCol=:tmin,tmaxCol=:tmax,dtCol=:datetime) 62 | 63 | dts = forcings[!,dtCol] 64 | tmin = forcings[!,tminCol] 65 | tmax = forcings[!,tmaxCol] 66 | len = length(tmax) 67 | Gsc = 367 68 | lhov = 2.257 69 | 70 | doy = map(dayofyear, dts) 71 | 72 | tavg = map(mean,zip(tmin,tmax)) 73 | 74 | eto = zeros(len) 75 | 76 | for (i,t) in enumerate(doy) 77 | b = 2 * pi * (Int16(t)/365) 78 | Rav = 1.00011 + 0.034221*cos(b) + 0.00128*sin(b) + 0.000719*cos(2*b) + 0.000077*sin(2*b) 79 | Ho = ((Gsc * Rav) * 86400)/1e6 80 | 81 | eto[i] = (0.0023 * Ho * (tmax[i]-tmin[i])^0.5 * (tavg[i]+17.8)) 82 | end 83 | 84 | return eto 85 | end 86 | 87 | function simulate(forcings; precipCol=:precip,petCol=:pet,initFlow=true,cmax=1.0,bexp=0.0,alpha=0.2,ks=0.01,kq=0.5) 88 | p = forcings[!,precipCol] 89 | e = forcings[!,petCol] 90 | 91 | lt_to_m = 0.001 92 | 93 | # HYMOD PROGRAM IS SIMPLE RAINFALL RUNOFF MODEL 94 | x_loss = 0.0 95 | # Initialize slow tank state 96 | # value of 0 init flow works ok if calibration data starts with low discharge 97 | x_slow = initFlow ? (2.3503 / (ks * 22.5)) : 0 98 | # Initialize state(s) of quick tank(s) 99 | x_quick = zeros(Float64, 3) 100 | outflow = zeros(Float64, length(p)) 101 | output = zeros(Float64, length(p)) 102 | # START PROGRAMMING LOOP WITH DETERMINING RAINFALL - RUNOFF AMOUNTS 103 | 104 | for t in 1:length(p) 105 | Pval = p[t] 106 | PETval = e[t] 107 | # Compute excess precipitation and evaporation 108 | ER1, ER2, x_loss = _excess(x_loss, cmax, bexp, Pval, PETval) 109 | # Calculate total effective rainfall 110 | ET = ER1 + ER2 111 | # Now partition ER between quick and slow flow reservoirs 112 | UQ = alpha * ET 113 | US = (1 - alpha) * ET 114 | # Route slow flow component with single linear reservoir 115 | x_slow, QS = linearReservoir(x_slow, US, ks) 116 | # Route quick flow component with linear reservoirs 117 | inflow = UQ 118 | 119 | for i = 1:length(x_quick) 120 | # Linear reservoir 121 | x_quick[i], outflow = linearReservoir(x_quick[i], inflow, kq) 122 | inflow = outflow 123 | end 124 | 125 | # Compute total flow for timestep 126 | output[t] = ((QS + outflow)/lt_to_m) 127 | end 128 | 129 | output[output.<0] .= 0 130 | 131 | return output 132 | end 133 | 134 | function calibrate(forcing, obs, paramSpace, samples; precipCol=:precip, petCol=:pet, saveResults=false) 135 | # implementation of a monte carlo sampling 136 | 137 | params = Calibration.monteCarlo(paramSpace,samples=samples) 138 | keyList = collect(keys(params)) 139 | nIter = samples 140 | losses = zeros(nIter) 141 | 142 | print("Running $nIter iterations...") 143 | for i in 1:nIter 144 | vals = [params[k][i] for k in keyList] 145 | pars = Dict(keyList .=> vals) 146 | q = simulate(forcing, precipCol=:precip, petCol=:pet; pars...) 147 | losses[i] = Utils.nse(q[365:end],obs[365:end]) 148 | end 149 | 150 | losses = convert(Vector{Union{Missing,Float64}}, losses) 151 | losses[map(isnan,losses)] .= missing 152 | 153 | finalLoss, idx = findmax(skipmissing(losses)) 154 | vals = [params[k][idx] for k in keyList] 155 | finalPars = Dict(keyList .=> vals) 156 | finalQ = simulate(forcing, precipCol=:precip, petCol=:pet; finalPars...) 157 | 158 | if saveResults 159 | t = now() 160 | finalPars[:loss] = finalLoss 161 | tomlPars = Dict(String(k)=>v for (k,v) in finalPars) 162 | print(tomlPars) 163 | open("hymod_calibration_results_$t.toml","w") do io 164 | TOML.print(io, tomlPars) 165 | end 166 | end 167 | 168 | return finalQ, finalPars, finalLoss 169 | 170 | end 171 | 172 | # end module 173 | end 174 | -------------------------------------------------------------------------------- /julia/src/Utils.jl: -------------------------------------------------------------------------------- 1 | module Utils 2 | 3 | using DataFrames, Dates, Statistics 4 | 5 | function parseDates(df; format="y-m-d",dateCol=:date) 6 | dfmat = DateFormat(format) 7 | tCol = df[!,dateCol] 8 | len = length(tCol) 9 | dts = Array{Date}(undef,len) 10 | for i in 1:len 11 | dts[i] = Date(tCol[i],dfmat) 12 | end 13 | return dts 14 | end 15 | 16 | function renameCols(df,newNames) 17 | @assert length(names(df)) == length(newNames) "List of new names does not equal to columns" 18 | names!(df,[Symbol(i) for i in newNames]) 19 | return df 20 | end 21 | 22 | function nse(sim,obs) 23 | numerator = sum((obs-sim).^2) 24 | denominator = sum((obs.-mean(obs)).^2) 25 | return 1 - (numerator/denominator) 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # hymod.py 2 | 3 | Python package for calibrating and running the Hymod rainfall-runoff model. 4 | 5 | 6 | The example can be run using the following command: 7 | 8 | ``` 9 | $ python run_hymod.py 10 | ``` 11 | -------------------------------------------------------------------------------- /python/calibrate_hymod.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from src.hymod import Hymod 3 | 4 | obsPath = "../example_data/LA011201_obs.csv" 5 | obs = pd.read_csv(obsPath) 6 | obs["datetime"] = pd.to_datetime(obs["datetime"]) 7 | 8 | path = "../example_data/LA011201_forcings.csv" 9 | forcings = pd.read_csv(path) 10 | 11 | forcings["datetime"] = pd.to_datetime(forcings["isodate"]) 12 | forcings['pet'] = Hymod.hargreaves(forcings,dtCol="datetime") 13 | 14 | nIters = 10000 15 | 16 | paramSpace = dict( 17 | cmax = dict(lower = 1.0, upper = 100), 18 | bexp = dict(lower = 0.0, upper = 2.0), 19 | alpha = dict(lower = 0.2, upper = 0.99), 20 | ks = dict(lower = 0.01, upper = 0.5), 21 | kq = dict(lower = 0.5, upper = 1.2) 22 | ) 23 | 24 | calStart = "1986-01-01" 25 | calEnd = "1995-12-31" 26 | 27 | forcingMask = (forcings["datetime"]>= calStart) & (forcings["datetime"]<=calEnd) 28 | calForcings = forcings.loc[forcingMask] 29 | 30 | obsMask = (obs["datetime"]>= calStart) & (obs["datetime"]<=calEnd) 31 | obsQ = obs.loc[obsMask]["discharge"] 32 | 33 | calQ, calPars, loss = Hymod.calibrate(calForcings,obsQ,paramSpace,nIters) 34 | 35 | print(calPars, loss) 36 | -------------------------------------------------------------------------------- /python/run_hymod.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import datetime 3 | from src.hymod import Hymod 4 | 5 | t1 = datetime.datetime.now() 6 | obsPath = "../example_data/LA011201_obs.csv" 7 | obs = pd.read_csv(obsPath) 8 | obs["datetime"] = pd.to_datetime(obs["datetime"]) 9 | 10 | path = "../example_data/LA011201_forcings.csv" 11 | forcings = pd.read_csv(path) 12 | 13 | forcings["datetime"] = pd.to_datetime(forcings["isodate"]) 14 | forcings['pet'] = Hymod.hargreaves(forcings,dtCol="datetime") 15 | 16 | pars = Hymod.get_random_params() 17 | q = Hymod.simulate(forcings,**pars) 18 | print(f"Processing time: {datetime.datetime.now()-t1}") 19 | -------------------------------------------------------------------------------- /python/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KMarkert/hymod/e6f9fa7805c9d072b4688172c33ad91185bc4d9e/python/src/__init__.py -------------------------------------------------------------------------------- /python/src/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KMarkert/hymod/e6f9fa7805c9d072b4688172c33ad91185bc4d9e/python/src/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /python/src/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KMarkert/hymod/e6f9fa7805c9d072b4688172c33ad91185bc4d9e/python/src/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /python/src/__pycache__/calibration.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KMarkert/hymod/e6f9fa7805c9d072b4688172c33ad91185bc4d9e/python/src/__pycache__/calibration.cpython-36.pyc -------------------------------------------------------------------------------- /python/src/__pycache__/hymod.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KMarkert/hymod/e6f9fa7805c9d072b4688172c33ad91185bc4d9e/python/src/__pycache__/hymod.cpython-36.pyc -------------------------------------------------------------------------------- /python/src/__pycache__/hymod.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KMarkert/hymod/e6f9fa7805c9d072b4688172c33ad91185bc4d9e/python/src/__pycache__/hymod.cpython-37.pyc -------------------------------------------------------------------------------- /python/src/calibration.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | -------------------------------------------------------------------------------- /python/src/hymod.py: -------------------------------------------------------------------------------- 1 | from __future__ import division, print_function 2 | 3 | 4 | import os 5 | import numpy as np 6 | import pandas as pd 7 | import datetime 8 | import itertools 9 | from collections import defaultdict 10 | 11 | 12 | from src import calibration 13 | 14 | 15 | class Hymod: 16 | def __init__(self): 17 | return 18 | 19 | # Get random parameter set 20 | @staticmethod 21 | def get_random_params(): 22 | 23 | param_bounds = defaultdict( 24 | cmax = defaultdict(lower = 1.0, upper = 100), 25 | bexp = defaultdict(lower = 0.0, upper = 2.0), 26 | alpha = defaultdict(lower = 0.2, upper = 0.99), 27 | ks = defaultdict(lower = 0.01, upper = 0.5), 28 | kq = defaultdict(lower = 0.5, upper = 1.2) 29 | ) 30 | 31 | out = defaultdict() 32 | minV,maxV,p = None,None,None 33 | 34 | for k in param_bounds.keys(): 35 | minV = param_bounds[k]["lower"] 36 | maxV = param_bounds[k]["upper"] 37 | p = np.random.uniform(minV,maxV) 38 | out[k] = p 39 | 40 | return out 41 | 42 | @staticmethod 43 | def _power(X,Y): 44 | X=abs(X) # Needed to capture invalid overflow with netgative values 45 | return X**Y 46 | 47 | 48 | @staticmethod 49 | def _excess(x_loss,cmax,bexp,Pval,PETval): 50 | # this function calculates excess precipitation and evaporation 51 | xn_prev = x_loss 52 | ct_prev = cmax * (1 - Hymod._power((1 - ((bexp + 1) * (xn_prev) / cmax)), (1 / (bexp + 1)))) 53 | # Calculate Effective rainfall 1 54 | ER1 = max((Pval - cmax + ct_prev), 0.0) 55 | Pval = Pval - ER1 56 | dummy = min(((ct_prev + Pval) / cmax), 1) 57 | xn = (cmax / (bexp + 1)) * (1 - Hymod._power((1 - dummy), (bexp + 1))) 58 | 59 | # Calculate Effective rainfall 2 60 | ER2 = max(Pval - (xn - xn_prev), 0) 61 | 62 | # Alternative approach 63 | evap = (1 - (((cmax / (bexp + 1)) - xn) / (cmax / (bexp + 1)))) * PETval # actual ET is linearly related to the soil moisture state 64 | xn = max(xn - evap, 0) # update state 65 | 66 | return ER1,ER2,xn 67 | 68 | 69 | @staticmethod 70 | def linearReservoir(x_slow,inflow,Rs): 71 | # Linear reservoir 72 | x_slow = (1 - Rs) * x_slow + (1 - Rs) * inflow 73 | outflow = (Rs / (1 - Rs)) * x_slow 74 | return x_slow,outflow 75 | 76 | @staticmethod 77 | def simulate(forcings, precipCol="precip", petCol="pet", cmax=None,bexp=None,alpha=None,ks=None,kq=None,initFlow=True): 78 | """ 79 | Implementation of the Hymod lumped hydrologic model 80 | See https://www.proc-iahs.net/368/180/2015/piahs-368-180-2015.pdf for a scientific paper. 81 | Args: precip (pandas.DataFrame): 1-column dataframe with time series of daily precipitation values 82 | pet (pandas.DataFrame): 1-column dataframe with time series of daily potential evapotranspiration values 83 | Kwargs: cmax (float): cmax parameter 84 | bexp (float): bexp parameter 85 | alpha (float): alpha parameter 86 | Ks (float): Ks parameter 87 | Kq (float): Kq parameter 88 | Returns: outDf (pandas.DataFrame): resulting discharge from the model 89 | """ 90 | 91 | #TODO: add warnings about n columns 92 | # if len(pcolumns) > 1: 93 | 94 | p = forcings[precipCol].values 95 | e = forcings[petCol].values 96 | 97 | lt_to_m = 0.001 98 | 99 | # HYMOD PROGRAM IS SIMPLE RAINFALL RUNOFF MODEL 100 | x_loss = 0.0 101 | # Initialize slow tank state 102 | # value of 0 init flow works ok if calibration data starts with low discharge 103 | x_slow = 2.3503 / (ks * 22.5) if initFlow else 0 104 | # Initialize state(s) of quick tank(s) 105 | x_quick = np.zeros(3) 106 | t = 0 107 | outflow = np.zeros_like(p) 108 | output = np.zeros_like(p) 109 | # START PROGRAMMING LOOP WITH DETERMINING RAINFALL - RUNOFF AMOUNTS 110 | 111 | while t <= len(p)-1: 112 | Pval = p[t] 113 | PETval = e[t] 114 | # Compute excess precipitation and evaporation 115 | ER1, ER2, x_loss = Hymod._excess(x_loss, cmax, bexp, Pval, PETval) 116 | # Calculate total effective rainfall 117 | ET = ER1 + ER2 118 | # Now partition ER between quick and slow flow reservoirs 119 | UQ = alpha * ET 120 | US = (1 - alpha) * ET 121 | # Route slow flow component with single linear reservoir 122 | x_slow, QS = Hymod.linearReservoir(x_slow, US, ks) 123 | # Route quick flow component with linear reservoirs 124 | inflow = UQ 125 | 126 | for i in range(3): 127 | # Linear reservoir 128 | x_quick[i], outflow = Hymod.linearReservoir(x_quick[i], inflow, kq) 129 | inflow = outflow 130 | 131 | # Compute total flow for timestep 132 | output[t] = ((QS + outflow)/lt_to_m) 133 | t = t+1 134 | 135 | return output 136 | 137 | 138 | @staticmethod 139 | def hargreaves(forcings, tminCol="tmin",tmaxCol="tmax",dtCol="datetime"): 140 | """ 141 | accepts panda Series 142 | returns panda Series 143 | """ 144 | Gsc = 367 145 | lhov = 2.257 146 | 147 | dts = forcings[dtCol] 148 | tmin = forcings[tminCol] 149 | tmax = forcings[tmaxCol] 150 | n = len(tmax) 151 | doy = [x.timetuple().tm_yday for x in dts] 152 | 153 | tavg = pd.concat([tmin,tmax],axis=1).mean(axis=1).rename('tavg') 154 | 155 | eto = np.zeros(n) 156 | 157 | for i,t in enumerate(doy): 158 | b = 2 * np.pi * (t/365) 159 | Rav = 1.00011 + 0.034221*np.cos(b) + 0.00128*np.sin(b) + 0.000719*np.cos(2*b) + 0.000077*np.sin(2*b) 160 | Ho = ((Gsc * Rav) * 86400)/1e6 161 | 162 | eto[i] = (0.0023 * Ho * (tmax[i]-tmin[i])**0.5 * (tavg[i]+17.8)) 163 | 164 | return eto 165 | 166 | @staticmethod 167 | def calibrate(forcings,obs,paramSpace, nsamples, precipCol="precip", petCol="pet", saveResults=False): 168 | samples = dict() 169 | 170 | keyList = list(paramSpace.keys()) 171 | 172 | for k in keyList: 173 | p = paramSpace[k] 174 | minV = p["lower"] 175 | maxV = p["upper"] 176 | samples[k] = np.random.uniform(minV,maxV,size=nsamples) 177 | 178 | losses = np.zeros(nsamples) 179 | 180 | print(f"Running {nsamples} iterations...") 181 | for i in range(nsamples): 182 | pars = {k:samples[k][i] for i,k in enumerate(keyList)} 183 | q = Hymod.simulate(forcings, precipCol=precipCol, petCol=petCol, **pars) 184 | losses[i] = nse(q,obs) 185 | 186 | loss,idx = losses.max(),losses.argmax() 187 | finalPars = {k:samples[k][idx] for k in keyList} 188 | finalQ = Hymod.simulate(forcings, precipCol=precipCol, petCol=petCol, **finalPars) 189 | finalLoss = nse(finalQ,obs) 190 | 191 | return finalQ, finalPars, loss 192 | 193 | 194 | def nse(sim,obs): 195 | numerator = np.nansum((obs-sim)**2) 196 | denominator = np.nansum((obs-np.nanmean(obs))**2) 197 | return 1 - (numerator/denominator) 198 | --------------------------------------------------------------------------------