fix(priors): correctness batch — crashes, log-partition, projections#1345
Merged
Conversation
…1344) Phase 1 of the #1331 priors/messages fix batch (verdicts on #1330): - LogGaussianPrior.with_limits: drop invalid lower/upper_limit kwargs (crashed on first call); fully repair _new_for_base_message (bad kwargs + stale self.instance().id -> self.id; method had no callers) - UniformPrior.logpdf: vectorise boundary snap with np.where; scalar path bit-identical - TruncatedNormalMessage.log_partition: add the missing untruncated Gaussian cumulant mu^2/2sigma^2 + log(sigma) so the generic exponential-family pdf integrates to 1.0 (was sigma*exp(mu^2/2sigma^2), e.g. 2.27) and the EP product log_norm identity telescopes - FixedMessage.logpdf: remove unbounded class-level cache that returned an aliased mutable array - inv_beta_suffstats: raise ValueError on a negative Newton-Raphson projection (the previous clamp wrote to a dead local — a no-op) - GammaMessage.from_mode: match mean+variance (alpha=m^2/V, beta=m/V); previous alpha=1+m^2*V had the variance inverted - log_prior_from_value: document the drop-constants contract and bring TruncatedNormalMessage into line (was the only fully-normalised one); add Prior.log_normalisation hook recovering the dropped constant on Uniform/LogUniform/Gaussian/LogGaussian/TruncatedGaussian Adds test_priors_messages_fixes_1331.py (12 regression tests) and updates the truncated-Gaussian log_prior test to the new contract. Full suite: 1437 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 1 of the priors/messages correctness batch from decision hub #1331 (re-verified findings on #1330; tracked by #1344). Seven fixes across
autofit/mapper/prior/andautofit/messages/: two crash fixes, the truncated-normal log-partition correction the EP machinery consumes, an aliased-cache removal, a silent-failure projection guard, the Gammafrom_modevariance inversion, and thelog_prior_from_valuenormalisation-contract cleanup. All EP-isolated or numerically inert for standard non-linear searches (see behaviour notes below).Implemented on Opus; statistics independently re-verified on Fable:
TruncatedNormalMessageexp-familylogpdfnow equalsscipy.stats.truncnorm.logpdfpointwise (1e-9) and the EP productlog_normtelescope identity holds numerically (it was broken by the incomplete cumulant);GammaMessage.from_modematches requested mean+variance exactly; all fivelog_normalisationhooks recover the scipy-normalised densities.Resolves the awaiting-input state on #1330/#1331 and unblocks the #1332 EP fix batch. Phase 2 (Decisions 5+2 —
RelativeWidthModifierabs+floor ordered with strict σ>0, plus amean=0chained-parameter regression test) is a separate follow-up, filed as this merges.API Changes
Additive plus behaviour fixes — nothing removed from the public surface.
Added
Prior.log_normalisation()(opt-in hook recovering the additive constant dropped bylog_prior_from_value). Fixed previously-crashing paths (LogGaussianPrior.with_limits, array input toUniformPrior.logpdf). Corrected wrong numerics in EP-facing internals (TruncatedNormalMessage.log_partition,GammaMessage.from_mode, Beta projection now raises instead of silently escaping negative parameters).TruncatedNormalMessage.log_prior_from_valuenow follows the same drop-constants convention as every other prior.See full details below.
Test Plan
test_autofit/suite: 1437 passed, 14 skipped (numpy-only, no JAX)test_autofit/mapper/prior/test_priors_messages_fixes_1331.py— 12 regression tests, one per fixtest_truncated_gaussian.pyupdated to the drop-constants contract + new hook-recovery test vsscipy.truncnorm.logpdflogpdf≡truncnorm.logpdf(3 geometries incl. half-bounded), EP productlog_normidentity vs quadrature, all 5 hooks vs scipy densities/smoke_test) — no workspace scripts touch the changed symbols directlyValidation checklist (--auto run — plan was not pre-approved in Plan Mode; decisions human-resolved on #1331)
Full API Changes (for automation & release notes)
Added
Prior.log_normalisation(xp=np)— base hook returning0.0; the additive constant dropped bylog_prior_from_value, solog_prior_from_value(x) + log_normalisation()is the fully normalised log density (evidence/Bayes-factor users)UniformPrior.log_normalisation()—-log(upper - lower)LogUniformPrior.log_normalisation()—-log(log(upper / lower))GaussianPrior.log_normalisation()—-log(sigma) - 0.5*log(2*pi)LogGaussianPrior.log_normalisation()—-log(sigma) - 0.5*log(2*pi)(the value-dependent-log(x)Jacobian stays inlog_prior_from_value)TruncatedGaussianPrior.log_normalisation()—-log(sigma) - 0.5*log(2*pi) - log(Z)withZthe truncation massChanged Behaviour
LogGaussianPrior.with_limits(lower, upper)— no longer raisesTypeErroron first call (invalidlower_limit/upper_limitctor kwargs dropped; a log-Gaussian has support (0, ∞))LogGaussianPrior._new_for_base_message— repaired (same bad kwargs, plus staleself.instance().id→self.id); previously dead code with no callers, doubly brokenUniformPrior.logpdf(x)— accepts array input (boundary snap vectorised withnp.where); scalar path bit-identicalTruncatedNormalMessage.log_partition()— now the complete exp-family cumulant: untruncated Gaussian termmu^2/(2 sigma^2) + log(sigma)pluslog Z. Generic exp-family pdf integrates to 1.0 (wassigma*exp(mu^2/2sigma^2), e.g. 2.27); the EP productlog_normbookkeeping identity now telescopes correctlyTruncatedNormalMessage.log_prior_from_value(⇒TruncatedGaussianPrior) — drop-constants convention like every other prior (was the only fully-normalised one). User-visible nuance: chained fits buildTruncatedGaussianPriors, so reported absolute log-posterior values in chained-fit output shift by a per-parameter constant; posterior shape, evidence, and inferred parameters are unchanged (constants cancel in the Metropolis ratio; nested samplers use the unit-cube transform). Dropped constant recoverable vialog_normalisation()GammaMessage.from_mode(mode, V)— preserves mean+variance (alpha=m^2/V,beta=m/V), consistent with the Normal family; the oldalpha = 1 + m^2*Vhad V inverted (requesting var 0.25 produced 2.0). Note:alpha < 1(mode at 0) is now reachable whenV > m^2— accepted trade per Priors & messages: 9 confirmed bugs — guidance wanted on 5 decisions #1331, the old guarantee was an artefact of the inverted formulaautofit.messages.beta.inv_beta_suffstats— raisesValueErrorwhen the Newton–Raphson projection goes negative (previous "clamp" wrote to a dead local and let invalid parameters escape with only a warning)FixedMessage.logpdf— returns a fresh zero array per call; the class-levellogpdf_cachedict (unbounded, returned aliased mutable arrays) is removedMigration
log_prior/log_posteriorfloats from truncated-Gaussian (chained) fits across versions should addlog_normalisation()if it needs the normalised density.Known, out of scope
inv_beta_suffstats's internalnp.linalg.solveis shape-fragile for multi-element batch inputs (pre-existing, orthogonal to the guard added here)🤖 Generated with Claude Code