Skip to content

Commit eeec791

Browse files
authored
Merge branch 'develop' into automated-reporting-and-visualization
2 parents 3097520 + 34597f1 commit eeec791

42 files changed

Lines changed: 2333 additions & 238 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ For more information about this file see also [Keep a Changelog](http://keepacha
3131
- PEcAn.SIPNET gains support for SIPNET v2, whose features includes management events, nitrogen cycle tracking, explicit N2O and methane fluxes, runtime setting of feature flags, and changes to the parameter set (now 73 parameters). SIPNET v1 is still fully supported, but workarounds for bugs in the legacy `sipnet.unk` version have been removed.
3232
- Added `PEcAn.data.land::to_co2e()` for converting SOC change, CH4, and N2O to CO2-equivalent emissions using IPCC Global Warming Potential values.
3333
- Added `PEcAn.data.land::event_parquet_to_json` for generating PEcAn `event.json` files from well-formatted event parquet files, with support for ensembles of events.
34+
- Added statewide synthetic fertilization and compost amendment event workflows for CA ag parcels. Outputs share an ensemble naming so a downstream cleaner unions them into one fertilization event type for SIPNET.
3435

3536
### Fixed
3637
- Docker GHA workflow no longer fails on pull requests opened from forks (#3618).
@@ -42,6 +43,7 @@ For more information about this file see also [Keep a Changelog](http://keepacha
4243
- `segment_dataframe()` now returns an empty dataframe when date filtering removes all crop-cycle segments, instead of a single row with NA columns that caused downstream segment config errors (#4007).
4344

4445
### Changed
46+
- Added `ensemble_downscale()`, a refactored version of `SDA_downscale()`.
4547
- `PEcAn.uncertainty::get.parameter.samples()`: replaced the `save_to_disk` flag (from #3860) with an `outdir` argument (default `settings$outdir`) controlling whether `samples.Rdata` is written; `outdir = NULL` skips the save. Existing callers are unaffected (@omkarrr2533, #4016)
4648
- Updated Docker architecture documentation to match current docker-compose.yml: removed portainer/minio/thredds, added rstudio/api sections, updated service lists and volumes (#3268).
4749
- Improved PEcAn.SIPNET documentation including README, model description, and current installation instructions (@Eshaan-byte; #3703, #3705).

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -262,16 +262,15 @@ runModule.run.write.configs <- function(settings,
262262
}
263263

264264
# Deprecation: internal design generation is going away. Passing input_design
265-
# explicitly (the generate_joint_ensemble_design() result) will become the
266-
# required path. Warn only when we are actually about to auto-generate.
267-
auto_generating <-
268-
(is.null(designs$ensemble) && need_ensemble) ||
269-
(is.null(designs$sensitivity) && need_sa)
270-
if (auto_generating) {
265+
# explicitly will become the required path. Warn only about the design we are
266+
# actually about to generate, so a caller who supplied one is not told to
267+
# supply it again.
268+
if (is.null(designs$ensemble) && need_ensemble) {
271269
PEcAn.logger::logger.warn(
272-
"Internal input design generation is deprecated and will be removed.",
273-
"Pass input_design explicitly as the list(design_matrix, samples) returned",
274-
"by generate_joint_ensemble_design(); this will become required."
270+
"Generating the ensemble design internally is deprecated and will be",
271+
"removed. Pass input_design explicitly as the list(design_matrix,",
272+
"samples) returned by generate_joint_ensemble_design(); this will",
273+
"become required."
275274
)
276275
}
277276

base/workflow/tests/testthat/test-runModule.run.write.configs.R

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,24 @@ test_that(".prepare_input_designs still accepts a design supplied as X", {
286286

287287
mockery::expect_called(gen, 0)
288288
expect_identical(designs$ensemble, supplied$X)
289+
})
290+
291+
test_that(".prepare_input_designs does not warn about a design the caller supplied", {
292+
tmp <- withr::local_tempdir()
293+
settings <- make_prep_settings(tmp)
294+
settings$sensitivity.analysis <- list(quantiles = c(0.025, 0.5, 0.975))
295+
296+
supplied <- list(design_matrix = data.frame(param = 1:3), samples = fake_bundle())
297+
298+
mockery::stub(.prepare_input_designs,
299+
"PEcAn.uncertainty::generate_OAT_SA_design",
300+
function(...) list(design_matrix = data.frame(param = 1:4)))
301+
302+
# the SA design is still generated internally, but there is no way to supply
303+
# one, so a caller who did pass a design should not be told to pass one
304+
msgs <- capture.output(
305+
invisible(.prepare_input_designs(settings, input_design = supplied)),
306+
type = "message"
307+
)
308+
expect_false(any(grepl("deprecated", msgs)))
289309
})

documentation/tutorials/Demo_02_Uncertainty_Analysis/uncertainty.qmd

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,24 @@ See Demo 1 Section 6 for details on what these functions do. Briefly, they read
121121

122122
# Write Model Configuration Files {#sec-write-configs}
123123

124-
This step generates the model-specific configuration files that will be used to run the ecosystem model. The process involves:
124+
This step decides which model runs to do, then writes the configuration files for them.
125+
126+
`generate_joint_ensemble_design()` builds the ensemble design: one row per run, holding indices that say which parameter draw each run uses. It returns those samples alongside the design, since the indices only mean anything together with the samples they point into. Because this demo also runs a sensitivity analysis, the SA design is generated internally as well; passing one in yourself isn't supported yet.
125127

126-
2. Generating SIPNET configuration files using the `runModule.run.write.configs()` function.
127128
```{r write-configs}
128-
settings <- PEcAn.workflow::runModule.run.write.configs(settings)
129+
design <- PEcAn.uncertainty::generate_joint_ensemble_design(
130+
settings,
131+
ensemble_size = settings$ensemble$size
132+
)
133+
134+
settings <- PEcAn.workflow::runModule.run.write.configs(
135+
settings,
136+
input_design = design
137+
)
129138
```
130139

140+
Generating the design yourself lets you inspect it before any runs happen: `design$design_matrix` is the table of runs, and `design$samples` holds the parameter draws behind it, which is what the sensitivity and variance decomposition steps later report on.
141+
131142
# Run Model Simulations
132143

133144
This section executes the SIPNET simulations and retrieves the results.

documentation/tutorials/Demo_1_Basic_Run/pecan.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
</pft>
1616
</pfts>
1717
<ensemble>
18+
<size>10</size>
1819
<variable>NPP</variable>
1920
</ensemble>
2021
<model>

documentation/tutorials/Demo_1_Basic_Run/run_pecan.qmd

Lines changed: 105 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ This notebook demonstrates how to:
3939

4040
We are modeling carbon and productivity dynamics at the Niwot Ridge Forest AmeriFlux site ([US-NR1](https://ameriflux.lbl.gov/sites/siteinfo/US-NR1), a high-elevation temperate coniferous forest in Colorado. The model configuration uses the SIPNET process-based ecosystem model, parameterized with a temperate coniferous plant functional type (PFT).
4141

42-
The simulation is run for the full year 2004 (January 1 – December 31) using AmeriFlux LBL meteorological drivers from the Niwot Ridge site. The ensemble setup specifies one model run focusing on net primary productivity (NPP) as the target output variable.
42+
The simulation is run for the full year 2004 (January 1 – December 31) using AmeriFlux LBL meteorological drivers from the Niwot Ridge site. The ensemble setup specifies a small ten member ensemble focusing on net primary productivity (NPP) as the target output variable, so the plots later on show the spread across members rather than a single line.
4343

4444
This scenario is designed to be a minimal, reproducible example to demonstrate how to run SIPNET within the PEcAn workflow. In later steps, this same framework can be extended to include more ensemble members, additional PFTs, longer time periods, or alternative meteorological inputs.
4545

@@ -192,12 +192,26 @@ Additional outputs include logs, a `STATUS` file that records the steps of the w
192192

193193
# Write Model Configuration Files
194194

195-
This step generates the model-specific configuration files and scripts that will be used to run the ecosystem model. The process involves generating SIPNET configuration files using the `runModule.run.write.configs()` function.
195+
This step decides which model runs to do, then writes the configuration files for them.
196+
197+
`generate_joint_ensemble_design()` builds the design: one row per run, holding indices that say which parameter draw and which input file each run uses. It returns those samples alongside the design, since the indices only mean anything together with the samples they point into.
198+
199+
`runModule.run.write.configs()` then takes that whole object and writes the SIPNET configuration files.
196200

197201
```{r write-configs}
198-
settings <- PEcAn.workflow::runModule.run.write.configs(settings)
202+
design <- PEcAn.uncertainty::generate_joint_ensemble_design(
203+
settings,
204+
ensemble_size = settings$ensemble$size
205+
)
206+
207+
settings <- PEcAn.workflow::runModule.run.write.configs(
208+
settings,
209+
input_design = design
210+
)
199211
```
200212

213+
You can call `runModule.run.write.configs()` without a design and it will build one for itself, but that path is deprecated and will warn. Generating the design yourself also lets you inspect it before any runs happen: `design$design_matrix` is the table of runs, and `design$samples` holds the parameter draws behind it.
214+
201215
# Run Model Simulations and Fetch Results
202216

203217
This section executes the actual model simulations and retrieves the results. The process is managed by PEcAn's workflow system, which handles the execution of your chosen ecosystem model.
@@ -215,27 +229,68 @@ This step generates raw model outputs in model-specific format (in this case, `s
215229

216230
After the model simulation completes, we need to extract the results and prepare them for analysis. This involves:
217231

218-
1. Reading the run ID
232+
1. Reading the run IDs
219233
2. Setting up output paths
220234
3. Defining time period
221235
4. Loading model output
222236
5. Convert to a standard format
223237

224238
```{r get-plot-vars}
225-
runid <- as.character(read.table(paste(settings$outdir, "/run/", "runs.txt", sep = ""))[1, 1]) # Note: if you are using an xml from a run with multiple ensembles this line will provide only the first run id
226-
outdir <- file.path(settings$outdir, "/out/", runid)
239+
runids <- as.character(read.table(paste(settings$outdir, "/run/", "runs.txt", sep = ""))[, 1])
227240
start.year <- lubridate::year(settings$run$start.date)
228241
end.year <- lubridate::year(settings$run$end.date)
229-
model_output <- PEcAn.utils::read.output(
230-
runid,
231-
outdir,
232-
start.year,
233-
end.year,
234-
variables = NULL,
235-
dataframe = TRUE,
236-
verbose = FALSE
237-
)
242+
243+
# one entry per ensemble member, keyed by run id
244+
ensemble_output <- lapply(runids, function(runid) {
245+
PEcAn.utils::read.output(
246+
runid,
247+
file.path(settings$outdir, "/out/", runid),
248+
start.year,
249+
end.year,
250+
variables = NULL,
251+
dataframe = TRUE,
252+
verbose = FALSE
253+
)
254+
})
255+
names(ensemble_output) <- runids
256+
257+
# the members share a variable set, so the first one describes them all
258+
model_output <- ensemble_output[[1]]
238259
available_vars <- names(model_output)[!names(model_output) %in% c("posix", "time_bounds")]
260+
261+
# sub-daily fluxes are too spiky to read as ten overlaid lines, so keep a
262+
# daily-averaged copy of each member for those plots
263+
daily_output <- lapply(ensemble_output, function(member) {
264+
day <- as.Date(member$posix)
265+
vars <- setdiff(names(member), c("posix", "time_bounds"))
266+
averaged <- lapply(member[vars], function(x) as.numeric(tapply(x, day, mean, na.rm = TRUE)))
267+
names(averaged) <- vars
268+
c(list(posix = as.POSIXct(sort(unique(day)), tz = "UTC")), averaged)
269+
})
270+
271+
# draw one line per ensemble member, so the spread across the ensemble is visible
272+
plot_ensemble <- function(vars, cols, ylab, main, labels = vars, data = ensemble_output) {
273+
yrange <- range(
274+
unlist(lapply(data, function(member) unlist(member[vars]))),
275+
na.rm = TRUE
276+
)
277+
# empty plot drawn off the real time axis, so the x axis stays dates
278+
plot(
279+
data[[1]]$posix,
280+
data[[1]][[vars[1]]],
281+
type = "n",
282+
ylim = yrange,
283+
xlab = "Date",
284+
ylab = ylab,
285+
main = main
286+
)
287+
for (i in seq_along(vars)) {
288+
for (member in data) {
289+
lines(member$posix, member[[vars[i]]], col = adjustcolor(cols[i], alpha.f = 0.4))
290+
}
291+
}
292+
legend("topright", legend = labels, col = cols, lty = 1)
293+
}
239294
```
240295

241296
Running this code will convert model specific output files into a standardized netCDF ([year].nc) that can be downloaded for visualization and analysis (R, Matlab, ncview, panoply, etc). This is a key step, because this standardization enables PEcAn to apply downstream analyses to outputs from different ecosystem models.
@@ -267,64 +322,67 @@ knitr::kable(vars_df, caption = "Model Output Variables and Descriptions")
267322

268323
This section provides examples of how to create time series plots of different model variables. The examples cover various ecosystem processes including carbon fluxes, carbon pools, water variables, and structural variables like Leaf Area Index (LAI).
269324

325+
Each plot draws one line per ensemble member. The members differ only in their parameter draws, so the spread between the lines is the parameter uncertainty carried through the model.
326+
270327
## Plot Carbon Fluxes
271328

329+
Some members run negative NPP even in the growing season. That happens when the drawn respiration parameters outweigh photosynthesis, and it shows up here because this demo samples from priors rather than from a meta-analysis, so the spread is wide. Demo 3 shows what constraining these parameters with data does to that spread.
330+
331+
GPP and NPP are sub-daily fluxes, so ten overlaid members would just fill the panel. These are averaged to daily values first, which is enough to see the members apart.
332+
272333
```{r plot-carbon-fluxes}
273-
# Plot Gross Primary Productivity (GPP) and Net Primary Productivity (NPP)
274-
plot(model_output$posix, model_output$GPP,
275-
type = "l",
276-
col = "green",
277-
xlab = "Date",
334+
plot_ensemble(
335+
vars = c("GPP", "NPP"),
336+
cols = c("green", "blue"),
278337
ylab = "Carbon Flux (kg C m-2 s-1)",
279-
main = "Carbon Fluxes Over Time"
338+
main = "Carbon Fluxes Over Time (daily average)",
339+
data = daily_output
280340
)
281-
lines(model_output$posix, model_output$NPP, col = "blue")
282-
legend("topright", legend = c("GPP", "NPP"), col = c("green", "blue"), lty = 1)
283341
```
284342

285343
## Plot Carbon Pools
286344

287345
```{r plot-carbon-pools}
288-
# Plot Total Live Biomass and Total Soil Carbon
289-
plot(model_output$posix, model_output$TotLivBiom,
290-
type = "l",
291-
col = "darkgreen",
292-
xlab = "Date",
346+
plot_ensemble(
347+
vars = c("TotLivBiom", "TotSoilCarb"),
348+
cols = c("darkgreen", "brown"),
349+
labels = c("Total Live Biomass", "Total Soil Carbon"),
293350
ylab = "Carbon Pool (kg C m-2)",
294351
main = "Carbon Pools Over Time"
295352
)
296-
lines(model_output$posix, model_output$TotSoilCarb, col = "brown")
297-
legend("topright", legend = c("Total Live Biomass", "Total Soil Carbon"), col = c("darkgreen", "brown"), lty = 1)
298353
```
299354

300355
## Plot Water Variables
301356

357+
Soil moisture separates into ten lines once the growing season starts, but snow water equivalent stays a single line. That is expected: snow is driven by the met input, which is the same file for every member, while soil moisture responds to the sampled parameters.
358+
302359
```{r plot-water-variables}
303-
# Plot Soil Moisture and Snow Water Equivalent
304-
plot(model_output$posix, model_output$SoilMoist,
305-
type = "l",
306-
col = "blue",
307-
xlab = "Date",
308-
ylab = "Soil Moisture (kg m-2)",
309-
main = "Soil Moisture Over Time"
360+
plot_ensemble(
361+
vars = c("SoilMoist", "SWE"),
362+
cols = c("blue", "lightblue"),
363+
labels = c("Soil Moisture", "Snow Water Equivalent"),
364+
ylab = "Water (kg m-2)",
365+
main = "Soil Moisture and Snow Water Equivalent Over Time"
310366
)
311-
lines(model_output$posix, model_output$SWE, col = "lightblue")
312-
legend("topright", legend = c("Soil Moisture", "Snow Water Equivalent"), col = c("blue", "lightblue"), lty = 1)
313367
```
314368

315-
## Plot LAI and Biomass
369+
## Plot LAI and Above Ground Wood
316370

317371
```{r plot-lai-biomass}
318-
# Plot Leaf Area Index (LAI) and Above Ground Wood
319-
plot(model_output$posix, model_output$LAI,
320-
type = "l",
321-
col = "darkgreen",
322-
xlab = "Date",
372+
plot_ensemble(
373+
vars = "LAI",
374+
cols = "darkgreen",
323375
ylab = "LAI (m2 m-2)",
324376
main = "Leaf Area Index Over Time"
325377
)
326-
lines(model_output$posix, model_output$AbvGrndWood, col = "brown")
327-
legend("topright", legend = c("LAI", "Above Ground Wood"), col = c("darkgreen", "brown"), lty = 1)
378+
379+
plot_ensemble(
380+
vars = "AbvGrndWood",
381+
cols = "brown",
382+
labels = "Above Ground Wood",
383+
ylab = "Wood (kg C m-2)",
384+
main = "Above Ground Wood Over Time"
385+
)
328386
```
329387

330388
# Conclusion

modules/assim.sequential/DESCRIPTION

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Suggests:
4343
glue,
4444
ggpubr,
4545
gridExtra,
46+
keras3 (>= 1.0.0),
4647
itertools,
4748
magic (>= 1.5.0),
4849
methods,
@@ -55,18 +56,17 @@ Suggests:
5556
plotrix,
5657
plyr (>= 1.8.4),
5758
randomForest,
58-
keras3 (>= 1.0.0),
5959
raster,
6060
readr,
6161
reshape2 (>= 1.4.2),
6262
rlist,
6363
sf,
64+
sp,
6465
stats,
6566
terra,
6667
testthat,
6768
tictoc,
6869
tidyr,
69-
sp,
7070
utils,
7171
xgboost,
7272
XML

modules/assim.sequential/NAMESPACE

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ export(assessParams)
2929
export(block_matrix)
3030
export(conj_wt_wishart_sampler)
3131
export(construct_nimble_H)
32+
export(downscale_metrics)
3233
export(downscale_qsub_main)
3334
export(dwtmnorm)
35+
export(ensemble_downscale)
3436
export(get_ensemble_weights)
3537
export(hop_test)
3638
export(interactive.plotting.sda)
@@ -39,7 +41,7 @@ export(load_data_paleon_sda)
3941
export(matrix_network)
4042
export(metSplit)
4143
export(obs_timestep2timepoint)
42-
export(outlier.detector.boxplot)
44+
export(outlier_detector_boxplot)
4345
export(piecew.poly.local)
4446
export(post.analysis.ggplot)
4547
export(post.analysis.ggplot.violin)
@@ -62,6 +64,7 @@ export(sda_assemble)
6264
export(sda_weights_site)
6365
export(simple.local)
6466
export(stack_covariates_2_geotiff)
67+
export(subset_ensemble)
6568
export(tobit.model)
6669
export(tobit2space.model)
6770
export(tobit_model_censored)

0 commit comments

Comments
 (0)