Nothing
# test-spde.R
# Tests for SPDE spatial Laplace (Matérn via FEM mesh)
# Uses fmesher for mesh generation and FEM matrices
test_that("SPDE Laplace fits binomial model on fmesher mesh", {
skip_if_not_installed("fmesher")
skip_on_cran()
set.seed(42)
n_obs <- 200
coords <- cbind(runif(n_obs), runif(n_obs))
# Build mesh
mesh <- fmesher::fm_mesh_2d(loc = coords, max.edge = c(0.15, 0.5), cutoff = 0.05)
n_mesh <- mesh$n
# FEM matrices
fem <- fmesher::fm_fem(mesh)
C0 <- fem$c0 # lumped mass (diagonal)
G1 <- fem$g1 # stiffness
# Projection matrix
A <- fmesher::fm_basis(mesh, loc = coords)
# Convert to CSC components for C++ (Matrix uses 0-based indices internally)
# A is dgCMatrix: slots @i (row indices, 0-based), @p (col pointers), @x (values)
A_csc <- as(A, "CsparseMatrix")
G1_csc <- as(G1, "CsparseMatrix")
# C0 diagonal
C0_diag <- Matrix::diag(C0)
# Simulate spatial field on mesh
# Simple: just use intercept + noise
beta0 <- -0.5
w_true <- rnorm(n_mesh, 0, 0.3)
w_true <- w_true - mean(w_true)
eta <- beta0 + as.numeric(A %*% w_true)
prob <- plogis(eta)
y <- rbinom(n_obs, size = 1, prob = prob)
# Design matrix: intercept only
X <- matrix(1.0, nrow = n_obs, ncol = 1)
# SPDE parameters: kappa and tau
# For range ~ 0.3, nu = 1: kappa = sqrt(8*1) / 0.3 ~ 9.4
# tau = 1 / (sqrt(4*pi) * kappa * sigma) where sigma ~ 0.5
range_true <- 0.3
sigma_true <- 0.5
kappa <- sqrt(8) / range_true
tau_spde <- 1.0 / (sqrt(4 * pi) * kappa * sigma_true)
result <- cpp_laplace_fit_spde(
y = as.integer(y),
n_trials = as.integer(rep(1L, n_obs)),
X = X,
re_idx = rep(0, n_obs), n_re_groups = 0L, sigma_re = 1.0,
A_x = A_csc@x,
A_i = A_csc@i,
A_p = A_csc@p,
n_obs = n_obs,
n_mesh = n_mesh,
C0_diag = C0_diag,
G1_x = G1_csc@x,
G1_i = G1_csc@i,
G1_p = G1_csc@p,
kappa = kappa,
tau_spde = tau_spde,
family = "binomial",
phi = 1.0,
max_iter = 100L,
tol = 1e-6,
n_threads = 1L
)
expect_true(result$converged)
expect_true(result$n_iter > 0)
expect_true(result$n_iter < 100)
expect_true(is.finite(result$log_det_Q))
expect_true(is.finite(result$log_marginal))
# Mode should have correct length: p + n_mesh
expect_equal(length(result$mode), 1L + n_mesh)
# Intercept should be in reasonable range
beta_hat <- result$mode[1]
expect_true(abs(beta_hat - beta0) < 2.0)
# Spatial effects should be centered
w_hat <- result$mode[2:(n_mesh + 1)]
expect_true(abs(mean(w_hat)) < 0.1)
})
test_that("SPDE Laplace works with Poisson family", {
skip_if_not_installed("fmesher")
skip_on_cran()
set.seed(123)
n_obs <- 150
coords <- cbind(runif(n_obs), runif(n_obs))
mesh <- fmesher::fm_mesh_2d(loc = coords, max.edge = c(0.2, 0.5), cutoff = 0.05)
fem <- fmesher::fm_fem(mesh)
A <- as(fmesher::fm_basis(mesh, loc = coords), "CsparseMatrix")
G1 <- as(fem$g1, "CsparseMatrix")
C0_diag <- Matrix::diag(fem$c0)
# Simulate Poisson
w_true <- rnorm(mesh$n, 0, 0.2)
w_true <- w_true - mean(w_true)
eta <- 1.0 + as.numeric(A %*% w_true)
y <- rpois(n_obs, lambda = exp(eta))
X <- matrix(1.0, nrow = n_obs, ncol = 1)
kappa <- sqrt(8) / 0.4
tau_spde <- 1.0 / (sqrt(4 * pi) * kappa * 0.3)
result <- cpp_laplace_fit_spde(
y = as.integer(y),
n_trials = as.integer(rep(1L, n_obs)),
X = X,
re_idx = rep(0, n_obs), n_re_groups = 0L, sigma_re = 1.0,
A_x = A@x, A_i = A@i, A_p = A@p,
n_obs = n_obs, n_mesh = mesh$n,
C0_diag = C0_diag,
G1_x = G1@x, G1_i = G1@i, G1_p = G1@p,
kappa = kappa, tau_spde = tau_spde,
family = "poisson", phi = 1.0,
max_iter = 100L, tol = 1e-6, n_threads = 1L
)
expect_true(result$converged)
expect_true(is.finite(result$log_marginal))
expect_equal(length(result$mode), 1L + mesh$n)
})
test_that("SPDE scales to 500+ mesh nodes with sparse Q", {
skip_if_not_installed("fmesher")
skip_on_cran()
set.seed(99)
n_obs <- 500
coords <- cbind(runif(n_obs), runif(n_obs))
mesh <- fmesher::fm_mesh_2d(loc = coords, max.edge = c(0.08, 0.3), cutoff = 0.03)
n_mesh <- mesh$n
fem <- fmesher::fm_fem(mesh)
A <- as(fmesher::fm_basis(mesh, loc = coords), "CsparseMatrix")
G1 <- as(fem$g1, "CsparseMatrix")
C0_diag <- Matrix::diag(fem$c0)
# Simulate
w_true <- rnorm(n_mesh, 0, 0.3)
w_true <- w_true - mean(w_true)
eta <- -0.5 + as.numeric(A %*% w_true)
y <- rbinom(n_obs, 1, plogis(eta))
X <- matrix(1.0, nrow = n_obs, ncol = 1)
kappa <- sqrt(8) / 0.2
tau_spde <- 1.0 / (sqrt(4 * pi) * kappa * 0.4)
t_elapsed <- system.time({
result <- cpp_laplace_fit_spde(
y = as.integer(y), n_trials = as.integer(rep(1L, n_obs)),
X = X,
re_idx = rep(0, n_obs), n_re_groups = 0L, sigma_re = 1.0,
A_x = A@x, A_i = A@i, A_p = A@p,
n_obs = n_obs, n_mesh = n_mesh,
C0_diag = C0_diag,
G1_x = G1@x, G1_i = G1@i, G1_p = G1@p,
kappa = kappa, tau_spde = tau_spde,
family = "binomial", phi = 1.0,
max_iter = 100L, tol = 1e-6, n_threads = 1L
)
})["elapsed"]
expect_true(result$converged)
expect_true(is.finite(result$log_marginal))
expect_equal(length(result$mode), 1L + n_mesh)
# Q should be sparse: nnz << n_mesh^2
q_density <- result$Q_nnz / (as.numeric(n_mesh) * n_mesh)
message("\n SPDE scale test: n_mesh=", n_mesh,
" Q_nnz=", result$Q_nnz,
" density=", round(q_density, 4),
" iters=", result$n_iter,
" time=", round(t_elapsed, 2), "s")
expect_true(q_density < 0.1) # Q should be < 10% dense
expect_true(n_mesh >= 500) # verify mesh is large enough
})
# =====================================================================
# Marginal H_beta for SPDE Laplace (issue #16)
# =====================================================================
test_that("tulpa_laplace returns finite PD H_beta for SPDE (#16)", {
skip_if_not_installed("tulpaMesh")
skip_on_cran()
set.seed(42)
n <- 200
coords <- cbind(runif(n), runif(n))
X <- cbind(1, rnorm(n), rnorm(n))
beta_true <- c(0.3, -0.6, 1.0)
# Smooth spatial field
d <- as.matrix(dist(coords))
Sigma_w <- exp(-d / 0.25)
w_true <- as.numeric(t(chol(Sigma_w + diag(1e-6, n))) %*% rnorm(n))
eta <- as.numeric(X %*% beta_true) + 0.7 * w_true
y <- rbinom(n, 1, plogis(eta))
mesh <- tulpaMesh::tulpa_mesh(coords, max_edge = c(0.1, 0.3), cutoff = 0.03)
spatial <- spatial_spde(coords, mesh = mesh, nu = 1,
prior_range = c(0.25, 0.5),
prior_sigma = c(1.0, 0.5))
fit <- tulpa_laplace(
y = y, n_trials = rep(1L, n), X = X,
family = "binomial", spatial = spatial,
max_iter = 100L, tol = 1e-6,
return_hessian = TRUE
)
expect_true(fit$converged)
expect_false(is.null(fit$H_beta))
expect_equal(dim(fit$H_beta), c(ncol(X), ncol(X)))
expect_true(all(is.finite(fit$H_beta)))
expect_true(isSymmetric(fit$H_beta, tol = 1e-8))
# Positive definite
ev <- eigen(fit$H_beta, symmetric = TRUE, only.values = TRUE)$values
expect_true(all(ev > 0))
# SEs finite and in a sensible range
sd <- sqrt(diag(solve(fit$H_beta)))
expect_true(all(is.finite(sd)))
expect_true(all(sd > 0 & sd < 5))
# return_hessian = FALSE should not populate H_beta
fit_noH <- tulpa_laplace(
y = y, n_trials = rep(1L, n), X = X,
family = "binomial", spatial = spatial,
max_iter = 100L, tol = 1e-6,
return_hessian = FALSE
)
expect_null(fit_noH$H_beta)
})
test_that("tulpa_laplace returns marginal H_beta for a fractional-nu SPDE (#85)", {
skip_if_not_installed("tulpaMesh")
skip_on_cran()
set.seed(7)
n <- 200
coords <- cbind(runif(n), runif(n))
X <- cbind(1, rnorm(n), rnorm(n))
beta_true <- c(0.3, -0.6, 1.0)
d <- as.matrix(dist(coords))
w_true <- as.numeric(t(chol(exp(-d / 0.25) + diag(1e-6, n))) %*% rnorm(n))
eta <- as.numeric(X %*% beta_true) + 0.7 * w_true
y <- rbinom(n, 1, plogis(eta))
mesh <- tulpaMesh::tulpa_mesh(coords, max_edge = c(0.1, 0.3), cutoff = 0.03)
# nu = 0.5 -> alpha = 1.5 (fractional, the rational SPDE path).
spatial <- spatial_spde(coords, mesh = mesh, nu = 0.5,
prior_range = c(0.25, 0.5), prior_sigma = c(1.0, 0.5))
expect_true(tulpa:::.spde_nu_is_fractional(spatial$nu))
fit <- tulpa_laplace(y = y, n_trials = rep(1L, n), X = X,
family = "binomial", spatial = spatial,
max_iter = 100L, tol = 1e-6, return_hessian = TRUE)
expect_false(is.null(fit$H_beta))
expect_equal(dim(fit$H_beta), c(ncol(X), ncol(X)))
expect_true(all(is.finite(fit$H_beta)) && isSymmetric(fit$H_beta, tol = 1e-8))
ev <- eigen(fit$H_beta, symmetric = TRUE, only.values = TRUE)$values
expect_true(all(ev > 0))
sd <- sqrt(diag(solve(fit$H_beta)))
expect_true(all(is.finite(sd) & sd > 0 & sd < 5))
# Strong wiring check: fit$H_beta must equal the Schur marginal precision of an
# INDEPENDENTLY assembled joint Hessian over (beta, auxiliary field x), built
# from the rational assembly at the fitted (range, sigma). The marginal beta
# covariance solve(H_beta) must match the beta-block of the full joint
# posterior covariance.
p <- ncol(X)
asm <- tulpa:::.spde_assemble_at(spatial, fit$range, fit$sigma,
order = spatial$rational_order %||% 2L)
A_full <- as(spatial$A, "CsparseMatrix")
beta_h <- fit$mode[seq_len(p)]
field_h <- fit$mode[p + seq_len(spatial$n_mesh)]
eta_h <- as.numeric(X %*% beta_h) + as.numeric(A_full %*% field_h)
W <- tulpa:::glmm_weights(eta_h, "binomial", rep(1L, n), 1.0)
D <- as(asm$A_eff, "CsparseMatrix")
XtWX <- crossprod(X, W * X)
XtWD <- as.matrix(Matrix::crossprod(X, W * D))
DtWD <- Matrix::crossprod(D, W * D)
P_uu <- as.matrix(Matrix::forceSymmetric(DtWD + asm$Q))
full <- rbind(cbind(as.matrix(XtWX), XtWD),
cbind(t(XtWD), P_uu))
beta_cov_full <- solve(full)[seq_len(p), seq_len(p)]
expect_equal(unname(solve(fit$H_beta)), unname(beta_cov_full), tolerance = 1e-5)
})
# =====================================================================
# Nested Laplace for SPDE: 2D grid over (range, sigma)
# =====================================================================
test_that("nested Laplace SPDE runs with 2D hyperparameter grid", {
skip_if_not_installed("fmesher")
skip_on_cran()
set.seed(42)
n_obs <- 200
coords <- cbind(runif(n_obs), runif(n_obs))
mesh <- fmesher::fm_mesh_2d(loc = coords, max.edge = c(0.15, 0.5), cutoff = 0.05)
fem <- fmesher::fm_fem(mesh)
A <- as(fmesher::fm_basis(mesh, loc = coords), "CsparseMatrix")
G1 <- as(fem$g1, "CsparseMatrix")
C0_diag <- Matrix::diag(fem$c0)
# Simulate
w_true <- rnorm(mesh$n, 0, 0.3)
w_true <- w_true - mean(w_true)
eta <- -0.5 + as.numeric(A %*% w_true)
y <- rbinom(n_obs, 1, plogis(eta))
X <- matrix(1.0, nrow = n_obs, ncol = 1)
# 2D grid: range × sigma (3 × 3 = 9 points)
range_vals <- exp(seq(log(0.1), log(0.6), length.out = 3))
sigma_vals <- exp(seq(log(0.2), log(1.0), length.out = 3))
grid <- expand.grid(range = range_vals, sigma = sigma_vals)
result <- cpp_nested_laplace_spde(
y = as.integer(y), n_trials = as.integer(rep(1L, n_obs)),
X = X,
re_idx = rep(0L, n_obs), n_re_groups = 0L, sigma_re = 1.0,
A_x = A@x, A_i = A@i, A_p = A@p,
n_obs = n_obs, n_mesh = mesh$n,
C0_diag = C0_diag,
G1_x = G1@x, G1_i = G1@i, G1_p = G1@p,
range_grid = grid$range,
sigma_grid = grid$sigma,
nu = 1.0,
family = "binomial", phi = 1.0,
max_iter = 50L, tol = 1e-6, n_threads = 1L
)
expect_equal(length(result$log_marginal), 9L)
expect_cells_solved(result)
expect_true(all(result$n_iter > 0))
# Log-marginals should vary across grid
lml_range <- max(result$log_marginal) - min(result$log_marginal)
expect_true(lml_range > 0.1)
})
test_that("nested Laplace SPDE warm-start reduces iterations", {
skip_if_not_installed("fmesher")
skip_on_cran()
set.seed(42)
n_obs <- 150
coords <- cbind(runif(n_obs), runif(n_obs))
mesh <- fmesher::fm_mesh_2d(loc = coords, max.edge = c(0.2, 0.5), cutoff = 0.05)
fem <- fmesher::fm_fem(mesh)
A <- as(fmesher::fm_basis(mesh, loc = coords), "CsparseMatrix")
G1 <- as(fem$g1, "CsparseMatrix")
C0_diag <- Matrix::diag(fem$c0)
w_true <- rnorm(mesh$n, 0, 0.2)
w_true <- w_true - mean(w_true)
eta <- 0 + as.numeric(A %*% w_true)
y <- rbinom(n_obs, 1, plogis(eta))
X <- matrix(1.0, nrow = n_obs, ncol = 1)
# Grid of nearby points (should benefit from warm-starting)
range_grid <- exp(seq(log(0.2), log(0.5), length.out = 5))
sigma_grid <- rep(0.5, 5)
result <- cpp_nested_laplace_spde(
y = as.integer(y), n_trials = as.integer(rep(1L, n_obs)),
X = X,
re_idx = rep(0L, n_obs), n_re_groups = 0L, sigma_re = 1.0,
A_x = A@x, A_i = A@i, A_p = A@p,
n_obs = n_obs, n_mesh = mesh$n,
C0_diag = C0_diag,
G1_x = G1@x, G1_i = G1@i, G1_p = G1@p,
range_grid = range_grid, sigma_grid = sigma_grid,
nu = 1.0, family = "binomial", phi = 1.0,
max_iter = 50L, tol = 1e-6, n_threads = 1L
)
expect_cells_solved(result)
# Later grid points should converge faster due to warm-start
expect_true(any(result$n_iter[2:5] <= result$n_iter[1]))
})
# ============================================================================
# Dispatch tests: tulpa_laplace(spatial = spatial_spde(...)) wires through
# ============================================================================
# Build a minimal SPDE spec without needing fmesher: a tiny synthetic mesh
# (4 nodes on a unit square) is enough to exercise the dispatch path.
make_synthetic_spde_spec <- function(n_obs = 30, seed = 7) {
set.seed(seed)
# 4 mesh nodes at the unit square corners
n_mesh <- 4L
C <- Matrix::Diagonal(n_mesh, x = 0.25)
# Stiffness for two right triangles spanning the unit square
G <- Matrix::Matrix(0, n_mesh, n_mesh, sparse = TRUE)
G[1, 1] <- 2; G[2, 2] <- 1; G[3, 3] <- 1; G[4, 4] <- 2
G[1, 2] <- G[2, 1] <- -1
G[1, 3] <- G[3, 1] <- -1
G[2, 4] <- G[4, 2] <- -0.5
G[3, 4] <- G[4, 3] <- -0.5
G <- as(G, "CsparseMatrix")
# Bilinear interpolation A: each obs to a single mesh node (nearest corner).
obs_xy <- matrix(runif(2 * n_obs), n_obs, 2)
corners <- rbind(c(0, 0), c(1, 0), c(0, 1), c(1, 1))
nearest <- apply(obs_xy, 1, function(p) which.min(rowSums((corners - p)^2)))
A <- Matrix::sparseMatrix(
i = seq_len(n_obs), j = nearest, x = 1.0,
dims = c(n_obs, n_mesh)
)
list(
spec = spatial_spde_custom(C = C, G = G, A = A,
nu = 1, prior_range = c(0.3, 0.5),
prior_sigma = c(1, 0.5)),
n_obs = n_obs
)
}
skip_on_cran_budget()
test_that("dispatch_laplace_spatial routes SPDE specs (no longer errors)", {
skip_on_cran_budget()
ss <- make_synthetic_spde_spec()
X <- matrix(1.0, nrow = ss$n_obs, ncol = 1)
y <- rbinom(ss$n_obs, 1, 0.5)
# Dispatcher previously threw "Spatial type 'spde' not yet supported in
# Laplace". The new branch should reach the C++ kernel and either return
# a result or surface a kernel error, but never the legacy dispatch gap.
res <- tryCatch(
dispatch_laplace_spatial(
y = y, n_trials = rep(1L, ss$n_obs), X = X,
re_idx = NULL, n_re_groups = 0L, sigma_re = 1.0,
spatial = ss$spec, family = "binomial", phi = 1.0,
max_iter = 5L, tol = 1e-3, n_threads = 1L
),
error = function(e) e
)
msg <- if (inherits(res, "error")) conditionMessage(res) else NA_character_
# Asserted on both branches, so the success case is checked too.
expect_false(isTRUE(grepl("not yet supported", msg, fixed = TRUE)))
if (is.na(msg)) expect_true(is.list(res))
})
skip_on_cran()
test_that("SPDE Laplace threads an additional iid RE block (issue #74)", {
skip_on_cran()
ss <- make_synthetic_spde_spec()
X <- matrix(1.0, nrow = ss$n_obs, ncol = 1)
set.seed(7)
y <- rbinom(ss$n_obs, 1, 0.5)
n_re <- 3L
re_idx <- sample.int(n_re, ss$n_obs, replace = TRUE)
fit <- dispatch_laplace_spatial(
y = y, n_trials = rep(1L, ss$n_obs), X = X,
re_idx = re_idx, n_re_groups = n_re, sigma_re = 1.0,
spatial = ss$spec, family = "binomial", phi = 1.0,
max_iter = 50L, tol = 1e-6, n_threads = 1L
)
# Latent layout [beta (p), re (n_re), w_mesh (n_mesh)].
expect_length(fit$mode, ncol(X) + n_re + ss$spec$n_mesh)
expect_true(is.finite(fit$log_marginal))
})
test_that("tulpa_laplace(spde + iid RE) returns a marginal H_beta (issue #74)", {
skip_on_cran()
ss <- make_synthetic_spde_spec(n_obs = 60)
set.seed(11)
X <- cbind(1, rnorm(ss$n_obs))
n_re <- 4L
re_idx <- sample.int(n_re, ss$n_obs, replace = TRUE)
y <- rbinom(ss$n_obs, 1, 0.5)
fit <- tulpa_laplace(
y = y, n_trials = rep(1L, ss$n_obs), X = X,
re_list = list(list(idx = re_idx, n_groups = n_re, sigma = 1.0)),
family = "binomial",
spatial = ss$spec,
max_iter = 50L, tol = 1e-6, n_threads = 1L,
return_hessian = TRUE
)
expect_length(fit$mode, ncol(X) + n_re + ss$spec$n_mesh)
expect_true(is.finite(fit$log_marginal))
# Schur marginal precision folds the RE columns out alongside the field.
expect_false(is.null(fit$H_beta))
expect_equal(dim(fit$H_beta), c(ncol(X), ncol(X)))
expect_true(all(is.finite(fit$H_beta)))
})
test_that("SPDE+RE collapses to the spatial-only fit as sigma_re -> 0 (issue #74)", {
skip_on_cran()
# With the RE prior precision sent to infinity the iid block is pinned at 0,
# so beta and the mesh field must match the n_re_groups = 0 fit exactly. This
# pins the [beta, re, w_mesh] layout: a mis-sliced field or a leaked RE
# coupling would shift beta / the field away from the spatial-only solution.
ss <- make_synthetic_spde_spec(n_obs = 60)
set.seed(3)
X <- cbind(1, rnorm(ss$n_obs))
y <- rbinom(ss$n_obs, 1, 0.5)
n_re <- 5L
re_idx <- sample.int(n_re, ss$n_obs, replace = TRUE)
p <- ncol(X)
base <- dispatch_laplace_spatial(
y, rep(1L, ss$n_obs), X, NULL, 0L, 1.0,
ss$spec, "binomial", 1.0, 200L, 1e-10, 1L
)
tiny <- dispatch_laplace_spatial(
y, rep(1L, ss$n_obs), X, re_idx, n_re, 1e-4,
ss$spec, "binomial", 1.0, 200L, 1e-10, 1L
)
expect_equal(tiny$mode[seq_len(p)], base$mode[seq_len(p)], tolerance = 1e-4)
expect_lt(max(abs(tiny$mode[p + seq_len(n_re)])), 1e-2)
expect_equal(tiny$mode[p + n_re + seq_len(ss$spec$n_mesh)],
base$mode[p + seq_len(ss$spec$n_mesh)], tolerance = 1e-3)
})
test_that("SPDE Laplace recovers an iid RE block under the field (issue #74)", {
skip_on_cran()
ss <- make_synthetic_spde_spec(n_obs = 600)
set.seed(202)
n_re <- 10L
# Centre the true group effects: the iid RE block has a zero-mean prior, so a
# non-zero sample mean would be absorbed by the global intercept and confound
# its recovery (the intercept and the RE mean are not separately identified).
u_true <- rnorm(n_re, 0, 0.9)
u_true <- u_true - mean(u_true)
re_idx <- rep(seq_len(n_re), length.out = ss$n_obs)
b0 <- 0.2
eta <- b0 + u_true[re_idx]
y <- rbinom(ss$n_obs, 1, plogis(eta))
X <- matrix(1.0, ss$n_obs, 1)
fit <- dispatch_laplace_spatial(
y, rep(1L, ss$n_obs), X, re_idx, n_re, 0.9,
ss$spec, "binomial", 1.0, 200L, 1e-9, 1L
)
blup <- fit$mode[ncol(X) + seq_len(n_re)]
expect_gt(cor(blup, u_true), 0.7)
expect_lt(abs(fit$mode[1] - b0), 0.5)
})
test_that("tulpa_laplace(spatial = spatial_spde_custom(...)) runs end-to-end", {
skip_on_cran()
ss <- make_synthetic_spde_spec(n_obs = 50)
X <- cbind(1, rnorm(ss$n_obs))
y <- rbinom(ss$n_obs, 1, 0.5)
fit <- tulpa_laplace(
y = y, n_trials = rep(1L, ss$n_obs), X = X,
re_list = list(),
family = "binomial",
spatial = ss$spec,
max_iter = 50L, tol = 1e-6, n_threads = 1L,
return_hessian = TRUE
)
# mode = c(beta, spatial_effects)
expect_length(fit$mode, ncol(X) + ss$spec$n_mesh)
expect_true(is.finite(fit$log_marginal))
# Since issue #16: H_beta is the Schur'd marginal precision for SPDE.
expect_false(is.null(fit$H_beta))
expect_equal(dim(fit$H_beta), c(ncol(X), ncol(X)))
expect_true(all(is.finite(fit$H_beta)))
})
test_that("fit_spde and dispatch_laplace_spatial agree on the same problem", {
skip_on_cran()
ss <- make_synthetic_spde_spec(n_obs = 40)
X <- matrix(1.0, nrow = ss$n_obs, ncol = 1)
y <- rbinom(ss$n_obs, 1, 0.5)
via_dispatch <- dispatch_laplace_spatial(
y = y, n_trials = rep(1L, ss$n_obs), X = X,
re_idx = NULL, n_re_groups = 0L, sigma_re = 1.0,
spatial = ss$spec, family = "binomial", phi = 1.0,
max_iter = 50L, tol = 1e-6, n_threads = 1L
)
via_fit_spde <- fit_spde(
y = y, X = X, spatial = ss$spec,
family = "binomial", n_trials = rep(1L, ss$n_obs),
range = ss$spec$prior_range[1], sigma = ss$spec$prior_sigma[1],
nested_laplace = FALSE,
control = list(max_iter = 50L, tol = 1e-6, n_threads = 1L)
)
# Same kernel, same hyperparameters → identical mode and log_marginal.
expect_equal(via_dispatch$mode, via_fit_spde$mode, tolerance = 1e-10)
expect_equal(via_dispatch$log_marginal, via_fit_spde$log_marginal,
tolerance = 1e-10)
})
# --- offset() threading through the SPDE Laplace paths (gcol33/tulpa#72) -------
test_that("fit_spde threads offset() into the field linear predictor (gcol33/tulpa#72)", {
skip_on_cran()
ss <- make_synthetic_spde_spec(n_obs = 60, seed = 11)
X <- matrix(1.0, nrow = ss$n_obs, ncol = 1)
set.seed(11)
# Poisson counts with a real per-observation log-exposure offset.
off <- log(runif(ss$n_obs, 0.5, 2))
y <- rpois(ss$n_obs, exp(0.3 + off))
r <- ss$spec$prior_range[1]; s <- ss$spec$prior_sigma[1]
fit <- function(o) fit_spde(
y = y, X = X, spatial = ss$spec, family = "poisson",
n_trials = rep(1L, ss$n_obs), range = r, sigma = s,
nested_laplace = FALSE, offset = o,
control = list(max_iter = 200L, tol = 1e-12))
base <- fit(NULL)
zero <- fit(rep(0, ss$n_obs))
# offset = 0 reproduces the no-offset fit exactly (no corruption of eta).
expect_equal(zero$mode, base$mode, tolerance = 1e-10)
expect_equal(zero$log_marginal, base$log_marginal, tolerance = 1e-10)
# A non-trivial offset moves the latent mode -- it is not silently dropped.
shifted <- fit(off)
expect_gt(max(abs(shifted$mode - base$mode)), 1e-3)
})
test_that("fit_spde absorbs a constant offset into the intercept (gcol33/tulpa#72)", {
skip_on_cran()
ss <- make_synthetic_spde_spec(n_obs = 60, seed = 12)
X <- matrix(1.0, nrow = ss$n_obs, ncol = 1)
set.seed(12)
y <- rpois(ss$n_obs, exp(0.4))
r <- ss$spec$prior_range[1]; s <- ss$spec$prior_sigma[1]
cc <- 0.5
fit <- function(o) fit_spde(
y = y, X = X, spatial = ss$spec, family = "poisson",
n_trials = rep(1L, ss$n_obs), range = r, sigma = s,
nested_laplace = FALSE, offset = o,
control = list(max_iter = 300L, tol = 1e-12))
base <- fit(NULL)
con <- fit(rep(cc, ss$n_obs))
# eta = c + b0 + field is invariant under (b0 -> b0 - c): the fitted intercept
# shifts by -c, the centered field and log-marginal unchanged to the weak
# intercept-prior tolerance.
expect_equal(con$beta[1], base$beta[1] - cc, tolerance = 1e-3)
expect_equal(con$spatial_effects, base$spatial_effects, tolerance = 1e-3)
expect_equal(con$log_marginal, base$log_marginal, tolerance = 1e-2)
})
test_that("fit_spde nested integrates offset (offset = 0 == no offset) (gcol33/tulpa#72)", {
skip_on_cran()
ss <- make_synthetic_spde_spec(n_obs = 50, seed = 13)
X <- matrix(1.0, nrow = ss$n_obs, ncol = 1)
set.seed(13)
y <- rpois(ss$n_obs, exp(0.3))
fit <- function(o) fit_spde(
y = y, X = X, spatial = ss$spec, family = "poisson",
n_trials = rep(1L, ss$n_obs), offset = o,
control = list(method = "grid", n_grid = 3L, diagnose_k = FALSE))
base <- fit(NULL)
zero <- fit(rep(0, ss$n_obs))
expect_equal(zero$log_marginal, base$log_marginal, tolerance = 1e-8)
})
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.