diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml index cf2e656..fec4123 100644 --- a/.github/workflows/r.yml +++ b/.github/workflows/r.yml @@ -16,7 +16,15 @@ concurrency: jobs: check: - runs-on: ubuntu-latest + # Pin to jammy (22.04): Posit P3M publishes precompiled Linux binaries for + # the current R release on jammy, but not yet for noble (24.04, the current + # ubuntu-latest). On noble, pak falls back to source-building the mirt -> + # SimDesign -> qs -> stringfish chain, and the source-built stringfish.so + # then fails to load with an Intel TBB ABI mismatch (undefined symbol + # tbb::internal::concurrent_vector_base_v3::internal_grow_by), aborting + # dependency install before R CMD check ever runs. Jammy binaries avoid the + # source compile entirely. Revisit once P3M ships noble binaries for these. + runs-on: ubuntu-22.04 env: R_PROFILE_USER: /dev/null diff --git a/docs/references.md b/docs/references.md new file mode 100644 index 0000000..6c13197 --- /dev/null +++ b/docs/references.md @@ -0,0 +1,55 @@ + + +# References + +This file records, in APA 7th-edition form, the psychometric literature that +grounds the regression and robustness tests under `tests/testthat/`. Each work +was verified against its publisher record (journal, volume, issue, pages, and +DOI) rather than reproduced from memory. + +> Note: a local Zotero API is not reachable from the build/CI sandbox, so this +> in-repo, version-controlled reference list is the achievable substitute for a +> Zotero-managed bibliography. Keep it in sync with the citation headers in the +> test files. + +## Cited works + +- Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood estimation + of item parameters: Application of an EM algorithm. *Psychometrika, 46*(4), + 443–459. +- Cai, L. (2010). High-dimensional exploratory item factor analysis by a + Metropolis–Hastings Robbins–Monro algorithm. *Psychometrika, 75*(1), 33–57. + +- Chalmers, R. P. (2012). mirt: A multidimensional item response theory + package for the R environment. *Journal of Statistical Software, 48*(6), + 1–29. +- Kim, S. (2006). A comparative study of IRT fixed parameter calibration + methods. *Journal of Educational Measurement, 43*(4), 355–381. + +- Kim, S., & Kolen, M. J. (2010). Linking item parameters to a base scale. + *Journal of Educational Measurement, 47*(2), 164–181. + +- Mislevy, R. J., & Wu, P.-K. (1996). *Missing responses and IRT ability + estimation: Omits, choice, time limits, and adaptive testing* (ETS Research + Report No. RR-96-30-ONR). Educational Testing Service. + + +## How each test file is grounded + +- `test-kim2006-fixed-anchor-invariant.R` — Fixed-item-parameter linking + invariant: anchors keep their old-form values and stay fixed while + non-anchors are estimated onto the base scale (Kim, 2006; Bock & Aitkin, + 1981; Chalmers, 2012). +- `test-se-hessian-vcov-preservation.R` — `SE = TRUE` observed-information / + covariance matrix and the second-order optimality test are preserved through + linking on the old, new, and linked models (Bock & Aitkin, 1981; Cai, 2010; + Chalmers, 2012). +- `test-concurrent-missing-robustness.R` — Planned-missing / non-overlapping + booklet designs calibrate without error under ignorable missingness, and + anchors stay fixed (Bock & Aitkin, 1981; Mislevy & Wu, 1996; Kim, 2006). +- `test-degenerate-response-robustness.R` — Zero-score, perfect-score, and + skewed (near-degenerate) response patterns produce finite item parameters, + MAP abilities, and expected scores rather than crashing (Bock & Aitkin, + 1981; Mislevy & Wu, 1996; Kim, 2006). +- `test-fixed-parameter-calibration.R` (pre-existing) — End-to-end linking + contract fixture (Kim, 2006). diff --git a/tests/testthat/test-concurrent-missing-robustness.R b/tests/testthat/test-concurrent-missing-robustness.R new file mode 100644 index 0000000..a6ecba5 --- /dev/null +++ b/tests/testthat/test-concurrent-missing-robustness.R @@ -0,0 +1,125 @@ +# Directive item 3 — Concurrent calibration missing-value robustness. +# +# In common-item nonequivalent-groups (NEAT) and planned-missing booklet +# designs, examinees respond to only a subset of the items; the unadministered +# cells are structurally missing (NA). Under the standard ignorable / MAR +# assumption for such designs, marginal maximum likelihood integrates the +# person likelihood over only the OBSERVED responses, so missing-by-design +# cells neither bias nor break estimation (Bock & Aitkin, 1981; Mislevy & Wu, +# 1996). This test builds two non-overlapping booklets per form — every +# examinee sees all anchor items but only one form-specific block of unique +# items — and asserts that autoFIPC calibrates end-to-end without error while +# the Kim (2006) fixed-anchor invariant still holds. +# +# References (APA 7th): +# Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood estimation of +# item parameters: Application of an EM algorithm. Psychometrika, 46(4), +# 443-459. https://doi.org/10.1007/BF02293801 +# Kim, S. (2006). A comparative study of IRT fixed parameter calibration +# methods. Journal of Educational Measurement, 43(4), 355-381. +# https://doi.org/10.1111/j.1745-3984.2006.00021.x +# Mislevy, R. J., & Wu, P.-K. (1996). Missing responses and IRT ability +# estimation: Omits, choice, time limits, and adaptive testing (ETS +# Research Report No. RR-96-30-ONR). Educational Testing Service. +# https://doi.org/10.1002/j.2333-8504.1996.tb01708.x + +test_that("autoFIPC calibrates planned-missing booklets and keeps anchors fixed", { + skip_on_cran() + skip_if_not_installed("mirt") + + set.seed(20260731) + + old_item_names <- paste0("old_", 1:7) + new_item_names <- paste0("new_", 1:7) + # Items 1:4 are the fully-observed anchors; 5:7 are the form-specific block + # split across two booklets. + old_common_items <- old_item_names[1:4] + new_common_items <- new_item_names[1:4] + + common_a <- c(0.95, 1.18, 0.82, 1.27) + common_d <- c(-0.70, -0.15, 0.40, 0.85) + old_a <- matrix(c(common_a, 1.05, 0.88, 1.16), ncol = 1) + old_d <- c(common_d, -0.35, 0.30, 0.60) + new_a <- matrix(c(common_a, 0.90, 1.22, 0.79), ncol = 1) + new_d <- c(common_d, 0.20, -0.50, 0.45) + + n_persons <- 1600 + old_data <- as.data.frame(mirt::simdata( + a = old_a, d = old_d, + itemtype = rep("2PL", length(old_item_names)), N = n_persons + )) + new_data <- as.data.frame(mirt::simdata( + a = new_a, d = new_d, + itemtype = rep("2PL", length(new_item_names)), N = n_persons + )) + names(old_data) <- old_item_names + names(new_data) <- new_item_names + + # Planned-missing booklet spiral: anchors seen by everyone; the two unique + # blocks are administered to disjoint halves, the rest are NA (missing by + # design, not by response behavior). + booklet <- rep(c(1L, 2L), length.out = n_persons) + old_block_a <- old_item_names[5:6] + old_block_b <- old_item_names[7] + new_block_a <- new_item_names[5:6] + new_block_b <- new_item_names[7] + old_data[booklet == 1L, old_block_b] <- NA + old_data[booklet == 2L, old_block_a] <- NA + new_data[booklet == 1L, new_block_b] <- NA + new_data[booklet == 2L, new_block_a] <- NA + + # Structural missingness must be present but the anchors must stay complete. + expect_true(anyNA(old_data)) + expect_true(anyNA(new_data)) + expect_false(anyNA(old_data[, old_common_items])) + expect_false(anyNA(new_data[, new_common_items])) + + old_model <- mirt::mirt( + old_data, 1, itemtype = "2PL", method = "EM", SE = TRUE, + verbose = FALSE, technical = list(NCYCLES = 800) + ) + new_model <- mirt::mirt( + new_data, 1, itemtype = "2PL", method = "EM", SE = TRUE, + verbose = FALSE, technical = list(NCYCLES = 800) + ) + + # A thrown error here fails the test: the calibration must complete despite + # the structurally missing (planned-missing) cells. + linked <- aFIPC::autoFIPC( + newformXData = new_model, + oldformYData = old_model, + newformCommonItemNames = new_common_items, + oldformCommonItemNames = old_common_items, + itemtype = "2PL", + checkIPD = FALSE, + tryEM = TRUE, + freeMEAN = FALSE, + forceNormalZeroOne = TRUE, + confirmCommonItems = TRUE + ) + + expect_type(linked, "list") + expect_true(all(c("LinkedModel", "ThetaLinkedform") %in% names(linked))) + + # Anchors remain fixed to their old-form values despite the missing blocks. + old_values <- mirt::mod2values(old_model) + linked_values <- mirt::mod2values(linked$LinkedModel) + for (i in seq_along(old_common_items)) { + old_fixed <- old_values[ + old_values$item == old_common_items[i] & + old_values$name %in% c("a1", "d"), + c("name", "value") + ] + linked_fixed <- linked_values[ + linked_values$item == new_common_items[i] & + linked_values$name %in% c("a1", "d"), + c("name", "value", "est") + ] + expect_equal(linked_fixed$value, old_fixed$value, tolerance = 1e-6) + expect_false(any(linked_fixed$est)) + } + + # Every calibrated person receives a finite ability estimate on the linked + # scale even though each answered only a booklet-sized subset. + expect_true(all(is.finite(linked$ThetaLinkedform))) +}) diff --git a/tests/testthat/test-degenerate-response-robustness.R b/tests/testthat/test-degenerate-response-robustness.R new file mode 100644 index 0000000..105a381 --- /dev/null +++ b/tests/testthat/test-degenerate-response-robustness.R @@ -0,0 +1,117 @@ +# Directive item 4 — Zero-score / perfect-score robustness. +# +# Real calibration samples contain examinees who answer every item incorrectly +# (all-0) or every item correctly (all-1), and items whose responses are highly +# skewed toward one category. Maximum-likelihood ability estimates diverge to +# +/- infinity for perfect and zero scores, but the marginal / Bayesian +# machinery aFIPC relies on keeps them finite: MML estimation integrates over a +# proper population prior (Bock & Aitkin, 1981), and the MAP ability scores +# autoFIPC returns are shrunk toward that prior and therefore finite even at the +# boundaries (Mislevy & Wu, 1996). This test injects all-0 and all-1 response +# vectors plus deliberately extreme (near-degenerate) unique items and asserts +# that fixed-item calibration (Kim, 2006) neither crashes nor emits non-finite +# item parameters, ability estimates, or expected scores. +# +# Note on scope: a fully CONSTANT item (a single observed category) is not +# identifiable under any IRT estimator, so this fixture keeps every item +# non-constant by construction (the injected all-0 and all-1 rows guarantee at +# least one response in each category) while still exercising the skewed, +# boundary-heavy regime the directive targets. +# +# References (APA 7th): +# Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood estimation of +# item parameters: Application of an EM algorithm. Psychometrika, 46(4), +# 443-459. https://doi.org/10.1007/BF02293801 +# Kim, S. (2006). A comparative study of IRT fixed parameter calibration +# methods. Journal of Educational Measurement, 43(4), 355-381. +# https://doi.org/10.1111/j.1745-3984.2006.00021.x +# Mislevy, R. J., & Wu, P.-K. (1996). Missing responses and IRT ability +# estimation: Omits, choice, time limits, and adaptive testing (ETS +# Research Report No. RR-96-30-ONR). Educational Testing Service. +# https://doi.org/10.1002/j.2333-8504.1996.tb01708.x + +test_that("autoFIPC survives zero-score, perfect-score, and skewed items", { + skip_on_cran() + skip_if_not_installed("mirt") + + set.seed(20260801) + + old_item_names <- paste0("old_", 1:6) + new_item_names <- paste0("new_", 1:6) + old_common_items <- old_item_names[1:4] + new_common_items <- new_item_names[1:4] + + common_a <- c(1.00, 0.90, 1.20, 0.85) + common_d <- c(-0.60, -0.10, 0.30, 0.65) + # Unique items 5:6 are deliberately extreme in difficulty to create heavily + # skewed, near-degenerate response columns. + old_a <- matrix(c(common_a, 1.15, 0.80), ncol = 1) + old_d <- c(common_d, 3.2, -3.0) + new_a <- matrix(c(common_a, 0.95, 1.10), ncol = 1) + new_d <- c(common_d, -3.1, 3.3) + + old_data <- as.data.frame(mirt::simdata( + a = old_a, d = old_d, + itemtype = rep("2PL", length(old_item_names)), N = 1500 + )) + new_data <- as.data.frame(mirt::simdata( + a = new_a, d = new_d, + itemtype = rep("2PL", length(new_item_names)), N = 1500 + )) + names(old_data) <- old_item_names + names(new_data) <- new_item_names + + # Inject boundary examinees: rows 1:3 all-incorrect, rows 4:6 all-correct. + # This also guarantees every item has >=3 zeros and >=3 ones (non-constant). + old_data[1:3, ] <- 0 + old_data[4:6, ] <- 1 + new_data[1:3, ] <- 0 + new_data[4:6, ] <- 1 + + # Sanity guard: no item collapsed to a single category. + expect_true(all(vapply(old_data, function(x) length(unique(x)) >= 2, logical(1)))) + expect_true(all(vapply(new_data, function(x) length(unique(x)) >= 2, logical(1)))) + + old_model <- mirt::mirt( + old_data, 1, itemtype = "2PL", method = "EM", SE = TRUE, + verbose = FALSE, technical = list(NCYCLES = 800) + ) + new_model <- mirt::mirt( + new_data, 1, itemtype = "2PL", method = "EM", SE = TRUE, + verbose = FALSE, technical = list(NCYCLES = 800) + ) + + # A thrown error here fails the test: calibration must complete despite the + # boundary (all-0 / all-1) examinees and skewed items. + linked <- aFIPC::autoFIPC( + newformXData = new_model, + oldformYData = old_model, + newformCommonItemNames = new_common_items, + oldformCommonItemNames = old_common_items, + itemtype = "2PL", + checkIPD = FALSE, + tryEM = TRUE, + freeMEAN = FALSE, + forceNormalZeroOne = TRUE, + confirmCommonItems = TRUE + ) + + expect_type(linked, "list") + + # Linked item parameters must all be finite despite the boundary examinees. + linked_values <- mirt::mod2values(linked$LinkedModel) + item_params <- linked_values[ + linked_values$name %in% c("a1", "d"), "value" + ] + expect_true(all(is.finite(item_params))) + + # MAP ability estimates on all three forms remain finite at the boundaries. + expect_true(all(is.finite(linked$ThetaOldform))) + expect_true(all(is.finite(linked$ThetaNewform))) + expect_true(all(is.finite(linked$ThetaLinkedform))) + + # Expected test scores remain finite as well. + expect_true(all(is.finite(linked$ExpectedScoreOldform))) + expect_true(all(is.finite(linked$ExpectedScoreNewform))) + expect_true(all(is.finite(linked$ExpectedScoreLinkedform))) +}) diff --git a/tests/testthat/test-kim2006-fixed-anchor-invariant.R b/tests/testthat/test-kim2006-fixed-anchor-invariant.R new file mode 100644 index 0000000..eacb133 --- /dev/null +++ b/tests/testthat/test-kim2006-fixed-anchor-invariant.R @@ -0,0 +1,109 @@ +# Directive item 1 — Fixed-item-parameter linking invariant (Kim, 2006). +# +# Kim (2006) formalizes fixed parameter calibration (FPC): the old-form anchor +# (common) item parameters are treated as KNOWN and held fixed while the new +# form is calibrated, so the new form is placed directly on the established base +# scale rather than transformed after the fact. This suite pins that invariant +# end-to-end through autoFIPC(): after linking, every declared anchor must carry +# its old-form parameter value verbatim and must be flagged as NOT estimated, +# while every non-anchor new-form parameter must remain free to move onto the +# fixed base scale. +# +# This complements test-fixed-parameter-calibration.R with an independent, +# differently-parameterized scenario (distinct seed, item counts, and anchor +# set) so the invariant is guarded by more than a single fixture. +# +# References (APA 7th): +# Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood estimation of +# item parameters: Application of an EM algorithm. Psychometrika, 46(4), +# 443-459. https://doi.org/10.1007/BF02293801 +# Chalmers, R. P. (2012). mirt: A multidimensional item response theory +# package for the R environment. Journal of Statistical Software, 48(6), +# 1-29. https://doi.org/10.18637/jss.v048.i06 +# Kim, S. (2006). A comparative study of IRT fixed parameter calibration +# methods. Journal of Educational Measurement, 43(4), 355-381. +# https://doi.org/10.1111/j.1745-3984.2006.00021.x + +test_that("autoFIPC holds anchor parameters at their old-form values (Kim, 2006)", { + skip_on_cran() + skip_if_not_installed("mirt") + + set.seed(20260729) + + old_item_names <- paste0("old_", 1:8) + new_item_names <- paste0("new_", 1:8) + old_common_items <- old_item_names[1:5] + new_common_items <- new_item_names[1:5] + + # Common items (1:5) share identical true parameters across forms so the base + # scale is genuinely established; unique items (6:8) differ between forms. + common_a <- c(0.91, 1.14, 1.32, 0.78, 1.05) + common_d <- c(-0.95, -0.30, 0.20, 0.75, -0.55) + old_a <- matrix(c(common_a, 0.88, 1.21, 0.69), ncol = 1) + old_d <- c(common_d, 0.40, -0.70, 0.30) + new_a <- matrix(c(common_a, 1.10, 0.83, 1.27), ncol = 1) + new_d <- c(common_d, -0.20, 0.60, -0.45) + + old_data <- as.data.frame(mirt::simdata( + a = old_a, d = old_d, + itemtype = rep("2PL", length(old_item_names)), N = 1500 + )) + new_data <- as.data.frame(mirt::simdata( + a = new_a, d = new_d, + itemtype = rep("2PL", length(new_item_names)), N = 1500 + )) + names(old_data) <- old_item_names + names(new_data) <- new_item_names + + old_model <- mirt::mirt( + old_data, 1, itemtype = "2PL", method = "EM", SE = TRUE, + verbose = FALSE, technical = list(NCYCLES = 500) + ) + new_model <- mirt::mirt( + new_data, 1, itemtype = "2PL", method = "EM", SE = TRUE, + verbose = FALSE, technical = list(NCYCLES = 500) + ) + + linked <- aFIPC::autoFIPC( + newformXData = new_model, + oldformYData = old_model, + newformCommonItemNames = new_common_items, + oldformCommonItemNames = old_common_items, + itemtype = "2PL", + checkIPD = FALSE, + tryEM = TRUE, + freeMEAN = FALSE, + forceNormalZeroOne = TRUE, + confirmCommonItems = TRUE + ) + + old_values <- mirt::mod2values(old_model) + linked_values <- mirt::mod2values(linked$LinkedModel) + + # Invariant, first half: each anchor keeps its OLD-form value and is fixed. + for (i in seq_along(old_common_items)) { + old_fixed <- old_values[ + old_values$item == old_common_items[i] & + old_values$name %in% c("a1", "d"), + c("name", "value") + ] + linked_fixed <- linked_values[ + linked_values$item == new_common_items[i] & + linked_values$name %in% c("a1", "d"), + c("name", "value", "est") + ] + expect_equal(linked_fixed$name, old_fixed$name) + expect_equal(linked_fixed$value, old_fixed$value, tolerance = 1e-6) + expect_false(any(linked_fixed$est)) + } + + # Invariant, second half: non-anchor new-form parameters stay free so they + # are estimated onto the fixed base scale (Kim, 2006). + new_unique_items <- setdiff(new_item_names, new_common_items) + free_new <- linked_values[ + linked_values$item %in% new_unique_items & + linked_values$name %in% c("a1", "d"), + "est" + ] + expect_true(all(free_new)) +}) diff --git a/tests/testthat/test-se-hessian-vcov-preservation.R b/tests/testthat/test-se-hessian-vcov-preservation.R new file mode 100644 index 0000000..9382888 --- /dev/null +++ b/tests/testthat/test-se-hessian-vcov-preservation.R @@ -0,0 +1,112 @@ +# Directive item 2 — SE / Hessian / vcov / secondordertest preservation. +# +# When a mirt fit is requested with SE = TRUE, marginal maximum likelihood +# estimation (Bock & Aitkin, 1981) returns not only point estimates but also +# the observed-information matrix at the solution. mirt exposes its inverse as +# the parameter covariance matrix (vcov) and reports whether the estimate is a +# proper interior maximum via the second-order optimality test +# (extract.mirt(fit, "secondordertest")) — a positive-definite information +# matrix at the optimum (Cai, 2010, for the MH-RM standard-error machinery that +# generalizes this to high dimensions). aFIPC must not degrade this evidence: +# the old-form, new-form, and LINKED models it returns must each keep a usable, +# finite, positive-definite vcov and a passing second-order test, so downstream +# consumers can still form standard errors on the linked scale. +# +# This test asserts, for all three returned models: +# * a non-empty vcov whose entries are finite and symmetric; +# * strictly positive eigenvalues (positive definiteness => invertible +# information matrix => usable standard errors); +# * extract.mirt(fit, "secondordertest") == TRUE (retained, not dropped). +# +# References (APA 7th): +# Bock, R. D., & Aitkin, M. (1981). Marginal maximum likelihood estimation of +# item parameters: Application of an EM algorithm. Psychometrika, 46(4), +# 443-459. https://doi.org/10.1007/BF02293801 +# Cai, L. (2010). High-dimensional exploratory item factor analysis by a +# Metropolis-Hastings Robbins-Monro algorithm. Psychometrika, 75(1), 33-57. +# https://doi.org/10.1007/s11336-009-9136-x +# Chalmers, R. P. (2012). mirt: A multidimensional item response theory +# package for the R environment. Journal of Statistical Software, 48(6), +# 1-29. https://doi.org/10.18637/jss.v048.i06 +# Kim, S. (2006). A comparative study of IRT fixed parameter calibration +# methods. Journal of Educational Measurement, 43(4), 355-381. +# https://doi.org/10.1111/j.1745-3984.2006.00021.x + +test_that("SE=TRUE information matrix and second-order test survive linking", { + skip_on_cran() + skip_if_not_installed("mirt") + + set.seed(20260730) + + old_item_names <- paste0("old_", 1:6) + new_item_names <- paste0("new_", 1:6) + old_common_items <- old_item_names[1:4] + new_common_items <- new_item_names[1:4] + + common_a <- c(1.02, 0.86, 1.29, 0.94) + common_d <- c(-0.80, -0.20, 0.35, 0.70) + old_a <- matrix(c(common_a, 1.11, 0.77), ncol = 1) + old_d <- c(common_d, -0.45, 0.55) + new_a <- matrix(c(common_a, 0.83, 1.24), ncol = 1) + new_d <- c(common_d, 0.25, -0.60) + + old_data <- as.data.frame(mirt::simdata( + a = old_a, d = old_d, + itemtype = rep("2PL", length(old_item_names)), N = 1500 + )) + new_data <- as.data.frame(mirt::simdata( + a = new_a, d = new_d, + itemtype = rep("2PL", length(new_item_names)), N = 1500 + )) + names(old_data) <- old_item_names + names(new_data) <- new_item_names + + old_model <- mirt::mirt( + old_data, 1, itemtype = "2PL", method = "EM", SE = TRUE, + verbose = FALSE, technical = list(NCYCLES = 500) + ) + new_model <- mirt::mirt( + new_data, 1, itemtype = "2PL", method = "EM", SE = TRUE, + verbose = FALSE, technical = list(NCYCLES = 500) + ) + + linked <- aFIPC::autoFIPC( + newformXData = new_model, + oldformYData = old_model, + newformCommonItemNames = new_common_items, + oldformCommonItemNames = old_common_items, + itemtype = "2PL", + checkIPD = FALSE, + tryEM = TRUE, + freeMEAN = FALSE, + forceNormalZeroOne = TRUE, + confirmCommonItems = TRUE + ) + + # Shared assertion helper: a usable, positive-definite information/vcov plus a + # retained, passing second-order optimality test. + expect_usable_information <- function(fit, label) { + # The @vcov slot holds the parameter covariance matrix (inverse of the + # observed-information matrix) when SE = TRUE; a 1x1 NA placeholder when not. + vc <- as.matrix(fit@vcov) + expect_gt(nrow(vc), 0) + expect_true(all(is.finite(vc)), + info = paste(label, "vcov must be entirely finite")) + expect_true(isSymmetric(unname(vc), tol = 1e-6), + info = paste(label, "vcov must be symmetric")) + + eigenvalues <- eigen(vc, symmetric = TRUE, only.values = TRUE)$values + # Positive definite <=> all eigenvalues > 0 <=> the observed-information + # matrix at the optimum is invertible, so standard errors exist. + expect_true(all(eigenvalues > 0), + info = paste(label, "vcov must be positive definite")) + + soc <- mirt::extract.mirt(fit, "secondordertest") + expect_true(isTRUE(soc), + info = paste(label, "second-order test must be retained/TRUE")) + } + + expect_usable_information(linked$oldFormModel, "old form") + expect_usable_information(linked$newFormModel, "new form") + expect_usable_information(linked$LinkedModel, "linked form") +})