Skip to content

feat: implement gradient/Hessian for TruncatedNormalMessage - #1238

Merged
Jammy2211 merged 1 commit into
mainfrom
feature/truncated-normal-gradient-hessian
Apr 29, 2026
Merged

Jammy2211 merged 1 commit into
mainfrom
feature/truncated-normal-gradient-hessian

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

Implement TruncatedNormalMessage._normal_gradient_hessian so EP + Laplace optimisation actually updates parameters with truncated Gaussian priors. The previous NotImplementedError stub was caught silently by the EP outer loop, leaving truncated-prior parameters glued to their priors across the entire run.

Closes #1237

API Changes

TruncatedNormalMessage.logpdf_gradient and .logpdf_gradient_hessian now return real values instead of raising NotImplementedError. TruncatedNaturalNormal gets the same behaviour via an override that reconstructs the underlying Gaussian from its natural parameters. No public signatures changed — this is a behaviour fix that unblocks a previously unimplemented code path.
See full details below.

Test Plan

  • New unit tests in test_autofit/messages/test_truncated_normal.py (12 tests, all passing)
  • Gradient and Hessian agree with NormalMessage inside the support; logl shifted by exactly -log Z
  • Out-of-bounds returns logl = -inf, grad = 0
  • Scalar and array x shapes verified
  • Analytic results match numerical_logpdf_gradient / numerical_logpdf_gradient_hessian to 1e-2 tolerance
  • TruncatedNaturalNormal returns finite gradients matching the equivalent TruncatedNormalMessage
  • Existing related tests pass: test_autofit/mapper/prior/test_truncated_gaussian.py, test_autofit/graphical/functionality/test_messages.py
  • Full pytest test_autofit regression: 1245 passed, 0 failed
  • Integration reproducer (z_projects/concr/scripts/toy/ep.py with TruncatedGaussianPrior on normalization and sigma) now runs to completion with no NotImplementedError, and per-dataset truncated-prior posteriors visibly move off their priors
Full API Changes (for automation & release notes)

Changed Behaviour

  • TruncatedNormalMessage._normal_gradient_hessian(x) — previously raised NotImplementedError; now returns (logl, grad_logl, hess_logl) with the same shape conventions as NormalMessage._normal_gradient_hessian. Inside [lower_limit, upper_limit] the gradient and Hessian match the untruncated Gaussian; logl is shifted by -log Z where Z = Φ((b-μ)/σ) - Φ((a-μ)/σ). Outside the support, logl = -inf and grad_logl = 0.
  • TruncatedNormalMessage.logpdf_gradient(x) and .logpdf_gradient_hessian(x) — propagate the new behaviour (no longer raise).
  • TruncatedNaturalNormal.logpdf_gradient(x) and .logpdf_gradient_hessian(x) — propagate the new behaviour. The class now overrides _normal_gradient_hessian to reconstruct the underlying Gaussian (μ, σ) from its natural parameters, because its inherited mean / sigma properties return the truncated moments via scipy.stats.truncnorm.

Added (internal)

  • TruncatedNormalMessage._normal_gradient_hessian_from(mean, sigma, x) — extracted helper that runs the body using explicit (μ, σ). Used by both the base class and the TruncatedNaturalNormal override. Not part of the public API.

Migration

None — this is a pure bug fix. Existing code that called these methods previously hit NotImplementedError; that code now simply works.

🤖 Generated with Claude Code

TruncatedNormalMessage._normal_gradient_hessian previously raised
NotImplementedError, which silently broke EP + Laplace optimisation
for any GaussianPrior with finite limits — the EP outer loop catches
the exception per factor, so runs looked successful but truncated
parameters never received their cavity update.

Implement the gradient and Hessian w.r.t. x: inside the truncation
support these match the untruncated Gaussian (Z and log sigma are
constant in x); logl additionally subtracts log Z. Outside the
support, return logl = -inf and grad = 0 so the optimiser sees a
flat region rather than NaNs.

Override on TruncatedNaturalNormal because its mean / sigma
properties return *truncated* moments (via scipy.stats.truncnorm),
not the underlying Gaussian's parameters that the gradient formula
needs; the override reconstructs the underlying (mu, sigma) from
the natural parameters.

Closes #1237

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Jammy2211 Jammy2211 added the pending-release PR queued for the next release build label Apr 29, 2026
@Jammy2211
Jammy2211 merged commit 5841536 into main Apr 29, 2026
3 checks passed
@Jammy2211
Jammy2211 deleted the feature/truncated-normal-gradient-hessian branch April 29, 2026 19:36
@Jammy2211 Jammy2211 removed the pending-release PR queued for the next release build label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: implement gradient/Hessian for TruncatedNormalMessage

1 participant