You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: log_prior_from_value sign convention — density form across Prior subclasses
Phase 0 of #1266: switch Prior.log_prior_from_value to return log p(x) in
density form (negative for low-density, zero at the mode) across the
Gaussian-family priors and LogUniformPrior. NumPy + JAX paths both updated.
What was broken
---------------
Fitness._call computes `figure_of_merit = log_likelihood + sum(log_priors)`
which Emcee / Zeus / MLE-Drawer maximise (and LBFGS minimises via
-2 * figure_of_merit). This expects log_priors in density form. But the
existing bodies returned cost form `-log p(x)` (positive for low-density):
NormalMessage.log_prior_from_value: +(value - mean)**2 / (2 * sigma**2)
LogGaussianPrior.log_prior_from_value: +(log value - mean)**2 / ... - log(value)
TruncatedNormalMessage.log_prior_from_value: density-form (already correct)
UniformPrior.log_prior_from_value: 0.0 (sign-agnostic, fine)
LogUniformPrior.log_prior_from_value: 1.0 / value (Jacobian gradient, not a log)
Adding a positive quadratic to log_likelihood turns the maximand into
log_lik - log_prior_density rather than log_lik + log_prior_density, so MCMC
samples drift AWAY from prior modes and LBFGS minimises in the wrong
direction. Empirically confirmed: Emcee + flat-likelihood + GaussianPrior(5,1)
diverges to ~10^146 instead of clustering at 5.0; LBFGS converges to 8e143
instead of 5.0. See autofit_workspace_test/scripts/prior_correctness/
{emcee,lbfgs}_gaussian_bias_check.py (added in the matching workspace_test PR).
What's fixed
------------
- NormalMessage.log_prior_from_value: returns -(value - mean)**2 / (2 sigma**2)
- LogGaussianPrior.log_prior_from_value: returns
-(log value - mean)**2 / (2 sigma**2) - log(value) on both NumPy + JAX paths,
matching the change-of-variables Jacobian convention
- LogUniformPrior.log_prior_from_value: returns -log(value); JAX path adds
bounds-guard with xp.where(in_bounds, -xp.log(value), -xp.inf)
The -log(sigma * sqrt(2 * pi)) and -log(log(upper/lower)) normalisation
constants are dropped, mirroring UniformPrior dropping -log(b - a) to return
0.0 — these constants don't affect posterior shape and the existing
in-house convention is to omit them.
Unchanged (already correct)
---------------------------
- UniformPrior.log_prior_from_value (0.0, sign-agnostic)
- TruncatedNormalMessage.log_prior_from_value (density form with -inf out-of-bounds)
- Fitness._call / Fitness.call_wrap / Fitness._jit / Fitness._vmap
- The entire graphical / expectation-propagation framework (EP uses
Message.logpdf directly, not Prior.log_prior_from_value — confirmed unaffected)
Test pin updates
----------------
- test_autofit/mapper/prior/test_prior.py — three updated pins to assert
density-form values (LogUniform, Gaussian parametrize, LogGaussian)
- test_autofit/mapper/model/test_model_mapper.py — one updated pin for
Model.log_prior_list_from_vector to assert density-form values
Migration warning
-----------------
Cached samples.csv from Emcee / Zeus / MLE-Drawer / LBFGS / BFGS fits with
ANY non-uniform prior (Gaussian, LogGaussian, TruncatedGaussian, LogUniform)
are BIASED and should be re-run. Nested-sampler chains (Dynesty / Nautilus)
are UNAFFECTED — those route priors through prior_transform and only the
stored log_prior column in samples.csv is wrong-signed; it is automatically
re-derived from parameters on next load.
Closes#1266
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0 commit comments