fix(test): seed the flaky interpolator covariance tests - #1451
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1450.
Makes
test_autofit/interpolator/test_covariance.pydeterministic. This is theClass 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_maxcallfixture caps the search atmaxcall=1):abs=5.0(the assertion)abs=4.0abs=3.0Recovered 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.0boundary. Soabs=5.0iscorrectly calibrated and should not be loosened.
limit_maxcall(added with #1386 for speed) caps every search in this module ata 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) arebit-identical; all the variance is inside
search.fit(...).Seeding
np.randomalone would not have fixed itThree independent generators feed the result:
numpy.random— the twonp.random.random()calls in the test body.Replacing them with a fixed-seed local
Generatorleaves std at 1.95 (of2.05) and the test still fails. Not the dominant term.
randommodule —autofit/non_linear/initializer.py:301draws thesearch's initial unit values with
random.uniform.rstate—NestedSamplerdefaults it tonp.random.Generator(PCG64(None)), seeded from OS entropy and reachable fromneither of the above (
dynesty/dynesty.py:607-608,814).This is the dominant term.
It also fixes a worse sibling
test_single_variablecontains nonp.randomcall at all, yet over 500 runsproduced 500 distinct values and missed its
abs=2.0tolerance18/500 = 3.6% of the time (std 0.88, range 22.26 – 29.66). It depends on
source (3) alone, so seeding
np.randomwould never have touched it.Ruled out: the
scipy.linalg.LinAlgErrorguards ontest_interpolate/test_relationships/test_interpolate_other_field/test_linear_analysis_for_value(e29c69ef2) are not this nondeterminism indisguise. The
interpolatorfixture inconftest.pyuses no RNG; its covariancematrix 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
import randomand a module-levelSEED = 20260802.seed_search_randomness(monkeypatch)fixture besidelimit_maxcall, which patchesdynesty.dynesty.get_random_generator(thebinding both
NestedSamplerandDynamicNestedSamplerresolve at call time)and seeds the two global generators, restoring their state in a
finallyafter
yieldso the seeding cannot leak into whatever runs next in the sameprocess.
test_variable_and_constantbuilds its samples from a localnp.random.default_rng(SEED)instead of the globalnp.random.limit_maxcallis unchanged; the module still runs in ~1s.Testing
selection contexts (whole module vs
-kfiltered):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).pytest test_autofit/: 1642 passed, 6 skipped, confirming the statesave/restore leaks nothing.
Could not verify locally
test_autofit/non_linear/search/nest/test_nautilus.py::test__single_core_builds_no_poolfails with
ModuleNotFoundError: No module named 'nautilus'— the optionalsampler 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=1both arefrozen-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 at2.67 s/draw, full → std 0.004 at ~25 s/draw. I left
limit_maxcallalonedeliberately; that trade is yours.
Patching a third-party module from a test is not where this belongs. It is
only necessary because
AbstractDynesty.search_kwargsis a closed dict, sorstatecannot be threaded through even thoughdynestyaccepts it. The samegap means users cannot reproduce a fit. Filed as a follow-up in PyAutoMind
(
draft/feature/autofit/search_seed_reproducibility.md) rather than folded inhere, to keep the release-unblocking change test-only.
🤖 Generated with Claude Code
https://claude.ai/code/session_016vdGab4C5rWumTCXkWHPyh
Generated by Claude Code