Skip to content

Red main: the stage-3 Oakes mirt oracle is stale, not the estimator #2024

Description

@seonghobae

cargo test on main has failed continuously since 2026-09-17T04:08Z. Every PR in this repository inherits the red rust job. One test is responsible:

crates/mlsirm-core/tests/bifactor_oakes_mirt.rs:178
rust_oakes_se_matches_mirt_oakes_fixture ... FAILED
info at mirt's converged MLE must be PD; reason:
  observed information is not positive definite (cholesky pivot 29 = -1.623e1 <= 1e-12)

This is not an estimator defect. The Rust Oakes assembly is correct; the committed oracle is stale.

What the evidence shows

The failure is deterministic, not numerical. Sweeping fd_step over three orders of magnitude and quadrature over 15/31/61 nodes moves the negative pivot only in the third digit:

fd_step q=15 q=31 q=61
1e-3 -1.597e1 -1.598e1 -1.599e1
1e-4 -1.621e1 -1.623e1 -1.623e1
1e-6 -1.623e1 -1.625e1 -1.625e1

So it is neither finite-difference noise nor quadrature error. The next check explains it: the fixture point is not a stationary point of our likelihood.

point our marginal loglik worst |∂loglik| information PD
mirt_d as committed -8304.664 64.04 no
-d, reversed within each item -8192.986 0.0875 yes

The fixture records mirt's own converged loglik as -8192.99570. Our value at the transformed point is -8192.986 — the 0.0095 residual is the quadpts 15 → 61 difference. At the point the test actually passes in, we are 112 nats away from mirt's optimum.

(y reversed, d as-is) and (y as-is, -d reversed) both give -8192.9862, which is the exact category-reversal symmetry of the graded model: reversing category coding maps d_k → -d_{K-k}. The empirical marginals break the tie. For items whose middle threshold is not near-symmetric, the sign matches the transformed vector, not the committed one:

item empirical logit P(Y≥2) committed d2 -d reversed
0 +0.035 -0.049 +0.049
5 +0.211 -0.423 +0.423
7 -0.030 +0.048 -0.048

The oracle was fitted on category-reversed data relative to the dataset the test now reads.

Root cause

  • f7cd5450 (09-17 06:27) generated tests/fixtures/bifactor_grm_stage3_oakes/mirt_oakes_fixture.json from the stage-1 dataset as it stood then, which the generator's own "Orientation note" documents as category-reversed.
  • 0781051b (09-17 11:47, five hours later) rewrote tests/fixtures/bifactor_grm_stage1/dataset.csv (1570 lines) to correct that reversal, and regenerated bifactor_grm_stage1/mirt_fixture.json — but not the stage-3 Oakes fixture. 0781051b is not an ancestor of f7cd5450; the two landed on separate lines.

The generator's note reasoned that "both estimators fit the same committed data, so the implementation comparison is unaffected". That was true when it was written. Correcting the dataset underneath the oracle invalidated it, and nothing re-ran the R script.

Fix

Re-run the oracle against the corrected dataset:

Rscript tests/fixtures/bifactor_grm_stage3_oakes/generate_mirt_oakes_fixture.R

with R 4.x and mirt 1.46.1, as the script header pins. The regenerated mirt_d, mirt_se_* and mirt_vcov should land near the transformed values above, and the PD assertion, the 5% SE band and the 10% vcov band should all hold unchanged.

Please do not patch the test by negating and reversing mirt_d in Rust. The identity d_k → -d_{K-k} happens to reproduce the right point here, but baking it into the test would hard-code a data-orientation accident as if it were a parameterization convention, and the next fixture regeneration would silently break it. crates/mlsirm-core/src/bifactor_grm.rs:12 and crates/mlsirm-core/src/poly.rs:129 both use P(Y≥k) = logistic(aθ + d_k) — the same intercept convention as mirt — and that is correct as written.

While regenerating, check whether tests/fixtures/two_tier_grm_oakes/ shares the exposure: it reads the stage-4 dataset, a separate lineage, and its test currently passes, but it was produced by the same pattern.

I could not regenerate it here — no R toolchain on this host.

Why this is urgent rather than cosmetic

main has been red for ~44 hours. Every PR in this repository shows a failing required rust check regardless of its contents, which makes the signal useless and blocks the merge queue.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YHVBDaZS5NZT9aQcbRg9Av

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions