Skip to content

fix(test): seed the flaky interpolator covariance tests - #1451

Merged
Jammy2211 merged 1 commit into
mainfrom
claude/covariance-interpolator-rng-seed-8zexp3
Aug 5, 2026
Merged

fix(test): seed the flaky interpolator covariance tests#1451
Jammy2211 merged 1 commit into
mainfrom
claude/covariance-interpolator-rng-seed-8zexp3

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Closes #1450.

Makes test_autofit/interpolator/test_covariance.py deterministic. This is the
Class B release blocker — the sole reason the 2026.8.2.1 live release failed
(PyAutoHands run 30736527569, release_test_pypi (3.12, PyAutoFit, main) step 9:
1641 passed, 1 failed, assert 30.121646313498022 == 25.0 ± 5).

Test-only. No library source or public API change. One file, +53/−2.

Measured pre-fix failure rate

Measured before changing anything, so seeding would not be applied over a
genuinely wrong tolerance. 2000 independent runs under the same conditions CI
uses (the autouse limit_maxcall fixture caps the search at maxcall=1):

tolerance failures rate 95% CI (Wilson)
abs=5.0 (the assertion) 33 / 2000 1.65% 1.18% – 2.31%
abs=4.0 109 / 2000 5.45% 4.54% – 6.53%
abs=3.0 293 / 2000 14.65% 13.17% – 16.27%

Recovered value: mean 25.24, std 2.05, range 17.82 – 32.04. The observed CI
failure (30.12) sits comfortably inside that distribution — a marginal miss, not
a regression.

The tolerance is fine; the estimator is broken

With a full untruncated search the same test recovers 25.0496 ± 0.0039
(n=20) — 500× tighter and nowhere near the abs=5.0 boundary. So abs=5.0 is
correctly calibrated and should not be loosened.

limit_maxcall (added with #1386 for speed) caps every search in this module at
a single likelihood call, so the recovered value is an unconverged draw whose
spread is set entirely by the search's randomness. Stage-wise check with all
seeds fixed: the interpolator's inputs (x, y, inverse covariance) are
bit-identical; all the variance is inside search.fit(...).

Seeding np.random alone would not have fixed it

Three independent generators feed the result:

  1. numpy.random — the two np.random.random() calls in the test body.
    Replacing them with a fixed-seed local Generator leaves std at 1.95 (of
    2.05) and the test still fails. Not the dominant term.
  2. the stdlib random module — autofit/non_linear/initializer.py:301 draws the
    search's initial unit values with random.uniform.
  3. dynesty's rstateNestedSampler defaults it to
    np.random.Generator(PCG64(None)), seeded from OS entropy and reachable from
    neither of the above (dynesty/dynesty.py:607-608, 814).
    This is the dominant term.

It also fixes a worse sibling

test_single_variable contains no np.random call at all, yet over 500 runs
produced 500 distinct values and missed its abs=2.0 tolerance
18/500 = 3.6% of the time (std 0.88, range 22.26 – 29.66). It depends on
source (3) alone, so seeding np.random would never have touched it.

Ruled out: the scipy.linalg.LinAlgError guards on test_interpolate /
test_relationships / test_interpolate_other_field /
test_linear_analysis_for_value (e29c69ef2) are not this nondeterminism in
disguise. The interpolator fixture in conftest.py uses no RNG; its covariance
matrix is bit-identical across builds and rank-deficient (rank 6 of 9, each
3×3 block rank 2 of 3, condition number 1.3e17). Those guards cover inverting a
singular matrix — deterministic input, platform-LAPACK dependent. Separate issue.

The change

  1. import random and a module-level SEED = 20260802.
  2. A new autouse seed_search_randomness(monkeypatch) fixture beside
    limit_maxcall, which patches dynesty.dynesty.get_random_generator (the
    binding both NestedSampler and DynamicNestedSampler resolve at call time)
    and seeds the two global generators, restoring their state in a finally
    after yield
    so the seeding cannot leak into whatever runs next in the same
    process.
  3. test_variable_and_constant builds its samples from a local
    np.random.default_rng(SEED) instead of the global np.random.

limit_maxcall is unchanged; the module still runs in ~1s.

Testing

  • Recovered values bit-identical across 8 pytest invocations and in both
    selection contexts (whole module vs -k filtered):
    test_single_variable24.577316739353 (dev 0.42 vs tol 2.0);
    test_variable_and_constant22.718814166116 (dev 2.28 vs tol 5.0).
  • Full pytest test_autofit/: 1642 passed, 6 skipped, confirming the state
    save/restore leaks nothing.

Could not verify locally

test_autofit/non_linear/search/nest/test_nautilus.py::test__single_core_builds_no_pool
fails with ModuleNotFoundError: No module named 'nautilus' — the optional
sampler is absent from the sandbox. Pre-existing and unrelated: it reproduces
identically with this change stashed. CI has the optional deps, so it should pass
there.

Reviewer note — two judgement calls left to you

These assertions are now weaker than they look. Under maxcall=1 both are
frozen-RNG regression checks, not accuracy checks: a seeded draw 2.28 away from
the true 25.05 still passes at abs=5.0. Restoring real meaning costs suite time
— measured maxcall=200 → std 1.06 at 0.76 s/draw, maxcall=1000 → std 0.46 at
2.67 s/draw, full → std 0.004 at ~25 s/draw. I left limit_maxcall alone
deliberately; that trade is yours.

Patching a third-party module from a test is not where this belongs. It is
only necessary because AbstractDynesty.search_kwargs is a closed dict, so
rstate cannot be threaded through even though dynesty accepts it. The same
gap means users cannot reproduce a fit. Filed as a follow-up in PyAutoMind
(draft/feature/autofit/search_seed_reproducibility.md) rather than folded in
here, to keep the release-unblocking change test-only.

🤖 Generated with Claude Code

https://claude.ai/code/session_016vdGab4C5rWumTCXkWHPyh


Generated by Claude Code

test_variable_and_constant is non-deterministic and fails ~1 CI run in 60.
It is the sole reason the 2026.8.2.1 live release failed (1641 passed,
1 failed, assert 30.121646313498022 == 25.0 +/- 5).

Measured before changing anything, over 2000 runs under the same conditions
CI uses (the autouse limit_maxcall fixture caps the search at maxcall=1):
33/2000 = 1.65% miss abs=5.0 (95% CI 1.18-2.31%), recovered value
mean 25.24, std 2.05, range 17.82-32.04. The observed 30.12 sits well
inside that distribution.

The tolerance is not the problem. With a full, untruncated search the same
test recovers 25.0496 +/- 0.0039 - 500x tighter. limit_maxcall caps every
search here at one likelihood call, so the recovered value is an
unconverged draw whose spread is set entirely by the search's randomness.
With all seeds fixed the interpolator's inputs (x, y, inverse covariance)
are bit-identical; all the variance is inside search.fit.

Three generators feed it, so seeding any one is insufficient:

  1. numpy.random - the test body's two np.random.random() calls. Seeding
     only these leaves std at 1.95 and still fails. Not dominant.
  2. the stdlib random module - initializer.py draws initial unit values
     with random.uniform.
  3. dynesty's rstate - defaults to Generator(PCG64(None)), OS entropy,
     reachable from neither of the above. Dominant.

Seeding all three from one autouse fixture, restoring global generator
state on teardown so nothing leaks into later tests in the same process.

This also fixes a worse sibling: test_single_variable makes no random call
of its own, yet produced 500 distinct values over 500 runs and missed its
abs=2.0 tolerance 3.6% of the time. It depends on source (3) alone, so
seeding np.random would never have fixed it.

Ruled out: the scipy.linalg.LinAlgError guards on test_interpolate and
friends are not this nondeterminism in disguise. The interpolator fixture
uses no RNG; its covariance matrix is bit-identical across builds and
rank-deficient (rank 6 of 9, cond 1.3e17). Those guards cover inverting a
singular matrix - deterministic input, platform-LAPACK dependent.

Recovered values are now bit-identical across 8 invocations and in both
selection contexts: test_single_variable 24.577316739353 (dev 0.42 vs
tol 2.0), test_variable_and_constant 22.718814166116 (dev 2.28 vs tol 5.0).

Test-only; no library source or public API change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vdGab4C5rWumTCXkWHPyh
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.

fix(test): seed the flaky interpolator covariance tests (1.65% flake blocked 2026.8.2.1)

2 participants