Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,10 @@

- Bound `git rev-parse` in the release evidence index builder with a fail-closed timeout.

#### ATA constraint-map validation trust boundary

- Validate ATA content-constraint keys/counts, exposure maps, seed, and exposure_max as admitted types before item-information evaluation, rejecting hostile string/integer conversion callbacks while preserving accepted Python/NumPy string keys and exact integers.

#### Fit-statistics require compiled Rust core

- Public `chi2_sf` and `benjamini_hochberg` fail closed with a stable RuntimeError when the compiled Rust core is unavailable, preventing silent pure-Python numerical ownership.
Expand Down Expand Up @@ -457,6 +461,10 @@

- Bound LSR/I-LSR ranking CSR materialization (`MAX_RANKING_CSR_BYTES`, per-ranking `n+1` cap) and redact ordinary iterable failures at the Python validation boundary.

#### ATA constraint-map validation trust boundary

- Keep invalid ATA semantic controls on a stable package-owned error surface rather than allowing arbitrary `__str__`/`__int__`/`__index__` callbacks during constraint-map coercion.

#### Descriptor-safe bounded JSON input for automation scripts

- Consolidated governed automation JSON readers behind a descriptor-safe shared
Expand Down
16 changes: 9 additions & 7 deletions crates/mlsirm-core/src/inference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,19 +215,21 @@ mod tests {

#[test]
fn standard_errors_preserve_nonfinite_diagonals() {
// row-major 5x5 with targeted diagonal entries
let mut v = vec![0.0_f64; 25];
// row-major 6x6 with targeted diagonal entries
let mut v = vec![0.0_f64; 36];
v[0] = 4.0; // SE=2
v[6] = 0.0; // SE=0
v[12] = -1.0; // clamp to 0
v[18] = f64::NAN; // preserve NaN
v[24] = f64::INFINITY; // preserve +inf
let se = standard_errors_from_vcov(&v, 5).unwrap();
v[7] = 0.0; // SE=0
v[14] = -1.0; // clamp to 0
v[21] = f64::NAN; // preserve NaN
v[28] = f64::INFINITY; // preserve +inf
v[35] = f64::NEG_INFINITY; // preserve -inf
let se = standard_errors_from_vcov(&v, 6).unwrap();
assert!((se[0] - 2.0).abs() < 1e-15);
assert_eq!(se[1], 0.0);
assert_eq!(se[2], 0.0);
assert!(se[3].is_nan());
assert!(se[4].is_infinite() && se[4].is_sign_positive());
assert!(se[5].is_infinite() && se[5].is_sign_negative());
}

#[test]
Expand Down
21 changes: 17 additions & 4 deletions docs/doctoring/inference_nonfinite_uncertainty.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,26 @@
After observed-information inversion, diagonal elements of a covariance matrix may be non-finite when the numeric path encounters undefined or unbounded curvature. Mapping those diagonals to zero would report perfect certainty and is scientifically false. The Rust-owned `standard_errors_from_vcov` therefore:

1. returns `sqrt(d)` for finite positive `d`;
2. clamps finite non-positive `d` to `0.0` (numerical noise / negative curvature residue); and
3. preserves `NaN` and signed infinities unchanged.
2. clamps finite non-positive `d` to `0.0` only as the repository's existing compatibility treatment for negative roundoff/curvature residue that cannot produce a real-valued standard error; and
3. preserves `NaN`, positive infinity, and negative infinity unchanged.

Non-finite Hessian entries fail closed before inversion.
Non-finite Hessian entries fail closed before inversion. Preserving non-finite sentinels is an explicit repository safety and representation decision; the measurement/statistical sources below motivate truthful uncertainty reporting and ordinary finite-positive standard-error semantics, while Rust's primary `f64` documentation establishes the implementation-level availability of distinct `NaN` and signed-infinity values.

## Source-specific rationale

- **AERA/APA/NCME Standards.** The Standards govern technically sound score interpretation and appropriate communication of measurement limitations. They support the no-false-precision invariant: undefined or unbounded uncertainty must remain observable rather than being rendered as exact zero uncertainty.
- **NIST/SEMATECH uncertainty guidance.** NIST defines standard uncertainty through root-sum-of-squares combination of standard-deviation components and expanded uncertainty through a coverage multiplier. This supports square-root-based finite-positive uncertainty semantics and the requirement to distinguish meaningful finite uncertainty from an undefined or unbounded state.
- **Rust `f64` primary documentation.** Rust exposes distinct `NAN`, `INFINITY`, and `NEG_INFINITY` constants and classification operations such as `is_nan` and `is_infinite`. The implementation therefore tests and preserves both infinity signs rather than collapsing them into one finite sentinel.
- **Statistical inference reference.** Covariance-derived standard errors use the square root of a meaningful finite variance. An undefined or unbounded covariance diagonal cannot truthfully be interpreted as zero standard error.

Changing the current finite-negative-diagonal compatibility rule from clamping to fail-closed behavior would be a separate scientific/API decision and requires its own test-first review; this document does not silently change that contract.

## References

American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association.
American Educational Research Association, American Psychological Association, & National Council on Measurement in Education. (2014). *Standards for educational and psychological testing*. American Educational Research Association. https://www.aera.net/Publications/Books/Standards-for-Educational-Psychological-Testing-2014-Edition

Casella, G., & Berger, R. L. (2002). *Statistical inference* (2nd ed.). Duxbury.

National Institute of Standards and Technology. (n.d.). *Standard and expanded uncertainties*. In *NIST/SEMATECH e-Handbook of statistical methods*. https://www.itl.nist.gov/div898/handbook/mpc/section5/mpc57.htm

The Rust Project Developers. (2026). *Primitive type `f64`*. Rust documentation. https://doc.rust-lang.org/stable/core/primitive.f64.html
8 changes: 6 additions & 2 deletions tests/test_inference_nonfinite_uncertainty.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,29 @@

def test_standard_errors_preserve_invalid_and_infinite_uncertainty() -> None:
"""Undefined/infinite covariance must never be converted into zero uncertainty."""
vcov = np.diag(np.array([4.0, 0.0, -1.0, np.nan, np.inf], dtype=np.float64))
vcov = np.diag(
np.array([4.0, 0.0, -1.0, np.nan, np.inf, -np.inf], dtype=np.float64)
)

result = standard_errors_from_vcov(vcov)

assert np.array_equal(result[:3], np.array([2.0, 0.0, 0.0]))
assert np.isnan(result[3])
assert np.isposinf(result[4])
assert np.isneginf(result[5])


def test_direct_rust_standard_errors_match_public_nonfinite_semantics() -> None:
"""Rust and public wrappers must agree on non-finite uncertainty semantics."""
vcov = np.diag(np.array([1.0, np.nan, np.inf], dtype=np.float64))
vcov = np.diag(np.array([1.0, np.nan, np.inf, -np.inf], dtype=np.float64))

rust = np.asarray(core.standard_errors_from_vcov(vcov), dtype=np.float64)
public = standard_errors_from_vcov(vcov)

assert np.array_equal(rust[:1], public[:1])
assert np.isnan(rust[1]) and np.isnan(public[1])
assert np.isposinf(rust[2]) and np.isposinf(public[2])
assert np.isneginf(rust[3]) and np.isneginf(public[3])


@pytest.mark.parametrize(
Expand Down
Loading