Modeling snow#
R.A. Collenteur, University of Graz / Eawag, November 2021
In this notebook it is shown how to account for snowfall and smowmelt on groundwater recharge and groundwater levels, using a degree-day snow model. This notebook is work in progress and will be extended in the future.
import matplotlib.pyplot as plt
import pandas as pd
import pastas as ps
ps.set_log_level("ERROR")
ps.show_versions()
Pastas : 2.0.0
Python : 3.14.0
Numpy : 2.4.6
Pandas : 3.0.3
Scipy : 1.17.1
Matplotlib : 3.10.9
Numba : 0.65.1
1. Load data#
In this notebook we will look at some data for a well near Heby, Sweden. All the meteorological data is taken from the E-OBS database. As can be observed from the temperature time series, the temperature regularly drops below zero in winter. Given this observation, we may expect precipitation to (partially) fall as snow during these periods.
head = pd.read_csv("data/heby_head.csv", index_col=0, parse_dates=True).squeeze()
evap = pd.read_csv("data/heby_evap.csv", index_col=0, parse_dates=True).squeeze()
prec = pd.read_csv("data/heby_prec.csv", index_col=0, parse_dates=True).squeeze()
temp = pd.read_csv("data/heby_temp.csv", index_col=0, parse_dates=True).squeeze()
ps.plots.series(head, stresses=[prec, evap, temp]);
2. Make a simple model#
First we create a simple model with precipitation and potential evaporation as input, using the non-linear FlexModel to compute the recharge flux. We not not yet take snowfall into account, and thus assume all precipitation occurs as snowfall. The model is calibrated and the results are visualized for inspection.
# Settings
tmin = "1985" # Needs warmup
tmax = "2010"
ml1 = ps.Model(head)
sm1 = ps.RechargeModel(
prec, evap, recharge=ps.rch.FlexModel(), rfunc=ps.Gamma(), name="rch"
)
ml1.add_stressmodel(sm1)
# Solve the Pastas model in two steps
ml1.solve(tmin=tmin, tmax=tmax, fit_constant=False, report=False)
ml1.add_noisemodel(ps.ArNoiseModel())
ml1.set_parameter("rch_srmax", vary=False)
ml1.solve(tmin=tmin, tmax=tmax, fit_constant=False, initial=False)
ml1.plot(figsize=(10, 3));
Fit report solver Fit Statistics
==================================================
nfev 35 EVP 39.56
nobs 590 R2 0.40
noise True RMSE 0.13
tmin 1985-01-01 00:00:00 AICc -3230.06
tmax 2010-01-01 00:00:00 BIC -3199.60
freq D Obj nan
freq_obs None ___
warmup 3650 days 00:00:00 Interp. No
Parameters (7 optimized)
==================================================
optimal initial vary
rch_A 1.719370 1.194705 True
rch_n 1.335785 2.942982 True
rch_a 196.847269 57.023121 True
rch_srmax 3.257276 3.257276 False
rch_lp 0.250000 0.250000 False
rch_ks 2796.538938 7.478223 True
rch_gamma 16.344691 1.121169 True
rch_kv 0.953019 1.999999 True
rch_simax 2.000000 2.000000 False
constant_d 77.763197 0.000000 False
noise_alpha 108.514849 15.000000 True
The model fit with the data is not too bad, but we are clearly missing the highs and lows of the observed groundwater levels. This could have many causes, but in this case we may suspect that the occurrence of snowfall and melt impacts the results.
3. Account for snowfall and snow melt#
A second model is now created that accounts for snowfall and melt through a degree-day snow model (see e.g., Kavetski & Kuczera (2007). To run the model we add the keyword snow=True to the FlexModel and provide a time series of mean daily temperature to the RechargeModel. The temperature time series is used to split the precipitation into snowfall and rainfall.
ml2 = ps.Model(head)
sm2 = ps.RechargeModel(
prec,
evap,
recharge=ps.rch.FlexModel(snow=True),
rfunc=ps.Gamma(),
name="rch",
temp=temp,
)
ml2.add_stressmodel(sm2)
# Solve the Pastas model in two steps
ml2.solve(tmin=tmin, tmax=tmax, fit_constant=False, report=False)
ml2.add_noisemodel(ps.ArNoiseModel())
ml2.set_parameter("rch_srmax", vary=False)
ml2.solve(tmin=tmin, tmax=tmax, fit_constant=False, initial=False)
Fit report solver Fit Statistics
==================================================
nfev 31 EVP 67.37
nobs 590 R2 0.67
noise True RMSE 0.10
tmin 1985-01-01 00:00:00 AICc -3374.82
tmax 2010-01-01 00:00:00 BIC -3340.03
freq D Obj nan
freq_obs None ___
warmup 3650 days 00:00:00 Interp. No
Parameters (8 optimized)
==================================================
optimal initial vary
rch_A 0.736747 0.577219 True
rch_n 1.125507 1.453839 True
rch_a 202.438169 107.147494 True
rch_srmax 154.653019 154.653019 False
rch_lp 0.250000 0.250000 False
rch_ks 320.932699 186.293467 True
rch_gamma 13.635115 18.173956 True
rch_kv 0.634416 0.705211 True
rch_simax 2.000000 2.000000 False
rch_tt 0.000000 0.000000 False
rch_k 1.000000 1.000033 True
constant_d 78.221846 0.000000 False
noise_alpha 69.551240 15.000000 True
Warnings! (1)
==================================================
Parameter 'rch_k' on lower bound: 1.00e+00
Compare results#
From the fit_report we can already observe that the model fit improved quite a bit. We can also visualize the results to see how the model improved.
ax = ml2.plot(figsize=(10, 3))
ml1.simulate().plot(ax=ax)
plt.legend(
[
"Observations",
"Model w Snow NSE={:.2f}".format(ml2.stats.nse()),
"Model w/o Snow NSE={:.2f}".format(ml1.stats.nse()),
],
ncol=3,
)
<matplotlib.legend.Legend at 0x774e7b8e3b60>
Extract the water balance (States & Fluxes)#
df = ml2.stressmodels["rch"].get_water_balance(
ml2.get_parameters("rch"), tmin=tmin, tmax=tmax
)
df.plot(subplots=True, figsize=(10, 10));
References#
Kavetski, D. and Kuczera, G. (2007). Model smoothing strategies to remove microscale discontinuities and spurious secondary optima in objective functions in hydrological calibration. Water Resources Research, 43(3).