This vignette showcases a simplified version of the Bayesian
calibration to with a joint log-likelihood to multiple target variables
taken from the documentation paper Paredes et
al. (2025). The separate vignette ‘Parameter calibration (P-Model)
and cost functions’ (see vignette("new_cost_function"))
provides simpler examples.
You may be interested in calibrating the model to different target variables simultaneously, like flux and leaf trait measurements.
In this section we present an example, where we use
cost_likelihood_pmodel() to compute the joint likelihood of
all the targets specified (that is, by summing the log-likelihoods of
GPP and bigD13C) and ultimately calibrate the
beta_unitcostratio parameter. In the section after, we
calibrate another example to multiple sites.
# Define calibration settings for two targets
params_to_estimate <- list(
beta_unitcostratio = list(lower = 14.6, upper = 440, init = 146.0), # uniform priors
kc_jmax = list(lower = 0.2, upper = 0.8, init = 0.41), # uniform priors
err_gpp = list(lower = 0.001, upper = 4, init = 1),
err_bigD13C = list(lower = 0.01, upper = 3, init = 0.8))
params_fixed <- list( # fix parameter value from previous calibration
kphio = 0.041,
kphio_par_a = 0.0,
kphio_par_b = 16,
soilm_thetastar = 0.6 * 240, # to recover paper setup with soil moisture stress
soilm_betao = 0.01,
rd_to_vcmax = 0.014, # value from Atkin et al. 2015 for C3 herbaceous
tau_acclim = 30.0)
settings_joint_likelihood <- list(
method = "BayesianTools",
metric = rsofun::cost_likelihood_pmodel,
control = list(
sampler_runMCMC = "DREAMzs",
settings_runMCMC = list(
# burnin = 1500, # kept artificially low
# iterations = 3000,
burnin = 0,
iterations = 50, # kept artificially low
nrChains = 1
),
# optional parallelization
n_parallel_nrChains = 1 # 2, this can be parallelized
),
# parameters
par = params_to_estimate
)
# Run the calibration on the observational data of two sites
drivers <- pmodel_drivers |>
dplyr::filter(sitename %in% c("FR-Pue", "lon_+146.13_lat_-032.97"))
obs <- pmodel_validation |>
dplyr::filter(sitename %in% c("FR-Pue", "lon_+146.13_lat_-032.97"))
par_calib_join <- calib_sofun(
drivers = drivers,
obs = obs,
settings_calib = settings_joint_likelihood,
# arguments for the cost function
targets = c("gpp"="fluxnet", "bigD13C"="cornwell"),
par_fixed = params_fixed
)Calibration with calib_sofun returns an object that
contains the maximum-a-posteriori (MAP) estimates of the parameters in
par and the full distribution of the parameters in the MCMC
chains in mod. Besides those it also reports additional
information.
par_calib_join
#> $par
#> beta_unitcostratio kc_jmax err_gpp err_bigD13C
#> 137.3691802 0.4620867 1.0686654 1.4392762
#>
#> $mod
#> [1] "mcmcSamplerList - you can use the following methods to summarize, plot or reduce this class:"
#> [1] plot print summary
#> see '?methods' for accessing help and source code
#>
#> $name
#> [1] "out_calib_my_calibration_name.rds.log.txt"
#>
#> $logpath
#> [1] ""
#>
#> $walltime
#> Time difference of 0.991451 secsNote that GPP predictions are directly compared to GPP observations
on that day, but the leaf trait bigD13C is predicted by the P-model as
“onestep” prediction for average conditions over the growing season and
compared to the (multiple) bigD13C observations taken across trees per
site. This is specified by the arguments drivers,
obs, and targets. Namely, it is specified by
the column params_siml$onestep within drivers,
which can be FALSE (for daily simulation) or
TRUE (for onestep acclimation), by the corresponding column
forcing containing daily or average conditions, and by the
column source specified for each simulation site (i.e. each
row in obs) which is used by the targets
argument to specify what variable to calibrate from each source data
set.
For more details, unfold the code below or check the documentation of
pmodel_drivers and pmodel_validation.
# Note the column 'data' (containing observations)
pmodel_validation
#> # A tibble: 20 × 3
#> sitename source data
#> <chr> <chr> <list>
#> 1 CH-Dav fluxnet <tibble [714 × 7]>
#> 2 CZ-BK1 fluxnet <tibble [479 × 7]>
#> 3 DK-Sor fluxnet <tibble [720 × 7]>
#> 4 FI-Hyy fluxnet <tibble [704 × 7]>
#> 5 FR-Pue fluxnet <tibble [1,924 × 7]>
#> 6 GF-Guy fluxnet <tibble [319 × 7]>
#> 7 US-Ha1 fluxnet <tibble [502 × 7]>
#> 8 US-PFa fluxnet <tibble [669 × 7]>
#> 9 lon_+010.52_lat_+051.08 cornwell <tibble [7 × 2]>
#> 10 lon_+011.10_lat_+048.30 cornwell <tibble [1 × 2]>
#> 11 lon_+112.58_lat_+023.13 cornwell <tibble [85 × 2]>
#> 12 lon_+145.13_lat_-005.83 cornwell <tibble [1 × 2]>
#> 13 lon_+146.13_lat_-032.97 cornwell <tibble [16 × 2]>
#> 14 lon_+148.30_lat_-036.10 cornwell <tibble [2 × 2]>
#> 15 lon_+153.00_lat_-026.85 cornwell <tibble [25 × 2]>
#> 16 lon_-079.10_lat_+035.97 cornwell <tibble [3 × 2]>
#> 17 lon_-116.45_lat_+047.16 cornwell <tibble [18 × 2]>
#> 18 lon_-119.82_lat_+034.50 cornwell <tibble [13 × 2]>
#> 19 lon_-122.98_lat_+038.40 cornwell <tibble [1 × 2]>
#> 20 lon_-149.61_lat_+063.97 cornwell <tibble [1 × 2]>
cornwell_sites <- pmodel_validation$sitename[pmodel_validation$source == "cornwell"]
fluxnet_sites <- pmodel_validation$sitename[pmodel_validation$source == "fluxnet"]
pmodel_validation |> dplyr::filter(source == "cornwell") |> tidyr::unnest(data)
#> # A tibble: 173 × 4
#> sitename source id bigD13C
#> <chr> <chr> <int> <dbl>
#> 1 lon_+010.52_lat_+051.08 cornwell 1 19.4
#> 2 lon_+010.52_lat_+051.08 cornwell 2 19.7
#> 3 lon_+010.52_lat_+051.08 cornwell 3 19.5
#> 4 lon_+010.52_lat_+051.08 cornwell 4 18.0
#> 5 lon_+010.52_lat_+051.08 cornwell 5 20.0
#> 6 lon_+010.52_lat_+051.08 cornwell 6 19.4
#> 7 lon_+010.52_lat_+051.08 cornwell 7 18.2
#> 8 lon_+011.10_lat_+048.30 cornwell 1 22.2
#> 9 lon_+112.58_lat_+023.13 cornwell 1 22.8
#> 10 lon_+112.58_lat_+023.13 cornwell 2 21.7
#> # ℹ 163 more rows
pmodel_validation |> dplyr::filter(source == "fluxnet") |> tidyr::unnest(data)
#> # A tibble: 6,031 × 9
#> sitename source date gpp gpp_qc nee nee_qc le le_qc
#> <chr> <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 CH-Dav fluxnet 2007-01-01 0.855 1 1.44 1 9.49 1
#> 2 CH-Dav fluxnet 2007-01-02 -0.943 1 2.62 1 65.0 1
#> 3 CH-Dav fluxnet 2007-01-03 -0.951 1 2.39 1 48.6 1
#> 4 CH-Dav fluxnet 2007-01-04 1.23 0.979 0.958 0.979 14.7 1
#> 5 CH-Dav fluxnet 2007-01-05 1.36 1 0.992 1 37.5 1
#> 6 CH-Dav fluxnet 2007-01-06 1.67 1 1.02 1 31.4 1
#> 7 CH-Dav fluxnet 2007-01-07 2.27 0.979 0.575 0.979 16.9 1
#> 8 CH-Dav fluxnet 2007-01-08 2.00 1 0.854 1 21.5 1
#> 9 CH-Dav fluxnet 2007-01-09 1.20 0.979 1.53 0.979 24.6 1
#> 10 CH-Dav fluxnet 2007-01-10 0.732 0.958 1.72 0.958 28.3 1
#> # ℹ 6,021 more rows
pmodel_drivers |> dplyr::filter(sitename %in% cornwell_sites) |> tidyr::unnest(forcing)
#> # A tibble: 12 × 8
#> sitename params_siml site_info temp vpd ppfd co2 patm
#> <chr> <list> <list> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 lon_+010.52_lat_+051.… <tibble> <tibble> 11.6 363. 2.61e-4 370. 9.75e4
#> 2 lon_+011.10_lat_+048.… <tibble> <tibble> 12.4 399. 2.99e-4 370. 9.53e4
#> 3 lon_+112.58_lat_+023.… <tibble> <tibble> 23.7 666. 3.27e-4 370. 9.92e4
#> 4 lon_+145.13_lat_-005.… <tibble> <tibble> 16.0 382. 3.46e-4 370. 7.29e4
#> 5 lon_+146.13_lat_-032.… <tibble> <tibble> 21.5 1231. 4.27e-4 370. 9.94e4
#> 6 lon_+148.30_lat_-036.… <tibble> <tibble> 10.7 384. 3.82e-4 370. 8.60e4
#> 7 lon_+153.00_lat_-026.… <tibble> <tibble> 23.6 813. 4.28e-4 370. 1.01e5
#> 8 lon_-079.10_lat_+035.… <tibble> <tibble> 19.0 762. 3.58e-4 370. 9.94e4
#> 9 lon_-116.45_lat_+047.… <tibble> <tibble> 14.3 696. 3.85e-4 370. 9.16e4
#> 10 lon_-119.82_lat_+034.… <tibble> <tibble> 18.3 818. 4.23e-4 370. 9.43e4
#> 11 lon_-122.98_lat_+038.… <tibble> <tibble> 17.1 651. 3.84e-4 370. 9.81e4
#> 12 lon_-149.61_lat_+063.… <tibble> <tibble> 10.9 495. 3.24e-4 370. 9.27e4
pmodel_drivers |> dplyr::filter(sitename %in% fluxnet_sites) |> tidyr::unnest(forcing)
#> # A tibble: 7,300 × 17
#> sitename params_siml site_info date temp vpd ppfd netrad
#> <chr> <list> <list> <date> <dbl> <dbl> <dbl> <dbl>
#> 1 CH-Dav <tibble [1 × 12]> <tibble> 2007-01-01 2.87 234. 4.26e-5 -28.0
#> 2 CH-Dav <tibble [1 × 12]> <tibble> 2007-01-02 -4.67 110. 6.21e-5 -0.394
#> 3 CH-Dav <tibble [1 × 12]> <tibble> 2007-01-03 -3.67 101. 9.80e-5 -7.05
#> 4 CH-Dav <tibble [1 × 12]> <tibble> 2007-01-04 0.463 45.1 7.24e-5 -0.0927
#> 5 CH-Dav <tibble [1 × 12]> <tibble> 2007-01-05 -1.30 95.3 1.55e-4 23.4
#> 6 CH-Dav <tibble [1 × 12]> <tibble> 2007-01-06 0.982 149. 1.50e-4 -21.3
#> 7 CH-Dav <tibble [1 × 12]> <tibble> 2007-01-07 2.01 149. 1.52e-4 -15.4
#> 8 CH-Dav <tibble [1 × 12]> <tibble> 2007-01-08 2.05 270. 1.22e-4 -26.7
#> 9 CH-Dav <tibble [1 × 12]> <tibble> 2007-01-09 2.21 20.9 6.22e-5 6.76
#> 10 CH-Dav <tibble [1 × 12]> <tibble> 2007-01-10 6.19 237. 1.57e-4 -37.7
#> # ℹ 7,290 more rows
#> # ℹ 9 more variables: patm <dbl>, snow <dbl>, rain <dbl>, tmin <dbl>,
#> # tmax <dbl>, wind <dbl>, fapar <dbl>, co2 <dbl>, ccov <dbl>
pmodel_drivers |> dplyr::filter(sitename %in% cornwell_sites) |> tidyr::unnest(params_siml)
#> # A tibble: 12 × 5
#> sitename lc4 onestep site_info forcing
#> <chr> <lgl> <lgl> <list> <list>
#> 1 lon_+010.52_lat_+051.08 FALSE TRUE <tibble [1 × 4]> <tibble [1 × 5]>
#> 2 lon_+011.10_lat_+048.30 FALSE TRUE <tibble [1 × 4]> <tibble [1 × 5]>
#> 3 lon_+112.58_lat_+023.13 FALSE TRUE <tibble [1 × 4]> <tibble [1 × 5]>
#> 4 lon_+145.13_lat_-005.83 FALSE TRUE <tibble [1 × 4]> <tibble [1 × 5]>
#> 5 lon_+146.13_lat_-032.97 FALSE TRUE <tibble [1 × 4]> <tibble [1 × 5]>
#> 6 lon_+148.30_lat_-036.10 FALSE TRUE <tibble [1 × 4]> <tibble [1 × 5]>
#> 7 lon_+153.00_lat_-026.85 FALSE TRUE <tibble [1 × 4]> <tibble [1 × 5]>
#> 8 lon_-079.10_lat_+035.97 FALSE TRUE <tibble [1 × 4]> <tibble [1 × 5]>
#> 9 lon_-116.45_lat_+047.16 FALSE TRUE <tibble [1 × 4]> <tibble [1 × 5]>
#> 10 lon_-119.82_lat_+034.50 FALSE TRUE <tibble [1 × 4]> <tibble [1 × 5]>
#> 11 lon_-122.98_lat_+038.40 FALSE TRUE <tibble [1 × 4]> <tibble [1 × 5]>
#> 12 lon_-149.61_lat_+063.97 FALSE TRUE <tibble [1 × 4]> <tibble [1 × 5]>
pmodel_drivers |> dplyr::filter(sitename %in% fluxnet_sites) |> tidyr::unnest(params_siml)
#> # A tibble: 8 × 15
#> sitename spinup spinupyears recycle outdt ltre ltne ltrd ltnd lgr3 lgn3
#> <chr> <lgl> <dbl> <dbl> <dbl> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl>
#> 1 CH-Dav TRUE 10 1 1 FALSE FALSE FALSE FALSE TRUE FALSE
#> 2 CZ-BK1 TRUE 10 1 1 FALSE FALSE FALSE FALSE TRUE FALSE
#> 3 DK-Sor TRUE 10 1 1 FALSE FALSE FALSE FALSE TRUE FALSE
#> 4 FI-Hyy TRUE 10 1 1 FALSE FALSE FALSE FALSE TRUE FALSE
#> 5 FR-Pue TRUE 10 1 1 FALSE FALSE FALSE FALSE TRUE FALSE
#> 6 GF-Guy TRUE 10 1 1 FALSE FALSE FALSE FALSE TRUE FALSE
#> 7 US-Ha1 TRUE 10 1 1 FALSE FALSE FALSE FALSE TRUE FALSE
#> 8 US-PFa TRUE 10 1 1 FALSE FALSE FALSE FALSE TRUE FALSE
#> # ℹ 4 more variables: lgr4 <lgl>, onestep <lgl>, site_info <list>,
#> # forcing <list>Below we define some helper functions to facilitate model calibration with different target variables, resulting thus in different calibration setups. These helper functions simplify the use of parallel execution and avoid code repetition.
Here, we showcase two different setups. The first setup uses only
D13C as target variable (corresponds to Setup S1 in the documentation
paper). The second setup uses D13C and GPP as calibration targets and
(truncated) normal priors for the parameters (Setup S6). In below code,
we define first these two different calibration setups by specifying the
function setup_rsofun_calibration(), that returns an object
with the necessary specifications.
These specifications can differ in the selection of calibration
parameters and priors, in the data used and potentially in the
test-train split. Here, setups are uniquely identified with a numeric
identifer. Below code defines setup_rsofun_calibration()
for setups 226 and 231.
# 226: Setup c) Delta^'13 C*',VJ
# 231: Setup h) Delta^'13 C*',VJ, GPP
setup_rsofun_calibration <- function(scenario) {
## Load forcing and targets data from disk (here now from package) ----
bigD13C_vj_gpp_drivers <- rsofun::pmodel_drivers
bigD13C_vj_gpp_obs <- rsofun::pmodel_validation
# ## Read test-train split from disk ----
# read_csv(here::here("data/01_test_train_split.csv"))
# Not needed here for vignette
## Preprocess observation data (gpp) ----
## # no additinal QC needed
## Apply test-train split to data ----
train_drivers <- bigD13C_vj_gpp_drivers
train_obs <- bigD13C_vj_gpp_obs
test_drivers <- bigD13C_vj_gpp_drivers |> slice(0)
test_obs <- bigD13C_vj_gpp_obs |> slice(0)
## Setup the settings for the different calibration scenarios ----
## Define default parameter
default_par_fixed <- list( # fix parameter value from previous calibration
kphio = 0.04998, # value from Stocker et al. 2020
kphio_par_a = 0.0, # 0 corresponds to no temperature dependency of kphio (ORG setup in Stocker et al. 2020)
kphio_par_b = 1.0, #
soilm_thetastar = 0.6 * 240, # to recover paper setup with soil moisture stress
soilm_betao = 0.01, # 1 corresponds to no reduction, 0 to full reduction at theta==0
beta_unitcostratio = 146.0, # value from Stocker et al. 2020
rd_to_vcmax = 0.014, # value from Atkin et al. 2015 for C3 herbaceous
tau_acclim = 14.0, # value from Liu et al. 2024
kc_jmax = 0.41 # value from Stocker et al. 2024 (citing Wang et al. 2017)
)
## Define parameters to estimate and their priors
if (scenario %in% c(223, 226, 231)) { # 231 is like 223 but using posteriors from 226
par_to_estimate <- list(
kphio = list(lower = 0.02, upper = 0.15, init = 0.05),
kphio_par_a = list(lower = -0.004, upper = -0.001, init = -0.0025),
kphio_par_b = list(lower = 10, upper = 30, init = 20),
soilm_thetastar = list(lower = 1, upper = 250, init = 40),
beta_unitcostratio = as.list(c(lower = 0.1, upper = 3.0, init = 1.0) * 146.0),
# truncated normal, with ~14 days as mean, taken from Liu et al.
# 2024, Nat.Plants and Mäkelä et al. 2004, Tree Phys.
tau_acclim = list(mean = 14, sd = 8, lower = 0.01, upper = 40),
kc_jmax = as.list(0.41 * c(lower = 0.1, upper = 3.0, init = 1.0)),
err_gpp = list(lower = 0.01, upper = 3, init = 0.8),
err_bigD13C = list(lower = 0.01, upper = 3, init = 0.8)
)
if (scenario %in% c(223, 226, 231)) {
par_to_estimate$err_bigD13C <- list(lower = 0.01, upper = 15, init = 0.8)
}
} else {
stop(sprintf("Unsupported scenario: %d", scenario))
}
if (scenario %in% c(231)) { # 231 is like 223 but using posteriors from 226
# for beta_unitcostratio and kc_jmax as priors
# Below was done iteratively
# calib_scen226 <- readr::read_rds(file.path(rsofun_doc_output_path, "data","calibrations","out_calib__scen226_DREAMzs-100000-0iter_8x3chains_on_CPU8x1_continued.rds"))
# # i) extract samples as a data.frame
# burnins_scen226 <- 30000
# samples_scen226 <- getSample(calib_scen226$mod, thin = 1, start = burnins_scen226) %>% as.data.frame()
#
# # ii) fit normal and lognormal distributions for each parameter
# param_normals_scen226 <- lapply(setNames(names(samples_scen226), names(samples_scen226)), function(p) {
# list(mean = mean(samples_scen226[[p]]),
# sd = sd( samples_scen226[[p]]))
# })[c('beta_unitcostratio', 'kc_jmax')] # only keep these
#
# # then pass on these as prior for these
# par_to_estimate$beta_unitcostratio <- param_normals_scen226$beta_unitcostratio
# par_to_estimate$kc_jmax <- param_normals_scen226$kc_jmax
par_to_estimate$beta_unitcostratio <- list(mean = 207.86, sd = 6.79)
par_to_estimate$kc_jmax <- list(mean = 0.4244, sd = 0.0217)
if (scenario %in% c(231)) {
# truncated normal:
par_to_estimate$beta_unitcostratio <- list(mean = 207.86, sd = 6.79,
lower = 207.86 - 3 * 6.79,
upper = 207.86 + 3 * 6.79)
# normal:
par_to_estimate$kc_jmax <- list(mean = 0.4244, sd = 0.0217)
}
}
# Remove parameters that are defined to be estimated from default_par_fixed
par_to_fix <- default_par_fixed[!(names(default_par_fixed) %in% names(par_to_estimate))]
## Setup the data (drivers and obs) for the three calibration scenarios ----
# Subset different combination sites to define target variables.
# For easier handling do this in combined drivobs-object.
drivobs_train_bigD13C_vj_gpp <- dplyr::inner_join(
train_drivers,
train_obs,
by = join_by(sitename))
drivobs_test_bigD13C_vj_gpp <- dplyr::inner_join(
test_drivers,
test_obs,
by = join_by(sitename))
all_potential_targets <- c("bigD13C" = "cornwell", "gpp" = "fluxnet", "le" = "fluxnet")
if (scenario %in% c(223, 224, 225, 229, 230, 231)) { # GPP and traits data
targets_to_keep <- c("gpp", "bigD13C")
} else if (scenario %in% c(226)) { # only traits data, either both, or vj only, or bigD13C only
targets_to_keep <- c("bigD13C")
# } else if (scenario %in% c(220)) { # only GPP data from FR-Pue
# targets_to_keep <- c("gpp")
# sites_to_keep <- "FR-Pue"
} else {
stop(sprintf("Unsupported scenario: %d", scenario))
}
stopifnot(all(targets_to_keep %in% names(all_potential_targets)))
targets_to_request <- all_potential_targets[targets_to_keep]
drivobs_train <- drivobs_train_bigD13C_vj_gpp |>
# keep only sites with targets we want
dplyr::filter(source %in% unname(targets_to_request))
drivobs_test <- drivobs_test_bigD13C_vj_gpp ## for the test data set keep all
## return ---
return(list(
drivobs_train = drivobs_train,
drivobs_test = drivobs_test,
targets = targets_to_request,
par_fixed = par_to_fix,
par = par_to_estimate
))
}
setup_rsofun_calibration(226)
#> $drivobs_train
#> # A tibble: 12 × 6
#> sitename params_siml site_info forcing source data
#> <chr> <list> <list> <list> <chr> <list>
#> 1 lon_+010.52_lat_+051.08 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 2 lon_+011.10_lat_+048.30 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 3 lon_+112.58_lat_+023.13 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 4 lon_+145.13_lat_-005.83 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 5 lon_+146.13_lat_-032.97 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 6 lon_+148.30_lat_-036.10 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 7 lon_+153.00_lat_-026.85 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 8 lon_-079.10_lat_+035.97 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 9 lon_-116.45_lat_+047.16 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 10 lon_-119.82_lat_+034.50 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 11 lon_-122.98_lat_+038.40 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 12 lon_-149.61_lat_+063.97 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#>
#> $drivobs_test
#> # A tibble: 0 × 6
#> # ℹ 6 variables: sitename <chr>, params_siml <list>, site_info <list>,
#> # forcing <list>, source <chr>, data <list>
#>
#> $targets
#> bigD13C
#> "cornwell"
#>
#> $par_fixed
#> $par_fixed$soilm_betao
#> [1] 0.01
#>
#> $par_fixed$rd_to_vcmax
#> [1] 0.014
#>
#>
#> $par
#> $par$kphio
#> $par$kphio$lower
#> [1] 0.02
#>
#> $par$kphio$upper
#> [1] 0.15
#>
#> $par$kphio$init
#> [1] 0.05
#>
#>
#> $par$kphio_par_a
#> $par$kphio_par_a$lower
#> [1] -0.004
#>
#> $par$kphio_par_a$upper
#> [1] -0.001
#>
#> $par$kphio_par_a$init
#> [1] -0.0025
#>
#>
#> $par$kphio_par_b
#> $par$kphio_par_b$lower
#> [1] 10
#>
#> $par$kphio_par_b$upper
#> [1] 30
#>
#> $par$kphio_par_b$init
#> [1] 20
#>
#>
#> $par$soilm_thetastar
#> $par$soilm_thetastar$lower
#> [1] 1
#>
#> $par$soilm_thetastar$upper
#> [1] 250
#>
#> $par$soilm_thetastar$init
#> [1] 40
#>
#>
#> $par$beta_unitcostratio
#> $par$beta_unitcostratio$lower
#> [1] 14.6
#>
#> $par$beta_unitcostratio$upper
#> [1] 438
#>
#> $par$beta_unitcostratio$init
#> [1] 146
#>
#>
#> $par$tau_acclim
#> $par$tau_acclim$mean
#> [1] 14
#>
#> $par$tau_acclim$sd
#> [1] 8
#>
#> $par$tau_acclim$lower
#> [1] 0.01
#>
#> $par$tau_acclim$upper
#> [1] 40
#>
#>
#> $par$kc_jmax
#> $par$kc_jmax$lower
#> [1] 0.041
#>
#> $par$kc_jmax$upper
#> [1] 1.23
#>
#> $par$kc_jmax$init
#> [1] 0.41
#>
#>
#> $par$err_gpp
#> $par$err_gpp$lower
#> [1] 0.01
#>
#> $par$err_gpp$upper
#> [1] 3
#>
#> $par$err_gpp$init
#> [1] 0.8
#>
#>
#> $par$err_bigD13C
#> $par$err_bigD13C$lower
#> [1] 0.01
#>
#> $par$err_bigD13C$upper
#> [1] 15
#>
#> $par$err_bigD13C$init
#> [1] 0.8
setup_rsofun_calibration(226)$drivobs_train
#> # A tibble: 12 × 6
#> sitename params_siml site_info forcing source data
#> <chr> <list> <list> <list> <chr> <list>
#> 1 lon_+010.52_lat_+051.08 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 2 lon_+011.10_lat_+048.30 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 3 lon_+112.58_lat_+023.13 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 4 lon_+145.13_lat_-005.83 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 5 lon_+146.13_lat_-032.97 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 6 lon_+148.30_lat_-036.10 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 7 lon_+153.00_lat_-026.85 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 8 lon_-079.10_lat_+035.97 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 9 lon_-116.45_lat_+047.16 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 10 lon_-119.82_lat_+034.50 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 11 lon_-122.98_lat_+038.40 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
#> 12 lon_-149.61_lat_+063.97 <tibble [1 × 2]> <tibble> <tibble> cornwell <tibble>
setup_rsofun_calibration(231)
#> $drivobs_train
#> # A tibble: 20 × 6
#> sitename params_siml site_info forcing source data
#> <chr> <list> <list> <list> <chr> <list>
#> 1 CH-Dav <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 2 CZ-BK1 <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 3 DK-Sor <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 4 FI-Hyy <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 5 FR-Pue <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 6 GF-Guy <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 7 US-Ha1 <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 8 US-PFa <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 9 lon_+010.52_lat_+051.08 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 10 lon_+011.10_lat_+048.30 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 11 lon_+112.58_lat_+023.13 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 12 lon_+145.13_lat_-005.83 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 13 lon_+146.13_lat_-032.97 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 14 lon_+148.30_lat_-036.10 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 15 lon_+153.00_lat_-026.85 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 16 lon_-079.10_lat_+035.97 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 17 lon_-116.45_lat_+047.16 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 18 lon_-119.82_lat_+034.50 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 19 lon_-122.98_lat_+038.40 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 20 lon_-149.61_lat_+063.97 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#>
#> $drivobs_test
#> # A tibble: 0 × 6
#> # ℹ 6 variables: sitename <chr>, params_siml <list>, site_info <list>,
#> # forcing <list>, source <chr>, data <list>
#>
#> $targets
#> gpp bigD13C
#> "fluxnet" "cornwell"
#>
#> $par_fixed
#> $par_fixed$soilm_betao
#> [1] 0.01
#>
#> $par_fixed$rd_to_vcmax
#> [1] 0.014
#>
#>
#> $par
#> $par$kphio
#> $par$kphio$lower
#> [1] 0.02
#>
#> $par$kphio$upper
#> [1] 0.15
#>
#> $par$kphio$init
#> [1] 0.05
#>
#>
#> $par$kphio_par_a
#> $par$kphio_par_a$lower
#> [1] -0.004
#>
#> $par$kphio_par_a$upper
#> [1] -0.001
#>
#> $par$kphio_par_a$init
#> [1] -0.0025
#>
#>
#> $par$kphio_par_b
#> $par$kphio_par_b$lower
#> [1] 10
#>
#> $par$kphio_par_b$upper
#> [1] 30
#>
#> $par$kphio_par_b$init
#> [1] 20
#>
#>
#> $par$soilm_thetastar
#> $par$soilm_thetastar$lower
#> [1] 1
#>
#> $par$soilm_thetastar$upper
#> [1] 250
#>
#> $par$soilm_thetastar$init
#> [1] 40
#>
#>
#> $par$beta_unitcostratio
#> $par$beta_unitcostratio$mean
#> [1] 207.86
#>
#> $par$beta_unitcostratio$sd
#> [1] 6.79
#>
#> $par$beta_unitcostratio$lower
#> [1] 187.49
#>
#> $par$beta_unitcostratio$upper
#> [1] 228.23
#>
#>
#> $par$tau_acclim
#> $par$tau_acclim$mean
#> [1] 14
#>
#> $par$tau_acclim$sd
#> [1] 8
#>
#> $par$tau_acclim$lower
#> [1] 0.01
#>
#> $par$tau_acclim$upper
#> [1] 40
#>
#>
#> $par$kc_jmax
#> $par$kc_jmax$mean
#> [1] 0.4244
#>
#> $par$kc_jmax$sd
#> [1] 0.0217
#>
#>
#> $par$err_gpp
#> $par$err_gpp$lower
#> [1] 0.01
#>
#> $par$err_gpp$upper
#> [1] 3
#>
#> $par$err_gpp$init
#> [1] 0.8
#>
#>
#> $par$err_bigD13C
#> $par$err_bigD13C$lower
#> [1] 0.01
#>
#> $par$err_bigD13C$upper
#> [1] 15
#>
#> $par$err_bigD13C$init
#> [1] 0.8
setup_rsofun_calibration(231)$drivobs_train
#> # A tibble: 20 × 6
#> sitename params_siml site_info forcing source data
#> <chr> <list> <list> <list> <chr> <list>
#> 1 CH-Dav <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 2 CZ-BK1 <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 3 DK-Sor <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 4 FI-Hyy <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 5 FR-Pue <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 6 GF-Guy <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 7 US-Ha1 <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 8 US-PFa <tibble [1 × 12]> <tibble> <tibble> fluxnet <tibble>
#> 9 lon_+010.52_lat_+051.08 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 10 lon_+011.10_lat_+048.30 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 11 lon_+112.58_lat_+023.13 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 12 lon_+145.13_lat_-005.83 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 13 lon_+146.13_lat_-032.97 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 14 lon_+148.30_lat_-036.10 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 15 lon_+153.00_lat_-026.85 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 16 lon_-079.10_lat_+035.97 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 17 lon_-116.45_lat_+047.16 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 18 lon_-119.82_lat_+034.50 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 19 lon_-122.98_lat_+038.40 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>
#> 20 lon_-149.61_lat_+063.97 <tibble [1 × 2]> <tibble> <tibble> cornwe… <tibble>Then we also define a function to run the Markov Chain Monte Carlo
(MCMC) sampling run_mcmc_rsofun(). This function reads in
the unique setup identifier and uses the function
setup_rsofun_calibration() to get all the necessary
definitions. By doing so this code can easily be parallelized in
multiple jobs.
run_mcmc_rsofun <- function(
curr_calibration_scenario,
# MCMC setup:
iterations = 6,
burnin = 0,
n_chains_independent = 3, # number of independent chains (called 'nrChains' in runMCMC)
n_chains_within_sampler = 3, # number of internal chains to be sampled (called 'startValue' in runMCMC, at least 2 for DEzs)
# parallelization:
n_parallel_independent = 3, # number of cores for parallelization of independent chains https://cran.r-project.org/web/packages/BayesianTools/vignettes/InterfacingAModel.html#running-several-mcmcs-in-parallel
outpath = "data",
suffix_str = "",
sampler = "DREAMzs"
) {
# Setup simulation model
res <- setup_rsofun_calibration(scenario = curr_calibration_scenario)
# res$drivobs_train
# res$drivobs_test
# res$par_fixed
# res$par
# res$targets
# Load loglikelihood (e.g. if using a tailor-made one, instead of package-provided):
# source("R/calibration_helpers.R", echo = FALSE)
# source("R/cost_likelihood_pmodel.R", echo = FALSE)
# Setup MCMC # uses drivobs_train, not drivobs_test
driver_to_use_for_mcmc <- select(res$drivobs_train, sitename, params_siml, site_info, forcing)
obs_to_use_for_mcmc <- select(res$drivobs_train, sitename, source, data)
calib_sofun_settings <- list(
method = "BayesianTools",
metric = rsofun::cost_likelihood_pmodel,
control = list(
sampler_runMCMC = sampler,
settings_runMCMC = list(
burnin = burnin, # 10000,
iterations = iterations, # 50000,
nrChains = n_chains_independent, # number of independent chains to be sampled
startValue = n_chains_within_sampler # number of internal chains to be sampled
),
n_parallel_nrChains = n_parallel_independent
),
par = res$par
)
# setup output directories for parallel calibration
logpath <- file.path(outpath, "calibrations", paste0("out_calib_", suffix_str, ".rds.log.txt"))
rds_path <- gsub(".log.txt", "", logpath)
dir.create(path = dirname(logpath), showWarnings = FALSE)
# Run calibration in parallel
cat(paste0(Sys.time(), ": start sampling of ", paste0("out_calib_", suffix_str, ".rds")))
out_calib <- calib_sofun(
drivers = driver_to_use_for_mcmc,
obs = obs_to_use_for_mcmc,
settings_calib = calib_sofun_settings,
logpath = logpath,
# other arguments for the cost function
targets = res$targets,
par_fixed = res$par_fixed
)
## Store result object to file: ----
## This enables easy continuation of MCMC sampling
saveRDS(out_calib, file = rds_path, compress = "xz")
cat(paste0(Sys.time(), ": end sampling of ", out_calib$name,
". \nWritten *.rds-output to: ", rds_path))
# return only performance results as tibble
timings <- tibble(
setup = curr_calibration_scenario,
name = gsub(".rds.log.txt", "", out_calib$name),
resultfile = rds_path,
# sampling options:
sampler = calib_sofun_settings$control$sampler_runMCMC,
burnin = burnin,
iterations = iterations,
n_chains = n_chains_independent,
n_chains_inner = n_chains_within_sampler,
# performance results:
cores = n_parallel_independent,
walltime = out_calib$walltime,
# logfile = out_calib$logpath
)
return(timings)
}We additionally showcase below how to parallelize multiple chains to
different cores (n_parallel_independent = 8) and thus how
to significantly speed up long-running calibration tasks.
For long-running calibrations it can also be useful to be able to
continue an existing sampling chain with
continue_mcmc_rsofun(). While this is not shown in this
vignette, code for this is available on Zenodo (10.5281/zenodo.17204361).
With these helper functions specified, we can then run the two calibration setups. The sampled MCMC chains are stored to a specified path so that they can easily be loaded at a later time in a separate R session.
Below, the second setup is run a second time making use of parallel sampling of the independent chains.
We make use of arguments of the user-defined function
run_mcmc_rsofun() that allow us to specify the calibration
setup, the MCMC setup, and the parallelization strategy.
dir.create("vignettes/files/param_calib_multitarget/", showWarnings = FALSE)
df_timings226 <- run_mcmc_rsofun(226, suffix_str = "setup_226",
outpath = "vignettes/files/param_calib_multitarget/",
# mcmc setup:
sampler = "DREAMzs",
iterations = 600,
burnin = 0,
n_chains_independent = 8,
n_chains_within_sampler = 3,
# parallelization
n_parallel_independent = 1
)
# 2026-07-09 10:39:19.622351: start sampling of out_calib_setup_226.rds
# runMCMC terminated after 4.39500000000407seconds00 . Current logp -389.1087 -421.1468 -396.6275 Please wait!
# runMCMC terminated after 3.95499999998719seconds00 . Current logp -377.7371 -379.5134 -377.3347 Please wait!
# ...
# runMCMC terminated after 4.23500000000058seconds00 . Current logp -397.9886 -379.6501 -382.6354 Please wait!
df_timings231 <- run_mcmc_rsofun(231, suffix_str = "setup_231",
outpath = "vignettes/files/param_calib_multitarget/",
# mcmc setup:
sampler = "DREAMzs",
iterations = 600,
burnin = 0,
n_chains_independent = 8,
n_chains_within_sampler = 3,
# parallelization
n_parallel_independent = 1
)
# 2026-07-09 10:39:53.032407: start sampling of out_calib_setup_231.rds
# Running DREAM-MCMC, chain 1 iteration 300 of 600 . Current logp -14645.97 -14571.83 -15471.88 Please wait!
# runMCMC terminated after 16.8090000000084seconds00 . Current logp -14616.55 -14462.62 -14529.8 Please wait!
# Running DREAM-MCMC, chain 2 iteration 300 of 600 . Current logp -16760.01 -15828.73 -14948.44 Please wait!
# runMCMC terminated after 16.4020000000019seconds00 . Current logp -14955.26 -14941.47 -14441.98 Please wait!
# ...
# runMCMC terminated after 17.8410000000003seconds00 . Current logp -15023.04 -14854.75 -14265.61 Please wait!
df_timings231p <- run_mcmc_rsofun(231, suffix_str = "setup_231_parallel",
outpath = "vignettes/files/param_calib_multitarget/",
# mcmc setup:
sampler = "DREAMzs",
iterations = 600,
burnin = 0,
n_chains_independent = 8,
n_chains_within_sampler = 3,
# parallelization
n_parallel_independent = 8 # here now we parallelize the 8 chains to 8 cores
)
# 2026-07-09 10:42:24.395628: start sampling of out_calib_setup_231_parallel.rds
# Writing MCMC sampling log to: files/param_calib_multitarget//calibrations/out_calib_setup_231_parallel.rds.log.txt
# ...
# 2026-07-09 10:43:07.689861: end sampling of out_calib_setup_231_parallel.rds.log.txt.
# Written *.rds-output to: files/param_calib_multitarget//calibrations/out_calib_setup_231_parallel.rds# calib_timings <- bind_rows(
# df_timings226,
# df_timings231,
# df_timings231p)
knitr::kable(calib_timings |>
dplyr::mutate(name = gsub(".rds.log.txt", "", name)) |>
dplyr::mutate(resultfile = gsub("vignettes/files/param_calib_multitarget//", "", resultfile)) |>
dplyr::select(-"resultfile", "resultfile")
)| setup | name | sampler | burnin | iterations | n_chains | n_chains_inner | cores | walltime | resultfile |
|---|---|---|---|---|---|---|---|---|---|
| 226 | out_calib_setup_226 | DREAMzs | 0 | 600 | 8 | 3 | 1 | 34.00443 secs | calibrations/out_calib_setup_226.rds |
| 231 | out_calib_setup_231 | DREAMzs | 0 | 600 | 8 | 3 | 1 | 151.33366 secs | calibrations/out_calib_setup_231.rds |
| 231 | out_calib_setup_231_parallel | DREAMzs | 0 | 600 | 8 | 3 | 8 | 43.93430 secs | calibrations/out_calib_setup_231_parallel.rds |
Note how the parallelized version reduced the walltime by 4x. With additional independent MCMC chains (e.g. the rsofun documentation paper used up to 8 chains), this can become important.
Since MCMC sampling can be long running tasks, the resulting MCMC sampling chains are stored to disk, so that they can easily be recovered in a later R session for an analysis similar to the one below.
During the parallel MCMC sampling the workers don’t send output to the console. This is why a logfile is created and progress can be checked there. The above example resulted in a common log file of all eight workers:
starting worker pid=91053 on localhost:11767 at 10:42:24.511
starting worker pid=91066 on localhost:11767 at 10:42:24.598
starting worker pid=91079 on localhost:11767 at 10:42:24.687
starting worker pid=91092 on localhost:11767 at 10:42:24.769
starting worker pid=91105 on localhost:11767 at 10:42:24.854
starting worker pid=91118 on localhost:11767 at 10:42:24.941
starting worker pid=91131 on localhost:11767 at 10:42:25.025
starting worker pid=91144 on localhost:11767 at 10:42:25.108
Loading required package: rsofun
loaded rsofun and set parent environment
...
Running DREAM-MCMC, chain 1 iteration 300 of 600 . Current logp -15821.4 ... Please wait!
Running DREAM-MCMC, chain 1 iteration 300 of 600 . Current logp -15415.3 ... Please wait!
Running DREAM-MCMC, chain 1 iteration 300 of 600 . Current logp -14739.4 ... Please wait!
...
Running DREAM-MCMC, chain 1 iteration 600 of 600 . Current logp -14654.7 ... Please wait!
runMCMC terminated after 32.359seconds
Running DREAM-MCMC, chain 1 iteration 600 of 600 . Current logp -14927.0 ... Please wait!
runMCMC terminated after 35.474seconds
After the MCMC sampling we could (potentially in a new R script), load the sampled chains and analyse the chain convergence and posterior distribution. To do so we load the previous results from RDS files:
# This loads MCMC chains that might have been generated with a separate R script
out_calib_226 <- readRDS("files/param_calib_multitarget/calibrations/out_calib_setup_226.rds")
out_calib_231p <- readRDS("files/param_calib_multitarget/calibrations/out_calib_setup_231_parallel.rds")We define some labelling changes to obtain nicer looking plots.
rsofun_symbol_parname_description <- dplyr::tribble(
~variable, ~label, ~Symbol_R,
"kphio", "italic(phi)[0]^'*'*' (mol mol-\u00B9)'", "italic(phi)[0]^'*'",
"kphio_par_a", "italic(a)[italic(phi)]*' (\u00B0C-\u00B2)'", "italic(a)[italic(phi)]",
"kphio_par_b", "italic(b)[italic(phi)]*' (\u00B0C)'", "italic(b)[italic(phi)]",
"soilm_thetastar", "italic(theta)^'*'*' ('*'mm'*')'", "italic(theta)^'*'",
"soilm_betao", "italic(beta)[0]*' (-)'", "italic(beta)[0]",
"beta_unitcostratio", "italic(beta)*' (-)'", "italic(beta)",
"rd_to_vcmax", "italic(b)[0]*' (-)'", "italic(b)[0]",
"tau_acclim", "italic(tau)*' (days)'", "italic(tau)",
"kc_jmax", "italic(c)^'*'*' (-)'", "italic(c)^'*'",
"err_gpp", "italic(sigma)['GPP']*' (gC m -\u00B2 s-\u00B9)'", "italic(sigma)['GPP']",
"err_bigD13C", "italic(sigma)[Delta]*' ('*'\u2030'*')'", "italic(sigma)[Delta]"
)
label_vec_short <- setNames(rsofun_symbol_parname_description$Symbol_R, rsofun_symbol_parname_description$variable)
custom_labeller_variable <- function(labels, multi_line = TRUE) { # adapted from label_parsed
replaced_labels <- left_join(
select(labels, variable), # NOTE: this has variable hardcoded
select(rsofun_symbol_parname_description, variable, label),
by = join_by(variable))
# print(tibble(replaced_labels))
replaced_labels <- replaced_labels |> select(-variable)
replaced_labels <- label_value(replaced_labels, multi_line = multi_line)
# print(replaced_labels)
lapply(unname(replaced_labels), lapply, function(values) {
c(parse(text = as.character(values)))
})
}And we define a function plot_mcmc_trace() to plot a
trace plot of the MCMC sampling including Gelman-Rubin statistics to
easily assess convergence for the two calibration setups. For this
vignette examples we did not run the MCMC long enough to see converging
trace plots. In a real application, one would need to increase the
number of iterations. What we can see is the number of chains, which are
the 8 independent chains, each consisting of 3 internal chains for a
total of 24 traces in the plots. Note that the 3 internal chains also
reduce the length of the iterations from the specified 600 to 200.
# Define plotting function
plot_mcmc_trace <- function(out_calib, nr_internal_chains, burnin_to_skip, burnin_to_skip_gelman = burnin_to_skip, dont_thin = FALSE, end = NULL) {
x <- out_calib$mod
title <- basename(out_calib$name)
curr_iter <- x[[1]]$settings$iterations
if (dont_thin || curr_iter < 10000) {
curr_thin <- 1
} else {
curr_thin <- floor(curr_iter / 10000)
}
xsample <- BayesianTools::getSample(x, coda = T, thin = curr_thin, start = burnin_to_skip, end = end)
# nr_internal_chains will have same color
dat_to_plot <- lapply(xsample, function(single_chain) {
as_tibble(single_chain) |> mutate(iteration = burnin_to_skip + curr_thin * (1:n()))
}) |>
dplyr::bind_rows(.id = "chain_id") |>
tidyr::pivot_longer(-c(iteration, chain_id), names_to = "variable") |>
# mark inner and outer chains (assumes DEzs or DREAMzs):
mutate(outerChain = as.factor(ceiling(as.numeric(chain_id) / 3)),
innerChain = (as.numeric(chain_id) + 2) %% 3 + 1,
innerChain_str = letters[innerChain],
chain_id_str = paste0(outerChain, letters[innerChain])) #|>
# fix order: in order of appearance
# mutate(variable = forcats::as_factor(variable))
pl <- ggplot(dat_to_plot,
aes(x = iteration, y = value, color = outerChain, linetype = innerChain_str)) + geom_line() +
# geom_rug(sides = "r") +
theme_classic() +
# scale_x_continuous(labels = scales::label_number(scale_cut = scales::cut_short_scale())) +
facet_wrap(~variable, nrow = 2, scales = "free_y", labeller = custom_labeller_variable) +
theme(
legend.position = "bottom", strip.text = element_text(size = 12)
) +
labs(y = "", color = "chain", linetype = "internal\nchains")
# add Gelman Diagnostics
get_gelman_diag <- function(mcmc, burnin_to_skip, end) {
gelman_df <- BayesianTools::gelmanDiagnostics(mcmc, start = burnin_to_skip, end = end)
psrf_values <- gelman_df$psrf[, "Point est."]
# psrf_strings <- paste0(substr(names(psrf_values),1,5), "..=", sprintf("%.2f", psrf_values))
psrf_strings <- paste0("'*", unname(label_vec_short[names(psrf_values)]), "*'=", sprintf("%.2f", psrf_values))
psrf_string <- paste0(psrf_strings, collapse = ", ")
subtitle <- sprintf("'GelmanDiagnostics: mpsrf=%.1f; psrf:%s'",
gelman_df$mpsrf,
psrf_string)
}
subtitle <- tryCatch(get_gelman_diag(x, burnin_to_skip_gelman + 1, end = end), error = function(e) {
e
}) # unsure why min burnin of 1 is needed
pl <- pl + ggtitle(title, subtitle = parse(text = subtitle))
pl <- pl + geom_vline(xintercept = burnin_to_skip_gelman, color = "red", linetype = "dashed")
return(pl)
}plot_mcmc_trace(out_calib_226, nr_internal_chains = 3, burnin_to_skip = 0, burnin_to_skip_gelman = 150)plot_mcmc_trace(out_calib_231p, nr_internal_chains = 3, burnin_to_skip = 0, burnin_to_skip_gelman = 150)We illustrate the posterior (derived from these unconverged example
MCMC chains) by defining a function
plot_prior_posterior_density() that shows the prior and
posterior distributions.
We can clearly distinguish the different priors used in the the two setups, and how the calibration constrains the posteriors to different parameter values.
plot_prior_posterior_density <- function(x, burnin_to_skip) {
require(BayesianTools)
require(dplyr)
require(tidyr)
require(ggplot2)
# Get matrices of prior and posterior samples
posteriorMat <- BayesianTools::getSample(x, parametersOnly = TRUE, start = burnin_to_skip)
priorMat <- BayesianTools:::getSetup(x)$prior$sampler(10000) # nPriorDraws = 10000
# Parameter names
parNames <- colnames(posteriorMat)
# rename columns priorMat
colnames(priorMat) <- parNames
# Create data frame for plotting
df_plot <- rbind(
data.frame(priorMat, par_estimation = "prior"),
data.frame(posteriorMat, par_estimation = "posterior")
) |>
# dplyr::mutate(par_estimation = forcats::fct_inorder(par_estimation)) |> # order by appearance
tibble()
levels(df_plot$par_estimation)
# Plot with facet wrap
gg <- df_plot |>
tidyr::pivot_longer(-c(par_estimation), names_to = "variable") |>
# dplyr::mutate(variable = forcats::fct_inorder(variable)) |> # order by appearance
ggplot(
aes(x = value, fill = par_estimation)
) +
geom_density(alpha = 0.8) +
theme_classic() +
facet_wrap(~variable, nrow = 2, scales = "free", labeller = custom_labeller_variable) +
# facet_wrap( ~ variable , nrow = 2, scales = "free") +
theme(
legend.position = "bottom",
axis.title.x = element_text(""),
axis.ticks.y = element_blank(),
axis.text.y = element_blank(),
) +
scale_fill_manual(NULL, values = c("posterior" = "#29a274ff",
"prior" = "#777055ff")) # GECO colors
return(gg)
}library(cowplot)
p1 <- plot_prior_posterior_density(out_calib_226$mod, burnin_to_skip = 200) + ggtitle(out_calib_226$name)
#> Loading required package: BayesianTools
#> Loading required package: tidyr
p2 <- plot_prior_posterior_density(out_calib_231p$mod, burnin_to_skip = 20) + ggtitle(out_calib_231p$name)
cowplot::plot_grid(p1, p2, ncol = 1)The joint prior distributions of the parameters can be assessed for correlation between parameters, indicating compensating effects between them which is often also called equifinality - same model output with different parameter sets.
Eventually, we want to use the calibrated model for predictions and
also check their agreement with the observations used for calibration.
For that we define another helper function
run_prediction_rsofun(), which samples parameter sets from
the MCMC chain to make predictions. A second function samples errors
from the identified structural uncertainty (error model) and combines
them with the predictions. To do so they use helper functions
(setup_rsofun_calibration(),
apply_bias_correction_and_sample_error(), and
predict_sofun_parallelized()).
# Samples posterior parameters (MAP or N=n_samples) and runs
# model predictions
#
# This allows to do two things:
# A) make predictions at sites/dates/... where we have observations
# B) make predictions also on dates/... where we have no observations, e.g. for continuous time series
# we do B) once {by setting `return_continuous_timeseries = TRUE`} and then derive A) from it.
run_prediction_rsofun <- function(
mcmc_posterior,
prediction = c("both", "test", "train"),
burnin_to_skip = 0,
n_samples = 100, # if n_samples == 1, use MAP
n_cores = NULL) {
if (length(prediction) == 1 && (prediction %in% c("both", "test", "train"))) {
# as expected
} else {
stop("Provide prediction as either: 'both', 'test' or 'train'")
}
n_cores <- ifelse(is.null(n_cores), 1, n_cores)
stopifnot(is(mcmc_posterior$mod, "mcmcSamplerList"))
curr_calibration_scenario <- as.integer(gsub(
".*_setup_([0-9]*)_.*", # NOTE: hardcoded expected format of filename
"\\1",
mcmc_posterior$name))
stopifnot(!is.na(curr_calibration_scenario)) # Catches if expected format is inaccurate
# Setup simulation model
res <- setup_rsofun_calibration(scenario = curr_calibration_scenario)
# Set random seed for reproducibility
set.seed(2023)
# Sample parameters from MCMC posterior
# Evaluation of the uncertainty coming from the model parameters' uncertainty
if (n_samples > 1) {
samples_par <- getSample(
mcmc_posterior$mod,
thin = 1,
start = burnin_to_skip, numSamples = n_samples
) |>
as.data.frame() |>
# Add sample IDs
dplyr::mutate(posterior_sample_id = 1:n()) |>
tidyr::nest(.by = posterior_sample_id, .key = "pars")
} else {
# mcmc_posterior$par # these are already precomputed...
# but more robust to recompute:
samples_par <- BayesianTools::MAP(mcmc_posterior$mod)$parametersMAP |>
as.list() |> as_tibble() |>
# Add sample IDs
dplyr::mutate(posterior_sample_id = 0L) |> # posterior_sample_id == 0 means MAP
tidyr::nest(.by = posterior_sample_id, .key = "pars")
}
# Setup prediction
predict_sofun_settings <- list(n_cores = n_cores)
if (prediction == "both") {
curr_driver <- bind_rows(res$drivobs_train, res$drivobs_test) |>
select(sitename, params_siml, site_info, forcing)
curr_obs <- bind_rows(res$drivobs_train, res$drivobs_test) |>
select(sitename, source, data)
} else if (prediction == "train") {
curr_driver <- select(res$drivobs_train, sitename, params_siml, site_info, forcing)
curr_obs <- select(res$drivobs_train, sitename, source, data)
} else if (prediction == "test") {
curr_driver <- select(res$drivobs_test, sitename, params_siml, site_info, forcing)
curr_obs <- select(res$drivobs_test, sitename, source, data)
stopifnot(nrow(curr_obs) > 0)
}
# Run prediction
df_pred_vs_obs <- predict_sofun_parallelized(
drivers = curr_driver,
obs = curr_obs,
settings = predict_sofun_settings,
par = samples_par,
par_fixed = res$par_fixed
)
return(df_pred_vs_obs)
}
# Runs the requested model predictions and returns results
# Requested model predictions are defined by data.frame `par_df` containing
# model parameter sets
predict_sofun_parallelized <- function(
drivers,
obs,
settings,
targets,
par_fixed,
par_df = NULL) {
# Set number of cores if not specified
if (is.null(settings$n_cores)) {
settings$n_cores <- min(detectCores() - 1, 20) # at most 20
}
# ensure not more than needed
settings$n_cores <- min(nrow(par_df), settings$n_cores)
# Function to run prediction for a single parameter set
run_pmodel_single_prediction <- function(par, par_fixed, targets, drivers, obs) {
# Function that runs the P-model for a sample of parameters
# but does not add the observation error
# Taken from cost_likelihood_pmodel()
stopifnot(nrow(obs) > 0) # ensure some observation data are provided
stopifnot(nrow(drivers) > 0) # ensure some driver data are provided
# A) Include current parameters ----
stopifnot(length(intersect(names(par), names(par_fixed))) == 0) # no overlap
params_modl <- c(par, par_fixed)
# B1) append column targets to obs data.frame: (code copy from cost_likelihood)
# This ensures that variant that requires targets as function argument can be
# run with the code below (which was written with targets as a column of obs defined as:
# \item{targets}{A single one-row tibble with a column for each target
# containing TRUE or FALSE (or NA_logical)}
targets = c(
"gpp" = "fluxnet", # NOTE: hardcoding TODO: remove
"le" = "fluxnet", # NOTE: hardcoding TODO: remove
"bigD13C" = "cornwell", # NOTE: hardcoding TODO: remove
"vj" = "cornwell") # NOTE: hardcoding TODO: remove
targets_df <- tidyr::tibble(target = names(targets), source = unname(targets)) |> # or use enframe(..., name = "target", value = "source")
dplyr::group_by(source) |> dplyr::summarise(targets = unique(list(target)))
obs <- dplyr::left_join(obs, targets_df, by = dplyr::join_by("source")) |> dplyr::select(-"source")
# B,C) Run model and bring together with observed ----
df_pred_vs_obs <- rsofun:::get_mod_obs_pmodel(
drivers,
obs,
params_modl,
parallel = FALSE,
ncores = 1,
return_continuous_timeseries = TRUE)
# D) (DON'T) Sample error model ----
# NOTE: sampling is not done here, but can optionally be done before plotting
# Here we rename to clarify that no error model has yet been applied.
df_pred_vs_obs <- df_pred_vs_obs |>
# clarify name of model output (containing not yet any error model term)
rename(mod_no_err = mod) |>
relocate(c(mod_no_err, err_par_sd), .after = last_col())
return(df_pred_vs_obs)
}
# Run the P-model predictions for each set of parameters
## Prepare parallelization:
parallel_flag <- (settings$n_cores > 1 && nrow(par_df) > 1)
# setup helpers for conditional parallelization
do_if <- function(df, cond, f) { # enable conditional lines in dplyr piping
if (cond) f(df) else df
}
cl <- if (parallel_flag) {
multidplyr::new_cluster(settings$n_cores) |>
multidplyr::cluster_library(
packages = c("dplyr", "tidyr", "purrr", "rsofun")
)
}
## Run predictions:
df_model_predictions <- par_df |>
do_if(parallel_flag, function(df) multidplyr::partition(df, cl)) %>%
dplyr::mutate("sim" = purrr::map(
pars,
~ run_pmodel_single_prediction(
par = .x,
par_fixed,
targets,
drivers,
obs
)
)) |>
do_if(parallel_flag, function(df) dplyr::collect(df))
return(df_model_predictions)
}
apply_bias_correction_and_sample_error <- function(df_pred, N_sample_error) {
set.seed(1982)
df_pred |>
# lazy_dt() |> # if needed, uses lazy data.table and dtplyr for speed
# a) repeat lines: once for each sampled error
# following line is basically a cross_join: with
slice(rep(1:n(), each = N_sample_error)) |>
mutate(error_sample_id = as.integer(rep(1:N_sample_error, n() / N_sample_error))) |>
arrange(posterior_sample_id) |>
# b) sample the error:
group_by(err_par_sd, err_par_bias) |>
mutate(err_sample = rnorm(n(), sd = err_par_sd),
mod_biasremoved_no_err = mod_no_err - err_par_bias,
mod_biasremoved_with_err = mod_no_err - err_par_bias + err_sample) |>
ungroup() |>
as_tibble() |> # to access results of lazy dtplyr-computation as normal tibble()
# keep output light: i.e. remove unneded columns:
select(-err_sample) |>
mutate(across(where(is.character), as.factor))
}With these functions we can generate the predictions using e.g. the
Maximum-A-Posteriori (MAP) parameters as well as n=20
samples from the posterior.
# get MAP and run model for MAP parameter set
df_predict_MAP_train_231p <- run_prediction_rsofun(
mcmc_posterior = out_calib_231p,
prediction = "train",
burnin_to_skip = 200,
n_samples = 1, # n_samples == 1, requests MAP
n_cores = 1) # this could be increased e.g. to 4
# get parameter samples and run model for thes parameter sets
df_predict_train_231p <- run_prediction_rsofun(
mcmc_posterior = out_calib_231p,
prediction = "train",
burnin_to_skip = 200,
n_samples = 20,
n_cores = 1) # this could be increased e.g. to 4
df_predict <- bind_rows(
# df_predict_test |> mutate(is_train0_test1 = 1L, is_MAP = FALSE),
df_predict_train_231p |> mutate(is_train0_test1 = 0L, is_MAP = FALSE),
# df_predict_MAP_test |> mutate(is_train0_test1 = 1L, is_MAP = TRUE),
df_predict_MAP_train_231p |> mutate(is_train0_test1 = 0L, is_MAP = TRUE)
)
# Free memory:
# rm(df_predict_train_231p,
# df_predict_MAP_train_231p)
# df_predict_params<- df_predict |> select(posterior_sample_id, is_train0_test1, is_MAP, pars) |> unnest(pars)
df_predict_gpp <- df_predict |> select(posterior_sample_id, is_train0_test1, is_MAP, sim) |> unnest(sim) |> filter(target == "gpp") |> unnest(obs_metadata)
df_predict_bigD13C <- df_predict |> select(posterior_sample_id, is_train0_test1, is_MAP, sim) |> unnest(sim) |> filter(target == "bigD13C") |> unnest(obs_metadata)
# apply bias-correction and simulate structural error
N_sample_error <- 3
df_predict_231p_gpp_sampled <- apply_bias_correction_and_sample_error(
df_predict_gpp,
N_sample_error = N_sample_error)
df_predict_231p_bigD13C_sampled <- apply_bias_correction_and_sample_error(
df_predict_bigD13C,
N_sample_error = N_sample_error)# Save predictions to plot with separate script:
# FOR STORAGE REASONS unnest the obs_metadata and save different targets separately:
out_prediction_template <- file.path("vignettes/files/param_calib_multitarget/",
"predictions", "pred_231p_XXX.rds")
dir.create(dirname(out_prediction_template), showWarnings = FALSE)
fname_out_gpp <- gsub("_XXX",
paste0( "_gpp_sampled_N", N_sample_error, "errors"),
out_prediction_template)
fname_out_bigD13C <- gsub("_XXX",
paste0("_bigD13C_sampled_N", N_sample_error, "errors"),
out_prediction_template)
df_predict_231p_gpp_sampled |>
select(posterior_sample_id,error_sample_id,is_train0_test1,is_MAP,sitename,target,obs, date,err_par_bias,err_par_sd,mod_no_err,mod_biasremoved_no_err,mod_biasremoved_with_err) |>
saveRDS(fname_out_gpp, compress = "xz")
df_predict_231p_bigD13C_sampled |>
select(posterior_sample_id,error_sample_id,is_train0_test1,is_MAP,sitename,target,obs,id, err_par_bias,err_par_sd,mod_no_err,mod_biasremoved_no_err,mod_biasremoved_with_err) |>
saveRDS(fname_out_bigD13C, compress = "xz")# Load predictions for plotting with separate script:
df_predict_231p_gpp_sampled <- readRDS("files/param_calib_multitarget//predictions/pred_231p_gpp_sampled_N3errors.rds") # NOTE: for rendering remove prefix "vignettes/"
df_predict_231p_bigD13C_sampled <- readRDS("files/param_calib_multitarget//predictions/pred_231p_bigD13C_sampled_N3errors.rds") # NOTE: for rendering remove prefix "vignettes/"
# df_predict_231p_bigD13C_sampled <- readr::read_rds(paste0("/storage/scratch/giub_geco/fbernhard/rsofun_doc_outputs/data/predictions/out_predict_",n_post,"_30000burnin__out_calib__scen222_DREAMzs-100000-0iter_8x3chains_on_CPU8x1_continued.rds_vj_sampled",n_err,".rds"))
# df_predict_231p_gpp_sampled <- readr::read_rds(paste0("/storage/scratch/giub_geco/fbernhard/rsofun_doc_outputs/data/predictions/out_predict_",n_post,"_30000burnin__out_calib__scen222_DREAMzs-100000-0iter_8x3chains_on_CPU8x1_continued.rds_gpp_sampled",n_err,".rds"))With a function plot_predobs_gpp_timeseries3() and
analyse_modobs3(), model output and observations can be
compared as either time series plots or as scatter plots.
Below we plot a time series comparing predictions with observations, we can distinguish the parametric uncertainty (‘Posterior’) related to the width of the posterior distribtution of the parameters and structural uncertainty (or model uncertainty) that includes the samples of the error model.
# add transparency to colors
t_col <- function(color, percent = 50, name = NULL) {
# color = color name
# percent = % transparency
# name = an optional name for the color
rgb.val <- col2rgb(color) # Get RGB values for named color
## Make new color using input color as base and alpha set by transparency
t.col <- rgb(
rgb.val[1], rgb.val[2], rgb.val[3],
max = 255,
alpha = (100 - percent) * 255 / 100,
names = name)
invisible(t.col) ## Save the color
}
## copied from sofunCalVal package (and adapted to analyse_modobs3)
analyse_modobs3 <- function(df,
mod,
obs,
type = "points",
filnam = NA,
relative = FALSE,
lower_xlim = 0,
use_factor = NULL,
shortsubtitle = FALSE,
plot_subtitle = TRUE,
plot_linmod = TRUE,
pal = "batlowW",
...) {
require(ggplot2)
require(dplyr)
# if (identical(filnam, NA)) filnam <- "analyse_modobs.pdf"
## rename to 'mod' and 'obs' and remove rows with NA in mod or obs
df <- df %>%
as_tibble() %>%
ungroup() %>%
dplyr::select(dplyr::all_of(c(mod = mod, obs = obs))) %>%
tidyr::drop_na(mod, obs)
## get linear regression (coefficients)
linmod <- lm(obs ~ mod, data = df)
## construct metrics table using the 'yardstick' library
df_metrics <- df %>%
yardstick::metrics(obs, mod) %>%
dplyr::bind_rows(tibble(.metric = "n", .estimator = "standard", .estimate = summarise(df, numb = n()) %>% unlist())) %>%
dplyr::bind_rows(tibble(.metric = "slope", .estimator = "standard", .estimate = coef(linmod)[2])) %>%
# dplyr::bind_rows( tibble( .metric = "nse", .estimator = "standard", .estimate = hydroGOF::NSE( obs, mod, na.rm=TRUE ) ) ) %>%
dplyr::bind_rows(tibble(.metric = "mean_obs", .estimator = "standard", .estimate = summarise(df, mean = mean(obs, na.rm = TRUE)) %>% unlist())) %>%
dplyr::bind_rows(tibble(
.metric = "prmse", .estimator = "standard",
.estimate = dplyr::filter(., .metric == "rmse") %>% dplyr::select(.estimate) %>% unlist() /
dplyr::filter(., .metric == "mean_obs") %>%
dplyr::select(.estimate) %>%
unlist()
)) %>%
dplyr::bind_rows(tibble(
.metric = "pmae", .estimator = "standard",
.estimate = dplyr::filter(., .metric == "mae") %>% dplyr::select(.estimate) %>% unlist() /
dplyr::filter(., .metric == "mean_obs") %>%
dplyr::select(.estimate) %>%
unlist()
)) %>%
dplyr::bind_rows(tibble(.metric = "bias", .estimator = "standard", .estimate = dplyr::summarise(df, mean((mod - obs), na.rm = TRUE)) %>% unlist())) %>%
dplyr::bind_rows(tibble(.metric = "pbias", .estimator = "standard", .estimate = dplyr::summarise(df, mean((mod - obs) / obs, na.rm = TRUE)) %>% unlist()))
rsq_val <- df_metrics %>%
dplyr::filter(.metric == "rsq") %>%
dplyr::select(.estimate) %>%
unlist() %>%
unname()
rmse_val <- df_metrics %>%
dplyr::filter(.metric == "rmse") %>%
dplyr::select(.estimate) %>%
unlist() %>%
unname()
mae_val <- df_metrics %>%
dplyr::filter(.metric == "mae") %>%
dplyr::select(.estimate) %>%
unlist() %>%
unname()
bias_val <- df_metrics %>%
dplyr::filter(.metric == "bias") %>%
dplyr::select(.estimate) %>%
unlist() %>%
unname()
slope_val <- df_metrics %>%
dplyr::filter(.metric == "slope") %>%
dplyr::select(.estimate) %>%
unlist() %>%
unname()
n_val <- df_metrics %>%
dplyr::filter(.metric == "n") %>%
dplyr::select(.estimate) %>%
unlist() %>%
unname()
if (relative) {
rmse_val <- rmse_val / mean(df$obs, na.rm = TRUE)
bias_val <- bias_val / mean(df$obs, na.rm = TRUE)
}
rsq_lab <- format(rsq_val, digits = 2)
rmse_lab <- format(rmse_val, digits = 3)
mae_lab <- format(mae_val, digits = 3)
bias_lab <- format(bias_val, digits = 3)
slope_lab <- format(slope_val, digits = 3)
n_lab <- format(n_val, digits = 3)
results <- tibble(rsq = rsq_val, rmse = rmse_val, mae = mae_val, bias = bias_val, slope = slope_val, n = n_val)
if (shortsubtitle) {
subtitle <- bquote(italic(R)^2 == .(rsq_lab) ~ ~
RMSE == .(rmse_lab))
} else {
subtitle <- bquote(italic(R)^2 == .(rsq_lab) ~ ~
RMSE == .(rmse_lab) ~ ~
bias == .(bias_lab) ~ ~
slope == .(slope_lab) ~ ~
italic(N) == .(n_lab))
}
if (type == "hex") {
upper_xlim <- round(max(quantile(df$mod, 0.9999), quantile(df$obs, 0.9999)))
stopifnot(is.null(lower_xlim) || (is.numeric(lower_xlim) && length(lower_xlim) == 1))
lower_xlim <- ifelse(is.null(lower_xlim),
round(min(quantile(df$mod, 0.0001), quantile(df$obs, 0.0001))),
lower_xlim)
## ggplot hexbin
gg <- df %>%
ggplot2::ggplot(aes(x = mod, y = obs)) +
geom_hex(bins = 50, show.legend = FALSE) +
geom_abline(intercept = 0, slope = 1, linetype = "dotted") +
# geom_hline(yintercept = 0, linetype = "dotted") +
# geom_vline(xintercept = 0, linetype = "dotted") +
coord_fixed() +
xlim(lower_xlim, upper_xlim) +
ylim(lower_xlim, upper_xlim) +
theme_classic() +
labs(x = mod, y = obs)
if (pal == "batlowW") {
gg <- gg + khroma::scale_fill_batlowW(trans = "log", reverse = TRUE)
} else if (pal == "davos") {
gg <- gg + khroma::scale_fill_davos(trans = "log", reverse = TRUE)
}
if (plot_subtitle) gg <- gg + labs(subtitle = subtitle)
if (plot_linmod) gg <- gg + geom_smooth(method = "lm", color = "red", linewidth = 0.5, se = FALSE)
if (!identical(filnam, NA)) {
ggsave(filnam, width = 5, height = 5)
}
}
return(list(df_metrics = df_metrics, gg = gg, linmod = linmod, results = results))
}
plot_predobs_gpp_timeseries3 <- function(ts_to_plot) {
# separate obs
df_tsplot_gpp_obs <- ts_to_plot |>
select(sitename, target, date, obs, Scenario, dataset) |>
distinct()
# compute stats of sampled distributions before plotting them
tibble_to_plot <- ts_to_plot |>
filter(target == "gpp") |>
# dtplyr::lazy_dt() |> # THIS WAS NEEDED FOR THE FULL DATA.FRAME
group_by(Scenario, dataset, parameters, sitename, target, date, model_output_type, y_facet) |>
summarise( # mod_no_err_p50 = quantile(mod_no_err, 0.5),
modelled_p50 = quantile(modelled, 0.5),
modelled_p95 = quantile(modelled, 0.95),
modelled_p05 = quantile(modelled, 0.05)
) |>
as_tibble() |>
# and bind back obsevations
left_join(
df_tsplot_gpp_obs,
by = join_by(sitename, target, date, Scenario, dataset))
n_sites <- tibble_to_plot$sitename |> unique() |> length()
pl_timeseries_gpp <- ggplot(
data = tibble_to_plot,
mapping = aes(x = date, y = modelled_p50)) +
# Observations underneath (following Cameron 2022)
geom_point(
data = function(df) df |> select(-parameters, -model_output_type, -y_facet, -starts_with("modelled")) |> distinct(),
# data = df_tsplot_gpp_obs, # variant 1, but does not allow %+%-replacement of underlying data
mapping = aes(y = obs), color = "black", shape = 4, alpha = 0.5, size = 0.5) +
# Structural uncertainty (including error model), a.k.a prediction band
geom_ribbon(
alpha = 0.5,
data = function(df) df |> filter(model_output_type == "with struct. uncert."),
mapping = aes(ymin = modelled_p05, ymax = modelled_p95, fill = "Post.+Error")) +
# Parametric uncertainty (without error model, only parameter sampling), a.k.a confidence band
geom_ribbon(
alpha = 0.5,
data = function(df) df |> filter(model_output_type == "rsofun"),
mapping = aes(ymin = modelled_p05, ymax = modelled_p95, fill = "Posterior")) +
geom_line(
data = function(df) df |> filter(model_output_type == "rsofun"),
mapping = aes(y = modelled_p50, color = "Posterior")) +
# layout
facet_wrap(~sitename, scales = "free_x") +
theme_classic() + theme(legend.position = "bottom") +
labs(
x = NULL, #' Date',
y = expression(paste("GPP (g C m"^-2, "s"^-1, ")"))
) +
scale_fill_manual(NULL, aesthetics = c("colour", "fill"),
breaks = c("Post.+Error",
"Posterior"),
values = c("Posterior" = t_col("#29a274ff"),
"Post.+Error" = t_col("#777055ff"))
) +
scale_x_date(date_breaks = "12 months", date_labels = "%Y-%m")
}# Time series of GPP (Figure C5)
dflong_gpp_train <- df_predict_231p_gpp_sampled |>
mutate(Scenario = "231") |>
# select only training sites
filter(is_train0_test1 == 0) |>
select(posterior_sample_id, error_sample_id, Scenario, is_train0_test1, is_MAP, sitename, target,
obs, date, # these are target specific observation_metadata
mod_no_err, mod_biasremoved_no_err, mod_biasremoved_with_err) |>
# pivot the model_output_types to long
tidyr::pivot_longer(c(mod_no_err, mod_biasremoved_no_err, mod_biasremoved_with_err),
names_to = "model_output_type", values_to = "modelled") |>
mutate(model_output_type = factor(
model_output_type,
levels = c("mod_no_err", "mod_biasremoved_no_err", "mod_biasremoved_with_err"),
labels = c("rsofun", "bias-corrected", "with struct. uncert."))) |>
# derive column `parameters` ("MAP" or "Posterior") from `is_MAP`
mutate(is_MAP = factor(ifelse(is_MAP, "MAP", "Posterior"))) |>
rename(parameters = is_MAP) |>
# derive column `dataset` ("train" or "test") from column `is_train0_test1`
mutate(is_train0_test1 = factor(ifelse(is_train0_test1 == 1, "test", "train"))) |>
rename(dataset = is_train0_test1)
df_B3_timeseries <- dflong_gpp_train |>
# remove the bias-corrected values for gpp since we did not fit a bias
filter(!(model_output_type %in% c("bias-corrected"))) |>
# select what to plot and how to name it
mutate(y_facet = case_when(
Scenario == "231" & model_output_type == "rsofun" & parameters == "MAP" ~ "MAP",
Scenario == "231" & model_output_type == "rsofun" & parameters == "Posterior" ~ "Posterior",
Scenario == "231" & model_output_type == "with struct. uncert." & parameters == "Posterior" ~ "Post.+Error",
# all else is not plotted
TRUE ~ "remove") |> factor(levels = c("MAP", "Posterior", "Post.+Error"))) |>
filter(y_facet != "remove")
# Plot raw predictions
pl_timeseries_gpp <- plot_predobs_gpp_timeseries3(df_B3_timeseries)
pl_timeseries_gpp# fake output since prediction was run previous to vignette rendering
knitr::include_graphics("files/param_calib_multitarget/predictions/pl_timeseries_gpp.png", # NOTE: for rendering remove prefix "vignettes/"
dpi = 100)Below we show a density scatter plot comparing predictions with observations. Since we use the MAP values for predictions, the uncertainty of the parameters is not taken into account. These plots are useful to detect model biases for specific target variables or specific ranges of the target variables.
gpp_labs <- function(xNULL = FALSE) {
list(labs(x = ifelse(xNULL, " ", "Predicted GPP (g C m-\u00B2 s-\u00B9)"),
y = "Observed GPP (g C m-\u00B2 s-\u00B9)"))
}
bigD13C_labs <- function(xNULL = FALSE) {
list(labs(x = ifelse(xNULL, " ", "Predicted Δ (\u2030)"),
y = "Observed Δ (\u2030)"))
}
# compute scatters (and skills) for MAP of test set:
list_of_scenarios_to_loop_over <- list(
s231 = list("bigD13C" = df_predict_231p_bigD13C_sampled, "gpp" = df_predict_231p_gpp_sampled)
)
parameter_set <- "MAP"
list_of_scatters <- lapply(list_of_scenarios_to_loop_over, function(list_of_targets) {
lapply(list_of_targets, function(df_target_prediction) {
curr_target <- first(df_target_prediction$target)
# (MAP, Posterior, Posterior+Error)
# MAP: is posterior_sample_id==0
# e.g. filter(df_gpp, is_train0_test1 == 1, is_MAP, error_sample_id==1)
# Posterior: just take one error sampling, but all (~25 posterior samples)
# e.g. filter(df_gpp, is_train0_test1 == 1, error_sample_id==1),
# Posterior+Error: take all errors
# e.g. filter(df_gpp, is_train0_test1 == 1, error_sample_id>=1)
df <- df_target_prediction %>%
{
if (parameter_set == "MAP") filter(., is_MAP) else .
} %>% filter(error_sample_id == 1) # , is_train0_test1 == 1 # TODO: why was this removed?
mod <- "mod_biasremoved_no_err"
lower_xlim <- ifelse(curr_target == "gpp", list(0), list(NULL))[[1]]
if (nrow(df) == 0) {
return(list(gg = ggplot() + theme_void()))
} else {
return(analyse_modobs3(df, mod = mod, obs = "obs", type = "hex"))
}
})
})
### single comparison plot: ----
mark_as_target <- theme(panel.background = element_rect(fill = t_col("darkgreen", 80)))
pl_scatter_comparison <- cowplot::plot_grid(
list_of_scatters$s231$bigD13C$gg + mark_as_target + bigD13C_labs(),
list_of_scatters$s231$gpp$gg + mark_as_target + gpp_labs()
)
pl_scatter_comparison# fake output since prediction was run previous to vignette rendering
knitr::include_graphics("files/param_calib_multitarget/predictions/pl_scatter_comparison.png", # NOTE: for rendering remove prefix "vignettes/"
dpi = 150)The above vignette hopefully gives a first glimpse into a realistic
workflow that calibrates rsofun for multiple sites to
multiple targets. For more details refer to the model documentation
paper (Paredes et
al. 2025) and the code referred therein.