fix: EP projection raises ProjectionException on non-finite sufficient statistics; factor_step recovers (#1653) - #1655
Merged
Conversation
…— direct project cases + factor_step recovery Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Up1fQmjtGAh4RhLrWHfcfg
…ufficient statistics; factor_step recovers (PyAutoFit#1653) AbstractMessage.project ended on a bare `assert np.isfinite(suff_stats).all()`. AssertionError is not in factor_step's recovery tuple, so one factor's bad projection killed a whole EP run (RAL 342411: ic50 ladder, N=50, sweep 4, global factor), with no hint of which input was non-finite. - exc.ProjectionException(ValueError) replaces the assert. The new _nonfinite_projection_reason classmethod (failure path only) names the cause: non-finite samples (counts, first index), nan / +inf log weights, all log weights -inf, or overflow of T(x)·w (max |x|). Moment maths is unchanged. - Prior.project re-raises with the prior id and type (the id does not reach AbstractMessage.project through TransformedMessage.project), and Result.projected_model re-raises with the parameter path (loop instead of a dict comprehension; the zero-weight guard stays). - factor_step lists exc.ProjectionException in its except tuple (already covered via ValueError; listed for intent): a non-finite projection of one factor's samples is a failed sweep update, not a failed graph fit. It gets StatusFlag.EXCEPTION, keeps the previous message, and counts toward max_consecutive_failures. AssertionError is deliberately not caught. Why not MessageException: mean_field.py, hierarchical.py and laplace/newton.py turn that into a silent revert or a -inf likelihood. Why not SearchException: it is uncaught and means a misconfigured search. Behaviour change: outside EP, Result.projected_model callers now get a clear ProjectionException (a ValueError) where they got AssertionError; no autofit/ code catches AssertionError. Under `python -O` the old assert vanished and the non-finite statistics flowed on silently. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Up1fQmjtGAh4RhLrWHfcfg
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.
Summary
AbstractMessage.projectguarded its importance-weighted sufficient statistics with a bareassert np.isfinite(suff_stats).all(). One factor whose samples or log weights went non-finite (nan/inf samples, nan/+inf log weights, all weights zero, or overflow ofT(x)·w) therefore raised a context-freeAssertionError. That error escaped EP'sfactor_steprecovery tuple and killed the whole graph fit. Underpython -Othe check vanished and the non-finite message went through silently.This PR replaces the assert with a typed
autofit.exc.ProjectionException(ValueError). The message says which input caused the failure, and the prior id and model parameter path are added on the way up. EP'sfactor_stepnow recovers from it the way it already recovers fromInitializerException: the factor keeps its previous message for that sweep withStatusFlag.EXCEPTION, andmax_consecutive_failuresstill aborts a factor that keeps failing.API Changes
autofit.exc.ProjectionException, a subclass ofValueError.AbstractMessage.project,Prior.projectandResult.projected_modelraise it where the bareassertused to fire. The message names the failing input (non-finite samples / nan or +inf log weights / all -inf log weights / weighted-moment overflow), the prior id (Prior.project) and the parameter path (Result.projected_model). The check also survivespython -O.factor_steplists it in its recovery tuple, so one bad projection becomes a failed sweep update, not a failed graph fit.Result.projected_modelnow get aValueErrorsubclass instead ofAssertionError. Nothing inautofit/catchesAssertionError.See full details below.
Test Plan
AssertionErroratautofit/messages/abstract.py:316. That covers 6 parametrised direct cases, 2 prior-id cases, thefactor_step/EPOptimiser.runrecovery tests and theprojected_modelpath case.test_autofit/messages/test_project_nonfinite.py+test_autofit/graphical/functionality/test_factor_failure_recovery.pygive 29 passed;test_autofit/graphical/test_unification.pygives 34 passed.origin/main:python -m pytest test_autofit -x, 2946 passed, 2 skipped.PYAUTO_TEST_MODE=2,PYAUTO_DISABLE_JAX=1) against this branch, autofit_workspace_test:graphical/analytic_ep_minimal.pyPASS (MINIMAL EP SELF-TESTS: PASS, 2 s).graphical/analytic_gaussian_collapse.pyPASS (COLLAPSE CONFIG: PASS 5/5 seeds, 18 s).Full API Changes (for automation & release notes)
Added
autofit.exc.ProjectionException(ValueError): non-finite sufficient statistics in an importance-weighted message projection. Deliberately not aMessageException(callers turn those into silent reverts or -inf likelihoods) and not aSearchException(misconfigured search).AbstractMessage._nonfinite_projection_reason(samples, log_weight_list, suff_stats)(private classmethod). Called only on the failure path, it reports the first cause in propagation order.Changed Behaviour
autofit.messages.abstract.AbstractMessage.project: raisesProjectionExceptioninstead ofasserton non-finite sufficient statistics.autofit.mapper.prior.abstract.Prior.project: re-raisesProjectionExceptionwithprior id=<id> (<PriorClass>)context (from e).autofit.non_linear.result.Result.projected_model: re-raisesProjectionExceptionwithpath=<a.b.c>context (from e). Callers getValueErrorrather thanAssertionError.autofit.graphical.expectation_propagation.optimiser.factor_step:exc.ProjectionExceptionadded to the recovery tuple. The result is the previous message, aStatusFlag.EXCEPTIONstatus, and a logged exception.Migration
AssertionErrorfromprojected_model/projectshould catchValueError(orautofit.exc.ProjectionException). No such caller exists inautofit/.Generated by the PyAutoLabs agent workflow.
Heart RED override (development only)
--json, ts 2026-09-30T09:51:49Z, re-read ~11:45 BST unchanged): "PyAutoArray: 2 commit(s) behind origin"; "PyAutoLens: 2 commit(s) behind origin"; "release validation FAILED (stage integrate)". Yellow: "workspace validation not passing (0 failed, 1 timeout, cloud#36404726969: autolens_test scripts/multi_dataset/rectangular.py)"; "manifest drift: public front-door organ tables (generated) — 1 mismatch(es) vs PyAutoMind/repos.yaml".origin/mainclean;pytest test_autofit -x2946 passed, 2 skipped; red witness confirmed on the old source; graphical smoke 2/2 PASS./prm) and green required checks.🤖 Generated with Claude Code
https://claude.ai/code/session_01Up1fQmjtGAh4RhLrWHfcfg