Skip to content

Commit 28991a9

Browse files
authored
Merge pull request #3535 from blesson07asd/blesson-GSoC
joint_ensemble sampling
2 parents 7c50adc + 17ea7ca commit 28991a9

15 files changed

Lines changed: 217 additions & 57 deletions

base/workflow/NEWS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
### Joint Ensemble Sampling Implementation
2+
- **Behavior Change**: Ensemble runs now use shared input samples across all sites instead of independent sampling per site
3+
- **Affected Components**:
4+
- `get.parameter.samples.R`
5+
- `runModule.run.write.configs.R`
6+
- `run.write.configs.R`
7+
- `ensemble.R`
8+
- **New Default**: The sampling method default has changed from "uniform" to "random"
9+
- **Impact**: This ensures consistency across sites in ensemble runs but may produce different results compared to previous versions
10+
11+
112
# PEcAn.workflow 1.9.0
213

314
* PEcAn.workflow is now distributed under the BSD 3-clause license instead of the NCSA Open Source license.

base/workflow/R/run.write.configs.R

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
#' \code{write.config.*} function for your specific ecosystem model
66
#' (e.g. write.config.ED2, write.config.SIPNET).
77
#'
8+
#'
89
#' @param settings a PEcAn settings list
910
#' @param write should the runs be written to the database?
10-
#' @param ens.sample.method how to sample the ensemble members('halton' sequence or 'uniform' random)
11+
#' @param input_design input indices for samples
1112
#' @param posterior.files Filenames for posteriors for drawing samples for ensemble and sensitivity
1213
#' analysis (e.g. post.distns.Rdata, or prior.distns.Rdata)
1314
#' @param overwrite logical: Replace output files that already exist?
@@ -22,9 +23,13 @@
2223
#' @export
2324
#'
2425
#' @author David LeBauer, Shawn Serbin, Ryan Kelly, Mike Dietze
25-
run.write.configs <- function(settings, write = TRUE, ens.sample.method = "uniform",
26+
27+
run.write.configs <- function(settings, input_design, write = TRUE,
2628
posterior.files = rep(NA, length(settings$pfts)),
2729
overwrite = TRUE) {
30+
31+
32+
2833
## Skip database connection if settings$database is NULL or write is False
2934
if (!isTRUE(write) && is.null(settings$database)) {
3035
PEcAn.logger::logger.info("Not writing this run to database, so database connection skipped")
@@ -93,13 +98,23 @@ run.write.configs <- function(settings, write = TRUE, ens.sample.method = "unifo
9398
scipen <- getOption("scipen")
9499
options(scipen = 12)
95100

96-
PEcAn.uncertainty::get.parameter.samples(settings, posterior.files, ens.sample.method)
97101
samples.file <- file.path(settings$outdir, "samples.Rdata")
98102
if (file.exists(samples.file)) {
99103
samples <- new.env()
100104
load(samples.file, envir = samples) ## loads ensemble.samples, trait.samples, sa.samples, runs.samples, env.samples
101105
trait.samples <- samples$trait.samples
102-
ensemble.samples <- samples$ensemble.samples
106+
trait_sample_indices <- input_design[["param"]]
107+
ensemble.samples <- list()
108+
for (pft in names(trait.samples)) {
109+
pft_traits <- trait.samples[[pft]]
110+
ensemble.samples[[pft]] <- as.data.frame(
111+
lapply(
112+
names(pft_traits),
113+
function(trait) pft_traits[[trait]][trait_sample_indices]
114+
)
115+
)
116+
names(ensemble.samples[[pft]]) <- names(pft_traits)
117+
}
103118
sa.samples <- samples$sa.samples
104119
runs.samples <- samples$runs.samples
105120
## env.samples <- samples$env.samples
@@ -167,6 +182,7 @@ run.write.configs <- function(settings, write = TRUE, ens.sample.method = "unifo
167182
ensemble.samples = ensemble.samples,
168183
settings = settings,
169184
model = model,
185+
input_design = input_design,
170186
write.to.db = write)
171187

172188
# Store output in settings and output variables
@@ -190,6 +206,8 @@ run.write.configs <- function(settings, write = TRUE, ens.sample.method = "unifo
190206
file = file.path(settings$outdir, "samples.Rdata"))
191207
PEcAn.logger::logger.info("parameter values for runs in ", file.path(settings$outdir, "samples.RData"))
192208
options(scipen = scipen)
193-
194-
return(invisible(settings))
209+
invisible(settings)
210+
return(settings)
195211
}
212+
213+

base/workflow/R/runModule.run.write.configs.R

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,34 @@
22
#'
33
#' @param settings a PEcAn Settings or MultiSettings object
44
#' @param overwrite logical: Replace config files if they already exist?
5+
#' @param input_design the input indices for samples
56
#' @return A modified settings object, invisibly
67
#' @importFrom dplyr %>%
78
#' @export
8-
runModule.run.write.configs <- function(settings, overwrite = TRUE) {
99

10+
11+
runModule.run.write.configs <- function(settings, overwrite = TRUE, input_design = NULL) {
12+
1013
if (PEcAn.settings::is.MultiSettings(settings)) {
1114
if (overwrite && file.exists(file.path(settings$rundir, "runs.txt"))) {
1215
PEcAn.logger::logger.warn("Existing runs.txt file will be removed.")
1316
unlink(file.path(settings$rundir, "runs.txt"))
1417
}
15-
return(PEcAn.settings::papply(settings, runModule.run.write.configs, overwrite = FALSE))
18+
if (is.null(input_design)) {
19+
ensemble_size <- settings$ensemble$size
20+
input_design <- PEcAn.uncertainty::generate_joint_ensemble_design(settings=settings[1],ensemble_size=ensemble_size)
21+
}
22+
return(PEcAn.settings::papply(settings, runModule.run.write.configs, overwrite = FALSE,input_design=input_design))
1623
} else if (PEcAn.settings::is.Settings(settings)) {
1724
# double check making sure we have method for parameter sampling
1825
if (is.null(settings$ensemble$samplingspace$parameters$method)) {
1926
settings$ensemble$samplingspace$parameters$method <- "uniform"
2027
}
28+
if (is.null(input_design)) {
29+
ensemble_size <- settings$ensemble$size
30+
input_design <- PEcAn.uncertainty::generate_joint_ensemble_design( settings = settings, ensemble_size = ensemble_size )
31+
}
32+
2133

2234

2335
#check to see if there are posterior.files tags under pft
@@ -27,9 +39,9 @@ runModule.run.write.configs <- function(settings, overwrite = TRUE) {
2739
return(PEcAn.workflow::run.write.configs(
2840
settings = settings,
2941
write = isTRUE(settings$database$bety$write), # treat null as FALSE
30-
ens.sample.method = settings$ensemble$samplingspace$parameters$method,
3142
posterior.files = posterior.files,
32-
overwrite = overwrite
43+
overwrite = overwrite,
44+
input_design = input_design
3345
))
3446
} else {
3547
stop("runModule.run.write.configs only works with Settings or MultiSettings")

base/workflow/man/run.write.configs.Rd

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

base/workflow/man/runModule.run.write.configs.Rd

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

book_source/03_topical_pages/03_pecan_xml.Rmd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,9 @@ Tags related to ensemble setup are:
580580
* `size` : (required) the number of runs in the ensemble.
581581
* `samplingspace`: (optional) Contains tags for defining how the ensembles will be generated.
582582

583+
Shared sampling design: In multi-site workflows, PEcAn now generates one joint ensemble design and reuses the same input sample indices (parameters, meteorology, etc.) across all sites to ensure consistent draws; previously, inputs were sampled independently per site. This change does not introduce new XML tags and applies whenever multiple sites are processed together.
584+
The joint sampling design is created once at the start of configuration using generate_joint_ensemble_design(), called from the run configuration module, and the resulting indices are threaded through to write.ensemble.configs().
585+
583586
Each piece in the sampling space can potentially have a method tag and a parent tag. Method refers to the sampling method and parent refers to the cases where we need to link the samples of two components. When no tag is defined for one component, one sample will be generated and used for all the ensembles. This allows for partitioning/studying different sources of uncertainties. For example, if no met tag is defined then, one met path will be used for all the ensembles and as a result the output uncertainty will come from the variability in the parameters. At the moment no sampling method is implemented for soil and vegetation.
584587
Available sampling methods for `parameters` can be found in the documentation of the `PEcAn.utils::get.ensemble.samples` function.
585588
For the cases where we need simulations with a predefined set of parameters, met and initial condition we can use the restart argument. Restart needs to be a list with name tags of `runid`, `inputs`, `new.params` (parameters), `new.state` (initial condition), `ensemble.id` (ensemble ids), `start.time`, and `stop.time`.

models/sipnet/R/write.configs.SIPNET.R

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -682,11 +682,12 @@ write.config.SIPNET <- function(defaults, trait.values, settings, run.id, inputs
682682
soilWFrac <- ncdf4::ncvar_get(IC.nc, "SoilMoistFrac")
683683
if (!is.na(soilWFrac) && is.numeric(soilWFrac)) {
684684
param[param[, 1] == "soilWFracInit", 2] <- sum(soilWFrac) / 100
685+
## litterWFracInit fraction
686+
litterWFrac <- soilWFrac
685687
}
686688
}
687-
## litterWFracInit fraction
688-
litterWFrac <- soilWFrac
689-
689+
690+
690691
## snowInit cm water equivalent (cm = g / cm2 because 1 g water = 1 cm3 water)
691692
if (ic_has_ncvars[["SWE"]]) {
692693
snow <- ncdf4::ncvar_get(IC.nc, "SWE")

modules/uncertainty/NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
export(ensemble.filename)
44
export(ensemble.ts)
55
export(flux.uncertainty)
6+
export(generate_joint_ensemble_design)
67
export(get.change)
78
export(get.coef.var)
89
export(get.elasticity)

modules/uncertainty/R/ensemble.R

Lines changed: 53 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,12 @@ read.ensemble.output <- function(ensemble.size, pecandir, outdir, start.year, en
8080
##' @export
8181
##' @author David LeBauer, Istem Fer
8282
get.ensemble.samples <- function(ensemble.size, pft.samples, env.samples,
83-
method = "uniform", param.names = NULL, ...) {
84-
85-
if (is.null(method)) {
86-
PEcAn.logger::logger.info("No sampling method supplied, defaulting to uniform random sampling")
87-
method <- "uniform"
83+
method = "random", param.names = NULL, ...) {
84+
85+
# Define supported methods
86+
supported_methods <- c("random", "uniform", "halton", "sobol", "lhc")
87+
if (!method %in% supported_methods) {
88+
stop("Invalid sampling method")
8889
}
8990

9091
## force as numeric for compatibility with Fortran code in halton()
@@ -139,48 +140,59 @@ get.ensemble.samples <- function(ensemble.size, pft.samples, env.samples,
139140

140141

141142
ensemble.samples <- list()
142-
143+
sampled.indices <- list()
143144

144145
col.i <- 0
145146
for (pft.i in seq(pft.samples)) {
146147
ensemble.samples[[pft.i]] <- matrix(nrow = ensemble.size, ncol = length(pft.samples[[pft.i]]))
148+
sampled.indices[[pft.i]] <- matrix(nrow = ensemble.size, ncol = length(pft.samples[[pft.i]]))
147149

148150
# meaning we want to keep MCMC samples together
149151
if(length(pft.samples[[pft.i]])>0 & !is.null(param.names)){
150152
if (method == "halton") {
151-
same.i <- round(randtoolbox::halton(ensemble.size) * length(pft.samples[[pft.i]][[1]]))
153+
same.i <- floor(randtoolbox::halton(ensemble.size) * length(pft.samples[[pft.i]][[1]]))+1
152154
} else if (method == "sobol") {
153-
same.i <- round(randtoolbox::sobol(ensemble.size, scrambling = 3) * length(pft.samples[[pft.i]][[1]]))
155+
same.i <- floor(randtoolbox::sobol(ensemble.size, scrambling = 3) * length(pft.samples[[pft.i]][[1]]))+1
154156
} else if (method == "torus") {
155-
same.i <- round(randtoolbox::torus(ensemble.size) * length(pft.samples[[pft.i]][[1]]))
157+
same.i <- floor(randtoolbox::torus(ensemble.size) * length(pft.samples[[pft.i]][[1]]))+1
156158
} else if (method == "lhc") {
157-
same.i <- round(c(PEcAn.emulator::lhc(t(matrix(0:1, ncol = 1, nrow = 2)), ensemble.size) * length(pft.samples[[pft.i]][[1]])))
159+
same.i <- floor(c(PEcAn.emulator::lhc(t(matrix(0:1, ncol = 1, nrow = 2)), ensemble.size) * length(pft.samples[[pft.i]][[1]])))+1
158160
} else if (method == "uniform") {
159161
same.i <- sample.int(length(pft.samples[[pft.i]][[1]]), ensemble.size)
160-
} else {
161-
PEcAn.logger::logger.info("Method ", method, " has not been implemented yet, using uniform random sampling")
162-
# uniform random
163-
same.i <- sample.int(length(pft.samples[[pft.i]][[1]]), ensemble.size)
162+
} else if (method == "random") {
163+
PEcAn.logger::logger.info("Using random row sampling for MCMC draws")
164+
same.i <- sample(nrow(pft.samples[[pft.i]][[1]]), ensemble.size, replace = TRUE)
165+
}
166+
else {
167+
PEcAn.logger::logger.error("Sampling method %s is not recognized", method)
168+
164169
}
165170

166171
}
167172

168173
for (trait.i in seq(pft.samples[[pft.i]])) {
169174
col.i <- col.i + 1
170-
if(names(pft.samples[[pft.i]])[trait.i] %in% param.names[[pft.i]]){ # keeping samples
171-
ensemble.samples[[pft.i]][, trait.i] <- pft.samples[[pft.i]][[trait.i]][same.i]
172-
}else{
175+
if (names(pft.samples[[pft.i]])[trait.i] %in% param.names[[pft.i]]) {
176+
ensemble.samples[[pft.i]][, trait.i] <- pft.samples[[pft.i]][[trait.i]][same.i]
177+
sampled.indices[[pft.i]][, trait.i] <- same.i
178+
}else{
179+
# Extract original trait values
180+
trait.values <- pft.samples[[pft.i]][[trait.i]]
181+
sampled.values <- stats::quantile(trait.values, random.samples[, col.i])
182+
173183
ensemble.samples[[pft.i]][, trait.i] <- stats::quantile(pft.samples[[pft.i]][[trait.i]],
174184
random.samples[, col.i])
175-
}
176-
} # end trait
177-
ensemble.samples[[pft.i]] <- as.data.frame(ensemble.samples[[pft.i]])
178-
colnames(ensemble.samples[[pft.i]]) <- names(pft.samples[[pft.i]])
179-
} #end pft
180-
names(ensemble.samples) <- names(pft.samples)
181-
ans <- ensemble.samples
185+
sampled.indices[[pft.i]][, trait.i] <- sapply(sampled.values, function(val) {which.min(abs(trait.values - val)) })
186+
}
187+
}
188+
ensemble.samples[[pft.i]] <- as.data.frame(ensemble.samples[[pft.i]])
189+
colnames(ensemble.samples[[pft.i]]) <- names(pft.samples[[pft.i]])
190+
191+
} #end pft
192+
names(ensemble.samples) <- names(pft.samples)
193+
ans <- ensemble.samples
182194
}
183-
return(ans)
195+
return(list(ans,sampled.indices))
184196
} # get.ensemble.samples
185197

186198

@@ -190,6 +202,7 @@ get.ensemble.samples <- function(ensemble.size, pft.samples, env.samples,
190202
##' Given a pft.xml object, a list of lists as supplied by get.sa.samples,
191203
##' a name to distinguish the output files, and the directory to place the files.
192204
##'
205+
##' @param input_design the input indices for samples
193206
##' @param defaults pft
194207
##' @param ensemble.samples list of lists supplied by \link{get.ensemble.samples}
195208
##' @param settings list of PEcAn settings
@@ -211,7 +224,7 @@ get.ensemble.samples <- function(ensemble.size, pft.samples, env.samples,
211224
##' @importFrom rlang .data
212225
##' @export
213226
##' @author David LeBauer, Carl Davidson, Hamze Dokoohaki
214-
write.ensemble.configs <- function(defaults, ensemble.samples, settings, model,
227+
write.ensemble.configs <- function(defaults, ensemble.samples, settings, model, input_design ,
215228
clean = FALSE, write.to.db = TRUE, restart = NULL, samples = NULL, rename = FALSE) {
216229

217230

@@ -314,18 +327,21 @@ for (input_tag in names(settings$run$inputs)) {
314327
samp.ordered <- samp[c(order, names(samp)[!(names(samp) %in% order)])]
315328
if(is.null(samples)){
316329
#performing the sampling
317-
samples<-list()
318-
# For the tags specified in the xml I do the sampling
319-
for(i in seq_along(samp.ordered)){
320-
myparent<-samp.ordered[[i]]$parent # do I have a parent ?
321-
#call the function responsible for generating the ensemble
322-
samples[[names(samp.ordered[i])]] <- input.ens.gen(settings=settings,
323-
input=names(samp.ordered)[i],
324-
method=samp.ordered[[i]]$method,
325-
parent_ids=if( !is.null(myparent)) samples[[myparent]]) # if I have parent then give me their ids - this is where the ordering matters making sure the parent is done before it's asked
326-
}
330+
samples <- list()
331+
input_tags <- names(settings$run$inputs)
332+
333+
for (input_tag in input_tags) {
334+
if (input_tag %in% colnames(input_design)) {
335+
input_paths <- settings$run$inputs[[input_tag]]$path
336+
input_indices <- input_design[[input_tag]]
337+
338+
samples[[input_tag]] <- list(
339+
samples = lapply(input_indices, function(idx) input_paths[[idx]])
340+
)
341+
}
342+
327343
}
328-
344+
}
329345
# if there is a tag required by the model but it is not specified in the xml then I replicate n times the first element
330346
required_tags%>%
331347
purrr::walk(function(r_tag){

0 commit comments

Comments
 (0)