Nothing
test_that("algebra and scores paths agree for PCA engine on all adjacent pairs", {
skip_if_not_installed("psych")
set.seed(42)
n <- 150
f1 <- rnorm(n)
f2 <- rnorm(n)
data <- data.frame(
x1 = f1 + 0.3 * rnorm(n),
x2 = f1 + 0.3 * rnorm(n),
x3 = f1 + 0.3 * rnorm(n),
x4 = f2 + 0.3 * rnorm(n),
x5 = f2 + 0.3 * rnorm(n),
x6 = f2 + 0.3 * rnorm(n)
)
R <- cor(data)
x <- cached(ackwards(data, k_max = 4))
E_scores_all <- compute_edges(
levels = x$levels,
R = R,
edge_method = "scores",
pairs = "adjacent",
data = data
)$matrices
for (key in names(x$edges$matrices)) {
E_alg <- x$edges$matrices[[key]]
E_sc <- E_scores_all[[key]]
expect_lt(
max(abs(abs(E_alg) - abs(E_sc))), 1e-6,
label = paste("algebra vs scores for pair", key)
)
}
})
test_that("compute_edges errors when algebra forced but R missing", {
skip_if_not_installed("psych")
x <- cached(ackwards(psych::bfi[, 1:25], k_max = 2))
expect_error(
compute_edges(x$levels, R = NULL, edge_method = "algebra"),
"conditions not met"
)
})
test_that("compute_edges with explicit edge_method='algebra' and valid R succeeds", {
# Covers the TRUE branch inside the algebra case of the switch,
# which is only reached when edge_method = "algebra" and algebra_ok = TRUE.
skip_if_not_installed("psych")
x <- cached(ackwards(bfi25[, 1:6], k_max = 3))
R <- cor(bfi25[, 1:6], use = "pairwise.complete.obs")
result <- compute_edges(x$levels, R = R, edge_method = "algebra")
expect_type(result, "list")
expect_true(!is.null(result$matrices))
})
test_that("compute_edges errors when scores needed but data absent", {
skip_if_not_installed("psych")
x <- cached(ackwards(psych::bfi[, 1:25], k_max = 2))
expect_error(
compute_edges(x$levels, R = NULL, edge_method = "scores", data = NULL),
"data"
)
})
test_that("pairs='all' produces skip-level edge matrices in ackwards()", {
skip_if_not_installed("psych")
set.seed(1)
n <- 200
g <- rnorm(n)
s1 <- rnorm(n)
s2 <- rnorm(n)
data <- data.frame(
x1 = g + s1 + rnorm(n, sd = 0.3),
x2 = g + s1 + rnorm(n, sd = 0.3),
x3 = g + s1 + rnorm(n, sd = 0.3),
x4 = g + s2 + rnorm(n, sd = 0.3),
x5 = g + s2 + rnorm(n, sd = 0.3),
x6 = g + s2 + rnorm(n, sd = 0.3)
)
x_adj <- cached(ackwards(data, k_max = 4, pairs = "adjacent"))
x_all <- cached(ackwards(data, k_max = 4, pairs = "all"))
# Default is adjacent
expect_equal(x_adj$meta$pairs, "adjacent")
expect_equal(x_all$meta$pairs, "all")
# adjacent: only consecutive level keys
adj_keys <- names(x_adj$edges$matrices)
expect_setequal(adj_keys, c("1:2", "2:3", "3:4"))
# all: includes skip-level keys
all_keys <- names(x_all$edges$matrices)
expect_true(all(c("1:2", "2:3", "3:4", "1:3", "1:4", "2:4") %in% all_keys))
# Adjacent edges are consistent between the two modes (same aligned weights)
for (key in c("1:2", "2:3", "3:4")) {
expect_equal(
x_adj$edges$matrices[[key]],
x_all$edges$matrices[[key]],
tolerance = 1e-10,
label = paste("adjacent edge", key, "consistent between pairs modes")
)
}
})
test_that("skip-level edges in pairs='all' have is_primary=FALSE and correct level_from/to", {
skip_if_not_installed("psych")
set.seed(2)
n <- 200
g <- rnorm(n)
s1 <- rnorm(n)
s2 <- rnorm(n)
data <- data.frame(
x1 = g + s1 + rnorm(n, sd = 0.3),
x2 = g + s1 + rnorm(n, sd = 0.3),
x3 = g + s1 + rnorm(n, sd = 0.3),
x4 = g + s2 + rnorm(n, sd = 0.3),
x5 = g + s2 + rnorm(n, sd = 0.3),
x6 = g + s2 + rnorm(n, sd = 0.3)
)
x <- cached(ackwards(data, k_max = 3, pairs = "all"))
tidy <- x$edges$tidy
# Skip-level pair 1:3 should exist
skip <- tidy[tidy$level_from == 1 & tidy$level_to == 3, ]
expect_gt(nrow(skip), 0)
# All skip-level edges must be non-primary
expect_true(all(!skip$is_primary))
# level_from < level_to throughout
expect_true(all(tidy$level_from < tidy$level_to))
})
test_that("algebra and scores agree for all-levels edges (PCA)", {
skip_if_not_installed("psych")
set.seed(3)
n <- 200
g <- rnorm(n)
s1 <- rnorm(n)
s2 <- rnorm(n)
data <- data.frame(
x1 = g + s1 + rnorm(n, sd = 0.3),
x2 = g + s1 + rnorm(n, sd = 0.3),
x3 = g + s1 + rnorm(n, sd = 0.3),
x4 = g + s2 + rnorm(n, sd = 0.3),
x5 = g + s2 + rnorm(n, sd = 0.3),
x6 = g + s2 + rnorm(n, sd = 0.3)
)
R <- cor(data)
x <- cached(ackwards(data, k_max = 4, pairs = "all"))
E_scores_all <- compute_edges(
levels = x$levels,
R = R,
edge_method = "scores",
pairs = "all",
data = data
)$matrices
for (key in names(x$edges$matrices)) {
E_alg <- x$edges$matrices[[key]]
E_sc <- E_scores_all[[key]]
expect_lt(
max(abs(abs(E_alg) - abs(E_sc))), 1e-6,
label = paste("algebra vs scores for all-pairs edge", key)
)
}
})
# --- match_parents unit tests ---------------------------------------------------
# Adjacent levels always have n_b = n_a + 1 (non-square). LSAP (bijection) would
# require a padding row that can return index > nrow(E) → subscript OOB.
# The greedy argmax is correct: each child independently picks its closest parent;
# multiple children may share a parent, which is normal in the hierarchy.
test_that("match_parents: non-square (n_b > n_a) assigns correct greedy parents", {
# Level k-1 has 2 factors (rows), level k has 3 factors (cols).
# Col 1 and 2 both peak at row 1; col 3 peaks at row 2.
E <- matrix(
c(
0.9, 0.8, 0.1,
0.1, 0.2, 0.7
),
nrow = 2
)
result <- ackwards:::match_parents(E)
expect_equal(result, c(1L, 1L, 2L))
# All returned indices must be within 1..nrow(E)
expect_true(all(result >= 1L & result <= nrow(E)))
})
test_that("match_parents: square case assigns correct parents", {
E <- matrix(
c(
0.9, 0.1, 0.1,
0.2, 0.8, 0.2,
0.1, 0.2, 0.7
),
nrow = 3, byrow = TRUE
)
result <- ackwards:::match_parents(E)
expect_equal(result, c(1L, 2L, 3L))
})
# --- .align_signs sign-propagation unit tests -----------------------------------
# Regression (M35): a level-k factor's flip must be chosen against its primary
# parent's *aligned* sign, so that a parent which is itself flipped does not leave
# its children's PRIMARY edge displaying negative. Sign propagates top-down
# (DESIGN s.7: "propagating top-down").
test_that(".align_signs: a flipped parent keeps its child's primary edge positive", {
# Level 1: 1 factor (positive colSum -> no flip).
# Level 2: 2 factors; factor 2's raw edge to m1f1 is negative -> it is flipped.
# Level 3: 3 factors; factor 2's primary parent is the flipped level-2 factor 2,
# with a raw positive edge (+0.9). Pre-fix this displayed as -0.9.
loadings_list <- list(
matrix(rep(0.7, 4), ncol = 1),
matrix(seq_len(8) / 10, ncol = 2),
matrix(seq_len(12) / 10, ncol = 3)
)
edges_list <- list(
"1:2" = matrix(c(0.8, -0.9), nrow = 1),
"2:3" = matrix(c(0.85, -0.1, 0.05, 0.90, 0.1, 0.7), nrow = 2)
)
lineage <- list(NULL, c(1L, 1L), c(1L, 2L, 1L))
aligned <- ackwards:::.align_signs(loadings_list, edges_list, lineage)
# Level-2 factor 2 was flipped negative (its raw edge to m1f1 was -0.9).
expect_equal(aligned$signs[[2]], c(1L, -1L))
# Level-3 factor 2's primary parent is the *flipped* level-2 factor 2 with a
# raw positive edge (+0.9): the child must flip too (-1) so its primary edge
# displays positive once ackwards() recomputes edges from the flipped
# weights. Under the pre-M35 raw-edge rule it stayed +1 (the bug). Factors 1
# and 3 hang off the unflipped factor 1 with positive raw edges -> no flip.
expect_equal(aligned$signs[[3]], c(1L, -1L, 1L))
# Loadings columns are flipped consistently with the recorded signs.
expect_equal(aligned$loadings[[2]][, 2], -loadings_list[[2]][, 2])
expect_equal(aligned$loadings[[3]][, 2], -loadings_list[[3]][, 2])
expect_equal(aligned$loadings[[3]][, c(1, 3)], loadings_list[[3]][, c(1, 3)])
})
test_that("ackwards(): every primary edge is non-negative across engines", {
skip_if_not_installed("psych")
check_primary_nonneg <- function(x, label) {
te <- x$edges$tidy
prim <- te[!is.na(te$is_primary) & te$is_primary, , drop = FALSE]
expect_gt(nrow(prim), 0L)
expect_true(all(prim$r >= 0), label = paste("primary edges >= 0:", label))
}
for (eng in c("pca", "efa")) {
check_primary_nonneg(
suppressWarnings(ackwards(sim16, k_max = 5, engine = eng)),
paste("sim16", eng)
)
check_primary_nonneg(
suppressWarnings(ackwards(bfi25, k_max = 5, engine = eng)),
paste("bfi25", eng)
)
}
skip_if_not_installed("lavaan")
check_primary_nonneg(
suppressWarnings(ackwards(.make_esem_data(), k_max = 3, engine = "esem")),
"esem"
)
})
test_that("ackwards() parent indices are always within bounds (regression: LSAP padding)", {
skip_if_not_installed("psych")
# This call previously triggered subscript OOB when clue was installed,
# because LSAP padding rows returned indices > nrow(E) for non-square levels.
x <- cached(ackwards(psych::bfi[, 1:25], k_max = 5))
expect_s3_class(x, "ackwards")
expect_equal(x$k_max, 5L)
# Verify every lineage index is in bounds for its level
for (ki in seq(2L, x$k_max)) {
parents <- x$lineage[[as.character(ki)]]
n_parents <- ki - 1L # nrow of edge matrix for level ki-1 → ki
expect_true(
all(parents >= 1L & parents <= n_parents),
label = paste("lineage indices in bounds at level", ki)
)
}
})
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.