Nothing
# The cell third-derivative tensor contraction (gcol33/tulpa#301), at cell
# granularity, away from any solve.
#
# `cpp_cell_coupling_curvature3()` returns sum_{a,b,c} T^{abc} u^a u^b u^c for one
# cell of a registered CellCouplingSpec, which by definition is the third
# derivative of that cell's log-density along the direction u. So the arbiter is
# a direct numerical third derivative of the spec's own `value`: a five-point
# central stencil on t -> log p_cell(eta + t u), computed from
# `cpp_cell_coupling_evaluate()` and touching none of the contraction's own
# machinery (no Hessian, no cross blocks, no step policy).
#
# The two-arm occupancy mixture is the fixture because its cells come in both
# shapes: a cell with a detection FACTORISES (every cross derivative is zero, so
# the tensor must reduce to the separable per-eta sum), while an all-undetected
# cell does not (the occupancy state sits inside the same logarithm as every
# visit).
.c3_register <- function() {
cpp_register_test_occupancy_mixture_coupling()
testthat::skip_if_not(
cpp_cell_coupling_registry_has("test_occupancy_mixture"),
"test_occupancy_mixture coupling spec not registered")
}
.c3_value <- function(a, v, y) {
cpp_cell_coupling_evaluate("test_occupancy_mixture",
eta = list(a, v), y = list(0, y),
family = c("binomial", "binomial"), phi = c(1, 1))$value
}
# d^3/dt^3 log p_cell(eta + t u) at t = 0, five-point central stencil.
.c3_brute <- function(a, v, u_a, u_v, y, h = 1e-3) {
f <- function(t) .c3_value(a + t * u_a, v + t * u_v, y)
(f(2 * h) - 2 * f(h) + 2 * f(-h) - f(-2 * h)) / (2 * h^3)
}
.c3_tensor <- function(a, v, u_a, u_v, y, per_arm_step = TRUE) {
cpp_cell_coupling_curvature3("test_occupancy_mixture",
eta = list(a, v), u = list(u_a, u_v), y = list(0, y),
family = c("binomial", "binomial"), phi = c(1, 1),
per_arm_step = per_arm_step)
}
test_that("the tensor builder refuses an unregistered spec by name", {
expect_error(
cpp_cell_coupling_curvature3("no_such_spec", eta = list(0), u = list(0),
y = list(0), family = "binomial", phi = 1),
"not registered")
})
skip_on_cran()
test_that("the contraction reproduces a direct third derivative of the cell log-density", {
skip_on_cran()
.c3_register()
a <- 0.35
v <- c(-0.6, 0.2, -1.1, 0.4)
u_a <- 0.17
u_v <- c(0.05, -0.09, 0.13, 0.02)
# The coupled branch: nothing factorises, so every cross-arm and cross-visit
# entry of T is nonzero and the contraction has something to get wrong.
y_dark <- rep(0, 4)
b <- .c3_brute(a, v, u_a, u_v, y_dark)
expect_gt(abs(b), 1e-6) # the arbiter is not itself zero
expect_equal(.c3_tensor(a, v, u_a, u_v, y_dark), b, tolerance = 1e-2)
# The factorising branch: log psi + a per-visit Bernoulli sum.
y_seen <- c(1, 0, 0, 0)
b2 <- .c3_brute(a, v, u_a, u_v, y_seen)
expect_gt(abs(b2), 1e-8)
expect_equal(.c3_tensor(a, v, u_a, u_v, y_seen), b2, tolerance = 1e-2)
})
test_that("on a factorising cell the tensor collapses to the separable per-eta sum", {
skip_on_cran()
.c3_register()
# A cell with a detection is log psi(eta_a) + sum_v Bern(y_v | p_v), so
# T^{abc} is diagonal and the contraction must equal sum_j l_j'''(eta_j) u_j^3
# -- the K = 1 formula the separable path uses. For a logit-Bernoulli,
# l'''(eta) = -p q (q - p) and it does not depend on y.
a <- -0.4
v <- c(0.8, -0.25, 1.3, 0.05)
u_a <- 0.11
u_v <- c(-0.07, 0.16, 0.03, -0.12)
l3 <- function(e) { p <- stats::plogis(e); q <- 1 - p; -p * q * (q - p) }
separable <- l3(a) * u_a^3 + sum(l3(v) * u_v^3)
expect_equal(.c3_tensor(a, v, u_a, u_v, c(1, 0, 0, 0)), separable,
tolerance = 1e-3)
})
test_that("a direction that does not reach the cell contributes exactly zero", {
skip_on_cran()
.c3_register()
# Not NaN (the cell is perfectly scorable) and not a small number: the cubic
# form of a zero direction is identically zero.
expect_identical(
.c3_tensor(0.2, c(-0.5, 0.3, 0.1, -0.9), 0, rep(0, 4), rep(0, 4)), 0)
})
test_that("the per-arm step is at least as accurate as one global step", {
skip_on_cran()
.c3_register()
# With the arms on the same eta scale the two policies pick the same step and
# agree to the last bits; the per-arm policy exists for the case they do not.
a <- 0.35; v <- c(-0.6, 0.2, -1.1, 0.4)
u_a <- 0.17; u_v <- c(0.05, -0.09, 0.13, 0.02)
y <- rep(0, 4)
expect_equal(.c3_tensor(a, v, u_a, u_v, y, per_arm_step = TRUE),
.c3_tensor(a, v, u_a, u_v, y, per_arm_step = FALSE),
tolerance = 1e-8)
# Occupancy logit 67x the detection logit, comparable directions: one global
# step is sized off the larger arm and is then far too coarse for the smaller.
a2 <- 20.0; v2 <- rep(-0.3, 4)
u_a2 <- 0.4; u_v2 <- rep(0.4, 4)
b <- .c3_brute(a2, v2, u_a2, u_v2, y)
err_per_arm <- abs(.c3_tensor(a2, v2, u_a2, u_v2, y, TRUE) - b) / abs(b)
err_global <- abs(.c3_tensor(a2, v2, u_a2, u_v2, y, FALSE) - b) / abs(b)
expect_lt(err_per_arm, err_global)
expect_lt(err_per_arm, 1e-5)
})
# --------------------------------------------------------------------------- #
# (gcol33/tulpa#448) An unreadable difference is not a smaller skew #
# #
# gamma_3 feeds the skew correction, and understating it moves the correction #
# toward zero -- the direction that reads as "the Gaussian approximation was #
# fine". A cell whose arm the difference quotient could not be formed on is #
# precisely a cell where it was not, so the term most likely to drop is the #
# term that mattered most. The contraction reports NaN there. #
# #
# The per-arm drop and the whole-cell drop are not separable on this fixture: #
# a CellCouplingSpec is evaluated cell-globally, so a non-finite eta or #
# direction anywhere in the cell takes every arm's quotient down together. #
# What is pinned here is the contract at the door -- no non-finite input ever #
# comes back as a number -- and, below it, that the fix did not turn the one #
# legitimate skip into a decline. #
# --------------------------------------------------------------------------- #
test_that("no non-finite input is scored as a finite cubic term", {
skip_on_cran()
.c3_register()
a <- 0.35; v <- c(-0.6, 0.2, -1.1, 0.4)
u_a <- 0.17; u_v <- c(0.05, -0.09, 0.13, 0.02)
y <- rep(0, 4)
expect_true(is.finite(.c3_tensor(a, v, u_a, u_v, y)))
for (bad in c(NaN, Inf, -Inf)) {
for (step in c(TRUE, FALSE)) {
# non-finite eta, on either arm
expect_true(is.na(.c3_tensor(bad, v, u_a, u_v, y, step)))
expect_true(is.na(.c3_tensor(a, replace(v, 2L, bad), u_a, u_v, y, step)))
# non-finite direction, on either arm
expect_true(is.na(.c3_tensor(a, v, bad, u_v, y, step)))
expect_true(is.na(.c3_tensor(a, v, u_a, replace(u_v, 3L, bad), y, step)))
}
}
})
test_that("an arm the direction leaves alone is a zero, not a decline", {
skip_on_cran()
.c3_register()
# The one skip inside the per-arm loop that is a VALUE: an arm with no
# displacement contributes exactly zero to the cubic form, so the cell is
# still scorable through the arm that did move. Reading it as unreadable
# would decline every probe direction confined to one arm.
a <- 0.35; v <- c(-0.6, 0.2, -1.1, 0.4); y <- rep(0, 4)
occ_only <- .c3_tensor(a, v, 0.17, rep(0, 4), y)
det_only <- .c3_tensor(a, v, 0.0, c(0.05, -0.09, 0.13, 0.02), y)
expect_true(is.finite(occ_only))
expect_true(is.finite(det_only))
expect_false(occ_only == 0)
expect_false(det_only == 0)
# And each reproduces a direct third derivative of the cell log-density. The
# detection-only direction lands near 4e-06, where the five-point stencil is
# itself dominated by cancellation, so it is read on the looser tolerance.
expect_equal(occ_only, .c3_brute(a, v, 0.17, rep(0, 4), y), tolerance = 1e-3)
expect_equal(det_only, .c3_brute(a, v, 0.0, c(0.05, -0.09, 0.13, 0.02), y),
tolerance = 0.1)
})
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.