diff --git a/modules/meta.analysis/tests/testthat/test.run.meta.analysis.R b/modules/meta.analysis/tests/testthat/test.run.meta.analysis.R index 1f9c9c9a3fd..334390648bd 100644 --- a/modules/meta.analysis/tests/testthat/test.run.meta.analysis.R +++ b/modules/meta.analysis/tests/testthat/test.run.meta.analysis.R @@ -30,4 +30,231 @@ test_that("`run.meta.analysis.pft` throws an error for missing posteriorid", { run.meta.analysis.pft(pft = pft, iterations = 1, dbfiles = NULL, dbcon = NULL, update = TRUE), "Missing posteriorid" ) +}) + + +# Characterization tests for meta_analysis_standalone() +# Verify return structure and file side effects. + +test_that("meta_analysis_standalone returns a list with all three expected elements", { + skip_if_not_installed("coda") + + # Load existing fixture data from the package's test data directory + data_dir <- system.file("tests", "testthat", "data", + package = "PEcAn.MA", + mustWork = FALSE) + if (!nzchar(data_dir) || !dir.exists(data_dir)) { + data_dir <- file.path( + getwd(), + "..", "..", "tests", "testthat", "data" + ) + # Try a direct relative path (for devtools::test) + if (!dir.exists(data_dir)) { + data_dir <- "data" + } + } + skip_if_not( + dir.exists(data_dir) && + file.exists(file.path(data_dir, "trait.mcmc.RData")) && + file.exists(file.path(data_dir, "prior.distns.RData")), + "Fixture data directory not found" + ) + + # Load fixture priors and MCMC + prior_env <- new.env() + load(file.path(data_dir, "prior.distns.RData"), envir = prior_env) + mcmc_env <- new.env() + load(file.path(data_dir, "trait.mcmc.RData"), envir = mcmc_env) + + priors <- prior_env$prior.distns + trait.mcmc <- mcmc_env$trait.mcmc + + # Build minimal trait_data from the mcmc fixture (one obs per trait) + traits_with_mcmc <- names(trait.mcmc) + trait_data <- list() + for (trait in traits_with_mcmc) { + post_mean <- mean(as.matrix(trait.mcmc[[trait]][, "beta.o"])) + trait_data[[trait]] <- data.frame( + mean = post_mean, + stat = 1.0, + n = 10L, + statname = "SD", + site_id = 1L, + greenhouse = 0L, + name = trait, + treatment_id = 1L, + control = 1L, + specie_id = 1L, + citation_id = 1L, + cultivar_id = NA_integer_, + date = NA_character_, + time = NA_character_, + stringsAsFactors = FALSE + ) + } + + outdir <- tempfile("ma_test_") + dir.create(outdir) + on.exit(unlink(outdir, recursive = TRUE), add = TRUE) + + result <- meta_analysis_standalone( + trait_data = trait_data, + priors = priors[traits_with_mcmc, , drop = FALSE], + iterations = 1000, + outdir = outdir, + pft_name = "test_pft", + random = FALSE, + threshold = 5.0 # Lenient threshold so nothing is discarded + ) + + # Return should be a list with exactly these three elements + expect_true(is.list(result)) + expect_named(result, c("trait.mcmc", "post.distns", "jagged.data"), + ignore.order = TRUE) + + # trait.mcmc: named list of mcmc.list objects + expect_true(is.list(result$trait.mcmc)) + if (length(result$trait.mcmc) > 0) { + first_mcmc <- result$trait.mcmc[[1]] + expect_true(inherits(first_mcmc, "mcmc.list"), + info = "Each element of trait.mcmc should be a coda::mcmc.list") + expect_true("beta.o" %in% colnames(as.matrix(first_mcmc))) + } + + # post.distns: data frame with expected columns + expect_s3_class(result$post.distns, "data.frame") + for (col in c("distn", "parama", "paramb")) { + expect_true(col %in% names(result$post.distns), + info = paste("post.distns missing column:", col)) + } + + # jagged.data: named list of data frames + expect_true(is.list(result$jagged.data)) + if (length(result$jagged.data) > 0) { + first_jag <- result$jagged.data[[1]] + expect_s3_class(first_jag, "data.frame") + expect_true("Y" %in% names(first_jag), + info = "jagified data should have a 'Y' column") + } +}) + + +# Characterization tests for run.meta.analysis.pft() +# Verify it reads expected upstream files and produces expected downstream files. + +test_that("run.meta.analysis.pft reads upstream files and produces downstream files", { + skip_if_not_installed("coda") + skip_if_not_installed("mockery") + + # Set up a temp directory with the upstream fixtures + pft_outdir <- tempfile("rma_pft_test_") + dbfiles_dir <- tempfile("rma_dbfiles_") + dir.create(pft_outdir, recursive = TRUE) + dir.create(dbfiles_dir, recursive = TRUE) + on.exit(unlink(c(pft_outdir, dbfiles_dir), recursive = TRUE), add = TRUE) + + # Load existing fixture data + data_dir <- system.file("tests", "testthat", "data", + package = "PEcAn.MA", + mustWork = FALSE) + if (!nzchar(data_dir) || !dir.exists(data_dir)) { + data_dir <- "data" + } + skip_if_not( + dir.exists(data_dir) && + file.exists(file.path(data_dir, "trait.mcmc.RData")) && + file.exists(file.path(data_dir, "prior.distns.RData")), + "Fixture data directory not found" + ) + + # Load fixtures to build upstream files + prior_env <- new.env() + load(file.path(data_dir, "prior.distns.RData"), envir = prior_env) + mcmc_env <- new.env() + load(file.path(data_dir, "trait.mcmc.RData"), envir = mcmc_env) + + # Build trait.data from mcmc fixture (one obs per trait) + traits_with_mcmc <- names(mcmc_env$trait.mcmc) + trait.data <- list() + for (trait in traits_with_mcmc) { + post_mean <- mean(as.matrix(mcmc_env$trait.mcmc[[trait]][, "beta.o"])) + trait.data[[trait]] <- data.frame( + mean = post_mean, + stat = 1.0, + n = 10L, + statname = "SD", + site_id = 1L, + greenhouse = 0L, + name = trait, + treatment_id = 1L, + control = 1L, + specie_id = 1L, + citation_id = 1L, + cultivar_id = NA_integer_, + date = NA_character_, + time = NA_character_, + stringsAsFactors = FALSE + ) + } + + prior.distns <- prior_env$prior.distns[traits_with_mcmc, , drop = FALSE] + + # Write upstream files (normally produced by get.trait.data.pft) + save(trait.data, file = file.path(pft_outdir, "trait.data.Rdata")) + save(prior.distns, file = file.path(pft_outdir, "prior.distns.Rdata")) + + pft <- list( + name = "test_pft", + outdir = pft_outdir, + posteriorid = 99999L + ) + + # Stub out DB file registration (we don't have a real DB) + mockery::stub(run.meta.analysis.pft, "PEcAn.DB::dbfile.insert", TRUE) + + result <- run.meta.analysis.pft( + pft = pft, + iterations = 1000, + random = FALSE, + threshold = 5.0, # Lenient + dbfiles = dbfiles_dir, + dbcon = NULL, + use_ghs = TRUE, + update = TRUE + ) + + # Verify expected downstream files exist + expect_true( + file.exists(file.path(pft_outdir, "trait.mcmc.Rdata")), + info = "trait.mcmc.Rdata should be created" + ) + expect_true( + file.exists(file.path(pft_outdir, "post.distns.MA.Rdata")), + info = "post.distns.MA.Rdata should be created" + ) + expect_true( + file.exists(file.path(pft_outdir, "post.distns.Rdata")), + info = "post.distns.Rdata (symlink) should be created" + ) + expect_true( + file.exists(file.path(pft_outdir, "jagged.data.Rdata")), + info = "jagged.data.Rdata should be created" + ) + + # Verify file contents have correct object names and types + mcmc_check <- new.env() + load(file.path(pft_outdir, "trait.mcmc.Rdata"), envir = mcmc_check) + expect_true("trait.mcmc" %in% ls(mcmc_check)) + expect_true(is.list(mcmc_check$trait.mcmc)) + + pd_check <- new.env() + load(file.path(pft_outdir, "post.distns.MA.Rdata"), envir = pd_check) + expect_true("post.distns" %in% ls(pd_check)) + expect_s3_class(pd_check$post.distns, "data.frame") + expect_true(all(c("distn", "parama", "paramb") %in% names(pd_check$post.distns))) + + jd_check <- new.env() + load(file.path(pft_outdir, "jagged.data.Rdata"), envir = jd_check) + expect_true("jagged.data" %in% ls(jd_check)) + expect_true(is.list(jd_check$jagged.data)) }) \ No newline at end of file diff --git a/modules/uncertainty/tests/testthat/helper-workflow-fixtures.R b/modules/uncertainty/tests/testthat/helper-workflow-fixtures.R new file mode 100644 index 00000000000..11036205e67 --- /dev/null +++ b/modules/uncertainty/tests/testthat/helper-workflow-fixtures.R @@ -0,0 +1,116 @@ +# Mock PFT settings list +# Mirrors the `pft` list element used throughout the pipeline +# (get.trait.data.pft -> run.meta.analysis.pft -> get.parameter.samples). +make_mock_pft <- function(name = "temperate.Hardwood", + outdir = tempfile("pft_"), + posteriorid = 9999L) { + dir.create(outdir, recursive = TRUE, showWarnings = FALSE) + list( + name = name, + outdir = outdir, + posteriorid = posteriorid, + constants = list() + ) +} + +# Mock trait data (as saved in trait.data.Rdata) +# A named list of data frames, one per trait. +make_mock_trait_data <- function(traits = c("SLA", "Vcmax"), + n_obs = 10, + seed = 42) { + set.seed(seed) + result <- list() + for (trait in traits) { + result[[trait]] <- data.frame( + mean = rnorm(n_obs, mean = ifelse(trait == "SLA", 20, 40), sd = 3), + stat = rep(1.5, n_obs), + n = rep(5L, n_obs), + statname = rep("SD", n_obs), + site_id = seq_len(n_obs), + greenhouse = rep(FALSE, n_obs), + name = rep(trait, n_obs), + treatment_id = rep(1L, n_obs), + control = rep(1L, n_obs), + specie_id = rep(1L, n_obs), + citation_id = rep(1L, n_obs), + cultivar_id = rep(NA_integer_, n_obs), + date = rep(NA_character_, n_obs), + time = rep(NA_character_, n_obs), + stringsAsFactors = FALSE + ) + } + result +} + +# Mock prior distributions (as saved in prior.distns.Rdata) +make_mock_prior_distns <- function(traits = c("SLA", "Vcmax")) { + data.frame( + distn = rep("norm", length(traits)), + parama = c(20, 40)[seq_along(traits)], + paramb = c(5, 10)[seq_along(traits)], + n = c(50L, 30L)[seq_along(traits)], + row.names = traits, + stringsAsFactors = FALSE + ) +} + +# Mock MCMC results (as saved in trait.mcmc.Rdata) +# Returns a named list of `coda::mcmc.list` objects (one per trait), +# mimicking output from `pecan.ma()`. +make_mock_trait_mcmc <- function(traits = c("SLA"), + n_samples = 200, + seed = 42) { + set.seed(seed) + result <- list() + for (trait in traits) { + chain <- matrix( + rnorm(n_samples, mean = ifelse(trait == "SLA", 20, 40), sd = 2), + ncol = 1 + ) + colnames(chain) <- "beta.o" + result[[trait]] <- coda::mcmc.list(coda::mcmc(chain)) + } + result +} + +# Mock posterior distributions (as saved in post.distns.Rdata) +make_mock_post_distns <- function(traits = c("SLA", "Vcmax")) { + data.frame( + distn = rep("norm", length(traits)), + parama = c(20.5, 39.8)[seq_along(traits)], + paramb = c(2.1, 4.5)[seq_along(traits)], + n = c(50L, 30L)[seq_along(traits)], + row.names = traits, + stringsAsFactors = FALSE + ) +} + +# Write a full set of trait-pipeline fixtures to disk +# Saves the .Rdata files that run.meta.analysis.pft() would normally +# produce, so downstream steps (get.parameter.samples) can be tested +# without running the actual meta-analysis. +write_trait_pipeline_fixtures <- function(outdir, + traits = c("SLA", "Vcmax"), + n_obs = 10, + seed = 42) { + dir.create(outdir, recursive = TRUE, showWarnings = FALSE) + + trait.data <- make_mock_trait_data(traits, n_obs = n_obs, seed = seed) + save(trait.data, file = file.path(outdir, "trait.data.Rdata")) + + prior.distns <- make_mock_prior_distns(traits) + save(prior.distns, file = file.path(outdir, "prior.distns.Rdata")) + + trait.mcmc <- make_mock_trait_mcmc(traits, seed = seed) + save(trait.mcmc, file = file.path(outdir, "trait.mcmc.Rdata")) + + post.distns <- make_mock_post_distns(traits) + save(post.distns, file = file.path(outdir, "post.distns.Rdata")) + save(post.distns, file = file.path(outdir, "post.distns.MA.Rdata")) + file.symlink( + file.path(outdir, "post.distns.MA.Rdata"), + file.path(outdir, "post.distns.link.check") + ) + + invisible(outdir) +} diff --git a/modules/uncertainty/tests/testthat/test-samples-rdata-structure.R b/modules/uncertainty/tests/testthat/test-samples-rdata-structure.R new file mode 100644 index 00000000000..e19b78c3f3d --- /dev/null +++ b/modules/uncertainty/tests/testthat/test-samples-rdata-structure.R @@ -0,0 +1,189 @@ +skip_if_not_installed("PEcAn.priors") +skip_if_not_installed("PEcAn.utils") +skip_if_not_installed("PEcAn.logger") +skip_if_not_installed("coda") + +# samples.Rdata structure +test_that("samples.Rdata contains all 5 expected objects with correct types", { + + outdir <- tempfile("samples_test_") + dir.create(outdir, recursive = TRUE) + on.exit(unlink(outdir, recursive = TRUE), add = TRUE) + + pft_outdir <- file.path(outdir, "pft", "test_pft") + dir.create(pft_outdir, recursive = TRUE) + + # Write upstream fixtures to disk (normally produced by meta-analysis) + prior.distns <- make_mock_prior_distns(c("SLA", "Vcmax")) + save(prior.distns, file = file.path(pft_outdir, "prior.distns.Rdata")) + + trait.mcmc <- make_mock_trait_mcmc(c("SLA"), seed = 42) + save(trait.mcmc, file = file.path(pft_outdir, "trait.mcmc.Rdata")) + + post.distns <- make_mock_post_distns(c("SLA", "Vcmax")) + save(post.distns, file = file.path(pft_outdir, "post.distns.Rdata")) + + # Call the pure function + result <- get_parameter_samples( + pft_names = "test_pft", + prior_distns_list = list(prior.distns), + trait_mcmc_list = list(trait.mcmc), + ensemble.size = 10, + ens.sample.method = "uniform", + sa_quantiles = c(0.025, 0.5, 0.975), + do_ensemble = TRUE, + independent = TRUE + ) + + # Save to disk the same way get.parameter.samples() does + ensemble.samples <- result$ensemble.samples + trait.samples <- result$trait.samples + sa.samples <- result$sa.samples + runs.samples <- result$runs.samples + env.samples <- result$env.samples + save(ensemble.samples, trait.samples, sa.samples, runs.samples, env.samples, + file = file.path(outdir, "samples.Rdata")) + + # Now load it back and verify structure + samples_file <- file.path(outdir, "samples.Rdata") + expect_true(file.exists(samples_file)) + + env <- new.env() + load(samples_file, envir = env) + + # All 5 objects must be present + expected_names <- c("ensemble.samples", "trait.samples", "sa.samples", + "runs.samples", "env.samples") + for (name in expected_names) { + expect_true(name %in% ls(env), + info = paste("samples.Rdata missing object:", name)) + } + + # No unexpected objects + expect_equal(sort(ls(env)), sort(expected_names)) +}) + + + +# trait.samples structure +test_that("trait.samples is a nested list: PFT -> trait -> numeric vector", { + priors <- make_mock_prior_distns(c("SLA", "Vcmax")) + mcmc <- make_mock_trait_mcmc("SLA", n_samples = 100) + + result <- get_parameter_samples( + pft_names = "temperate.Hardwood", + prior_distns_list = list(priors), + trait_mcmc_list = list(mcmc), + ensemble.size = 10, + ens.sample.method = "uniform", + do_ensemble = TRUE + ) + + ts <- result$trait.samples + expect_true(is.list(ts)) + expect_true("temperate.Hardwood" %in% names(ts)) + + pft_ts <- ts[["temperate.Hardwood"]] + expect_true(is.list(pft_ts)) + expect_true("SLA" %in% names(pft_ts)) + expect_true("Vcmax" %in% names(pft_ts)) + + # Each trait's samples should be a numeric vector + expect_true(is.numeric(pft_ts[["SLA"]])) + expect_true(is.numeric(pft_ts[["Vcmax"]])) + expect_true(length(pft_ts[["SLA"]]) > 0) +}) + + + +# sa.samples structure +test_that("sa.samples has correct matrix structure when quantiles provided", { + priors <- make_mock_prior_distns(c("SLA", "Vcmax")) + + result <- get_parameter_samples( + pft_names = "test_pft", + prior_distns_list = list(priors), + trait_mcmc_list = list(NULL), + ensemble.size = 10, + sa_quantiles = c(0.025, 0.5, 0.975), + do_ensemble = FALSE + ) + + sa <- result$sa.samples + expect_true(is.list(sa)) + expect_true(length(sa) > 0) + + # SA samples for each PFT should be a named list of matrices + # keyed by trait, where each matrix has rows = quantiles + pft_sa <- sa[["test_pft"]] + expect_true(is.list(pft_sa)) +}) + + + +# ensemble.samples structure +test_that("ensemble.samples has correct structure", { + priors <- make_mock_prior_distns(c("SLA")) + + result <- get_parameter_samples( + pft_names = "test_pft", + prior_distns_list = list(priors), + trait_mcmc_list = list(NULL), + ensemble.size = 20, + ens.sample.method = "uniform", + do_ensemble = TRUE + ) + + ens <- result$ensemble.samples + expect_true(is.list(ens)) + expect_true(length(ens) > 0) +}) + + + +# runs.samples and env.samples structure +test_that("runs.samples and env.samples are empty lists", { + priors <- make_mock_prior_distns("SLA") + + result <- get_parameter_samples( + pft_names = "test_pft", + prior_distns_list = list(priors), + trait_mcmc_list = list(NULL), + ensemble.size = 5, + ens.sample.method = "uniform", + do_ensemble = TRUE + ) + + expect_true(is.list(result$runs.samples)) + expect_equal(length(result$runs.samples), 0) + + expect_true(is.list(result$env.samples)) + expect_equal(length(result$env.samples), 0) +}) + + + +# Multi-PFT samples.Rdata has all PFTs present +test_that("samples.Rdata for multiple PFTs preserves all PFT entries", { + priors1 <- make_mock_prior_distns("SLA") + priors2 <- make_mock_prior_distns("Vcmax") + mcmc1 <- make_mock_trait_mcmc("SLA", seed = 1) + + result <- get_parameter_samples( + pft_names = c("hardwood", "conifer"), + prior_distns_list = list(priors1, priors2), + trait_mcmc_list = list(mcmc1, NULL), + ensemble.size = 10, + ens.sample.method = "uniform", + do_ensemble = TRUE + ) + + # trait.samples should have entries for both PFTs + expect_true("hardwood" %in% names(result$trait.samples)) + expect_true("conifer" %in% names(result$trait.samples)) + + # ensemble.samples is list(pft_data, sampled_indices) from get.ensemble.samples() + # PFT names live inside the first element + expect_true("hardwood" %in% names(result$ensemble.samples[[1]])) + expect_true("conifer" %in% names(result$ensemble.samples[[1]])) +})