fix(priors): width-modifier safety + sigma validation agreement#1348
Merged
Conversation
Phase 2 of the #1331 priors/messages batch (Decisions 5 + 2, one PR by design so the width fix and the sigma check land together): - RelativeWidthModifier: sigma = value * abs(mean) (a negative posterior median previously produced a negative sigma that silently flipped the passed prior's scale) + opt-in absolute_floor (config round-trips via from_dict/dict; WidthModifier.__eq__ now compares full dicts) - Prior passing (mapper_from_prior_means, default width-modifier path): a computed width <= 0 (mean=0, relative modifier, no floor) raises a parameter-named PriorException with remediation guidance instead of silently freezing the parameter behind a sigma=0 delta prior. Explicit a=/r= widths keep their historical, test-pinned point-mass semantics. - NormalMessage: reject sigma < 0 (previously constructed silently with deceptive variance = sigma^2 > 0); the broken jax.lax.cond assert branch is retired (JAX defers to NaN propagation). sigma == 0 stays permitted: it is the established point-mass idiom (latent variables' simple_model_for_kwargs, from_mode(covariance=0), and model_centred_relative at mean=0 all depend on it) — evidence-based adjustment from the strict #1331 rec, disclosed on #1346. - TruncatedNormalMessage: unchanged sigma < 0 rejection with a clearer message — the two classes now agree, which was the actual defect. Adds test_prior_width_safety.py (11 regression tests incl. the mean=0 chained-parameter gate). Full suite: 1447 passed, 14 skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 10, 2026
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
Phase 2 of the #1331 priors/messages batch (tracker #1346; Phase 1 merged via #1345) — the behaviour-changing width-modifier pair, shipped as one PR by design so the width fix and the sigma check land together.
RelativeWidthModifiernow returnsvalue * abs(mean)with an opt-in, config-round-trippingabsolute_floor; the default prior-passing path raises a parameter-namedPriorExceptionwith remediation guidance when a computed width is still ≤ 0 (previously: a silent σ=0 delta prior froze the parameter); andNormalMessagefinally rejects negative sigma, agreeing withTruncatedNormalMessage— the actual defect behind Decision 2.One evidence-based deviation from the #1331 rec, disclosed in detail on #1346: strict σ ≤ 0 rejection broke 20 tests across three subsystems that deliberately use σ = 0 as a point-mass idiom (latent variables'
simple_model_for_kwargs,from_mode(covariance=0),model_centred_relativepinningsigma == 0atmean=0). This PR ships the permissive variant #1331 itself listed as the alternative: σ < 0 rejected, σ = 0 permitted and documented. If strict is still wanted, the prerequisite is a first-class point-mass representation for the latent machinery — separate design prompt on request.API Changes
Behaviour changes on existing public symbols — nothing removed, one optional argument added.
RelativeWidthModifiergainsabsolute_floor=; its width is nowvalue * abs(mean)(wasvalue * mean, which went negative with the median). Default-path prior passing raises on non-positive computed widths instead of silently freezing the parameter.NormalMessage/GaussianPriorreject σ < 0 at construction (previously silent with sign-flippedvalue_for).See full details below.
Test Plan
test_autofit/suite: 1447 passed, 14 skippedtest_autofit/mapper/prior/test_prior_width_safety.py— 11 regression tests:abs(mean), floor engagement + config round-trip, σ<0 rejection on both classes, σ=0 point-mass preserved, and themean=0chained-parameter gate (clear error without floor / correct width with floor / positive width for negative medians)from_mode,model_relative— 67/67)Validation checklist (--auto run — plan on the issue; D2 scope adjusted mid-run, disclosed)
Full API Changes (for automation & release notes)
Added
RelativeWidthModifier(value, absolute_floor=None)— optional minimum passed-prior width; engages whenvalue * abs(mean)falls below it; round-trips through thewidth_modifierpriors-config entry (absolute_floorkey) andWidthModifier.from_dict/.dictChanged Behaviour
RelativeWidthModifier.__call__(mean)— returnsvalue * abs(mean)(wasvalue * mean). Chained fits with negative posterior medians previously received a negative sigma that constructed silently and sign-flippedvalue_for/sample; they now receive the correct positive width. This changes sampling of affected chained fits — correctly.AbstractPriorModel.mapper_from_prior_means(default width-modifier path only) — a computed width ≤ 0 (posterior median 0, relative modifier, no floor) raisesexc.PriorExceptionnaming the parameter path and pointing atAbsoluteWidthModifier/absolute_floorremediation. Previously the parameter was silently frozen behind a σ=0 delta prior. Explicita=/r=widths are exempt and keep their historical point-mass semantics (pinned bytest_model_relative).NormalMessage.__init__(⇒GaussianPrior) — rejects σ < 0 withexc.MessageException(previously constructed silently with deceptivevariance = σ² > 0). σ = 0 remains permitted as the documented point-mass idiom. The deadassert_sigma_non_negativejax.lax.condbranch (traced both branches, never raised underjit) is retired; the JAX path defers to NaN propagation.TruncatedNormalMessage.__init__— same σ < 0 rejection as before with a clearer message; the two classes now agree.WidthModifier.__eq__— compares full.dict(so modifiers differing only inabsolute_floorare unequal).Migration
mean=0parameter on the default width path now fail loudly with guidance — set anAbsoluteWidthModifierorabsolute_floorfor that parameter.🤖 Generated with Claude Code