Skip to content

fix: enforce prior bounds in the NumPy-path objective (UniformPrior, LogUniformPrior) - #1490

Merged
Jammy2211 merged 1 commit into
mainfrom
claude/uniformprior-bounds-numpy-1n1u3s
Aug 18, 2026
Merged

fix: enforce prior bounds in the NumPy-path objective (UniformPrior, LogUniformPrior)#1490
Jammy2211 merged 1 commit into
mainfrom
claude/uniformprior-bounds-numpy-1n1u3s

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

Restores the prior-support enforcement lost in release 2025.10.16.1, when assert_within_limits / PriorLimitException were removed (#1155, part of the JAX jit-compat cleanup) without a replacement on the NumPy path. Since then UniformPrior.log_prior_from_value returned 0.0 unconditionally and LogUniformPrior returned a finite -log(value) above its upper_limit, so the searches that form a log posterior on the NumPy path (Emcee, Zeus, Drawer, LBFGS) never penalised an out-of-support sample — an Emcee walker on a poorly-constrained parameter under UniformPrior(-0.1, 0.1) escaped to |value| ≈ 1e14, with 100% of accepted samples outside the box. Full reproduction, git archaeology, and the fix-shape decision are on #1489.

This is fix shape A from the issue: strict bounds in the priors themselves, exactly mirroring the JAX path (made strict in 2e3540771, which created the asymmetry). Shape C (defaulting LBFGS to ClipperPriorBox) is deliberately not included — orthogonal follow-up.

API Changes

No symbols added or removed — a behaviour restoration:

  • UniformPrior.log_prior_from_value (NumPy path): 0.0 inside [lower_limit, upper_limit], -inf outside (was: always 0.0). Limits inclusive, matching JAX.
  • LogUniformPrior.log_prior_from_value (NumPy path): -inf everywhere outside [lower_limit, upper_limit] (was: -inf only for value <= 0; finite -log(value) above upper_limit and below lower_limit).
  • Fitness.log_likelihood_from: skips the log-prior subtraction when the prior sum is non-finite, so a rejected point inverts to -inf rather than NaN.
  • Behavioural consequence: Emcee/Zeus reject out-of-box proposals (walker stays put); an unclipped LBFGS step out of the box now sees an infinite objective on the NumPy branch, as it always did on JAX.

See full details below.

Test Plan

  • New test_autofit/mapper/prior/test_prior_bounds_1489.py: scalar + array bounds for both priors (inclusive limits, no RuntimeWarning), -inf figure of merit for an out-of-box vector, NaN-free log_likelihood_from inversion, and an end-to-end Emcee containment test (guaranteed deterministic: walkers start in-box and every out-of-box proposal is rejected).
  • test_prior.py updated where it pinned the unbounded behaviour; test_clipper.py LBFGS end-to-end updated (its foil asserted the clipper-less fit escapes the box — that escape was the bug).
  • Full suite: 1734 passed, 8 skipped; 2 failures (test_messages.py::test_beta, test_nautilus.py::test__single_core_builds_no_pool) are missing-optional-dependency environment failures that reproduce identically on untouched main.

Measured before/after (same script, same seed)

before (main @ fe9f813) after (this PR) autofit 2025.5.10.1 (pre-regression)
Well-constrained 3-param Gaussian, posterior median (c, n, σ) 50.016, 24.562, 4.832 50.018, 24.580, 4.839
… posterior std 0.117, 0.487, 0.115 0.116, 0.487, 0.115
Unconstrained param under UniformPrior(-0.1, 0.1): samples outside box 630 / 630 0 / 540 0 / 630
… sampled min/max −5.5e14 / 6.0e14 −0.0997 / 0.0999 −0.0992 / 0.0999

The well-constrained posterior is statistically unchanged (median shifts ~0.02 against σ ≈ 0.1–0.5); the containment matches the pre-regression release.

Full API Changes (for automation & release notes)

Removed

  • (none)

Added

  • (none)

Changed behaviour

  • autofit.mapper.prior.uniform.UniformPrior.log_prior_from_value(value, xp=np) — NumPy path now returns -inf for value outside [lower_limit, upper_limit] (previously 0.0 unconditionally). JAX path unchanged.
  • autofit.mapper.prior.log_uniform.LogUniformPrior.log_prior_from_value(value, xp=np) — NumPy path now returns -inf for any value outside [lower_limit, upper_limit] (previously only value <= 0). JAX path unchanged.
  • autofit.non_linear.fitness.Fitness.log_likelihood_from — returns -inf (not NaN) for a figure of merit produced by an out-of-support vector.

Migration

  • Emcee / Zeus / Drawer / LBFGS results on models whose posterior mass genuinely pressed against a UniformPrior/LogUniformPrior bound may change: chains now respect the declared box. This restores pre-2025.10.16.1 behaviour; no code changes required.
  • A fit that relied on samples leaving a declared box should widen the prior instead.

Generated by the PyAutoLabs agent workflow.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DP4nPgsLuB2czRjrV4qcSk


Generated by Claude Code

…LogUniformPrior)

Restores the prior-support enforcement lost in release 2025.10.16.1, when
assert_within_limits / PriorLimitException were removed (#1155) without a
replacement on the NumPy path: UniformPrior.log_prior_from_value returned 0.0
unconditionally and LogUniformPrior returned a finite -log(value) outside
[lower_limit, upper_limit], so searches that form a log posterior on the NumPy
path (Emcee, Zeus, Drawer, LBFGS) never penalised an out-of-support sample.
An Emcee walker on a poorly-constrained parameter under UniformPrior(-0.1, 0.1)
escaped to |value| ~ 1e14 (100% of accepted samples outside the box).

- UniformPrior.log_prior_from_value: 0.0 inside the box, -inf outside, on the
  NumPy path exactly as on the JAX path (which was made strict in 2e35407
  for JAX only, creating the asymmetry).
- LogUniformPrior.log_prior_from_value: -inf outside [lower_limit, upper_limit]
  on the NumPy path (previously only value <= 0 was -inf); keeps the
  double-where so no log of a non-positive value is evaluated.
- Fitness.log_likelihood_from: skip the log-prior subtraction when the prior
  sum is non-finite — -inf minus -inf is NaN, and the inversion feeds
  history / quick-update / resume bookkeeping.
- clipper.py: dated correction of the module docstring's "MCMC samplers reject
  -inf proposals" claim, false in the 2025.10.16.1 -> #1489 window.
- bfgs/search.py: comment updated — both branches now see the hard wall.
- Tests: new test_prior_bounds_1489.py (scalar/array bounds on both priors,
  -inf figure of merit for out-of-box vectors, NaN-free inversion, end-to-end
  Emcee containment); test_prior.py updated where it pinned the unbounded
  behaviour; test_clipper.py LBFGS foil updated (the clipper-less fit can no
  longer escape past the prior edge).

Before/after (same seed): well-constrained 3-param Gaussian fit posterior
statistically unchanged (medians agree to ~0.02 with sigma ~0.1-0.5);
unconstrained-parameter fit goes from 630/630 samples outside the box to 0/540,
matching the pre-regression 2025.5.10.1 behaviour. Full suite: 1734 passed,
8 skipped (2 pre-existing environment failures also fail on untouched main).

Closes #1489

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DP4nPgsLuB2czRjrV4qcSk
@Jammy2211 Jammy2211 added the pending-release PR queued for the next release build label Aug 18, 2026 — with Claude
@Jammy2211
Jammy2211 merged commit c302f51 into main Aug 18, 2026
3 checks passed
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.

2 participants