Skip to content

fix(test): degenerate-band sanity check asserts on BLAS rounding, not the library - #438

Merged
Jammy2211 merged 1 commit into
mainfrom
claude/fnnls-degenerate-sanity-assertion
Aug 9, 2026
Merged

fix(test): degenerate-band sanity check asserts on BLAS rounding, not the library#438
Jammy2211 merged 1 commit into
mainfrom
claude/fnnls-degenerate-sanity-assertion

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

main is red. test__fnnls_cholesky__never_returns_a_non_finite_solution[0.0] has been failing on the CI runners since at least efaf3041 (run 31219374045, 2026-08-07) while passing locally.

Nothing is wrong with the library. The failing line is the test's own vacuity guard:

assert raised > 0

Why that assertion cannot hold reliably

It requires fnnls_cholesky to raise LinAlgError for at least one of 40 seeds. But raising is an allowed outcome here, not a required one — and this file already says so, in test__cholinsertlast__singular_insertion_never_yields_an_unusable_pivot:

The invariant is therefore NOT "always raise" (a small positive pivot is still a usable pivot, and rejecting it would change likelihood evaluations). It is: either raise, or return a strictly positive finite pivot.

The module comment goes further and identifies the exact mechanism:

the Cholesky insertion's Schur complement is then zero up to rounding, and which side of zero it lands on depends only on floating-point summation order — i.e. on the BLAS thread count, which is why the original failure was reproducible on CI but not locally.

So raised > 0 asserts on how the runner's BLAS rounds, not on library behaviour.

Measurements

Across the 40 seeds, on a single-threaded local BLAS:

jitter raised solved
0.0 1 39
1e-15 16 24
1e-12 20 20
1e-9 14 26

[0.0] passes locally by a single seed out of 40, and zero seeds raise on the CI runners. That is precisely the one parametrisation CI fails — the other three have comfortable margins.

The fix

The regression this test exists for — never hand back NaN as though it were a valid reconstruction — is untouched. Only the vacuity guard changes.

  • solved > 0 replaces raised > 0: at least one seed must actually have reached the finiteness assertion. That is what "not vacuous" means here, and it does not depend on rounding.
  • "The band really is degenerate" is pinned on the fixture instead, via cond(ZTZ) > 1e12. Measured range is 1.07e16 – 3.71e18 across every seed and jitter value, so the margin is wide and the property is deterministic on any machine.

Verification

Both directions checked by simulation, not just by the suite going green:

  • With no seed raising (the CI condition): all four parametrisations pass.
  • With every seed raising (genuine vacuity — the finiteness assertion never runs): the test still fails, so the guard keeps doing its job.

Full suite: 906 passed, 52 skipped. The 3 test_transformer.py failures are pre-existing and unrelated (missing optional pynufft).

Opened so #437 can merge onto a green base.


Generated by Claude Code

…t the library

`test__fnnls_cholesky__never_returns_a_non_finite_solution[0.0]` fails on the CI
runners while passing locally, and has been red on main since at least
efaf304 (run 31219374045).

The failing line is the test's own vacuity guard:

    assert raised > 0

It requires `fnnls_cholesky` to raise LinAlgError for at least one of 40 seeds.
But raising is an *allowed* outcome here, not a required one — this file already
says so, in
`test__cholinsertlast__singular_insertion_never_yields_an_unusable_pivot`:

    The invariant is therefore NOT "always raise" (a small positive pivot is
    still a usable pivot, and rejecting it would change likelihood
    evaluations). It is: either raise, or return a strictly positive finite
    pivot.

On a singular insertion the Schur complement is zero up to rounding, so which
side of zero it lands on is decided by floating-point summation order — the
module comment says exactly this, and cites it as why the original bug
reproduced on CI but not locally. `raised > 0` therefore asserts on the runner's
BLAS rather than on the library.

Measured across the 40 seeds:

  jitter    raised  solved
  0.0            1      39   <- passes by a single seed locally; 0 on CI
  1e-15         16      24
  1e-12         20      20
  1e-9          14      26

Only [0.0] is marginal, which is exactly the one parametrisation CI fails.

Fix, keeping the regression this test exists for (never hand back NaN as though
it were a valid reconstruction) fully intact:

- The vacuity guard becomes `solved > 0` — at least one seed must actually have
  reached the finiteness assertion. That is what "not vacuous" means here; it is
  independent of how the runner rounds.
- "The band really is degenerate" is now pinned on the fixture instead, via
  `cond(ZTZ) > 1e12`. Measured range is 1.07e16 to 3.71e18 across all seeds and
  jitter values, so the margin is wide and the property is deterministic.

Verified in both directions by simulation: with no seed raising (the CI
condition) all four parametrisations now pass; with every seed raising (genuine
vacuity, finiteness never checked) the test still fails.

Full suite: 906 passed, 52 skipped. The 3 test_transformer failures are
pre-existing and unrelated (missing optional pynufft).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KazMzMZYPLfaZoYQ79YQ8Q
@Jammy2211
Jammy2211 merged commit 007904b into main Aug 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant