Nothing
# The exact outer gradient (R/laplace_gradient.R) and the curvature-derivative
# ladder it stands on (src/laplace_family_curvature.h).
#
# The load-bearing check is agreement with a central difference of tulpa's OWN
# Laplace log-marginal: the derivation can be right on paper and still be wired
# to the wrong latent ordering, the wrong weight convention, or the wrong sign.
.exg_sim <- function(seed = 5L, G = 10L, per = 7L, nc = 1L, fam = "poisson",
phi = 2.5) {
set.seed(seed)
n <- G * per
grp <- rep(seq_len(G), each = per)
X <- cbind(1, rnorm(n))
Zc <- if (nc == 1L) NULL else cbind(1, rnorm(n))
Sig <- if (nc == 1L) matrix(0.5, 1, 1) else
matrix(c(0.5, 0.15, 0.15, 0.35), 2, 2)
b <- t(chol(Sig)) %*% matrix(rnorm(nc * G), nc, G)
zrow <- if (nc == 1L) matrix(1, n, 1) else Zc
eta <- as.numeric(X %*% c(0.3, 0.5)) +
rowSums(zrow * t(b)[grp, , drop = FALSE])
mu <- exp(eta)
# zero-truncated draw: reject the empty class the family conditions away.
pos <- function(rng) vapply(seq_len(n),
function(i) { repeat { v <- rng(i); if (v >= 1L) return(v) } }, numeric(1))
y <- switch(fam,
poisson = rpois(n, mu),
binomial = rbinom(n, 1L, plogis(eta)),
gaussian = rnorm(n, eta, 0.7),
neg_binomial_2 = rnbinom(n, size = phi, mu = mu),
neg_binomial_1 = rnbinom(n, size = mu / phi, mu = mu),
truncated_poisson = pos(function(i) rpois(1, mu[i])),
truncated_neg_binomial_2 = pos(function(i) rnbinom(1, size = phi, mu = mu[i])))
list(y = y, X = X, grp = grp, G = G, n = n, nc = nc, Zc = Zc, fam = fam,
n_trials = rep(1L, n))
}
.exg_theta_to_L <- function(theta, nc, full) {
L <- matrix(0, nc, nc)
if (!full) { diag(L) <- exp(theta); return(L) }
i <- 0
for (cc in seq_len(nc)) for (rr in cc:nc) {
i <- i + 1
L[rr, cc] <- if (rr == cc) exp(theta[i]) else theta[i]
}
L
}
.exg_layout <- function(d, full) {
list(list(nc = d$nc, full = full,
k = if (full) d$nc * (d$nc + 1) / 2 else d$nc,
n_groups = d$G, idx = d$grp, Z = d$Zc))
}
# Analytic gradient vs a central difference of tulpa_laplace()$log_marginal.
.exg_compare <- function(d, theta, full = FALSE, phi = 1.0) {
layout <- .exg_layout(d, full)
fit_at <- function(th, joint) {
L <- .exg_theta_to_L(th, d$nc, full)
tulpa_laplace(y = d$y, n_trials = d$n_trials, X = d$X,
re_list = .re_cov_build_re_list(list(L), layout),
family = d$fam, phi = phi, return_hessian = TRUE,
return_joint_hessian = joint,
max_iter = 300L, tol = 1e-12)
}
L0 <- .exg_theta_to_L(theta, d$nc, full)
ga <- .laplace_exact_re_grad(
fit = fit_at(theta, TRUE), y = d$y, X = d$X, n_trials = d$n_trials,
offset = NULL, weights = NULL,
re_list = .re_cov_build_re_list(list(L0), layout),
layout = layout, L_list = list(L0), family = d$fam, phi = phi)
if (is.null(ga)) return(NULL)
h <- 1e-5
gf <- vapply(seq_along(theta), function(j) {
tp <- theta; tp[j] <- tp[j] + h
tm <- theta; tm[j] <- tm[j] - h
(fit_at(tp, FALSE)$log_marginal - fit_at(tm, FALSE)$log_marginal) / (2 * h)
}, numeric(1))
list(analytic = ga, fd = gf)
}
# Closed-form outer Hessian vs a central difference of the analytic gradient --
# the Hessian analogue of .exg_compare. The gradient it differences is itself
# checked against tulpa's own marginal above, so this pins the second-derivative
# assembly (R/laplace_gradient.R) onto a verified first derivative.
.exg_hess_compare <- function(d, theta, full = FALSE, phi = 1.0) {
layout <- .exg_layout(d, full)
fit_at <- function(th) {
L <- .exg_theta_to_L(th, d$nc, full)
tulpa_laplace(y = d$y, n_trials = d$n_trials, X = d$X,
re_list = .re_cov_build_re_list(list(L), layout),
family = d$fam, phi = phi, return_hessian = TRUE,
return_joint_hessian = TRUE, max_iter = 300L, tol = 1e-12)
}
grad_at <- function(th) {
L <- .exg_theta_to_L(th, d$nc, full)
.laplace_exact_re_grad(
fit = fit_at(th), y = d$y, X = d$X, n_trials = d$n_trials,
offset = NULL, weights = NULL,
re_list = .re_cov_build_re_list(list(L), layout),
layout = layout, L_list = list(L), family = d$fam, phi = phi)
}
L0 <- .exg_theta_to_L(theta, d$nc, full)
r <- .laplace_exact_re_grad(
fit = fit_at(theta), y = d$y, X = d$X, n_trials = d$n_trials,
offset = NULL, weights = NULL,
re_list = .re_cov_build_re_list(list(L0), layout),
layout = layout, L_list = list(L0), family = d$fam, phi = phi,
want_hessian = TRUE)
if (is.null(r) || is.null(r$H)) return(NULL)
h <- 1e-5
k <- length(theta)
Hf <- matrix(0, k, k)
for (j in seq_len(k)) {
tp <- theta; tp[j] <- tp[j] + h
tm <- theta; tm[j] <- tm[j] - h
gp <- grad_at(tp); gm <- grad_at(tm)
if (is.null(gp) || is.null(gm)) return(NULL)
Hf[, j] <- (gp - gm) / (2 * h)
}
list(analytic = r$H, fd = (Hf + t(Hf)) / 2)
}
.exg_closed_H <- function(fam, phi, theta = log(0.7), G = 24L, per = 10L) {
d <- .exg_sim(fam = fam, G = G, per = per, phi = phi)
layout <- .exg_layout(d, FALSE)
at <- function(th, want_h) {
L0 <- .exg_theta_to_L(th, d$nc, FALSE)
rl <- .re_cov_build_re_list(list(L0), layout)
fit <- tulpa_laplace(y = d$y, n_trials = d$n_trials, X = d$X, re_list = rl,
family = fam, phi = phi, return_hessian = FALSE,
return_joint_hessian = TRUE, max_iter = 300L,
tol = 1e-12)
.laplace_exact_re_grad(
fit = fit, y = d$y, X = d$X, n_trials = d$n_trials, offset = NULL,
weights = NULL, re_list = rl, layout = layout, L_list = list(L0),
family = fam, phi = phi, want_jacobian = want_h, want_hessian = want_h)
}
list(r = at(theta, TRUE), at = at, theta = theta)
}
test_that("the chain rule export agrees with a direct dSigma trace", {
# cpp_recov_block_grad must equal 0.5 tr(dSigma_j . core) with Smat = 0.5 core.
set.seed(4)
nc <- 2L
L <- matrix(c(0.8, 0.25, 0, 0.6), 2, 2)
core <- crossprod(matrix(rnorm(4), 2, 2))
Smat <- 0.5 * core
got <- cpp_recov_block_grad(Smat, L, TRUE, 1.0)
dS <- .re_block_dSigma(L, nc, TRUE)
want <- vapply(dS, function(d) 0.5 * sum(diag(d %*% core)), numeric(1))
expect_equal(as.numeric(got), want, tolerance = 1e-10)
})
skip_on_cran_budget()
test_that("the exact gradient matches for a correlated block", {
skip_on_cran_budget()
d <- .exg_sim(seed = 5L, G = 12L, per = 8L, nc = 2L)
r <- .exg_compare(d, c(log(0.7), 0.2, log(0.6)), full = TRUE)
skip_if(is.null(r), "gradient unavailable")
expect_equal(r$analytic, r$fd, tolerance = 1e-6)
expect_length(r$analytic, 3L)
})
test_that("the closed-form Hessian matches for a correlated block", {
skip_on_cran_budget()
d <- .exg_sim(seed = 5L, G = 12L, per = 8L, nc = 2L)
r <- .exg_hess_compare(d, c(log(0.7), 0.2, log(0.6)), full = TRUE)
skip_if(is.null(r), "hessian unavailable")
expect_equal(dim(r$analytic), c(3L, 3L))
expect_equal(r$analytic, r$fd, tolerance = 1e-5)
})
test_that("the closed Hessian handles a working weight that is not the observed one", {
skip_on_cran_budget()
# truncated_neg_binomial_2 builds H from Var(y | y > 0), not from its observed
# curvature. The gradient's dW channel is v_r' (dx_hat/dtheta) and the mode
# motion follows the TRUE stationarity condition, so u is formed on the
# observed-curvature inverse Hinv_mode -- and differentiating it has to use
# that inverse too, through dH_true/dtheta. Differentiating through Hinv
# instead used to miss the exact H_theta by 2.7e-4 here, which is why the
# route declined; with d(W_obs - w)/deta registered it is exact and runs.
h <- .exg_closed_H("truncated_neg_binomial_2", 2.5)
expect_false(is.null(h$r$H))
step <- 1e-4
fd <- (h$at(h$theta + step, FALSE) - h$at(h$theta - step, FALSE)) / (2 * step)
expect_equal(as.numeric(h$r$H), as.numeric(fd), tolerance = 1e-5)
})
test_that("the closed Hessian handles neg_binomial_1", {
skip_on_cran_budget()
# The same split again. Its observed curvature is -s + r^2 (psi'(r) -
# psi'(y+r)), so d(W_obs - w)/deta carries psi'' -- which is why this route
# used to decline, missing H_theta by 2.6e-2 when forced on. With
# portable_tetragamma the correction is closed-form and the route is exact.
expect_true(
cpp_family_has_obs_curvature_delta_derivative("neg_binomial_1"))
h <- .exg_closed_H("neg_binomial_1", 2.5)
expect_false(is.null(h$r$H))
step <- 1e-4
fd <- (h$at(h$theta + step, FALSE) - h$at(h$theta - step, FALSE)) / (2 * step)
expect_equal(as.numeric(h$r$H), as.numeric(fd), tolerance = 1e-5)
})
test_that("the observed-curvature correction is exact at both derivative rungs", {
skip_on_cran_budget()
# What the two branches above rest on, checked directly rather than only
# through the assembled Hessian: d(W_obs - w)/deta against a difference of the
# correction itself, and -- for neg_binomial_1, the only family carrying a
# non-zero one at the next rung -- the same formula written over R's psigamma,
# which is what tests the portable polygamma series rather than the algebra
# wrapped around it.
eta <- seq(-1.2, 1.6, length.out = 12)
y <- c(0, 1, 2, 3, 5, 8, 0, 2, 4, 1, 6, 3)
nt <- rep(1L, length(eta))
h <- 1e-5
for (phi in c(0.4, 1.3, 4.0)) {
got <- cpp_family_obs_curvature_delta_deta_vec(y, nt, eta,
"neg_binomial_1", phi, NA_real_)
dl <- function(e) cpp_family_obs_curvature_delta_vec(y, nt, e,
"neg_binomial_1", phi, NA_real_)
expect_equal(got, (dl(eta + h) - dl(eta - h)) / (2 * h), tolerance = 1e-6)
mu <- exp(eta); r <- mu / phi
s <- r * (digamma(y + r) - digamma(r) - log1p(phi))
ref <- -s + 3 * r^2 * (trigamma(r) - trigamma(y + r)) +
r^3 * (psigamma(r, 2L) - psigamma(y + r, 2L)) - mu / (1 + phi)
expect_equal(got, ref, tolerance = 1e-7)
}
})
test_that("a gaussian response has a closed Hessian with the curvature at zero", {
skip_on_cran_budget()
# The control: dw/deta and d2w/deta2 are identically zero, so the Hessian is
# exercised through J, dR and dV alone -- the analogue of the gradient control.
d <- .exg_sim(fam = "gaussian")
layout <- .exg_layout(d, FALSE)
L <- .exg_theta_to_L(log(0.7), 1L, FALSE)
fit <- tulpa_laplace(y = d$y, n_trials = d$n_trials, X = d$X,
re_list = .re_cov_build_re_list(list(L), layout),
family = "gaussian", phi = 0.49,
return_hessian = TRUE, return_joint_hessian = TRUE)
r <- .laplace_exact_re_grad(
fit = fit, y = d$y, X = d$X, n_trials = d$n_trials, offset = NULL,
weights = NULL, re_list = .re_cov_build_re_list(list(L), layout),
layout = layout, L_list = list(L), family = "gaussian", phi = 0.49,
want_hessian = TRUE)
skip_if(is.null(r) || is.null(r$H), "hessian unavailable")
expect_equal(dim(r$H), c(1L, 1L))
expect_true(all(is.finite(r$H)))
})
test_that("a gaussian response is the control: dw/deta is zero throughout", {
skip_on_cran_budget()
# The curvature-movement term vanishes identically here, so this case is
# exact with or without it -- which is why it cannot be the only check.
d <- .exg_sim(fam = "gaussian")
r <- .exg_compare(d, log(0.7), phi = 0.49)
skip_if(is.null(r), "gradient unavailable")
expect_equal(r$analytic, r$fd, tolerance = 1e-8)
expect_true(all(cpp_family_curvature_deta_vec(
d$y, d$n_trials, rnorm(d$n), "gaussian", 0.7) == 0))
})
test_that("dw/deta differentiates the weight the Newton system uses", {
skip_on_cran_budget()
cases <- list(
list(family = "poisson", y = 4, n = 1L, phi = 1.0),
list(family = "binomial", y = 3, n = 10L, phi = 1.0),
list(family = "neg_binomial_2", y = 6, n = 1L, phi = 2.5),
list(family = "neg_binomial_1", y = 6, n = 1L, phi = 1.7),
list(family = "truncated_poisson", y = 3, n = 1L, phi = 1.0),
list(family = "truncated_neg_binomial_2", y = 3, n = 1L, phi = 2.0),
list(family = "beta_binomial", y = 3, n = 10L, phi = 4.0),
list(family = "gamma", y = 2.0, n = 1L, phi = 3.0),
list(family = "inverse_gaussian", y = 1.4, n = 1L, phi = 1.1),
list(family = "beta", y = 0.6, n = 1L, phi = 6.0),
list(family = "binomial_probit", y = 3, n = 10L, phi = 1.0),
list(family = "binomial_cloglog", y = 3, n = 10L, phi = 1.0),
list(family = "gaussian_log", y = 1.2, n = 1L, phi = 1.5)
)
h <- 1e-5
for (cs in cases) {
for (eta in c(-0.6, 0.0, 0.9)) {
w <- function(e) unname(
cpp_family_terms(cs$y, cs$n, e, cs$family, cs$phi)[["neg_hess"]])
got <- unname(cpp_family_curvature_deta(
cs$y, cs$n, eta, cs$family, cs$phi)[["dw_deta"]])
num <- (w(eta + h) - w(eta - h)) / (2 * h)
# Scaled by the larger magnitude with a floor: several families have a
# weight that is genuinely constant in eta, where the answer is 0 and a
# ratio against finite-difference noise is meaningless.
scale <- max(abs(num), abs(got), 1e-6)
expect_lt(abs(got - num) / scale, 1e-4)
}
}
})
test_that("families with an exact curvature derivative are gated honestly", {
skip_on_cran_budget()
for (f in c("poisson", "binomial", "neg_binomial_2", "gamma",
"binomial_probit", "gaussian_log")) {
expect_true(cpp_family_has_curvature_derivative(f), info = f)
}
# The gate is what stops the exact path being offered for a family whose
# weight has no closed-form eta-derivative here.
expect_false(cpp_family_has_curvature_derivative("not_a_family"))
})
test_that("the vectorized curvature derivative matches the scalar probe", {
skip_on_cran_budget()
set.seed(2)
eta <- rnorm(25)
y <- rpois(25, 2)
vec <- cpp_family_curvature_deta_vec(y, 1L, eta, "poisson", 1.0)
sc <- vapply(seq_along(eta), function(i)
unname(cpp_family_curvature_deta(y[i], 1L, eta[i], "poisson", 1.0)[["dw_deta"]]),
numeric(1))
expect_equal(vec, sc)
expect_error(cpp_family_curvature_deta_vec(y[1:3], 1L, eta, "poisson", 1.0),
"differ in length")
})
test_that("d2w/deta2 differentiates dw/deta (curvature the theta-Hessian needs)", {
skip_on_cran_budget()
# Same load-bearing idea as the dw/deta test: a central difference of the
# verified first curvature derivative is the ground truth the closed-form
# second derivative must reproduce, family by family.
cases <- list(
list(family = "poisson", y = 4, n = 1L, phi = 1.0),
list(family = "binomial", y = 3, n = 10L, phi = 1.0),
list(family = "neg_binomial_2", y = 6, n = 1L, phi = 2.5),
list(family = "neg_binomial_1", y = 6, n = 1L, phi = 1.7),
list(family = "beta_binomial", y = 3, n = 10L, phi = 4.0),
list(family = "tweedie", y = 2.0, n = 1L, phi = 1.3, phi2 = 1.6),
list(family = "gamma", y = 2.0, n = 1L, phi = 3.0),
list(family = "inverse_gaussian", y = 1.4, n = 1L, phi = 1.1),
list(family = "beta", y = 0.6, n = 1L, phi = 6.0),
list(family = "binomial_probit", y = 3, n = 10L, phi = 1.0),
list(family = "binomial_cloglog", y = 3, n = 10L, phi = 1.0),
list(family = "gaussian_log", y = 1.2, n = 1L, phi = 1.5)
)
h <- 1e-5
for (cs in cases) {
p2 <- if (is.null(cs$phi2)) NA_real_ else cs$phi2
for (eta in c(-0.6, 0.0, 0.9)) {
dw <- function(e) unname(cpp_family_curvature_deta(
cs$y, cs$n, e, cs$family, cs$phi, p2)[["dw_deta"]])
got <- unname(cpp_family_curvature_deta2(
cs$y, cs$n, eta, cs$family, cs$phi, p2)[["d2w_deta2"]])
num <- (dw(eta + h) - dw(eta - h)) / (2 * h)
# Floor as before: gamma+log has dw==0 identically, so this is the
# 0-vs-FD-noise case where a ratio would be meaningless.
scale <- max(abs(num), abs(got), 1e-6)
expect_lt(abs(got - num) / scale, 1e-4)
}
}
})
test_that("the second curvature derivative gate covers every family with a first", {
skip_on_cran_budget()
# The truncated pair carry the third truncation-shape derivative d3a, so their
# second eta-derivative is closed-form alongside every other family.
for (f in c("poisson", "binomial", "neg_binomial_2", "neg_binomial_1",
"beta_binomial", "t", "tweedie", "gamma", "beta",
"binomial_probit", "gaussian_log",
"truncated_poisson", "truncated_neg_binomial_2"))
expect_true(cpp_family_has_curvature_2nd_derivative(f), info = f)
expect_false(cpp_family_has_curvature_2nd_derivative("not_a_family"))
# The truncated second eta-derivative FD-checks against the first, the same
# gate proto_truncated_curvature.R pins in base R.
h <- 1e-6
for (cs in list(list(f = "truncated_poisson", phi = 1.0),
list(f = "truncated_neg_binomial_2", phi = 2.5))) {
for (eta in c(-0.6, 0.2, 0.9)) {
dw <- function(e) unname(cpp_family_curvature_deta(
3, 1L, e, cs$f, cs$phi, NA_real_)[["dw_deta"]])
got <- unname(cpp_family_curvature_deta2(
3, 1L, eta, cs$f, cs$phi, NA_real_)[["d2w_deta2"]])
num <- (dw(eta + h) - dw(eta - h)) / (2 * h)
expect_lt(abs(got - num) / max(abs(num), abs(got), 1e-6), 1e-4, label = cs$f)
}
}
})
test_that("the compiled zero-inflation gate needs a discrete base, not just a curvature", {
skip_on_cran_budget()
# has_observed_curvature() alone used to decide this, and was right only while
# the count families were the only ones carrying one. The mixture's y = 0
# branch reads log_lik_for_family(0, ...) as log P(Y = 0); for a continuous
# family that is a log-DENSITY -- finite and plausible for gaussian, infinite
# for gamma or beta -- so the gate needs the discreteness condition of its own.
for (f in c("poisson", "binomial", "neg_binomial_2", "neg_binomial_1",
"truncated_poisson", "truncated_neg_binomial_2"))
expect_true(cpp_family_compiled_zi_supported(f), info = f)
for (f in c("gaussian", "lognormal", "gamma", "beta", "inverse_gaussian",
"tweedie", "t"))
expect_false(cpp_family_compiled_zi_supported(f), info = f)
})
test_that("the vectorized second curvature derivative matches the scalar probe", {
skip_on_cran_budget()
set.seed(3)
eta <- rnorm(20)
y <- rpois(20, 2)
vec <- cpp_family_curvature_deta2_vec(y, 1L, eta, "poisson", 1.0)
sc <- vapply(seq_along(eta), function(i) unname(cpp_family_curvature_deta2(
y[i], 1L, eta[i], "poisson", 1.0)[["d2w_deta2"]]), numeric(1))
expect_equal(vec, sc)
expect_error(cpp_family_curvature_deta2_vec(y[1:3], 1L, eta, "poisson", 1.0),
"differ in length")
})
test_that("the joint Hessian round-trips as a symmetric matrix", {
skip_on_cran_budget()
d <- .exg_sim()
layout <- .exg_layout(d, FALSE)
L <- .exg_theta_to_L(log(0.7), 1L, FALSE)
f <- tulpa_laplace(y = d$y, n_trials = d$n_trials, X = d$X,
re_list = .re_cov_build_re_list(list(L), layout),
family = "poisson", return_hessian = TRUE,
return_joint_hessian = TRUE)
H <- f$H_joint
expect_false(is.null(H))
expect_equal(nrow(H), length(f$mode))
expect_equal(ncol(H), length(f$mode))
expect_true(Matrix::isSymmetric(H))
# It is a posterior precision, so it must be positive definite.
expect_true(min(eigen(as.matrix(H), symmetric = TRUE,
only.values = TRUE)$values) > 0)
# Absent unless asked for, so no other caller pays for it.
f0 <- tulpa_laplace(y = d$y, n_trials = d$n_trials, X = d$X,
re_list = .re_cov_build_re_list(list(L), layout),
family = "poisson", return_hessian = TRUE)
expect_null(f0$H_joint)
})
test_that("the gradient-driven outer fit lands where the derivative-free one does", {
skip_on_cran_budget()
d <- .exg_sim(seed = 7L, G = 20L, per = 8L)
re <- list(idx = d$grp, n_groups = d$G, n_coefs = 1L)
a <- tulpa_eb(d$y, NULL, d$X, re, family = "poisson")
orig <- cpp_family_has_curvature_derivative
on.exit(assignInNamespace("cpp_family_has_curvature_derivative", orig,
ns = "tulpa"), add = TRUE)
assignInNamespace("cpp_family_has_curvature_derivative",
function(family) FALSE, ns = "tulpa")
b <- tulpa_eb(d$y, NULL, d$X, re, family = "poisson")
expect_equal(unname(coef(a)), unname(coef(b)), tolerance = 1e-4)
expect_equal(unname(sqrt(diag(vcov(a)))), unname(sqrt(diag(vcov(b)))),
tolerance = 1e-4)
})
skip_on_cran()
test_that("the exact gradient matches a difference of tulpa's own marginal", {
skip_on_cran()
for (cs in list(list(fam = "poisson", th = log(0.7)),
list(fam = "binomial", th = log(0.7)),
list(fam = "poisson", th = log(1.6)))) {
d <- .exg_sim(fam = cs$fam, G = if (cs$fam == "binomial") 14L else 10L,
per = if (cs$fam == "binomial") 10L else 7L)
r <- .exg_compare(d, cs$th)
skip_if(is.null(r), "gradient unavailable")
expect_equal(r$analytic, r$fd, tolerance = 1e-6,
info = paste(cs$fam, "at theta =", signif(cs$th, 4)))
}
})
test_that("the closed-form Hessian matches a difference of the analytic gradient", {
skip_on_cran()
for (cs in list(list(fam = "poisson", th = log(0.7)),
list(fam = "binomial", th = log(0.7)),
list(fam = "poisson", th = log(1.6)))) {
d <- .exg_sim(fam = cs$fam, G = if (cs$fam == "binomial") 14L else 10L,
per = if (cs$fam == "binomial") 10L else 7L)
r <- .exg_hess_compare(d, cs$th)
skip_if(is.null(r), "hessian unavailable")
expect_equal(dim(r$analytic), c(1L, 1L))
expect_equal(r$analytic, r$fd, tolerance = 1e-5,
info = paste(cs$fam, "at theta =", signif(cs$th, 4)))
}
})
test_that("H_theta from the gradient stencil agrees with the objective's curvature", {
skip_on_cran()
# The stencil is what any family without a closed second-derivative route
# falls back to, so it has to be right on its own terms. Checked on the two
# families whose working weight is NOT the observed curvature -- the case that
# separates the two inverses, and the one where an error would hide -- against
# a second difference of the objective itself.
for (cs in list(list(fam = "neg_binomial_1", phi = 2.5),
list(fam = "truncated_neg_binomial_2", phi = 2.5))) {
d <- .exg_sim(fam = cs$fam, G = 24L, per = 10L, phi = cs$phi)
layout <- .exg_layout(d, FALSE)
obj <- function(th) {
L <- .exg_theta_to_L(th, d$nc, FALSE)
tulpa_laplace(y = d$y, n_trials = d$n_trials, X = d$X,
re_list = .re_cov_build_re_list(list(L), layout),
family = d$fam, phi = cs$phi, return_hessian = FALSE,
max_iter = 300L, tol = 1e-12)$log_marginal
}
grad <- function(th) {
L <- .exg_theta_to_L(th, d$nc, FALSE)
fit <- tulpa_laplace(y = d$y, n_trials = d$n_trials, X = d$X,
re_list = .re_cov_build_re_list(list(L), layout),
family = d$fam, phi = cs$phi, return_hessian = FALSE,
return_joint_hessian = TRUE, max_iter = 300L,
tol = 1e-12)
.laplace_exact_re_grad(
fit = fit, y = d$y, X = d$X, n_trials = d$n_trials, offset = NULL,
weights = NULL, re_list = .re_cov_build_re_list(list(L), layout),
layout = layout, L_list = list(L), family = d$fam, phi = cs$phi)
}
th <- log(0.7); h <- 1e-4
stencil <- (grad(th + h) - grad(th - h)) / (2 * h)
second <- (obj(th + h) - 2 * obj(th) + obj(th - h)) / (h * h)
expect_equal(stencil, second, tolerance = 2e-3, info = cs$fam)
}
})
test_that("the analytic mode Jacobian equals the differenced true inner mode", {
skip_on_cran()
# J = dx_hat/dtheta from true score stationarity carries the observed
# curvature; the check differences tulpa's own inner mode, catching a
# working-weight Jacobian directly rather than only through the Hessian.
for (cs in list(list(fam = "neg_binomial_2", phi = 2.5),
list(fam = "neg_binomial_1", phi = 2.5),
list(fam = "truncated_neg_binomial_2", phi = 2.5))) {
d <- .exg_sim(fam = cs$fam, G = 24L, per = 10L, phi = cs$phi)
layout <- .exg_layout(d, FALSE)
fit_at <- function(th) {
L <- .exg_theta_to_L(th, 1L, FALSE)
tulpa_laplace(y = d$y, n_trials = d$n_trials, X = d$X,
re_list = .re_cov_build_re_list(list(L), layout),
family = d$fam, phi = cs$phi, return_hessian = TRUE,
return_joint_hessian = TRUE, max_iter = 300L, tol = 1e-12)
}
L0 <- .exg_theta_to_L(log(0.7), 1L, FALSE)
r <- .laplace_exact_re_grad(
fit = fit_at(log(0.7)), y = d$y, X = d$X, n_trials = d$n_trials,
offset = NULL, weights = NULL,
re_list = .re_cov_build_re_list(list(L0), layout),
layout = layout, L_list = list(L0), family = d$fam, phi = cs$phi,
want_jacobian = TRUE)
skip_if(is.null(r) || is.null(r$J), "jacobian unavailable")
h <- 1e-5
Jtrue <- (fit_at(log(0.7) + h)$mode - fit_at(log(0.7) - h)$mode) / (2 * h)
expect_equal(as.numeric(r$J), Jtrue, tolerance = 1e-5, info = cs$fam)
}
})
test_that("the exact mode-Jacobian gate tracks whether the observed curvature is available", {
skip_on_cran()
# Every front-door family now carries an exact observed curvature: the
# explicitly branched ones directly, everything on the generic mu-space route
# through the grad_mu ladder. So the gate is open across the registry, and
# what varies is whether the observed form DIFFERS from the working weight.
for (f in c(family_names(), "gaussian_log", "binomial_probit"))
expect_true(cpp_family_has_exact_mode_jacobian(f), info = f)
# The observed-minus-working delta is identically zero exactly where the
# Newton weight already is the observed curvature -- a canonical link or a
# curvature carrying no y -- and nonzero everywhere else. A non-canonical link
# breaks the coincidence even for a family that has it canonically.
eta <- c(-0.5, 0.3, 1.1); y <- c(2, 3, 1)
zero_delta <- list(
list("poisson", 1), list("binomial", 1), list("neg_binomial_2", 2.5),
list("truncated_poisson", 1), list("gaussian", 1.4))
for (cs in zero_delta) {
expect_true(all(abs(cpp_family_obs_curvature_delta_vec(
y, 6L, eta, cs[[1]], cs[[2]])) < 1e-12), info = cs[[1]])
expect_true(cpp_family_working_weight_is_observed(cs[[1]]), info = cs[[1]])
}
# beta's Newton weight is the observed curvature under every link
# (gcol33/tulpa#936), so its delta is the zero function too; its response
# lives on (0, 1).
expect_true(all(abs(cpp_family_obs_curvature_delta_vec(
c(0.2, 0.55, 0.9), 1L, eta, "beta", 6)) < 1e-12))
expect_true(cpp_family_working_weight_is_observed("beta"))
nonzero_delta <- list(
list("truncated_neg_binomial_2", 2.5), list("neg_binomial_1", 2.5),
list("gamma", 3), list("inverse_gaussian", 0.6),
list("beta_binomial", 6), list("binomial_probit", 1), list("gaussian_log", 1.4))
for (cs in nonzero_delta) {
expect_true(any(abs(cpp_family_obs_curvature_delta_vec(
y, 6L, eta, cs[[1]], cs[[2]])) > 1e-8), info = cs[[1]])
expect_false(cpp_family_working_weight_is_observed(cs[[1]]), info = cs[[1]])
}
})
test_that("the closed, exact-stencil and finite-difference corrections agree", {
skip_on_cran()
d <- .exg_sim(seed = 3L, G = 20L, per = 8L)
re <- list(idx = d$grp, n_groups = d$G, n_coefs = 1L)
# Default route: the closed-form outer Hessian, no differencing of any solve.
a <- tulpa_eb(d$y, NULL, d$X, re, family = "poisson", marginal = TRUE)
skip_if(is.null(a$cov_marginal), "closed correction did not form")
orig2 <- cpp_family_has_curvature_2nd_derivative
orig1 <- cpp_family_has_curvature_derivative
on.exit({
assignInNamespace("cpp_family_has_curvature_2nd_derivative", orig2, ns = "tulpa")
assignInNamespace("cpp_family_has_curvature_derivative", orig1, ns = "tulpa")
}, add = TRUE)
# Hide only the second curvature derivative -> the closed route declines and
# the exact stencil (difference the analytic gradient) runs.
assignInNamespace("cpp_family_has_curvature_2nd_derivative",
function(family) FALSE, ns = "tulpa")
b <- tulpa_eb(d$y, NULL, d$X, re, family = "poisson", marginal = TRUE)
skip_if(is.null(b$cov_marginal), "exact-stencil correction did not form")
# Hide the first as well -> the finite-difference stencil.
assignInNamespace("cpp_family_has_curvature_derivative",
function(family) FALSE, ns = "tulpa")
cc <- tulpa_eb(d$y, NULL, d$X, re, family = "poisson", marginal = TRUE)
skip_if(is.null(cc$cov_marginal), "fallback correction did not form")
# Closed vs exact stencil: both differentiate the same analytic gradient, so
# they agree to the stencil's O(step^2) truncation. Against the objective-
# differencing fallback the tolerance is looser.
expect_equal(as.numeric(a$H_theta), as.numeric(b$H_theta), tolerance = 1e-4)
expect_equal(as.numeric(a$H_theta), as.numeric(cc$H_theta), tolerance = 1e-4)
expect_equal(sqrt(diag(a$cov_marginal)), sqrt(diag(cc$cov_marginal)),
tolerance = 1e-4)
})
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.