# Using satellite snow cover area data for calibrating and improving CemaNeige

# Introduction

## Scope

Rainfall-runoff models that include a snow accumulation and melt module are still often calibrated using only discharge observations. This can result in poor snow modeling as the swnow module parameters can distorted in order to allow skilful discharge simulation.

After the work of Riboust et al. (2019), we propose now in airGR an improved version of the degree-day CemaNeige snow and accumulation module. This new version is based on a more accurate representation of the relationship that exists at the basin scale between the Snow Water Equivalent (SWE) and the Snow Cover Area (SCA). To do so, a linear SWE-SCA hysteresis, which represents the fact that snow accumulation is rather homogeneous on the basin and snow melt is more heterogeneous, was implemented.

This new CemaNeige version has two more parameters to calibrate. It also presents the advantage of allowing using satellite snow data to constrain the calibration in addition to discharge. Riboust et al. (2019) show that while the simulated discharge is not significantly improved, the snow simulation is much improved. In addition, they show that the model is more robust (i.e. transferable in time) in terms of discharge, which has many implications for climate change impact studies.

The configuration that was identified as optimal by Riboust et al. (2019) includes a CemaNeige module run on 5 elevation bands and an objective function determine by a composite function of KGE’ calculated on discharge (75 % weight) and KGE’ calculated on each elevation band (5 % for each).

In this page, we show how to use and calibrate this new CemaNeige version.

## Data preparation

We load an example data set from the package. Please note that this data set includes MODIS data that was pre-calculated for 5 elevation bands and for which days with few data (more than 40 % cloud coverage) were assigned as missing values.

## loading catchment data

    data(X0310010)
    summary(BasinObs)

    ##      DatesR                          P              T
    ##  Min.   :1999-01-01 00:00:00   Min.   : 0.0   Min.   :-16.6
    ##  1st Qu.:2001-11-23 06:00:00   1st Qu.: 0.0   1st Qu.: -2.1
    ##  Median :2004-10-15 12:00:00   Median : 0.1   Median :  3.1
    ##  Mean   :2004-10-15 12:00:00   Mean   : 2.8   Mean   :  3.1
    ##  3rd Qu.:2007-09-07 18:00:00   3rd Qu.: 2.3   3rd Qu.:  8.6
    ##  Max.   :2010-07-31 00:00:00   Max.   :82.3   Max.   : 18.2
    ##
    ##        E             Qls              Qmm           SCA1
    ##  Min.   :0.00   Min.   :  5698   Min.   : 0    Min.   :0
    ##  1st Qu.:0.20   1st Qu.: 20710   1st Qu.: 1    1st Qu.:0
    ##  Median :0.80   Median : 32041   Median : 1    Median :0
    ##  Mean   :1.16   Mean   : 47487   Mean   : 2    Mean   :0
    ##  3rd Qu.:2.00   3rd Qu.: 55559   3rd Qu.: 2    3rd Qu.:0
    ##  Max.   :4.00   Max.   :433747   Max.   :16    Max.   :1
    ##                 NA's   :397      NA's   :397   NA's   :2058
    ##       SCA2           SCA3           SCA4           SCA5
    ##  Min.   :0      Min.   :0      Min.   :0      Min.   :0
    ##  1st Qu.:0      1st Qu.:0      1st Qu.:0      1st Qu.:0
    ##  Median :0      Median :0      Median :0      Median :0
    ##  Mean   :0      Mean   :0      Mean   :0      Mean   :0
    ##  3rd Qu.:1      3rd Qu.:1      3rd Qu.:1      3rd Qu.:1
    ##  Max.   :1      Max.   :1      Max.   :1      Max.   :1
    ##  NA's   :2211   NA's   :2266   NA's   :2349   NA's   :2456

## Object model preparation

We assume that the R global environment contains data and functions from the Get Started vignette.

The calibration period has been defined from 2000-09-01 to 2005-08-31, and the validation period from 2005-09-01 to 2010-07-31. CemaNeige will be used in coupling with GR4J in this vignette.

    ## preparation of the InputsModel object
    InputsModel <- CreateInputsModel(FUN_MOD = RunModel_CemaNeigeGR4J,
                                     DatesR = BasinObs$DatesR, Precip = BasinObs$P,
                                     PotEvap = BasinObs$E, TempMean = BasinObs$T,
                                     ZInputs = median(BasinInfo$HypsoData),
                                     HypsoData = BasinInfo$HypsoData, NLayers = 5)

    ## input series were successfully created on 5 elevation layers for use by CemaNeige

    ## ---- calibration step

    ## calibration period selection
    Ind_Cal <- seq(which(format(BasinObs$DatesR, format = "%Y-%m-%d") == "2000-09-01"),
                   which(format(BasinObs$DatesR, format = "%Y-%m-%d") == "2005-08-31"))

    ## ---- validation step

    ## validation period selection
    Ind_Val <- seq(which(format(BasinObs$DatesR, format = "%Y-%m-%d") == "2005-09-01"),
                   which(format(BasinObs$DatesR, format = "%Y-%m-%d") == "2010-07-31"))

# Calibration and evaluation of the new CemaNeige module

In order to use the Linear Hysteresis, a new argument (`IsHyst`) is added in the `CreateRunOptions()` and `CreateCalibOptions()` functions and has to be set to `TRUE`.

    ## preparation of the RunOptions object for the calibration period
    RunOptions_Cal <- CreateRunOptions(FUN_MOD = RunModel_CemaNeigeGR4J,
                                       InputsModel = InputsModel, IndPeriod_Run = Ind_Cal,
                                       IsHyst = TRUE)

    ## preparation of the RunOptions object for the validation period
    RunOptions_Val <- CreateRunOptions(FUN_MOD = RunModel_CemaNeigeGR4J,
                                       InputsModel = InputsModel, IndPeriod_Run = Ind_Val,
                                       IsHyst = TRUE)

    ## preparation of the CalibOptions object
    CalibOptions <- CreateCalibOptions(FUN_MOD = RunModel_CemaNeigeGR4J,
                                       FUN_CALIB = Calibration_Michel,
                                       IsHyst = TRUE)

In order to calibrate and assess the model performance, we will follow the recommendations of Riboust et al. (2019). This is now possible in airGR with the added functionality that permits calculating composite criteria by combining different metrics.

    ## efficiency criterion: 75 % KGE'(Q) + 5 % KGE'(SCA) on each of the 5 layers
    InputsCrit_Cal  <- CreateInputsCrit(FUN_CRIT = rep("ErrorCrit_KGE2", 6),
                                        InputsModel = InputsModel, RunOptions = RunOptions_Cal,
                                        Obs = list(BasinObs$Qmm[Ind_Cal],
                                                   BasinObs$SCA1[Ind_Cal],
                                                   BasinObs$SCA2[Ind_Cal],
                                                   BasinObs$SCA3[Ind_Cal],
                                                   BasinObs$SCA4[Ind_Cal],
                                                   BasinObs$SCA5[Ind_Cal]),
                                        VarObs = list("Q", "SCA", "SCA", "SCA", "SCA", "SCA"),
                                        Weights = list(0.75, 0.05, 0.05, 0.05, 0.05, 0.05))

    InputsCrit_Val  <- CreateInputsCrit(FUN_CRIT = rep("ErrorCrit_KGE2", 6),
                                        InputsModel = InputsModel, RunOptions = RunOptions_Val,
                                        Obs = list(BasinObs$Qmm[Ind_Val],
                                                   BasinObs$SCA1[Ind_Val],
                                                   BasinObs$SCA2[Ind_Val],
                                                   BasinObs$SCA3[Ind_Val],
                                                   BasinObs$SCA4[Ind_Val],
                                                   BasinObs$SCA5[Ind_Val]),
                                        VarObs = list("Q", "SCA", "SCA", "SCA", "SCA", "SCA"),
                                        Weights = list(0.75, 0.05, 0.05, 0.05, 0.05, 0.05))

We can now calibrate the model.

    ## calibration
    OutputsCalib <- Calibration(InputsModel = InputsModel, RunOptions = RunOptions_Cal,
                                InputsCrit = InputsCrit_Cal, CalibOptions = CalibOptions,
                                FUN_MOD = RunModel_CemaNeigeGR4J,
                                FUN_CALIB = Calibration_Michel)

    ## Grid-Screening in progress (0% 20% 40% 60% 80% 100%)
    ##   Screening completed (6561 runs)
    ##       Param =  432.681,   -0.020,   83.096,    2.384,    0.002,    3.787,   15.000,    0.850
    ##       Crit. Composite    = 0.8139
    ## Steepest-descent local search in progress
    ##   Calibration completed (107 iterations, 8248 runs)
    ##       Param =  419.893,    0.517,  275.687,    1.345,    0.632,    3.864,   16.911,    0.472
    ##       Crit. Composite    = 0.8995
    ##  Formula: sum(0.75 * KGE2[Q], 0.05 * KGE2[SCA], 0.05 * KGE2[SCA],
    ##           0.05 * KGE2[SCA], 0.05 * KGE2[SCA], 0.05 * KGE2[SCA])

Now we can run it on the calibration period and assess it.

    ## run on the calibration period
    OutputsModel_Cal <- RunModel_CemaNeigeGR4J(InputsModel = InputsModel,
                                               RunOptions = RunOptions_Cal,
                                               Param = OutputsCalib$ParamFinalR)

    ## evaluation
    OutputsCrit_Cal <- ErrorCrit(InputsCrit = InputsCrit_Cal, OutputsModel = OutputsModel_Cal)

Find below the performance of the model over the calibration period.

    str(OutputsCrit_Cal, max.level = 2)

    ## List of 7
    ##  $ CritValue      : num 0.899
    ##  $ CritName       : chr "Composite"
    ##  $ CritBestValue  : num 1
    ##  $ Multiplier     : num -1
    ##  $ Ind_notcomputed: NULL
    ##  $ CritCompo      :List of 3
    ##   ..$ MultiCritValues : Named num [1:6] 0.919 0.705 0.847 0.88 0.874 ...
    ##   .. ..- attr(*, "names")= chr [1:6] "IC1" "IC2" "IC3" "IC4" ...
    ##   ..$ MultiCritNames  : Named chr [1:6] "KGE2[Q]" "KGE2[SCA]" "KGE2[SCA]" "KGE2[SCA]" ...
    ##   .. ..- attr(*, "names")= chr [1:6] "IC1" "IC2" "IC3" "IC4" ...
    ##   ..$ MultiCritWeights: Named num [1:6] 0.75 0.05 0.05 0.05 0.05 0.05
    ##   .. ..- attr(*, "names")= chr [1:6] "IC1" "IC2" "IC3" "IC4" ...
    ##  $ MultiCrit      :List of 6
    ##   ..$ IC1:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC2:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC3:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC4:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC5:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC6:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##  - attr(*, "class")= chr [1:2] "Compo" "ErrorCrit"

Now we can run the model on the validation period and assess it.

    ## run on the validation period
    OutputsModel_Val <- RunModel_CemaNeigeGR4J(InputsModel = InputsModel,
                                               RunOptions = RunOptions_Val,
                                               Param = OutputsCalib$ParamFinalR)

    ## evaluation
    OutputsCrit_Val <- ErrorCrit(InputsCrit = InputsCrit_Val, OutputsModel = OutputsModel_Val)

Find below the performance of the model over the validation period.

    str(OutputsCrit_Val, max.level = 2)

    ## List of 7
    ##  $ CritValue      : num 0.903
    ##  $ CritName       : chr "Composite"
    ##  $ CritBestValue  : num 1
    ##  $ Multiplier     : num -1
    ##  $ Ind_notcomputed: NULL
    ##  $ CritCompo      :List of 3
    ##   ..$ MultiCritValues : Named num [1:6] 0.916 0.63 0.876 0.945 0.962 ...
    ##   .. ..- attr(*, "names")= chr [1:6] "IC1" "IC2" "IC3" "IC4" ...
    ##   ..$ MultiCritNames  : Named chr [1:6] "KGE2[Q]" "KGE2[SCA]" "KGE2[SCA]" "KGE2[SCA]" ...
    ##   .. ..- attr(*, "names")= chr [1:6] "IC1" "IC2" "IC3" "IC4" ...
    ##   ..$ MultiCritWeights: Named num [1:6] 0.75 0.05 0.05 0.05 0.05 0.05
    ##   .. ..- attr(*, "names")= chr [1:6] "IC1" "IC2" "IC3" "IC4" ...
    ##  $ MultiCrit      :List of 6
    ##   ..$ IC1:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC2:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC3:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC4:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC5:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC6:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##  - attr(*, "class")= chr [1:2] "Compo" "ErrorCrit"

# Comparison with the performance of the initial CemaNeige version

Here we use the same InputsModel object and calibration and validation periods. However, we have to redefine the way we run the model (`RunOptions` argument), calibrate and assess it (`InputsCrit` argument). The objective function is only based on KGE’(Q). Note how we set the `IsHyst` argument to `FALSE` in the `CreateRunOptions()` and the `CreateCalibOptions()` functions.

    ## preparation of RunOptions object
    RunOptions_Cal_NoHyst <- CreateRunOptions(FUN_MOD = RunModel_CemaNeigeGR4J,
                                              InputsModel = InputsModel,
                                              IndPeriod_Run = Ind_Cal,
                                              IsHyst = FALSE)

    RunOptions_Val_NoHyst <- CreateRunOptions(FUN_MOD = RunModel_CemaNeigeGR4J,
                                              InputsModel = InputsModel,
                                              IndPeriod_Run = Ind_Val,
                                              IsHyst = FALSE)

    InputsCrit_Cal_NoHyst <- CreateInputsCrit(FUN_CRIT = ErrorCrit_KGE2,
                                              InputsModel = InputsModel,
                                              RunOptions = RunOptions_Cal_NoHyst,
                                              Obs = BasinObs$Qmm[Ind_Cal], VarObs = "Q")

    ## preparation of CalibOptions object
    CalibOptions_NoHyst <- CreateCalibOptions(FUN_MOD = RunModel_CemaNeigeGR4J,
                                              FUN_CALIB = Calibration_Michel,
                                              IsHyst = FALSE)

We can now calibrate the model.

    ## calibration
    OutputsCalib_NoHyst <- Calibration(InputsModel = InputsModel, InputsCrit = InputsCrit_Cal_NoHyst,
                                       RunOptions = RunOptions_Cal_NoHyst, CalibOptions = CalibOptions_NoHyst,
                                       FUN_MOD = RunModel_CemaNeigeGR4J, FUN_CALIB = Calibration_Michel)

    ## Grid-Screening in progress (0% 20% 40% 60% 80% 100%)
    ##   Screening completed (729 runs)
    ##       Param =  432.681,   -0.020,   83.096,    2.384,    0.002,    3.787
    ##       Crit. KGE2[Q]      = 0.7830
    ## Steepest-descent local search in progress
    ##   Calibration completed (50 iterations, 1307 runs)
    ##       Param =  835.709,    0.400,  171.501,    1.330,    0.869,    4.103
    ##       Crit. KGE2[Q]      = 0.9370

And run it over the calibration and validation periods.

    OutputsModel_Cal_NoHyst <- RunModel_CemaNeigeGR4J(InputsModel = InputsModel,
                                                      RunOptions = RunOptions_Cal_NoHyst,
                                                      Param = OutputsCalib_NoHyst$ParamFinalR)

    OutputsModel_Val_NoHyst <- RunModel_CemaNeigeGR4J(InputsModel = InputsModel,
                                                      RunOptions = RunOptions_Val_NoHyst,
                                                      Param = OutputsCalib_NoHyst$ParamFinalR)

In order to assess the model performance over the two periods, we will use the InputsCrit objects prepared before, which allow assessing also the performance in terms of snow simulation.

    OutputsCrit_Cal_NoHyst <- ErrorCrit(InputsCrit = InputsCrit_Cal,
                                        OutputsModel = OutputsModel_Cal_NoHyst)

    OutputsCrit_Val_NoHyst <- ErrorCrit(InputsCrit = InputsCrit_Val,
                                        OutputsModel = OutputsModel_Val_NoHyst)

We can check the performance over the calibration and the validation period.

    str(OutputsCrit_Cal_NoHyst, max.level = 2)

    ## List of 7
    ##  $ CritValue      : num 0.836
    ##  $ CritName       : chr "Composite"
    ##  $ CritBestValue  : num 1
    ##  $ Multiplier     : num -1
    ##  $ Ind_notcomputed: NULL
    ##  $ CritCompo      :List of 3
    ##   ..$ MultiCritValues : Named num [1:6] 0.937 0.147 0.48 0.65 0.707 ...
    ##   .. ..- attr(*, "names")= chr [1:6] "IC1" "IC2" "IC3" "IC4" ...
    ##   ..$ MultiCritNames  : Named chr [1:6] "KGE2[Q]" "KGE2[SCA]" "KGE2[SCA]" "KGE2[SCA]" ...
    ##   .. ..- attr(*, "names")= chr [1:6] "IC1" "IC2" "IC3" "IC4" ...
    ##   ..$ MultiCritWeights: Named num [1:6] 0.75 0.05 0.05 0.05 0.05 0.05
    ##   .. ..- attr(*, "names")= chr [1:6] "IC1" "IC2" "IC3" "IC4" ...
    ##  $ MultiCrit      :List of 6
    ##   ..$ IC1:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC2:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC3:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC4:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC5:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC6:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##  - attr(*, "class")= chr [1:2] "Compo" "ErrorCrit"

    str(OutputsCrit_Val_NoHyst, max.level = 2)

    ## List of 7
    ##  $ CritValue      : num 0.773
    ##  $ CritName       : chr "Composite"
    ##  $ CritBestValue  : num 1
    ##  $ Multiplier     : num -1
    ##  $ Ind_notcomputed: NULL
    ##  $ CritCompo      :List of 3
    ##   ..$ MultiCritValues : Named num [1:6] 0.878 0.159 0.363 0.495 0.593 ...
    ##   .. ..- attr(*, "names")= chr [1:6] "IC1" "IC2" "IC3" "IC4" ...
    ##   ..$ MultiCritNames  : Named chr [1:6] "KGE2[Q]" "KGE2[SCA]" "KGE2[SCA]" "KGE2[SCA]" ...
    ##   .. ..- attr(*, "names")= chr [1:6] "IC1" "IC2" "IC3" "IC4" ...
    ##   ..$ MultiCritWeights: Named num [1:6] 0.75 0.05 0.05 0.05 0.05 0.05
    ##   .. ..- attr(*, "names")= chr [1:6] "IC1" "IC2" "IC3" "IC4" ...
    ##  $ MultiCrit      :List of 6
    ##   ..$ IC1:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC2:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC3:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC4:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC5:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##   ..$ IC6:List of 7
    ##   .. ..- attr(*, "class")= chr [1:2] "KGE2" "ErrorCrit"
    ##  - attr(*, "class")= chr [1:2] "Compo" "ErrorCrit"

We can see above that the performance of the initial model is slightly better than the new one over the calibration period in terms of discharge, but also that the new version calibrated using SCA provides better performance in terms of snow. However, over the validation period, we see that the discharge simulated by the new version brings better performance (in addition to improved SCA also). This shows the interests of the combined use of a linear hysteresis and of SCA data for calibration in CemaNeige.

# References

Riboust, Philippe, Guillaume Thirel, Nicolas Le Moine, and Pierre Ribstein. 2019. “Revisiting a Simple Degree-Day Model for Integrating Satellite Data: Implementation of Swe-Sca Hystereses.” Journal of Hydrology and Hydromechanics 67 (1): 70–81. doi:10.2478/johh-2018-0004.