Skip to content

fix(priors): correctness batch — crashes, log-partition, projections#1345

Merged
Jammy2211 merged 1 commit into
mainfrom
feature/priors-messages-fixes
Jul 10, 2026
Merged

fix(priors): correctness batch — crashes, log-partition, projections#1345
Jammy2211 merged 1 commit into
mainfrom
feature/priors-messages-fixes

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

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/ and autofit/messages/: two crash fixes, the truncated-normal log-partition correction the EP machinery consumes, an aliased-cache removal, a silent-failure projection guard, the Gamma from_mode variance inversion, and the log_prior_from_value normalisation-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: TruncatedNormalMessage exp-family logpdf now equals scipy.stats.truncnorm.logpdf pointwise (1e-9) and the EP product log_norm telescope identity holds numerically (it was broken by the incomplete cumulant); GammaMessage.from_mode matches requested mean+variance exactly; all five log_normalisation hooks 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 — RelativeWidthModifier abs+floor ordered with strict σ>0, plus a mean=0 chained-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 by log_prior_from_value). Fixed previously-crashing paths (LogGaussianPrior.with_limits, array input to UniformPrior.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_value now follows the same drop-constants convention as every other prior.
See full details below.

Test Plan

  • Full test_autofit/ suite: 1437 passed, 14 skipped (numpy-only, no JAX)
  • New test_autofit/mapper/prior/test_priors_messages_fixes_1331.py — 12 regression tests, one per fix
  • test_truncated_gaussian.py updated to the drop-constants contract + new hook-recovery test vs scipy.truncnorm.logpdf
  • Fable numeric re-verification: truncated logpdftruncnorm.logpdf (3 geometries incl. half-bounded), EP product log_norm identity vs quadrature, all 5 hooks vs scipy densities
  • Post-merge: downstream smoke (/smoke_test) — no workspace scripts touch the changed symbols directly

Validation checklist (--auto run — plan was not pre-approved in Plan Mode; decisions human-resolved on #1331)

  • Effective level: supervised (header: safe, cap: bug → supervised); user in-session, ship instructed live
  • Plan: on the issue (fix(priors): correctness batch — crashes, log-partition, projections #1344), written at start, unmodified since
  • Gate: tests 1437 pass / 14 skip · smoke pending post-PR (no direct workspace consumers) · review CLEAN · Heart YELLOW within live 6-reason ack (recorded in active.md)
  • Human: plan sound in hindsight?
  • Human: diff matches plan (no scope creep)?
  • Human: merge, amend, or reject — then log the outcome
Full API Changes (for automation & release notes)

Added

  • Prior.log_normalisation(xp=np) — base hook returning 0.0; the additive constant dropped by log_prior_from_value, so log_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 in log_prior_from_value)
  • TruncatedGaussianPrior.log_normalisation()-log(sigma) - 0.5*log(2*pi) - log(Z) with Z the truncation mass

Changed Behaviour

  • LogGaussianPrior.with_limits(lower, upper) — no longer raises TypeError on first call (invalid lower_limit/upper_limit ctor kwargs dropped; a log-Gaussian has support (0, ∞))
  • LogGaussianPrior._new_for_base_message — repaired (same bad kwargs, plus stale self.instance().idself.id); previously dead code with no callers, doubly broken
  • UniformPrior.logpdf(x) — accepts array input (boundary snap vectorised with np.where); scalar path bit-identical
  • TruncatedNormalMessage.log_partition() — now the complete exp-family cumulant: untruncated Gaussian term mu^2/(2 sigma^2) + log(sigma) plus log Z. Generic exp-family pdf integrates to 1.0 (was sigma*exp(mu^2/2sigma^2), e.g. 2.27); the EP product log_norm bookkeeping identity now telescopes correctly
  • TruncatedNormalMessage.log_prior_from_value (⇒ TruncatedGaussianPrior) — drop-constants convention like every other prior (was the only fully-normalised one). User-visible nuance: chained fits build TruncatedGaussianPriors, 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 via log_normalisation()
  • GammaMessage.from_mode(mode, V) — preserves mean+variance (alpha=m^2/V, beta=m/V), consistent with the Normal family; the old alpha = 1 + m^2*V had V inverted (requesting var 0.25 produced 2.0). Note: alpha < 1 (mode at 0) is now reachable when V > 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 formula
  • autofit.messages.beta.inv_beta_suffstats — raises ValueError when 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-level logpdf_cache dict (unbounded, returned aliased mutable arrays) is removed

Migration

  • None required. No symbols removed or renamed; no signature changes. Code comparing absolute log_prior/log_posterior floats from truncated-Gaussian (chained) fits across versions should add log_normalisation() if it needs the normalised density.

Known, out of scope

🤖 Generated with Claude Code

…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pending-release PR queued for the next release build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant