# ────────────────────────────────────────────────────────────────────────────── # helper-fixtures.R — session-cached fixtures & reusable test utilities # # This file is automatically sourced by testthat before any test file. # Follows the bgms pattern of lazy-cached fixtures and shared helpers. # ────────────────────────────────────────────────────────────────────────────── # ── Load package functions ──────────────────────────────────────────────────── # When running via devtools::test() or test_check() the package is loaded # automatically. When running via test_dir() or Rscript we source all R files # so that the in-development versions are used instead of any installed copy. # We always source to ensure local edits take precedence over the installed pkg. local({ here <- tryCatch(dirname(sys.frame(1)$ofile), error = function(e) ".") pkg_root <- normalizePath(file.path(here, "..", "..")) r_dir <- file.path(pkg_root, "R") if (dir.exists(r_dir)) { for (f in list.files(r_dir, pattern = "\\.R$", full.names = TRUE)) source(f) } }) # ── Session cache environment ──────────────────────────────────────────────── .test_cache <- new.env(parent = emptyenv()) # ── Fixture getters (each cached once per test session) ────────────────────── #' Get a simulated bernoulli dataset (cached) get_sim_bernoulli <- function() { if (!exists("sim_bernoulli", envir = .test_cache)) { .test_cache$sim_bernoulli <- sim_var( N = 10, T_obs = 50, p = 3, K = 1, family = "bernoulli", q = 2, seed = 42 ) } .test_cache$sim_bernoulli } #' Get a simulated gaussian dataset (cached) get_sim_gaussian <- function() { if (!exists("sim_gaussian", envir = .test_cache)) { .test_cache$sim_gaussian <- sim_var( N = 10, T_obs = 50, p = 3, K = 1, family = "gaussian", q = 2, seed = 43 ) } .test_cache$sim_gaussian } #' Get a simulated ordinal dataset (cached) get_sim_ordinal <- function() { if (!exists("sim_ordinal", envir = .test_cache)) { .test_cache$sim_ordinal <- sim_var( N = 10, T_obs = 50, p = 3, K = 1, family = "ordinal", q = 2, C = 5, seed = 44 ) } .test_cache$sim_ordinal } #' Get stan data for a bernoulli simulation (cached) get_standata_bernoulli <- function() { if (!exists("standata_bernoulli", envir = .test_cache)) { sim <- get_sim_bernoulli() .test_cache$standata_bernoulli <- to_stan_data( data = sim$data, family = "bernoulli", id_col = "id", time_col = "t", y_cols = paste0("y_", 1:3), x_cols = paste0("x_", 1:2), K = 1 ) } .test_cache$standata_bernoulli } #' Get stan data for a gaussian simulation (cached) get_standata_gaussian <- function() { if (!exists("standata_gaussian", envir = .test_cache)) { sim <- get_sim_gaussian() .test_cache$standata_gaussian <- to_stan_data( data = sim$data, family = "gaussian", id_col = "id", time_col = "t", y_cols = paste0("y_", 1:3), x_cols = paste0("x_", 1:2), K = 1 ) } .test_cache$standata_gaussian } #' Get stan data for an ordinal simulation (cached) get_standata_ordinal <- function() { if (!exists("standata_ordinal", envir = .test_cache)) { sim <- get_sim_ordinal() .test_cache$standata_ordinal <- to_stan_data( data = sim$data, family = "ordinal", id_col = "id", time_col = "t", y_cols = paste0("y_", 1:3), x_cols = paste0("x_", 1:2), K = 1 ) } .test_cache$standata_ordinal } # ── Reusable test helpers ──────────────────────────────────────────────────── #' Check if all values lie within [lo, hi] values_in_range <- function(x, lo, hi) { all(x >= lo & x <= hi, na.rm = TRUE) } #' Check if a matrix is symmetric is_symmetric <- function(m, tol = .Machine$double.eps^0.5) { is.matrix(m) && nrow(m) == ncol(m) && all(abs(m - t(m)) < tol) } #' Build a minimal long-format data frame for testing to_stan_data make_test_df <- function(N = 5, T_obs = 20, p = 2, q = 0, family = "bernoulli", seed = 123) { set.seed(seed) df <- expand.grid(t = 1:T_obs, id = 1:N) df <- df[order(df$id, df$t), ] for (j in seq_len(p)) { col <- paste0("y_", j) if (family == "bernoulli") { df[[col]] <- rbinom(nrow(df), 1, 0.5) } else if (family == "gaussian") { df[[col]] <- rnorm(nrow(df)) } else if (family == "ordinal") { df[[col]] <- sample(1:5, nrow(df), replace = TRUE) } } for (j in seq_len(q)) { df[[paste0("x_", j)]] <- rnorm(nrow(df)) } df } #' Build a mock bvarnet object without running Stan #' #' Returns a minimal bvarnet list with a synthetic 3D draws array, a matching #' summary data.frame, and stubbed diagnostics/timing/metadata fields. #' p=2 outcomes, K=1 lag, n_fe=2 (Intercept + x_1). #' For ordinal: C=3 (2 cutpoints). For gaussian: sigma included. #' When \code{n_re > 0}, \code{sd_u} and \code{u} draw columns are added #' with \code{J} subjects and \code{n_re} random-effect columns. #' #' \code{family} can be a scalar (applied to all p nodes) or a character #' vector of length p for mixed-family mocks. When mixed, sigma columns are #' only generated for gaussian nodes and kappa columns only for ordinal nodes. #' Ordinal nodes get \code{beta[1,j] = NA_real_} sentinel (D4). make_mock_bvarnet <- function(family = "bernoulli", n_iter = 20L, n_chains = 2L, n_re = 0L, J = 5L) { p <- 2L; K <- 1L # Normalise family to a named vector of length p y_cols <- c("y_1", "y_2") if (length(family) == 1L) { family_vec <- setNames(rep(family, p), y_cols) } else { stopifnot(length(family) == p) family_vec <- setNames(family, y_cols) } # Pure ordinal: no Intercept, n_fe = 1; otherwise n_fe = 2 is_pure_ordinal <- all(family_vec == "ordinal") n_fe <- if (is_pure_ordinal) 1L else 2L if (is_pure_ordinal) { beta_nm <- c("beta[1,1]", "beta[1,2]") } else { beta_nm <- c("beta[1,1]", "beta[2,1]", "beta[1,2]", "beta[2,2]") } phi_nm <- c("phi[1,1]", "phi[2,1]", "phi[1,2]", "phi[2,2]") par_nms <- c(beta_nm, phi_nm) # sigma only for gaussian nodes gauss_idx <- which(family_vec == "gaussian") if (length(gauss_idx) > 0L) { sigma_nm <- paste0("sigma[", gauss_idx, "]") par_nms <- c(par_nms, sigma_nm) } # kappa only for ordinal nodes (C=3 → 2 cutpoints each) ord_idx <- which(family_vec == "ordinal") if (length(ord_idx) > 0L) { kappa_nm <- character(0) for (node in ord_idx) for (k in 1:2) kappa_nm <- c(kappa_nm, sprintf("kappa[%d,%d]", node, k)) par_nms <- c(par_nms, kappa_nm) } # sd_u parameters: sd_u[node,re] if (n_re > 0L) { sd_u_nm <- character(0) for (node in seq_len(p)) for (re in seq_len(n_re)) sd_u_nm <- c(sd_u_nm, sprintf("sd_u[%d,%d]", node, re)) par_nms <- c(par_nms, sd_u_nm) } # u parameters: array[p] matrix[J, n_re] → u[node, subject, re] u_nm <- character(0) if (n_re > 0L) { for (node in seq_len(p)) for (subj in seq_len(J)) for (re in seq_len(n_re)) u_nm <- c(u_nm, sprintf("u[%d,%d,%d]", node, subj, re)) par_nms <- c(par_nms, u_nm) } n_par <- length(par_nms) set.seed(42L) draws <- array( rnorm(n_iter * n_chains * n_par), dim = c(n_iter, n_chains, n_par), dimnames = list(NULL, NULL, par_nms) ) # Make sigma draws positive if (length(gauss_idx) > 0L) { sigma_idx_arr <- grep("^sigma\\[", par_nms) draws[, , sigma_idx_arr] <- abs(draws[, , sigma_idx_arr]) + 0.1 } # Make kappa draws ordered (ascending per node) if (length(ord_idx) > 0L) { for (node in ord_idx) { k1 <- paste0("kappa[", node, ",1]") k2 <- paste0("kappa[", node, ",2]") draws[, , k1] <- -1 + runif(n_iter * n_chains, -0.2, 0.2) draws[, , k2] <- 1 + runif(n_iter * n_chains, -0.2, 0.2) } } # Set ordinal beta[1,j] to NA sentinel (D4) — only for mixed-family if (!is_pure_ordinal) for (j in ord_idx) { nm <- paste0("beta[1,", j, "]") draws[, , nm] <- NA_real_ } # Make sd_u draws positive (half-prior) if (n_re > 0L) { sd_u_idx <- grep("^sd_u\\[", par_nms) draws[, , sd_u_idx] <- abs(draws[, , sd_u_idx]) } smry <- data.frame( variable = par_nms, rhat = 1.001, ess_bulk = 3000, ess_tail = 2800, stringsAsFactors = FALSE ) n_obs <- 10L Y <- matrix(0L, n_obs, p, dimnames = list(NULL, c("y_1", "y_2"))) B <- matrix(0, n_obs, p * K, dimnames = list(NULL, c("lag1_y_1", "lag1_y_2"))) # Pure ordinal models have no Intercept in X; mixed/other families do is_pure_ordinal <- all(family_vec == "ordinal") if (is_pure_ordinal) { X <- matrix(0, n_obs, 1L, dimnames = list(NULL, "x_1")) n_fe <- 1L } else { X <- matrix(0, n_obs, n_fe, dimnames = list(NULL, c("Intercept", "x_1"))) } if (n_re > 0L) { re_colnames <- paste0("z_", seq_len(n_re)) Z <- matrix(rnorm(n_obs * n_re), n_obs, n_re, dimnames = list(NULL, re_colnames)) } else { Z <- matrix(0, n_obs, 0L) } sd_list <- list( p = p, K = K, n_fe = n_fe, n_re = n_re, n = n_obs, n_obs = n_obs, J = J, Y = Y, X = X, B = B, Z = Z, id = rep(seq_len(J), length.out = n_obs), id_levels = as.character(seq_len(J)), x_center_means = NULL, fe_interaction_terms = list(), design_spec = list( id_col = "id", time_col = "t", y_cols = c("y_1", "y_2"), x_cols = "x_1", center_x = FALSE, fe_interactions = NULL, re_interactions = NULL, re_cols = character(0), re_temporal = FALSE, K = K, skip_lag = TRUE, na_action = "listwise" ) ) if (length(ord_idx) > 0L) sd_list$C <- 3L if (length(ord_idx) > 0L) { C_per_node <- setNames(rep(NA_integer_, p), y_cols) C_per_node[ord_idx] <- 3L sd_list$C_per_node <- C_per_node } # Compute priors_needed for this mock priors_needed <- c("beta", "phi") if (n_re > 0L) priors_needed <- c(priors_needed, "sd_u") if (any(family_vec == "gaussian")) priors_needed <- c(priors_needed, "sigma") if (any(family_vec == "ordinal")) priors_needed <- c(priors_needed, "kappa") structure( list( draws = draws, convergence = smry, diagnostics = data.frame( num_divergent = integer(n_chains), num_max_treedepth = integer(n_chains), ebfmi = rep(1.0, n_chains) ), timing = list(total = 5.0), metadata = list(), return_codes = rep(0L, n_chains), family = family_vec, standata = sd_list, priors = set_priors(), priors_needed = priors_needed ), class = "bvarnet" ) }