Nothing
# test-numerical-validation.R
# Direct tests of internal mathematical functions for correctness and stability.
# All tests target the release code (mfrm_core.R) through the ::: accessor.
# ---- logsumexp ----
test_that("logsumexp computes log(sum(exp(x))) correctly", {
lse <- mfrmr:::logsumexp
expect_equal(lse(log(c(1, 2, 3))), log(6), tolerance = 1e-10)
expect_equal(lse(c(0, 0)), log(2), tolerance = 1e-10)
expect_equal(lse(0), 0, tolerance = 1e-10)
})
test_that("logsumexp is numerically stable for large values", {
lse <- mfrmr:::logsumexp
# Large positive values (would overflow naive exp)
expect_equal(lse(c(1000, 1001)), 1001 + log(1 + exp(-1)), tolerance = 1e-10)
# Large negative values (would underflow naive exp)
expect_equal(lse(c(-1000, -999)), -999 + log(1 + exp(-1)), tolerance = 1e-10)
})
# ---- weighted_mean ----
test_that("weighted_mean handles standard cases", {
wm <- mfrmr:::weighted_mean
expect_equal(wm(c(1, 2, 3), c(1, 1, 1)), 2)
expect_equal(wm(c(1, 3), c(1, 3)), 2.5)
})
test_that("weighted_mean handles edge cases safely", {
wm <- mfrmr:::weighted_mean
expect_true(is.na(wm(c(NA, NA), c(1, 1))))
expect_true(is.na(wm(c(1, 2), c(0, 0))))
expect_true(is.na(wm(numeric(0), numeric(0))))
# Inf is not finite, so it's excluded; result is just the finite value
expect_equal(wm(c(Inf, 1), c(1, 1)), 1)
})
# ---- get_weights ----
test_that("get_weights extracts Weight column when present", {
gw <- mfrmr:::get_weights
df <- data.frame(X = 1:3, Weight = c(1, 2, 0.5))
expect_equal(gw(df), c(1, 2, 0.5))
})
test_that("get_weights returns ones when Weight column absent", {
gw <- mfrmr:::get_weights
df <- data.frame(X = 1:3, Y = 4:6)
expect_equal(gw(df), c(1, 1, 1))
})
test_that("get_weights zeroes invalid weights", {
gw <- mfrmr:::get_weights
df <- data.frame(X = 1:3, Weight = c(1, -2, NA))
result <- gw(df)
expect_equal(result[1], 1)
expect_equal(result[2], 0)
expect_equal(result[3], 0)
})
# ---- gauss_hermite_normal ----
test_that("gauss_hermite_normal returns valid quadrature for n=1", {
gh <- mfrmr:::gauss_hermite_normal
q1 <- gh(1)
expect_equal(q1$nodes, 0)
expect_equal(q1$weights, 1)
})
test_that("gauss_hermite_normal weights sum to 1 for standard normal", {
gh <- mfrmr:::gauss_hermite_normal
for (n in c(3, 7, 15, 21)) {
q <- gh(n)
expect_equal(length(q$nodes), n)
expect_equal(length(q$weights), n)
expect_equal(sum(q$weights), 1, tolerance = 1e-10,
label = paste("n =", n))
}
})
test_that("gauss_hermite_normal nodes are symmetric around zero", {
gh <- mfrmr:::gauss_hermite_normal
q <- gh(15)
sorted <- sort(q$nodes)
expect_equal(sorted, -rev(sorted), tolerance = 1e-10)
})
# ---- center_sum_zero ----
test_that("center_sum_zero produces zero-sum vector", {
csz <- mfrmr:::center_sum_zero
x <- c(1, 2, 3, 10)
result <- csz(x)
expect_equal(sum(result), 0, tolerance = 1e-15)
expect_equal(result, x - mean(x), tolerance = 1e-15)
})
test_that("center_sum_zero handles empty input", {
expect_equal(mfrmr:::center_sum_zero(numeric(0)), numeric(0))
})
# ---- expand_facet ----
test_that("expand_facet produces sum-to-zero constraint", {
ef <- mfrmr:::expand_facet
result <- ef(c(0.5, -0.3), 3)
expect_equal(sum(result), 0, tolerance = 1e-15)
expect_equal(length(result), 3)
})
test_that("expand_facet returns zero for single level", {
expect_equal(mfrmr:::expand_facet(numeric(0), 1), 0)
})
# ---- zstd_from_mnsq ----
test_that("zstd_from_mnsq gives near-zero for perfect fit (MnSq=1)", {
zstd <- mfrmr:::zstd_from_mnsq
result <- zstd(1.0, 100)
expect_true(abs(result) < 0.5)
})
test_that("zstd_from_mnsq gives positive for overfit (MnSq>1)", {
zstd <- mfrmr:::zstd_from_mnsq
result <- zstd(2.0, 100)
expect_true(result > 0)
})
test_that("zstd_from_mnsq returns NA for NA or zero df", {
zstd <- mfrmr:::zstd_from_mnsq
expect_true(is.na(zstd(NA, 100)))
expect_true(is.na(zstd(1.0, NA)))
})
# ---- category_prob_rsm ----
test_that("category_prob_rsm probabilities sum to 1 per observation", {
cp <- mfrmr:::category_prob_rsm
eta <- c(-2, -1, 0, 1, 2)
step_cum <- c(0, -0.5, 0.5) # 3 categories (0, 1, 2)
probs <- cp(eta, step_cum)
expect_equal(nrow(probs), 5)
expect_equal(ncol(probs), 3)
row_sums <- rowSums(probs)
expect_equal(row_sums, rep(1, 5), tolerance = 1e-10)
expect_true(all(probs >= 0))
})
test_that("category_prob_rsm responds to eta direction", {
cp <- mfrmr:::category_prob_rsm
step_cum <- c(0, 0, 0) # 3 categories
# High eta should favor higher categories
probs_high <- cp(5, step_cum)
probs_low <- cp(-5, step_cum)
expect_true(probs_high[1, 3] > probs_high[1, 1])
expect_true(probs_low[1, 1] > probs_low[1, 3])
})
# ---- loglik_rsm ----
test_that("loglik_rsm returns finite non-positive values", {
ll <- mfrmr:::loglik_rsm
eta <- c(0, 0.5, -0.5)
score_k <- c(0L, 1L, 2L)
step_cum <- c(0, -0.3, 0.3)
result <- ll(eta, score_k, step_cum)
expect_true(is.finite(result))
expect_true(result <= 0)
})
# ---- category_prob_pcm ----
test_that("category_prob_pcm probabilities sum to 1", {
cp <- mfrmr:::category_prob_pcm
eta <- c(-1, 0, 1)
# step_cum_mat: rows = criterion levels, cols = categories
step_cum_mat <- matrix(c(0, -0.5, 0.5,
0, -0.3, 0.3), nrow = 2, byrow = TRUE)
step_idx <- c(1L, 2L, 1L)
probs <- cp(eta, step_cum_mat, step_idx)
expect_equal(nrow(probs), 3)
row_sums <- rowSums(probs)
expect_equal(row_sums, rep(1, 3), tolerance = 1e-10)
expect_true(all(probs >= 0))
})
# ---- JML vs MML directional consistency ----
test_that("JML and MML produce correlated facet estimates", {
skip_on_cran()
d <- mfrmr:::sample_mfrm_data(seed = 42)
fit_jml <- suppressWarnings(fit_mfrm(
d, "Person", c("Rater", "Task", "Criterion"), "Score",
method = "JML", model = "RSM", maxit = 30
))
fit_mml <- suppressWarnings(fit_mfrm(
d, "Person", c("Rater", "Task", "Criterion"), "Score",
method = "MML", model = "RSM", maxit = 30, quad_points = 15
))
jml_rater <- fit_jml$facets$others |>
dplyr::filter(.data$Facet == "Rater") |>
dplyr::arrange(.data$Level)
mml_rater <- fit_mml$facets$others |>
dplyr::filter(.data$Facet == "Rater") |>
dplyr::arrange(.data$Level)
cor_val <- cor(jml_rater$Estimate, mml_rater$Estimate)
expect_true(cor_val > 0.9, label = paste("JML-MML rater correlation:", round(cor_val, 3)))
})
# ---- build_param_sizes / split_params round-trip ----
test_that("split_params correctly partitions parameter vector", {
sp <- mfrmr:::split_params
sizes <- list(theta = 3, Rater = 2, steps = 2)
par <- c(0.1, 0.2, 0.3, 0.5, -0.5, 0.7, -0.7)
result <- sp(par, sizes)
expect_equal(result$theta, c(0.1, 0.2, 0.3))
expect_equal(result$Rater, c(0.5, -0.5))
expect_equal(result$steps, c(0.7, -0.7))
})
test_that("split_params handles zero-size correctly", {
sp <- mfrmr:::split_params
sizes <- list(theta = 0, Rater = 2)
par <- c(0.5, -0.5)
result <- sp(par, sizes)
expect_equal(result$theta, numeric(0))
expect_equal(result$Rater, c(0.5, -0.5))
})
# ---- sample_mfrm_data reproducibility ----
test_that("sample_mfrm_data is reproducible with same seed", {
d1 <- mfrmr:::sample_mfrm_data(seed = 999)
d2 <- mfrmr:::sample_mfrm_data(seed = 999)
expect_identical(d1, d2)
expect_true(is.data.frame(d1))
expect_true(all(c("Person", "Rater", "Task", "Criterion", "Score") %in% names(d1)))
})
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.