Calibration of subcatchments defined by multiple gauges in a catchment
Jean-Michel Perraud 2020-01-28
Calibration of subcatchments defined by multiple gauges in a catchment
Use case
This vignette demonstrates how one can calibrate a catchment using multiple gauging points available within this catchment. Instead of setting up a whole-of-catchment calibration definition, it makes sense, at least in a system where subareas above a gauge points do not have a behavior dependent on other catchment processes (meaning mostly, no managed reservoirs). SWIFT offers capabilities to calibrate such subcatchments sequentially, feeding the input flow of upstream and already calibrated subcatchments to other subcatchments, thus cutting down on the complexity and runtime of the overall catchment calibration.
Data
The sample data that comes with the package contains a model definition for the South Esk catchment, including a short subset of the climate and flow record data.
library(swift)
library(DiagrammeR)
<- 'GR4J'
modelId <- 'South_Esk'
siteId <- sampleCatchmentModel(siteId=siteId, configId='catchment')
simulation # simulation <- swapModel(simulation, 'MuskingumNonLinear', 'channel_routing')
<- swapModel(simulation, 'LagAndRoute', 'channel_routing') simulation
A visual of the catchment structure (note: may not render yet through GitHub)
DiagrammeR(getCatchmentDotGraph(simulation))
<- sampleSeries(siteId=siteId, varName='climate')
seClimate <- sampleSeries(siteId=siteId, varName='flow') seFlows
The names of the climate series is already set to the climate input identifiers of the model simulation, so setting them as inputs is easy:
playInput(simulation, seClimate)
setSimulationSpan(simulation, start(seClimate), end(seClimate))
setSimulationTimeStep(simulation, 'hourly')
Moving on to define the parameters, free or fixed. We will use (for now - may change) the package calibragem, companion to SWIFT.
configureHourlyGr4j(simulation)
We define a function creating a realistic feasible parameter space. This is not the main object of this vignette, so we do not describe in details.
<- function(simulation, refArea=250, timeSpan=3600L) {
createMetaParameterizer <- as.integer(timeSpan)
timeSpan <- defineGr4jScaledParameter(refArea, timeSpan)
parameterizer
# Let's define _S0_ and _R0_ parameters such that for each GR4J model instance, _S = S0 * x1_ and _R = R0 * x3_
<- linearParameterizer(
pStates paramName=c("S0","R0"),
stateName=c("S","R"),
scalingVarName=c("x1","x3"),
minPval=c(0.0,0.0),
maxPval=c(1.0,1.0),
value=c(0.9,0.9),
selectorType='each subarea')
<- makeStateInitParameterizer(pStates)
initParameterizer <- concatenateParameterizers(parameterizer, initParameterizer)
parameterizer
<- function() {
lagAndRouteParameterizer <- data.frame(Name = c('alpha', 'inverse_velocity'),
p Value = c(1, 1),
Min = c(1e-3, 1e-3),
Max = c(1e2, 1e2),
stringsAsFactors = FALSE)
<- createParameterizer('Generic links', p)
p return(p)
}
# Lag and route has several discrete storage type modes. One way to set up the modeP
<- function(simulation) {
setupStorageType <- data.frame(Name = c('storage_type'),
p Value = 1,
Min = 1,
Max = 1,
stringsAsFactors = FALSE)
<- createParameterizer('Generic links', p)
p applySysConfig(p, simulation)
}setupStorageType(simulation)
# transfer reach lengths to the Lag and route model
<- getLinkIds(simulation)
linkIds <- getStateValue(simulation, paste0('link.', linkIds, '.Length'))
reachLengths setStateValue(simulation, paste0('link.', linkIds, '.reach_length'), reachLengths)
<- lagAndRouteParameterizer()
lnrp <- concatenateParameterizers(parameterizer, lnrp)
parameterizer return(parameterizer)
}
<- createMetaParameterizer(simulation)
parameterizer parameterizerAsDataFrame(parameterizer)
## Name Min Max Value
## 1 log_x4 0.000000 2.380211 0.3054223
## 2 log_x1 0.000000 3.778151 0.5066903
## 3 log_x3 0.000000 3.000000 0.3154245
## 4 asinh_x2 -3.989327 3.989327 2.6377523
## 5 R0 0.000000 1.000000 0.9000000
## 6 S0 0.000000 1.000000 0.9000000
## 7 alpha 0.001000 100.000000 1.0000000
## 8 inverse_velocity 0.001000 100.000000 1.0000000
Now, checking that a default parameter set works structurally on the simulation:
setParameterValue(parameterizer, 'asinh_x2', 0)
applySysConfig(parameterizer, simulation)
execSimulation(simulation)
We are now ready to enter the main topic of this vignette, subsetting the catchment into subcatchments for calibration purposes.
The sample gauge data flow contains identifiers that are of course distinct from the network node identifiers. We create a map between them (note - this information used to be in the NodeLink file in swiftv1), and we use these node as splitting points to derive subcatchments
<- as.character(c( 92106, 592002, 18311, 93044, 25, 181))
gauges names(gauges) <- paste0('node.', as.character( c(7, 12, 25, 30, 40, 43) ))
<- names(gauges)
splitElementIds <- splitToSubcatchments(simulation, splitElementIds)
subCats str(subCats)
## List of 6
## $ node.40:Formal class 'ExternalObjRef' [package "cinterop"] with 2 slots
## .. ..@ obj :<externalptr>
## .. ..@ type: chr "MODEL_SIMULATION_PTR"
## $ node.25:Formal class 'ExternalObjRef' [package "cinterop"] with 2 slots
## .. ..@ obj :<externalptr>
## .. ..@ type: chr "MODEL_SIMULATION_PTR"
## $ node.12:Formal class 'ExternalObjRef' [package "cinterop"] with 2 slots
## .. ..@ obj :<externalptr>
## .. ..@ type: chr "MODEL_SIMULATION_PTR"
## $ node.7 :Formal class 'ExternalObjRef' [package "cinterop"] with 2 slots
## .. ..@ obj :<externalptr>
## .. ..@ type: chr "MODEL_SIMULATION_PTR"
## $ node.30:Formal class 'ExternalObjRef' [package "cinterop"] with 2 slots
## .. ..@ obj :<externalptr>
## .. ..@ type: chr "MODEL_SIMULATION_PTR"
## $ node.43:Formal class 'ExternalObjRef' [package "cinterop"] with 2 slots
## .. ..@ obj :<externalptr>
## .. ..@ type: chr "MODEL_SIMULATION_PTR"
The resulting list of subcatchment simulations is already ordered in an upstream to downstream order by SWIFT.
If we are to set up the first step of the sequential calibration:
<- names(subCats)[1]
elementId
<- gauges[elementId]
gaugeId <- seFlows[,gaugeId]
gaugeFlow <- subCats[[elementId]]
sc applySysConfig(parameterizer,sc)
<- 'Catchment.StreamflowRate'
varId recordState(sc,varId)
DiagrammeR(getCatchmentDotGraph(sc))
Let’s view the default, uncalibrated output
<- function(obs, calc, ylab="streamflow (m3/s)") {
obsVsCalc ::plotTwoSeries(obs, calc, ylab=ylab, startTime = start(obs), endTime = end(obs))
joki }
execSimulation(sc)
obsVsCalc(gaugeFlow, getRecorded(sc, varId))
Now, setting up an objective (NSE) and optimizer:
<- 'NSE'
objectiveId <- createObjective(sc, varId, observation=gaugeFlow, objectiveId, start(seFlows), end(seFlows))
objective <- getScore(objective,parameterizer) score
<- getMarginalTermination( tolerance = 1e-04, cutoffNoImprovement = 30, maxHours = 2/60)
termination <- swift::CreateSceTerminationWila_Pkg_R('relative standard deviation', c('0.05','0.0167'))
termination <- getDefaultSceParameters()
sceParams <- parameterizerAsDataFrame(parameterizer)
params <- length(which(abs(params$Max-params$Min)>0))
npars <- SCEParameters(npars)
sceParams <- createSceOptimSwift(objective,terminationCriterion = termination, populationInitializer = parameterizer,SCEpars = sceParams)
optimizer <- setCalibrationLogger(optimizer,"dummy")
calibLogger
<- lubridate::now();
optimStartTime <- executeOptimization(optimizer)
calibResults <- lubridate::now();
optimEndTime <- lubridate::as.duration(lubridate::interval(optimStartTime, optimEndTime))
optimWallClock
optimWallClock
## [1] "11.4364669322968s"
And the resulting hydrograph follows. The NSE score is decent, but the magnitude of the peak is not well represented. We used a uniform value for the routing parameters; having a scaling based on link properties may be a line of enquiry.
<- sortByScore(calibResults, 'NSE')
sortedResults head(scoresAsDataFrame(sortedResults))
## NSE log_x4 log_x1 log_x3 asinh_x2 R0 S0
## 1 0.4949697 2.082811 0.8537391 0.5311710 0.2138180 0.2610210 0.3526996
## 2 0.4949311 2.089526 0.8630253 0.5096557 0.2259685 0.1997594 0.3133773
## 3 0.4947418 2.080529 0.8402662 0.5453678 0.2241863 0.2453242 0.2806822
## 4 0.4947411 2.087740 0.8549421 0.4918872 0.2107718 0.2001515 0.3279347
## 5 0.4947155 2.082976 0.8540680 0.5326692 0.2326300 0.3102163 0.3848358
## 6 0.4947014 2.099333 0.8598832 0.4789433 0.1990987 0.2159813 0.3402485
## alpha inverse_velocity
## 1 57.43339 44.65305
## 2 50.28133 52.15916
## 3 54.01892 47.18598
## 4 63.34453 52.55784
## 5 63.39566 46.43670
## 6 57.53490 50.23801
<- swift::getScoreAtIndex(sortedResults, 1)
p <- GetSystemConfigurationWila_R(p)
p parameterizerAsDataFrame(p)
## Name Min Max Value
## 1 log_x4 0.000000 2.380211 2.0828106
## 2 log_x1 0.000000 3.778151 0.8537391
## 3 log_x3 0.000000 3.000000 0.5311710
## 4 asinh_x2 -3.989327 3.989327 0.2138180
## 5 R0 0.000000 1.000000 0.2610210
## 6 S0 0.000000 1.000000 0.3526996
## 7 alpha 0.001000 100.000000 57.4333924
## 8 inverse_velocity 0.001000 100.000000 44.6530483
applySysConfig(p, sc)
execSimulation(sc)
obsVsCalc(gaugeFlow, getRecorded(sc, varId))
We can create a subcatchment parameterizer, such that when applied to the whole of the South Esk, only the states of the subareas, links and nodes of the subcatchment are potentially affected.
<- subcatchmentParameterizer(p, sc)
sp applySysConfig(sp, simulation)
getStateValue(simulation, paste0('subarea.', 34:40, '.x2'))
## subarea.34.x2 subarea.35.x2 subarea.36.x2 subarea.37.x2 subarea.38.x2
## 0.0000000 0.0000000 0.0000000 0.1443521 0.1443521
## subarea.39.x2 subarea.40.x2
## 0.1443521 0.0000000
# saIds <- getSubareaIds(simulation)
<- tempfile()
spFile SaveParameterizer_R(sp, spFile)
# as of May 2016, cannot reload this on Linux. There is an issue leading to a segfault, possibly related to having added support nan, inf as numeric values.
# as of July 2016, this works on Windows however. Try and see.
<- LoadParameterizer_R(spFile)
sp2
if(file.exists(spFile)) { file.remove(spFile) }
## [1] TRUE
Whole of catchment calibration combining point gauges
<- as.character(c( 92106, 592002, 18311, 93044, 25, 181))
gauges names(gauges) <- paste0('node.', as.character( c(7, 12, 25, 30, 40, 43) ))
<- paste0('node.', as.character( c(7, 12) ))
calibNodes
<- names(subCats)[1]
elementId
<- gauges[calibNodes]
gaugeId <- seFlows[,gaugeId]
gaugeFlow
<- paste0(calibNodes, '.OutflowRate')
varId recordState(simulation,varId)
<- 'NSE'
objectiveId <- createObjective(simulation, varId[1], observation=gaugeFlow[,1], objectiveId, start(seFlows), end(seFlows))
objective
<- function(compositeObj, objective, weight, name) {
addToCompositeObjective <- objective
obj if (cinterop::isExternalObjRef(objective, 'OBJECTIVE_EVALUATOR_WILA_PTR')) {
<- UnwrapObjectiveEvaluatorWila_R(objective)
obj
}AddSingleObservationObjectiveEvaluator_R(compositeObj, obj, weight, name)
}
<- CreateEmptyCompositeObjectiveEvaluator_R()
co addToCompositeObjective(co, objective, 1.0, varId[1])
<- createObjective(simulation, varId[2], observation=gaugeFlow[,2], objectiveId, start(seFlows), end(seFlows))
objective addToCompositeObjective(co, objective, 1.0, varId[2])
<- swift::WrapObjectiveEvaluatorWila_R(co, clone=TRUE)
co
<- getScore(co,parameterizer)
score # scoresAsDataFrame(score)