test: cover NaN lane-step counter accumulation across a MultiStart resume - #85
Merged
Merged
Conversation
…sume PyAutoFit#1472 added `n_value_nan_lane_steps` / `n_grad_nan_lane_steps`, which restore via `search_internal.get(..., 0)` and are meant to be lifetime totals across a resume. That could not be demonstrated end-to-end until PyAutoFit#1474: `Fitness.check_log_likelihood` compared a stored log likelihood against the search's own figure of merit, so every multi-start resume raised a `SearchException` before reaching the fit loop. #1472 therefore shipped with the resume accumulation covered only by unit tests over hand-built `search_internal` dicts. This is the end-to-end cover. The script kills a `MultiStartAdam` mid-run as a subprocess and resumes it, asserting the counters match an uninterrupted reference run exactly. Equality is the load-bearing assertion, not "the counters went up": simulating the regression (resetting both counters on resume) loses the lane-step recorded before the kill and yields 402 against the reference's 403, which a `>=` check would happily accept. Both NaN traps sit on the descent path rather than at the edges of the prior, because `_broad_starts` rejects any draw whose objective or gradient is non-finite — every lane starts healthy by construction, so a trap at the prior edges is never reached. The gradient-NaN trap uses the `where`/`sqrt` pattern documented in `Fitness.call`, giving a finite value with a NaN derivative. A guard rejects a vacuous pass: the reference run must actually produce NaN lane-steps, and the kill must land before the search finishes. Not added to smoke_tests.txt — it is `real_search jax` like the sibling MultiStartResurrect.py, too slow for the smoke gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JNbe6eLQbxUtY52EGqej5o
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.
End-to-end cover for the value-NaN / gradient-NaN step counters added in PyAutoFit#1472 (
n_value_nan_lane_steps/n_grad_nan_lane_steps).Those counters restore via
search_internal.get(..., 0)and are meant to be lifetime totals across a resume. That could not be demonstrated end-to-end until PyAutoFit#1474:Fitness.check_log_likelihoodcompared a stored log likelihood against the search's own figure of merit, so every multi-start resume raised aSearchExceptionbefore reaching the fit loop. #1472 therefore shipped with the resume accumulation covered only by unit tests over hand-builtsearch_internaldicts.Result: no bug — the counters accumulate correctly. This locks that in.
What the script does
Kills a
MultiStartAdammid-run as a subprocess (iterations_per_full_update=2so checkpoints are written during the run) and resumes it by re-invoking the identical search, then compares against an uninterrupted reference run.Why equality is the assertion
The load-bearing invariant is equality with the uninterrupted reference, not "the counters went up". The search is deterministic (broad starts seeded 0), so the reference is exact.
This is not a stylistic choice. Simulating the regression — resetting both counters on resume — loses the lane-step recorded before the kill and yields 402 against the reference's 403. A
>=assertion accepts that happily; equality catches it. The weaker>=checks are kept alongside only so a failure reports which way it broke.Verified by injecting that regression into PyAutoFit locally, confirming this script fails, then restoring.
Why both traps sit on the descent path
_broad_startsrejects any draw whose objective or gradient is non-finite, so every lane begins healthy by construction and a NaN trap placed at the edges of the prior is never reached — early attempts reported counters of exactly 0 for this reason. Both traps are therefore positioned where descent actually runs into them, which is also the realistic case (a pixelized likelihood going degenerate near its solution):|centre - 50| < 2, the truth basin. Uses thewhere/sqrtpattern documented inFitness.call: inside the band the selected branch is a finite0.0while the unselectedsqrtof a negative is NaN, and reverse-mode gives0 * NaN = NaN. Value stays finite, only the gradient dies.sigma < 10.5. Truth sigma is 10.0, so surviving starts descend across the threshold and die there.resurrect=Falseso dead lanes stay dead and keep counting every step, making the totals grow monotonically.Guards against a vacuous pass
at_kill["total_steps"] < reference["total_steps"])Notes
smoke_tests.txt— it isreal_search jaxlike the siblingMultiStartResurrect.py, too slow for the smoke gate.Generated by Claude Code