Overview
Phase 2 of the #1331 priors/messages batch — the behaviour-changing pair, split from Phase 1 (#1344, merged via #1345) because it alters prior-passing widths in search chaining (SLaM-style pipelines). Verdicts: bug/priors/06 + 08 on #1330; decisions resolved on #1331 (Decision 5: minimal — abs(mean) + optional configurable absolute floor; Decision 2: reject σ ≤ 0 strictly).
Sequencing constraint (from the Phase-1 impact analysis): the width fix (D5) lands with the strict σ check (D2) in one PR so today's silent σ=0 degeneracy (a mean=0 chained parameter freezes into a delta function) never becomes an unexplained hard crash: where a computed width is still ≤ 0, prior passing raises a clear, parameter-named PriorException pointing at the width-modifier config — not a deep MessageException.
Plan
RelativeWidthModifier: sigma = value * abs(mean), plus opt-in absolute_floor (YAML-configurable; engages when value*abs(mean) falls below it). Negative means no longer produce negative sigma (today 0.5 * -1.0 = -0.5 flows silently into GaussianPrior and flips value_for).
- Prior passing (
gaussian_prior_model_for_arguments width application): if the final width ≤ 0 (e.g. mean=0, no floor), raise PriorException naming the parameter path with remediation guidance (set an absolute width modifier or an absolute_floor).
NormalMessage.__init__: enforce σ > 0 (numpy path; JAX defers to NaN propagation — the existing assert_sigma_non_negative helper's lax.cond branch is broken by design and is retired). GaussianPrior(mean, sigma<=0) and GaussianPrior.with_limits(5, 5) become loud.
TruncatedNormalMessage: extend its existing < 0 rejection to <= 0 so the two classes agree (strictly).
- Regression tests:
mean=0 chained parameter (clear error without floor; correct width with floor), abs(mean) on negative means, floor engagement + config round-trip, strict ctor rejection on both message classes.
- Full
test_autofit/ suite; pre-flight grep for tests that deliberately construct σ ≤ 0 messages (they may encode point-mass behaviour — FixedMessage is the sanctioned path for that).
Detailed implementation plan
Affected Repositories
- PyAutoLabs/PyAutoFit (primary)
Branch Survey
| Repository |
Current Branch |
Dirty? |
| ./PyAutoFit |
main (post-#1345, c0b6c94) |
clean |
Suggested branch: feature/prior-width-safety
Parallel disjoint PyAutoFit claim alongside ep-graphical-docs (#1334) + ep-diagnostics (#1335), both autofit/graphical — this work is autofit/mapper/prior/width_modifier.py, autofit/mapper/prior_model/abstract.py (width application site), autofit/messages/{normal,truncated_normal}.py. Zero overlap.
Implementation Steps
autofit/mapper/prior/width_modifier.py — RelativeWidthModifier.__init__(value, absolute_floor=None); __call__ returns max(value * abs(mean), floor) when floor set, else value * abs(mean); from_dict/dict round-trip the optional key.
autofit/mapper/prior_model/abstract.py — after width = width_modifier(mean) (and the explicit r/a paths), raise exc.PriorException naming the prior path if width <= 0.
autofit/messages/normal.py — numpy-path strict sigma > 0 enforcement in NormalMessage.__init__; retire the broken assert_sigma_non_negative JAX branch.
autofit/messages/truncated_normal.py — < 0 → <= 0.
- Tests per the plan bullet above (numpy-only).
Behaviour changes (user-visible, disclosed)
- Chained fits with parameters whose posterior median is negative get a positive passed width (previously negative sigma, silently sign-flipping
value_for). Sampling of affected chained fits changes — for the better.
- Chained fits with a
mean=0 unconfigured parameter now fail loudly with guidance (previously silently froze the parameter via a delta-function prior).
GaussianPrior/NormalMessage with σ ≤ 0 raise at construction.
Autonomy: supervised (--auto, human instructed live 2026-07-10 after Phase-1 merge; heart-ack 6-reason set recorded in active.md this chain).
🤖 Generated with Claude Code
Overview
Phase 2 of the #1331 priors/messages batch — the behaviour-changing pair, split from Phase 1 (#1344, merged via #1345) because it alters prior-passing widths in search chaining (SLaM-style pipelines). Verdicts:
bug/priors/06+08on #1330; decisions resolved on #1331 (Decision 5: minimal —abs(mean)+ optional configurable absolute floor; Decision 2: reject σ ≤ 0 strictly).Sequencing constraint (from the Phase-1 impact analysis): the width fix (D5) lands with the strict σ check (D2) in one PR so today's silent σ=0 degeneracy (a
mean=0chained parameter freezes into a delta function) never becomes an unexplained hard crash: where a computed width is still ≤ 0, prior passing raises a clear, parameter-namedPriorExceptionpointing at the width-modifier config — not a deepMessageException.Plan
RelativeWidthModifier:sigma = value * abs(mean), plus opt-inabsolute_floor(YAML-configurable; engages whenvalue*abs(mean)falls below it). Negative means no longer produce negative sigma (today0.5 * -1.0 = -0.5flows silently intoGaussianPriorand flipsvalue_for).gaussian_prior_model_for_argumentswidth application): if the final width ≤ 0 (e.g.mean=0, no floor), raisePriorExceptionnaming the parameter path with remediation guidance (set an absolute width modifier or anabsolute_floor).NormalMessage.__init__: enforce σ > 0 (numpy path; JAX defers to NaN propagation — the existingassert_sigma_non_negativehelper'slax.condbranch is broken by design and is retired).GaussianPrior(mean, sigma<=0)andGaussianPrior.with_limits(5, 5)become loud.TruncatedNormalMessage: extend its existing< 0rejection to<= 0so the two classes agree (strictly).mean=0chained parameter (clear error without floor; correct width with floor),abs(mean)on negative means, floor engagement + config round-trip, strict ctor rejection on both message classes.test_autofit/suite; pre-flight grep for tests that deliberately construct σ ≤ 0 messages (they may encode point-mass behaviour —FixedMessageis the sanctioned path for that).Detailed implementation plan
Affected Repositories
Branch Survey
Suggested branch:
feature/prior-width-safetyParallel disjoint PyAutoFit claim alongside ep-graphical-docs (#1334) + ep-diagnostics (#1335), both
autofit/graphical— this work isautofit/mapper/prior/width_modifier.py,autofit/mapper/prior_model/abstract.py(width application site),autofit/messages/{normal,truncated_normal}.py. Zero overlap.Implementation Steps
autofit/mapper/prior/width_modifier.py—RelativeWidthModifier.__init__(value, absolute_floor=None);__call__returnsmax(value * abs(mean), floor)when floor set, elsevalue * abs(mean);from_dict/dictround-trip the optional key.autofit/mapper/prior_model/abstract.py— afterwidth = width_modifier(mean)(and the explicitr/apaths), raiseexc.PriorExceptionnaming the prior path ifwidth <= 0.autofit/messages/normal.py— numpy-path strictsigma > 0enforcement inNormalMessage.__init__; retire the brokenassert_sigma_non_negativeJAX branch.autofit/messages/truncated_normal.py—< 0→<= 0.Behaviour changes (user-visible, disclosed)
value_for). Sampling of affected chained fits changes — for the better.mean=0unconfigured parameter now fail loudly with guidance (previously silently froze the parameter via a delta-function prior).GaussianPrior/NormalMessagewith σ ≤ 0 raise at construction.Autonomy: supervised (
--auto, human instructed live 2026-07-10 after Phase-1 merge; heart-ack 6-reason set recorded in active.md this chain).🤖 Generated with Claude Code