Nothing
# Forbes (2023) fidelity guard (M44).
#
# CLAUDE.md's baseline contract: the default output must reproduce Forbes's
# examples exactly. The fixture holds the Spearman correlation matrices of the
# paper's three simulation studies (regenerated from Forbes's public OSF
# script, set.seed(123)) plus expected outputs computed with her reference
# implementation ("ExtendedBassAckwards functions with annotation.R",
# https://osf.io/pcwm8/) -- see attr(fixture, "provenance"). The tests below
# run only ackwards on the fixed inputs and compare against those expected
# values, so no external code or network is needed at test time.
#
# Correspondence conventions (verified during the M44 feasibility study, where
# the 155-variable applied example also matched to 3.9e-14):
# * Forbes's comp.corr is t(W_a) %*% R %*% W_b with no sign alignment; ours
# is the same algebra with primary-parent alignment, so |values| must be
# identical entrywise (W'RW = I for PCA makes her unstandardized products
# equal our standardized edges).
# * Her cong is psych::factor.congruence, which rounds to 2 decimals; ours
# is exact Tucker's phi, so agreement is within 0.005.
# * Her component labels are letter-level + index in psych::pca column
# order ("a1"; "b1","b2"; ...); ours are m{k}f{j} in the same order.
# * Her comp.corr list enumerates pairs as: for c in 2..K, for i in 1..c-1.
.forbes_fixture <- function() {
readRDS(test_path("fixtures", "forbes2023_sims.rds"))
}
# Map a Forbes component label ("c3") to an ackwards one ("m3f3").
.forbes_to_ackwards <- function(lab) {
lv <- match(substr(lab, 1L, 1L), letters)
paste0("m", lv, "f", substr(lab, 2L, nchar(lab)))
}
# Forbes's redundancy "chase" on an ackwards object: from `node`, follow
# primary-parent links upward while |r| >= .9; return the topmost node
# reached (the node itself when its first upward link is below .9).
.chase <- function(x, node) {
lv <- as.integer(sub("^m(\\d+)f\\d+$", "\\1", node))
cur <- node
while (lv > 1L) {
E <- x$edges$matrices[[paste0(lv - 1L, ":", lv)]]
j <- match(cur, colnames(E))
pi <- which.max(abs(E[, j]))
if (abs(E[pi, j]) < 0.9) break
cur <- rownames(E)[pi]
lv <- lv - 1L
}
cur
}
test_that("default output reproduces Forbes's simulation examples exactly", {
skip_if_not_installed("psych")
sims <- .forbes_fixture()
for (nm in names(sims)) {
sim <- sims[[nm]]
suppressWarnings(suppressMessages(
x <- cached(ackwards(sim$R, k_max = 4, pairs = "all"))
))
# (1) Between-level correlations: |ours| == |hers| entrywise, all 6 pairs.
idx <- 0L
for (c2 in 2:4) {
for (i in 1:(c2 - 1L)) {
idx <- idx + 1L
E_forbes <- sim$comp_corr[[idx]]
E_ours <- x$edges$matrices[[paste0(i, ":", c2)]]
expect_equal(
abs(unname(E_ours)), abs(unname(E_forbes)),
tolerance = 1e-12,
label = paste0(nm, " |edges| ", i, ":", c2, " (ackwards)"),
expected.label = "Forbes reference"
)
}
}
# (2) Loading congruence: |ours| == |hers| within her 2-dp rounding.
phi_ours <- ackwards:::.phi_pairs(x$levels, "all")
idx <- 0L
for (c2 in 2:4) {
for (i in 1:(c2 - 1L)) {
idx <- idx + 1L
C_forbes <- sim$cong[[idx]]
sub <- phi_ours[phi_ours$level_from == i & phi_ours$level_to == c2, ]
C_ours <- matrix(sub$phi, nrow = i, ncol = c2, byrow = TRUE)
expect_lt(
max(abs(abs(C_ours) - abs(unname(C_forbes)))),
0.005 + 1e-12
)
}
}
# (3) Redundancy chase paths: for every component, following our
# primary-parent links upward while |r| >= .9 must land on the same
# component her ChaseCorrPaths reports ("X--null" = no move).
for (entry in sim$corr_chase) {
parts <- strsplit(entry, "--", fixed = TRUE)[[1L]]
from <- .forbes_to_ackwards(parts[1L])
expected_top <- if (parts[2L] == "null") from else .forbes_to_ackwards(parts[2L])
expect_identical(
.chase(x, from), expected_top,
label = paste0(nm, " chase(", parts[1L], ") (ackwards)"),
expected.label = paste0("Forbes '", entry, "'")
)
}
}
})
test_that("prune('redundant') flags Forbes's Simulation 1 chains with her retention rule", {
skip_if_not_installed("psych")
sims <- .forbes_fixture()
suppressWarnings(suppressMessages({
x <- cached(ackwards(sims$sim1$R, k_max = 4, pairs = "all"))
xp <- prune(x, "redundant")
}))
# Her chase found exactly three redundant links: c3--b2, d1--c1, d2--c2.
# Under her retention rule: the b2-c3 chain stops short of k_max, keeping
# the top (m2f2); the two chains reaching k_max keep their bottoms
# (m4f1, m4f2). Flagged = the other chain members.
flagged <- xp$prune$nodes$id[xp$prune$nodes$pruned]
expect_setequal(flagged, c("m3f3", "m3f1", "m3f2"))
ch <- xp$prune$chains
expect_setequal(ch$id[ch$retain], c("m2f2", "m4f1", "m4f2"))
})
# ---------------------------------------------------------------------------
# AMH applied example (M53).
#
# Forbes's 155-variable "Assessing Mental Health" applied example, k = 10 (OSF
# pcwm8, CC-BY 4.0). The matrix is the exported dataset `forbes2023` (M54);
# fixtures/forbes2023_amh.rds holds only the expected comp_corr/cong computed
# with HER reference implementation from that same md5-pinned matrix (see
# data-raw/forbes2023.R and attr(fixture, "provenance")). As with the simulations,
# only ackwards() runs here; no Forbes code or network at test time.
#
# Redundancy chase: Forbes's ChaseCorrPaths uses the DIRECT (skip-level)
# correlation to a component at each ancestor level -- the criterion our
# prune("redundant") adopts by default since M53 (redundancy_criterion =
# "direct"). On this deep 10-level hierarchy it diverges from an adjacent-hop
# walk on 7 of 54 components (correlation is non-transitive), so this is exactly
# where the direct criterion earns its keep. amh$corr_chase holds her raw
# ChaseCorrPaths output ("X--Y"; "X--null" = no chase) for all 54 components,
# and .direct_chase() below reproduces it from our all-levels edges. See M53.
.amh_fixture <- function() {
readRDS(test_path("fixtures", "forbes2023_amh.rds"))$amh
}
# Forbes's direct/skip-level chase on an ackwards object: from `node`, at each
# ancestor level take the node with the largest |direct r| and continue while
# |r| >= 0.9 contiguously; return the topmost node reached (the node itself when
# its best direct link to the next level up is < 0.9). This is what
# redundancy_criterion = "direct" traces internally.
.direct_chase <- function(x, node) {
lv <- as.integer(sub("^m(\\d+)f\\d+$", "\\1", node))
cur <- node
for (j in seq_len(lv - 1L)) {
E <- x$edges$matrices[[paste0(lv - j, ":", lv)]] # rows level lv-j, cols level lv
col <- E[, node]
pi <- which.max(abs(col))
if (abs(col[pi]) < 0.9) break
cur <- rownames(E)[pi]
}
cur
}
test_that("default output reproduces Forbes's AMH applied example (k = 10)", {
skip_if_not_installed("psych")
amh <- .amh_fixture()
K <- amh$k_max
suppressWarnings(suppressMessages(
x <- cached(ackwards(forbes2023, k_max = K, pairs = "all"))
))
# (1) Between-level correlations: |ours| == |hers| entrywise, all 45 pairs.
idx <- 0L
for (c2 in 2:K) {
for (i in 1:(c2 - 1L)) {
idx <- idx + 1L
E_forbes <- amh$comp_corr[[idx]]
E_ours <- x$edges$matrices[[paste0(i, ":", c2)]]
expect_equal(
abs(unname(E_ours)), abs(unname(E_forbes)),
tolerance = 1e-12,
label = paste0("AMH |edges| ", i, ":", c2, " (ackwards)"),
expected.label = "Forbes reference"
)
}
}
# (2) Loading congruence: within her factor.congruence 2-dp rounding.
phi_ours <- ackwards:::.phi_pairs(x$levels, "all")
idx <- 0L
for (c2 in 2:K) {
for (i in 1:(c2 - 1L)) {
idx <- idx + 1L
C_forbes <- amh$cong[[idx]]
sub <- phi_ours[phi_ours$level_from == i & phi_ours$level_to == c2, ]
C_ours <- matrix(sub$phi, nrow = i, ncol = c2, byrow = TRUE)
expect_lt(
max(abs(abs(C_ours) - abs(unname(C_forbes)))),
0.005 + 1e-12
)
}
}
})
test_that("direct criterion reproduces Forbes's AMH redundancy chase exactly (54/54)", {
skip_if_not_installed("psych")
amh <- .amh_fixture()
suppressWarnings(suppressMessages(
x <- cached(ackwards(forbes2023, k_max = amh$k_max, pairs = "all"))
))
# Every component's direct chase over our all-levels edges lands on the same
# node her ChaseCorrPaths reports ("X--null" = the node stays put). This is
# the criterion prune("redundant") uses by default; here it is the exact
# published AMH result, including the 7 components where an adjacent walk
# would diverge (non-transitivity).
expect_length(amh$corr_chase, 54L)
for (entry in amh$corr_chase) {
parts <- strsplit(entry, "--", fixed = TRUE)[[1L]]
from <- .forbes_to_ackwards(parts[1L])
expected_top <- if (parts[2L] == "null") from else .forbes_to_ackwards(parts[2L])
expect_identical(
.direct_chase(x, from), expected_top,
label = paste0("AMH direct chase(", parts[1L], ") (ackwards)"),
expected.label = paste0("Forbes '", entry, "'")
)
}
})
test_that("prune('redundant') on AMH: direct default vs adjacent opt-in", {
skip_if_not_installed("psych")
amh <- .amh_fixture()
suppressWarnings(suppressMessages({
x <- cached(ackwards(forbes2023, k_max = amh$k_max, pairs = "all"))
xp <- prune(x, "redundant") # default redundancy_criterion = "direct"
xa <- prune(x, "redundant", redundancy_criterion = "adjacent")
}))
ch <- xp$prune$chains
# The paper's d4 chain: m4f4 -> ... -> m10f4 (Forbes's d4->e4->f5->g5->h5->i4->j4).
# It reaches k_max, so the retention rule keeps the most-specific bottom node.
d4 <- ch[ch$chain_id == ch$chain_id[ch$id == "m10f4"], ]
expect_setequal(
d4$id,
c("m4f4", "m5f4", "m6f5", "m7f5", "m8f5", "m9f4", "m10f4")
)
expect_identical(d4$id[d4$retain], "m10f4")
# Direct (Forbes-faithful) decomposition -- regression pin.
expect_equal(sum(xp$prune$nodes$pruned), 37L)
expect_setequal(
unique(ch$id[ch$retain]),
c(
"m1f1", "m4f2", "m5f2", "m7f7",
"m10f1", "m10f2", "m10f3", "m10f4", "m10f5",
"m10f6", "m10f7", "m10f8", "m10f9"
)
)
# The adjacent opt-in gives a materially different answer on this deep
# hierarchy (it retains m3f3, which chases further under the direct rule).
expect_equal(sum(xa$prune$nodes$pruned), 36L)
expect_true("m3f3" %in% xa$prune$chains$id[xa$prune$chains$retain])
expect_false("m3f3" %in% ch$id[ch$retain])
})
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.