From e4d5e384300a3cbc0ad8475b5c737b50cad5744e Mon Sep 17 00:00:00 2001 From: Dorchies David <david.dorchies@irstea.fr> Date: Mon, 25 Jul 2022 09:28:21 +0200 Subject: [PATCH] feat: Calibration with ungauged nodes Refs #42 --- R/Calibration.GRiwrmInputsModel.R | 191 +++++++++++++++++++-- R/CreateRunOptions.GRiwrmInputsModel.R | 1 + vignettes/V05_Modelling_ungauged_nodes.Rmd | 93 ++++++++-- 3 files changed, 257 insertions(+), 28 deletions(-) diff --git a/R/Calibration.GRiwrmInputsModel.R b/R/Calibration.GRiwrmInputsModel.R index e53eed8..0ea852e 100644 --- a/R/Calibration.GRiwrmInputsModel.R +++ b/R/Calibration.GRiwrmInputsModel.R @@ -31,34 +31,65 @@ Calibration.GRiwrmInputsModel <- function(InputsModel, OutputsModel <- list() class(OutputsModel) <- append("GRiwrmOutputsModel", class(OutputsModel)) - for(IM in InputsModel) { - message("Calibration.GRiwrmInputsModel: Treating sub-basin ", IM$id, "...") + b <- sapply(InputsModel, function(IM) !IM$isUngauged) + gaugedIds <- names(b[b]) - if(useUpstreamQsim && any(IM$UpstreamIsRunoff)) { - # Update InputsModel$Qupstream with simulated upstream flows - IM <- UpdateQsimUpstream(IM, RunOptions[[IM$id]], OutputsModel) - } + for(id in gaugedIds) { + IM <- InputsModel[[id]] + message("Calibration.GRiwrmInputsModel: Treating sub-basin ", id, "...") - if (inherits(InputsCrit[[IM$id]], "InputsCritLavenneFunction")) { - IC <- getInputsCrit_Lavenne(IM$id, OutputsModel, InputsCrit) + if (inherits(InputsCrit[[id]], "InputsCritLavenneFunction")) { + IC <- getInputsCrit_Lavenne(id, OutputsModel, InputsCrit) + } else { + IC <- InputsCrit[[id]] + } + hasUngauged <- IM$hasUngauged + if (hasUngauged) { + Sdown <- IM$BasinAreas[length(IM$BasinAreas)] + l <- updateParameters4Ungauged(id, + InputsModel, + RunOptions, + OutputsModel, + useUpstreamQsim) + IM <- l$InputsModel + IM$FUN_MOD <- "RunModel_Ungauged" + attr(RunOptions[[id]], "GRiwrmRunOptions") <- l$RunOptions } else { - IC <- InputsCrit[[IM$id]] + if(useUpstreamQsim && any(IM$UpstreamIsRunoff)) { + # Update InputsModel$Qupstream with simulated upstream flows + IM <- UpdateQsimUpstream(IM, RunOptions[[id]], OutputsModel) + } } - OutputsCalib[[IM$id]] <- Calibration( + OutputsCalib[[id]] <- Calibration( InputsModel = IM, - RunOptions = RunOptions[[IM$id]], + RunOptions = RunOptions[[id]], InputsCrit = IC, - CalibOptions = CalibOptions[[IM$id]], + CalibOptions = CalibOptions[[id]], ... ) + if (hasUngauged) { + # Add OutputsCalib for ungauged nodes + g <- attr(InputsModel, "GRiwrm") + ungaugedIds <- g$id[g$gauged == id & g$id != id & !is.na(g$model)] + for (uId in ungaugedIds) { + OutputsCalib[[uId]] <- OutputsCalib[[id]] + PS <- attr(IM[[uId]], "ParamSettings") + OutputsCalib[[uId]]$ParamFinalR <- OutputsCalib[[uId]]$ParamFinalR[PS$Indexes] + if(PS$hasX4) { + OutputsCalib[[uId]]$ParamFinalR[PS$iX4] <- + OutputsCalib[[uId]]$ParamFinalR[PS$iX4] * (sum(IM[[uId]]$BasinAreas) / Sdown) ^ 0.3 + } + } + } + if(useUpstreamQsim) { # Run the model for the sub-basin - OutputsModel[[IM$id]] <- RunModel( + OutputsModel[[id]] <- RunModel( x = IM, - RunOptions = RunOptions[[IM$id]], - Param = OutputsCalib[[IM$id]]$ParamFinalR + RunOptions = RunOptions[[id]], + Param = OutputsCalib[[id]]$ParamFinalR ) } @@ -96,3 +127,133 @@ getInputsCrit_Lavenne <- function(id, OutputsModel, InputsCrit) { AprCrit <- ErrorCrit(InputsCrit[[AprioriId]], OutputsModel[[AprioriId]])$CritValue return(Lavenne_FUN(AprParamR, AprCrit)) } + + +#' Reduce a GRiwrm list object (InputsModel, RunOptions...) for a reduced network +#' +#' @param griwrm See [CreateGRiwrm]) +#' @param obj Either a *GRiwrmInputsModel*, *GRiwrmOptions*... object +#' +#' @return The object containing only nodes of the reduced model +#' @noRd +reduceGRiwrmObj4Ungauged <- function(griwrm, obj) { + objAttributes <- attributes(obj) + obj <- lapply(obj, function(o) { + if(o$id %in% griwrm$id) { + o + } else { + NULL + } + }) + obj[sapply(obj, is.null)] <- NULL + objAttributes$names <- names(obj) + attributes(obj) <- objAttributes + return(obj) +} + +updateParameters4Ungauged <- function(GaugedId, + InputsModel, + RunOptions, + OutputsModel, + useUpstreamQsim) { + + ### Set the reduced network of the basin containing ungauged nodes ### + # Select nodes identified with the current node as gauged node + griwrm <- attr(InputsModel, "GRiwrm") + g <- griwrm[griwrm$gauged == GaugedId, ] + # Add upstream nodes for routing upstream flows + upIds <- griwrm$id[griwrm$down %in% g$id & !griwrm$id %in% g$id] + g <- rbind(griwrm[griwrm$id %in% upIds, ], g) + g$model[g$id %in% upIds] <- NA + # Set downstream node + g$down[!g$down %in% g$id] <- NA + + ### Modify InputsModel for the reduced network ### + # Remove nodes outside of reduced network + InputsModel <- reduceGRiwrmObj4Ungauged(g, InputsModel) + # Update griwrm + attr(InputsModel, "GRiwrm") <- g + # Update Qupstream of reduced network upstream nodes + g2 <- griwrm[griwrm$gauged == GaugedId,] + upIds2 <- g2$id[!g2$id %in% g2$down] + for (id in upIds2) { + if(useUpstreamQsim && any(InputsModel[[id]]$UpstreamIsRunoff)) { + # Update InputsModel$Qupstream with simulated upstream flows + InputsModel[[id]] <- UpdateQsimUpstream(InputsModel[[id]], + RunOptions[[id]], + OutputsModel) + InputsModel[[id]]$UpstreamIsRunoff <- + rep(FALSE, length(InputsModel[[id]]$UpstreamIsRunoff)) + } + } + # Add extra info for Param processing + nbParam <- RunOptions[[GaugedId]]$FeatFUN_MOD$NbParam + for (id in names(InputsModel)) { + attr(InputsModel[[id]], "ParamSettings") <- + list(Indexes = ifelse(inherits(InputsModel[[id]], "SD"), 1, 2):nbParam, + hasX4 = grepl("RunModel_GR[456][HJ]", InputsModel[[id]]$FUN_MOD), + iX4 = ifelse(inherits(InputsModel[[id]], "SD"), 5, 4)) + } + # Add class InputsModel for airGR::Calibration checks + class(InputsModel) <- c("InputsModel", class(InputsModel)) + + ### Modify RunOptions for the reduced network ### + RunOptions <- reduceGRiwrmObj4Ungauged(g, RunOptions) + return(list(InputsModel = InputsModel, RunOptions = RunOptions)) +} + +updateRunOptions4Ungauged <- function(id, RunOptions) { + # Remove nodes outside of reduced network + RunOptions <- lapply(RunOptions, function(RO) { + if(RO$id %in% g$id) { + IM + } else { + NULL + } + }) + return(RunOptions) +} + + +#' RunModel for a sub-network of ungauged nodes +#' +#' The function simulates a network with one set of parameters +#' shared with ungauded nodes inside the basin. +#' +#' @details +#' The network should contains only one gauged station at downstream and other +#' nodes can be direct injection or ungauged nodes. +#' +#' This function works as functions similar to [airGR::RunModel_GR4J] except that +#' `InputsModel` is a *GRiwrmInputsModel* containing the network of ungauged nodes +#' and direct injection in the basin. +#' +#' `Param` is adjusted for each sub-basin using the method developped by +#' Lobligeois (2014) for GR models. +#' +#' @references Lobligeois, Florent. Mieux connaître la distribution spatiale des +#' pluies améliore-t-il la modélisation des crues ? Diagnostic sur 181 bassins +#' versants français. Phdthesis, AgroParisTech, 2014. +#' <https://pastel.archives-ouvertes.fr/tel-01134990/document> +#' +#' @inheritParams airGR::RunModel +#' +#' @inherit RunModel.GRiwrmInputsModel return return +#' @noRd +RunModel_Ungauged <- function(InputsModel, RunOptions, Param) { + InputsModel$FUN_MOD <- NULL + SBV <- sum(InputsModel[[length(InputsModel)]]$BasinAreas) + # Compute Param for each sub-basin + P <- lapply(InputsModel, function(IM) { + PS <- attr(IM, "ParamSettings") + p <- Param[PS$Indexes] + if(PS$hasX4) { + p[PS$iX4] <- Param[PS$iX4] * (IM$BasinAreas[length(IM$BasinAreas)] / SBV) ^ 0.3 + } + return(p) + }) + OM <- suppressMessages( + RunModel.GRiwrmInputsModel(InputsModel, attr(RunOptions, "GRiwrmRunOptions"), P) + ) + return(OM[[length(OM)]]) +} diff --git a/R/CreateRunOptions.GRiwrmInputsModel.R b/R/CreateRunOptions.GRiwrmInputsModel.R index e7f503c..7b49fae 100644 --- a/R/CreateRunOptions.GRiwrmInputsModel.R +++ b/R/CreateRunOptions.GRiwrmInputsModel.R @@ -8,6 +8,7 @@ CreateRunOptions.GRiwrmInputsModel <- function(x, IniStates = NULL, ...) { for(id in names(x)) { RunOptions[[id]] <- CreateRunOptions(x[[id]], IniStates = IniStates[[id]], ...) + RunOptions[[id]]$id <- id } return(RunOptions) } diff --git a/vignettes/V05_Modelling_ungauged_nodes.Rmd b/vignettes/V05_Modelling_ungauged_nodes.Rmd index 586b835..4aace25 100644 --- a/vignettes/V05_Modelling_ungauged_nodes.Rmd +++ b/vignettes/V05_Modelling_ungauged_nodes.Rmd @@ -25,20 +25,21 @@ library(airGRiwrm) ## Why modelling ungauged station in the semi-distributed model? -This vignette introduces the implementation in airGRiwrm of the model developped by @lobligeoisMieuxConnaitreDistribution2014 - -2 interests: +Ungauged nodes in the semi-distributed model can be used to reach two different goals: - increase spatial resolution of the rain fall to improve streamflow simulation [@lobligeoisWhenDoesHigher2014]. - simulate streamflows in location of interest for management purpose +This vignette introduces the implementation in airGRiwrm of the method developped by @lobligeoisMieuxConnaitreDistribution2014 for calibrating ungauged nodes in a +semi-distributed model. + ## Presentation of the study case -Using the study case of the vignette #1 and #2, we considere this time that nodes `54095`, `54001` and +Using the study case of the vignette #1 and #2, we considere this time that nodes `54001` and `54029` are ungauged. We simulate the streamflow at these locations by sharing hydrological parameters of the gauged node `54032`. -```{r, echo = FALSE} +```{r network, echo = FALSE} mmd <- function(x, ...) { # For avoiding crash of R CMD build in console mode if(Sys.getenv("RSTUDIO") == "1") { @@ -52,9 +53,10 @@ id95[54095] id01[54001] id29[54029] +id95 -->| 42 km| id01 + subgraph Shared parameters from node 54032 id01 -->| 45 km| 54032 -id95 -->| 42 km| id01 id29 -->| 32 km| 54032 end @@ -67,10 +69,10 @@ classDef IntUng fill:#efe classDef IntGau fill:#afa classDef DirInj fill:#faa -class id95,id29 UpUng +class id29 UpUng class 54057,54032 IntGau class id01 IntUng -class 54002 UpGau +class id95,54002 UpGau ") ``` @@ -85,11 +87,11 @@ With $X_4$ the unit hydrogram parameter for the entire basin at `54032` which as Ungauged stations are specified by using the model "Ungauged" in the `model` column provided in the `CreateGRiwrm` function: -```{r} +```{r griwrm} data(Severn) nodes <- Severn$BasinsInfo[, c("gauge_id", "downstream_id", "distance_downstream", "area")] nodes$model <- "RunModel_GR4J" -nodes$model[nodes$gauge_id %in% c("54095", "54029", "54001")] <- "Ungauged" +nodes$model[nodes$gauge_id %in% c("54029", "54001")] <- "Ungauged" griwrmV05 <- CreateGRiwrm( nodes, list(id = "gauge_id", down = "downstream_id", length = "distance_downstream") @@ -99,7 +101,7 @@ griwrmV05 On the following network scheme, the ungauged nodes are cleared than gauged ones with the same color (blue for upstream nodes and green for intermediate and downstream nodes) -```{r} +```{r plot_network} plot(griwrmV05) ``` @@ -108,7 +110,7 @@ plot(griwrmV05) The formatting of the input data is described in the vignette "V01_Structure_SD_model". The following code chunk resumes this formatting procedure: -```{r} +```{r obs} BasinsObs <- Severn$BasinsObs DatesR <- BasinsObs[[1]]$DatesR PrecipTot <- cbind(sapply(BasinsObs, function(x) {x$precipitation})) @@ -120,8 +122,73 @@ Qobs <- cbind(sapply(BasinsObs, function(x) {x$discharge_spec})) Then, the `GRiwrmInputsModel` object can be generated taking into account the new `GRiwrm` object: -```{r} +```{r InputsModel} IM_U <- CreateInputsModel(griwrmV05, DatesR, Precip, PotEvap, Qobs) ``` +## Calibration of the model integrating ungauged nodes + +Calibration options is detailed in vignette "V02_Calibration_SD_model". +We also apply a parameter regularization here but only where an upstream simulated catchment is available. + +The following code chunk resumes this procedure: + +```{r RunOptions} +IndPeriod_Run <- seq( + which(DatesR == (DatesR[1] + 365*24*60*60)), # Set aside warm-up period + length(DatesR) # Until the end of the time series +) +IndPeriod_WarmUp = seq(1,IndPeriod_Run[1]-1) +RunOptions <- CreateRunOptions(IM_U, + IndPeriod_WarmUp = IndPeriod_WarmUp, + IndPeriod_Run = IndPeriod_Run) +InputsCrit <- CreateInputsCrit(IM_U, + FUN_CRIT = ErrorCrit_KGE2, + RunOptions = RunOptions, Obs = Qobs[IndPeriod_Run,], + AprioriIds = c("54057" = "54032", "54032" = "54095"), + transfo = "sqrt", k = 0.15 +) +CalibOptions <- CreateCalibOptions(IM_U) +``` + +The **airGR** calibration process is applied on each hydrological node of the `GRiwrm` network from upstream nodes to downstream nodes but this time the calibration of the sub-basin `54032` invokes a semi-distributed model composed of the nodes `54029`, `54001` and `54032`. + +```{r Calibration} +OC_U <- suppressWarnings( + Calibration(IM_U, RunOptions, InputsCrit, CalibOptions)) +``` + +Hydrological parameters for sub-basins + +## Run the model with the optimized model parameters + +The hydrologic model uses parameters herited from the calibration of the gauged sub-basin `54032` for the ungauged nodes `54001` and `54029`: + +```{r param} +ParamV05 <- sapply(griwrmV05$id, function(x) {OC_U[[x]]$Param}) +dfParam <- do.call( + rbind, + lapply(ParamV05, function(x) + if(length(x)==4) {return(c(NA, x))} else return(x)) +) +colnames(dfParam) <- c("velocity", paste0("X", 1:4)) +knitr::kable(round(dfParam, 3)) +``` + +We can run the model with these calibrated parameters: + +```{r RunModel} +OutputsModels <- RunModel( + IM_U, + RunOptions = RunOptions, + Param = ParamV05 +) +``` + +and plot the comparison of the modelled and the observed flows including on the so-called "ungauged" stations : + +```{r plot, fig.height = 5, fig.width = 8} +plot(OutputsModels, Qobs = Qobs[IndPeriod_Run,], which = c("Regime", "CumFreq")) +``` + # References -- GitLab