Nothing
# Sparse path numerical equivalence against the dense path
# (gcol33/tulpa: Stage 1.5b of the joint sparse rollout).
#
# The joint multi-block kernel auto-routes to a sparse Newton path when
# n_x crosses SPARSE_THRESHOLD or when any block contributes through
# non-INDEXED_SINGLE semantics. At small n_x with classic blocks
# (icar/bym2/car_proper/rw1/rw2/ar1/iid) both paths produce mathematically
# identical Hessians; force_sparse = TRUE lets us exercise the sparse code
# at small scale against the validated dense reference.
#
# Tolerance: 1e-6 on log_marginal is well within Newton-step quantization;
# the modes typically agree to <1e-7 component-wise. We assert log_marginal
# equality first because mode comparison can have spurious differences
# when convergence stops early on either path.
skip_on_cran()
.chain_adj <- function(n_s) {
nbr <- lapply(seq_len(n_s),
function(s) setdiff(c(s - 1L, s + 1L), c(0L, n_s + 1L)))
n_neighbors <- vapply(nbr, length, integer(1))
list(
adj_row_ptr = as.integer(c(0L, cumsum(n_neighbors))),
adj_col_idx = as.integer(unlist(nbr)) - 1L,
n_neighbors = as.integer(n_neighbors),
n_spatial_units = n_s
)
}
.sim_joint_small <- function(seed = 42L, N = 200L, n_s = 25L,
sigma = 0.6, rho = 0.7, alpha_true = 1.0,
tau_b = 0.5) {
set.seed(seed)
adj <- .chain_adj(n_s)
# Sample a smooth field via independent draws + smoothing (good enough
# for a numerical-equivalence test; we don't care about recovery).
w <- as.numeric(arima.sim(n = n_s, list(ar = 0.6))) * sigma
w <- w - mean(w)
s_idx_1 <- sample(seq_len(n_s), N, replace = TRUE)
s_idx_2 <- sample(seq_len(n_s), N, replace = TRUE)
x1 <- rnorm(N); x2 <- rnorm(N)
eta1 <- 0.3 + 0.5 * x1 + w[s_idx_1]
eta2 <- -0.2 + 0.4 * x2 + alpha_true * w[s_idx_2]
y1 <- rbinom(N, 1L, plogis(eta1))
y2 <- rnorm(N, mean = eta2, sd = 0.4)
list(
n_s = n_s, N = N, adj = adj,
responses = list(
occ = list(y = as.numeric(y1), n_trials = rep(1L, N),
X = cbind(intercept = 1, x = x1),
spatial_idx = s_idx_1,
re_idx = rep(0L, N), n_re_groups = 0L,
sigma_re = 1.0,
family = "binomial", phi = 1.0),
pos = list(y = as.numeric(y2), n_trials = rep(1L, N),
X = cbind(intercept = 1, x = x2),
spatial_idx = s_idx_2,
re_idx = rep(0L, N), n_re_groups = 0L,
sigma_re = 1.0,
family = "gaussian", phi = 0.16)
)
)
}
.expect_equiv_fits <- function(fit_dense, fit_sparse,
lm_tol = 1e-6, mode_tol = 1e-5) {
expect_equal(length(fit_dense$log_marginal),
length(fit_sparse$log_marginal))
# Per-cell log_marginal must agree.
expect_equal(
as.numeric(fit_sparse$log_marginal),
as.numeric(fit_dense$log_marginal),
tolerance = lm_tol,
info = "per-cell log_marginal disagreement between dense and sparse paths"
)
# Modes should agree elementwise when both fits stored them.
if (!is.null(fit_dense$modes) && !is.null(fit_sparse$modes) &&
is.matrix(fit_dense$modes) && is.matrix(fit_sparse$modes)) {
expect_equal(dim(fit_sparse$modes), dim(fit_dense$modes))
expect_equal(as.numeric(fit_sparse$modes),
as.numeric(fit_dense$modes),
tolerance = mode_tol,
info = "per-cell mode disagreement between dense and sparse paths")
}
}
test_that("sparse path matches dense on joint BYM2 with copy", {
sim <- .sim_joint_small(seed = 11L, alpha_true = 1.2)
prior <- c(
list(type = "bym2",
sigma_grid = c(0.5, 0.6),
rho_grid = c(0.7)),
sim$adj
)
responses <- sim$responses
responses$pos$field_coef <- list(name = "alpha", grid = c(1.0, 1.2))
fit_dense <- tulpa_nested_laplace_joint(
responses = responses, prior = prior,
control = list(max_iter = 40L, tol = 1e-8, n_threads = 1L, verbose = FALSE,
force_sparse = FALSE)
)
fit_sparse <- tulpa_nested_laplace_joint(
responses = responses, prior = prior,
control = list(max_iter = 40L, tol = 1e-8, n_threads = 1L, verbose = FALSE,
force_sparse = TRUE)
)
.expect_equiv_fits(fit_dense, fit_sparse)
})
test_that("sparse path matches dense on joint ICAR (no copy)", {
sim <- .sim_joint_small(seed = 12L, alpha_true = 1.0)
prior <- c(
list(type = "icar",
sigma_grid = c(0.5, 0.7)),
sim$adj
)
fit_dense <- tulpa_nested_laplace_joint(
responses = sim$responses, prior = prior, copy = NULL,
control = list(max_iter = 40L, tol = 1e-8, n_threads = 1L, verbose = FALSE,
force_sparse = FALSE)
)
fit_sparse <- tulpa_nested_laplace_joint(
responses = sim$responses, prior = prior, copy = NULL,
control = list(max_iter = 40L, tol = 1e-8, n_threads = 1L, verbose = FALSE,
force_sparse = TRUE)
)
.expect_equiv_fits(fit_dense, fit_sparse)
})
test_that("sparse path matches dense on joint CAR_proper", {
sim <- .sim_joint_small(seed = 13L)
prior <- c(
list(type = "car_proper",
sigma_grid = c(0.5),
rho_car_grid = c(0.9, 0.95)),
sim$adj
)
fit_dense <- tulpa_nested_laplace_joint(
responses = sim$responses, prior = prior, copy = NULL,
control = list(max_iter = 40L, tol = 1e-8, n_threads = 1L, verbose = FALSE,
force_sparse = FALSE)
)
fit_sparse <- tulpa_nested_laplace_joint(
responses = sim$responses, prior = prior, copy = NULL,
control = list(max_iter = 40L, tol = 1e-8, n_threads = 1L, verbose = FALSE,
force_sparse = TRUE)
)
.expect_equiv_fits(fit_dense, fit_sparse)
})
# --- gcol33/tulpa#432: what the two paths EXPORT, not only what they report ---
#
# The comparisons above read log_marginal and the modes. The sparse loop also
# hands out a precision (store_Q) and a fixed-effect covariance block, and it
# used to hand out `H_builder` AFTER joint_pd_step_solve had run: in LM mode
# that call loads the diagonal on every failed factorization and never takes the
# load back off, so the export was of H + lambda I. A covariance read off that is
# smaller than the one read off H, so the standard errors come out low with
# nothing on the result saying so. The dense sibling gated the same export on a
# clean factorization; the sparse loop, whose header says it mirrors it exactly,
# carried no equivalent condition.
#
# The export is now a snapshot of the values the scatter left behind, taken
# before the conditioning step, and `pd_conditioned` records whether that step
# had to do anything.
.expect_equiv_fixed <- function(fit_dense, fit_sparse, tol = 1e-7) {
sd_ <- coef(summary(fit_dense))
ss_ <- coef(summary(fit_sparse))
expect_equal(as.numeric(ss_$estimate), as.numeric(sd_$estimate),
tolerance = tol, info = "fixed-effect estimates")
expect_equal(as.numeric(ss_$std.error), as.numeric(sd_$std.error),
tolerance = tol, info = "fixed-effect standard errors")
expect_equal(as.numeric(ss_[["2.5 %"]]), as.numeric(sd_[["2.5 %"]]),
tolerance = tol, info = "lower bounds")
# The retained per-cell fixed-effect precisions, which is what the summary
# marginalizes. Compared relatively: these run to 1e3 on this fixture.
expect_false(is.null(fit_dense$grid_hessians))
expect_false(is.null(fit_sparse$grid_hessians))
expect_length(fit_sparse$grid_hessians, length(fit_dense$grid_hessians))
for (k in seq_along(fit_dense$grid_hessians)) {
hd <- fit_dense$grid_hessians[[k]]
hs <- fit_sparse$grid_hessians[[k]]
expect_equal(dim(hs), dim(hd), info = paste("cell", k))
expect_lt(max(abs(hs - hd)) / max(abs(hd)), tol)
}
}
.fit_joint_pair <- function(prior, responses, ...) {
ctl <- utils::modifyList(
list(max_iter = 40L, tol = 1e-8, n_threads = 1L, verbose = FALSE,
keep_grid_hessians = TRUE, diagnose_k = FALSE, progress = FALSE),
list(...))
lapply(c(FALSE, TRUE), function(fs) {
tulpa_nested_laplace_joint(
responses = responses, prior = prior, copy = NULL,
control = utils::modifyList(ctl, list(force_sparse = fs)))
})
}
test_that("the two paths export the same fixed-effect block and precision", {
sim <- .sim_joint_small(seed = 12L, alpha_true = 1.0)
for (prior in list(
c(list(type = "icar", sigma_grid = c(0.5, 0.7)), sim$adj),
c(list(type = "bym2", sigma_grid = c(0.5, 0.6),
rho_grid = c(0.7)), sim$adj),
c(list(type = "car_proper", sigma_grid = c(0.5),
rho_car_grid = c(0.9, 0.95)), sim$adj))) {
fits <- .fit_joint_pair(prior, sim$responses)
.expect_equiv_fixed(fits[[1]], fits[[2]])
}
})
test_that("the export survives the sum-to-zero rank-1 storage", {
# With the augmentation's 1 1' left OFF the stored H and folded in at solve
# time instead, the sparse loop's factorization is of a matrix whose
# constant direction is unpinned -- the arrangement the export was taken
# from. TULPA_S2Z_DENSIFY_MAX = 0 forces it at any field size, and a large
# value stores the augmentation at any size.
old <- Sys.getenv("TULPA_S2Z_DENSIFY_MAX", unset = NA)
on.exit(if (is.na(old)) Sys.unsetenv("TULPA_S2Z_DENSIFY_MAX")
else Sys.setenv(TULPA_S2Z_DENSIFY_MAX = old), add = TRUE)
sim <- .sim_joint_small(seed = 12L, alpha_true = 1.0)
prior <- c(list(type = "icar", sigma_grid = c(0.5, 0.7)), sim$adj)
Sys.setenv(TULPA_S2Z_DENSIFY_MAX = "100000")
dense_store <- .fit_joint_pair(prior, sim$responses)[[2]]
Sys.setenv(TULPA_S2Z_DENSIFY_MAX = "0")
fits <- .fit_joint_pair(prior, sim$responses)
.expect_equiv_fixed(fits[[1]], fits[[2]], tol = 1e-6)
# ... and the rank-1 storage gives the same standard errors as storing the
# augmentation exactly, so the fold is not silently shrinking them.
expect_equal(as.numeric(coef(summary(fits[[2]]))$std.error),
as.numeric(coef(summary(dense_store))$std.error),
tolerance = 1e-6)
})
test_that("a conditioned factorization is reported rather than silent", {
coupled_occ_register()
# The occupancy mixture's dark-cell term is not concave, so a fit stopped
# after one Newton iteration sits at a point whose Hessian is indefinite and
# the final factorization has to condition it. That is the state the export
# gate exists for, and the flag is what makes it readable from R.
d <- coupled_occ_data(seed = 500001L, n_cells = 100L, n_visits = 4L,
b_occ = -2.5, b_det = -1.0)
fit_at <- function(fs, max_iter) suppressWarnings(tulpa_nested_laplace_joint(
responses = coupled_occ_arms(d, beta_prec = 0.25),
prior = coupled_occ_flat_prior(d),
cell_coupling = "test_occupancy_mixture",
control = list(max_iter = max_iter, tol = 1e-10, n_threads = 1L,
keep_grid_hessians = TRUE, diagnose_k = FALSE,
diagnose_skew = FALSE, auto_recenter = FALSE,
progress = FALSE, force_sparse = fs)))
for (fs in c(FALSE, TRUE)) {
stalled <- fit_at(fs, 1L)
expect_false(as.logical(stalled$converged)[1L], info = paste("sparse", fs))
expect_true(all(stalled$pd_conditioned), info = paste("sparse", fs))
# Nothing conditioned reaches a coefficient table: the retention
# declines upstream, on the cause that stopped the solve.
expect_identical(stalled$grid_fixed_declined, "not_converged")
expect_true(all(is.na(coef(stalled))))
converged <- fit_at(fs, 300L)
expect_true(as.logical(converged$converged)[1L])
expect_false(any(converged$pd_conditioned), info = paste("sparse", fs))
expect_true(is.na(converged$grid_fixed_declined))
}
})
test_that("the two backends agree on whether they had to condition", {
# A verdict that depended on the backend would put the dense and sparse
# reads of one model into disagreement about whether its export is clean.
sim <- .sim_joint_small(seed = 12L)
prior <- c(list(type = "icar", sigma_grid = c(0.5, 0.7)), sim$adj)
fits <- .fit_joint_pair(prior, sim$responses)
expect_identical(as.logical(fits[[2]]$pd_conditioned),
as.logical(fits[[1]]$pd_conditioned))
# This fixture factorizes on the first attempt, which is what makes it the
# control arm for the coupled test above.
expect_false(any(fits[[1]]$pd_conditioned))
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.