Overview
UniformPrior.log_prior_from_value short-circuits to return 0.0 whenever
xp is np, without ever evaluating the bound. The JAX path immediately below
it does the right thing. So a UniformPrior box is enforced in the objective
under JAX and silently not enforced under NumPy — which is the path Emcee,
Zeus, Drawer and LBFGS take.
This is not a regression. git log -L on the method puts the unconditional
NumPy return 0.0 at be2dbd0c (2025-11-30); the xp-dispatch commit that made
the JAX side strict is 2e35407 (2026-05-14). The asymmetry was created by
tightening JAX, not by loosening NumPy.
Plan
- Reproduce first. Run an Emcee fit with a deliberately unconstrained
parameter under a narrow UniformPrior and show samples landing outside the
box. If it cannot be reproduced, say so — the mechanism is still worth closing,
but the framing changes.
- Choose the fix deliberately: a strict NumPy path (behaviour change for every
existing Emcee/Zeus/LBFGS run — needs a measured before/after), versus a
Clipper-style opt-in for the NumPy searches, versus making LBFGS default to a
real clipper. These are not equivalent and the choice is a human one.
- Correct the phase-1 record, which is cited elsewhere as the reason the clipper
was scoped narrowly.
Detailed implementation plan
The code
autofit/mapper/prior/uniform.py:
def log_prior_from_value(self, value, xp=np):
if xp is np:
return 0.0 # <-- bound never evaluated
in_bounds = (value >= self.lower_limit) & (value <= self.upper_limit)
return xp.where(in_bounds, xp.zeros_like(value), -xp.inf)
Outside the box: NumPy sum(log_prior) == 0.0; JAX -inf.
Per prior type, NumPy path, outside support
| prior |
NumPy result |
penalised? |
UniformPrior |
0.0 |
no |
LogUniformPrior above upper_limit |
finite -log(value) |
no |
LogUniformPrior below 0 |
-inf |
yes |
TruncatedGaussianPrior |
-inf |
yes |
GaussianPrior |
unbounded by design |
n/a |
LogUniformPrior's own docstring already says this outright — "The NumPy path is
otherwise unnormalised and unbounded … The JAX path additionally returns
-inf outside [lower_limit, upper_limit]". The asymmetry is documented there
and undocumented for UniformPrior.
No other guard exists
instance_from_vector accepts an out-of-box vector without raising; Emcee and
Zeus have no bounds handling of their own; and the strict logpdf (which does
return -inf) is used only by the messages / EP machinery, never by the search
fitness path.
Who is exposed (fom_is_log_likelihood=False)
| search |
UniformPrior box enforced? |
| Emcee, Zeus, Drawer |
no |
| LBFGS / BFGS |
only with a clipper set — the default ClipperNone passes bounds=None, so no |
BlackJAXNUTS, MultiStartGradient (JAX) |
yes (-inf) |
| Nautilus, Dynesty |
unaffected — they propose in the unit cube |
This corrects the phase-1 record
The prior-support clipper record (#1477) claims "the MCMC samplers reject -inf
proposals so the walker simply stays put". There is no -inf to reject for a
UniformPrior on the NumPy path. That sentence is the load-bearing
justification for scoping the clipper to the gradient searches only, and it is
wrong for the reason above. Whatever lands here should correct it.
Severity, stated honestly
Not "results are wrong" — "the sampled posterior may not be the declared
model". Walkers are initialised within limits and the likelihood usually falls
away outside the sensible region, so the exposure is for poorly-constrained
parameters — exactly the ones that diffuse to the walls. Nautilus and Dynesty
are unaffected and are the production workhorses, so the blast radius is
Emcee / Zeus / Drawer / LBFGS users.
NOT VERIFIED: whether any real past fit actually drifted outside a box. The
mechanism is unguarded; that it has bitten is unproven. Establishing that is task
one, not an assumption.
Why it is not fixed inline
Making the NumPy path strict changes behaviour for every existing Emcee / Zeus /
LBFGS run. A walker that currently wanders outside a box and comes back would
start being rejected; chains, acceptance rates and stored results all move.
Orthogonal to #1483 — keep them straight
Emcee is immune to the per-parameter step-scaling problem (affine invariance)
and fully exposed to this one. A search can be immune to one and exposed to
the other. This must not be absorbed into #1483.
Key Files
autofit/mapper/prior/uniform.py — the short-circuit
autofit/mapper/prior/log_uniform.py — the same asymmetry, already documented
autofit/non_linear/clipper.py — the existing opt-in mechanism, if that route wins
autofit/non_linear/search/mle/bfgs/search.py — bounds=None under ClipperNone
Original Prompt
Click to expand starting prompt
UniformPrior bounds are not enforced in the objective on the NumPy path
Type: bug
Target: autofit
Repos:
- PyAutoFit
Difficulty: medium
Autonomy: human-required
Priority: high
Status: formalised
What this is
UniformPrior.log_prior_from_value short-circuits to return 0.0 whenever
xp is np, without ever evaluating the bound
(autofit/mapper/prior/uniform.py, in the if xp is np: branch). The JAX path
immediately below it does the right thing:
def log_prior_from_value(self, value, xp=np):
if xp is np:
return 0.0
in_bounds = (value >= self.lower_limit) & (value <= self.upper_limit)
return xp.where(in_bounds, xp.zeros_like(value), -xp.inf)
So for a value outside the box:
- NumPy:
sum(log_prior) == 0.0 — not penalised
- JAX:
-inf — correctly penalised
Per prior type on the NumPy path, outside support:
| prior |
NumPy result outside support |
penalised? |
UniformPrior |
0.0 |
no |
LogUniformPrior above upper_limit |
finite -log(value) |
no |
LogUniformPrior below 0 |
-inf |
yes |
TruncatedGaussianPrior |
-inf |
yes |
GaussianPrior |
unbounded by design |
n/a |
LogUniformPrior's own docstring already states this outright — "The NumPy path
is otherwise unnormalised and unbounded … The JAX path additionally
returns -inf outside [lower_limit, upper_limit]" — so the asymmetry is
documented there and undocumented for UniformPrior.
No other guard exists. instance_from_vector accepts an out-of-box vector
without raising; Emcee and Zeus have no bounds handling of their own; and the
strict logpdf (which does return -inf) is used only by the messages / EP
machinery, never by the search fitness path.
Who is exposed
For searches with fom_is_log_likelihood=False:
| search |
UniformPrior box enforced? |
| Emcee, Zeus, Drawer |
no |
| LBFGS / BFGS |
only if a clipper is set — the default ClipperNone passes bounds=None, so no |
BlackJAXNUTS, MultiStartGradient (JAX) |
yes (-inf) |
| Nautilus, Dynesty |
unaffected — they propose in the unit cube |
This corrects the phase-1 record
complete/2026/08/prior-support-clipper.md claims "the MCMC samplers reject
-inf proposals so the walker simply stays put". There is no -inf to reject
for a UniformPrior on the NumPy path. That sentence is the load-bearing
justification for why the clipper was scoped to the gradient searches only, and
it is wrong for the reason above.
Severity, honestly
Not "results are wrong" — "the sampled posterior may not be the declared model".
Walkers are initialised within limits and the likelihood usually falls away
outside the sensible region, so the exposure is for poorly-constrained
parameters — exactly the ones that diffuse to the walls. Nautilus and Dynesty
are unaffected and are the production workhorses, so the blast radius is
Emcee / Zeus / Drawer / LBFGS users.
NOT VERIFIED: whether any real past fit actually drifted outside a box. The
mechanism is unguarded; that it has bitten is unproven. Establishing that is the
first task, not an assumption.
Not a regression
Git history puts the NumPy return 0.0 before the May-2026 JAX
xp-dispatch commit that made the JAX side strict. The asymmetry was created by
tightening JAX, not by loosening NumPy. This is long-standing behaviour.
Why this is not fixed inline
Making the NumPy path strict changes behaviour for every existing Emcee / Zeus
/ LBFGS run. A walker that currently wanders outside a box and comes back would
start being rejected; chains, acceptance rates and stored results all move. That
is a deliberate, measured change with its own before/after, not a drive-by.
Orthogonal to per-parameter step scaling — keep them straight
Emcee is immune to the step-scaling problem (affine invariance) and fully
exposed to this one. A search can be immune to one and exposed to the other. Do
not let this be absorbed into active/per_parameter_step_scaling.md.
Suggested shape of the work
- Reproduce first. Run an Emcee fit with a deliberately unconstrained
parameter under a narrow UniformPrior and show samples outside the box. If
it cannot be reproduced, say so — the mechanism would still be worth closing,
but the framing changes.
- Decide the fix: strict NumPy path (behaviour change, needs a measured
before/after) versus a Clipper-style opt-in for the NumPy searches versus
making LBFGS default to a real clipper. These are not equivalent and the
choice is a human one.
- Whichever lands, correct the phase-1 record's "MCMC samplers reject
-inf"
sentence — it is cited elsewhere as a reason the clipper was scoped narrowly.
Out of scope
- Per-parameter step scaling (
active/per_parameter_step_scaling.md).
- Changing
Prior classes in a way that alters the nested samplers, where the
hard box currently works correctly.
Overview
UniformPrior.log_prior_from_valueshort-circuits toreturn 0.0wheneverxp is np, without ever evaluating the bound. The JAX path immediately belowit does the right thing. So a
UniformPriorbox is enforced in the objectiveunder JAX and silently not enforced under NumPy — which is the path Emcee,
Zeus, Drawer and LBFGS take.
This is not a regression.
git log -Lon the method puts the unconditionalNumPy
return 0.0atbe2dbd0c(2025-11-30); thexp-dispatch commit that madethe JAX side strict is
2e35407(2026-05-14). The asymmetry was created bytightening JAX, not by loosening NumPy.
Plan
parameter under a narrow
UniformPriorand show samples landing outside thebox. If it cannot be reproduced, say so — the mechanism is still worth closing,
but the framing changes.
existing Emcee/Zeus/LBFGS run — needs a measured before/after), versus a
Clipper-style opt-in for the NumPy searches, versus making LBFGS default to areal clipper. These are not equivalent and the choice is a human one.
was scoped narrowly.
Detailed implementation plan
The code
autofit/mapper/prior/uniform.py:Outside the box: NumPy
sum(log_prior) == 0.0; JAX-inf.Per prior type, NumPy path, outside support
UniformPrior0.0LogUniformPrioraboveupper_limit-log(value)LogUniformPriorbelow0-infTruncatedGaussianPrior-infGaussianPriorLogUniformPrior's own docstring already says this outright — "The NumPy path isotherwise unnormalised and unbounded … The JAX path additionally returns
-infoutside[lower_limit, upper_limit]". The asymmetry is documented thereand undocumented for
UniformPrior.No other guard exists
instance_from_vectoraccepts an out-of-box vector without raising; Emcee andZeus have no bounds handling of their own; and the strict
logpdf(which doesreturn
-inf) is used only by the messages / EP machinery, never by the searchfitness path.
Who is exposed (
fom_is_log_likelihood=False)ClipperNonepassesbounds=None, so noMultiStartGradient(JAX)-inf)This corrects the phase-1 record
The prior-support clipper record (#1477) claims "the MCMC samplers reject
-infproposals so the walker simply stays put". There is no
-infto reject for aUniformPrioron the NumPy path. That sentence is the load-bearingjustification for scoping the clipper to the gradient searches only, and it is
wrong for the reason above. Whatever lands here should correct it.
Severity, stated honestly
Not "results are wrong" — "the sampled posterior may not be the declared
model". Walkers are initialised within limits and the likelihood usually falls
away outside the sensible region, so the exposure is for poorly-constrained
parameters — exactly the ones that diffuse to the walls. Nautilus and Dynesty
are unaffected and are the production workhorses, so the blast radius is
Emcee / Zeus / Drawer / LBFGS users.
NOT VERIFIED: whether any real past fit actually drifted outside a box. The
mechanism is unguarded; that it has bitten is unproven. Establishing that is task
one, not an assumption.
Why it is not fixed inline
Making the NumPy path strict changes behaviour for every existing Emcee / Zeus /
LBFGS run. A walker that currently wanders outside a box and comes back would
start being rejected; chains, acceptance rates and stored results all move.
Orthogonal to #1483 — keep them straight
Emcee is immune to the per-parameter step-scaling problem (affine invariance)
and fully exposed to this one. A search can be immune to one and exposed to
the other. This must not be absorbed into #1483.
Key Files
autofit/mapper/prior/uniform.py— the short-circuitautofit/mapper/prior/log_uniform.py— the same asymmetry, already documentedautofit/non_linear/clipper.py— the existing opt-in mechanism, if that route winsautofit/non_linear/search/mle/bfgs/search.py—bounds=NoneunderClipperNoneOriginal Prompt
Click to expand starting prompt
UniformPrior bounds are not enforced in the objective on the NumPy path
Type: bug
Target: autofit
Repos:
Difficulty: medium
Autonomy: human-required
Priority: high
Status: formalised
What this is
UniformPrior.log_prior_from_valueshort-circuits toreturn 0.0wheneverxp is np, without ever evaluating the bound(
autofit/mapper/prior/uniform.py, in theif xp is np:branch). The JAX pathimmediately below it does the right thing:
So for a value outside the box:
sum(log_prior) == 0.0— not penalised-inf— correctly penalisedPer prior type on the NumPy path, outside support:
UniformPrior0.0LogUniformPrioraboveupper_limit-log(value)LogUniformPriorbelow0-infTruncatedGaussianPrior-infGaussianPriorLogUniformPrior's own docstring already states this outright — "The NumPy pathis otherwise unnormalised and unbounded … The JAX path additionally
returns
-infoutside[lower_limit, upper_limit]" — so the asymmetry isdocumented there and undocumented for
UniformPrior.No other guard exists.
instance_from_vectoraccepts an out-of-box vectorwithout raising; Emcee and Zeus have no bounds handling of their own; and the
strict
logpdf(which does return-inf) is used only by the messages / EPmachinery, never by the search fitness path.
Who is exposed
For searches with
fom_is_log_likelihood=False:ClipperNonepassesbounds=None, so noMultiStartGradient(JAX)-inf)This corrects the phase-1 record
complete/2026/08/prior-support-clipper.mdclaims "the MCMC samplers reject-infproposals so the walker simply stays put". There is no-infto rejectfor a
UniformPrioron the NumPy path. That sentence is the load-bearingjustification for why the clipper was scoped to the gradient searches only, and
it is wrong for the reason above.
Severity, honestly
Not "results are wrong" — "the sampled posterior may not be the declared model".
Walkers are initialised within limits and the likelihood usually falls away
outside the sensible region, so the exposure is for poorly-constrained
parameters — exactly the ones that diffuse to the walls. Nautilus and Dynesty
are unaffected and are the production workhorses, so the blast radius is
Emcee / Zeus / Drawer / LBFGS users.
NOT VERIFIED: whether any real past fit actually drifted outside a box. The
mechanism is unguarded; that it has bitten is unproven. Establishing that is the
first task, not an assumption.
Not a regression
Git history puts the NumPy
return 0.0before the May-2026 JAXxp-dispatch commit that made the JAX side strict. The asymmetry was created bytightening JAX, not by loosening NumPy. This is long-standing behaviour.
Why this is not fixed inline
Making the NumPy path strict changes behaviour for every existing Emcee / Zeus
/ LBFGS run. A walker that currently wanders outside a box and comes back would
start being rejected; chains, acceptance rates and stored results all move. That
is a deliberate, measured change with its own before/after, not a drive-by.
Orthogonal to per-parameter step scaling — keep them straight
Emcee is immune to the step-scaling problem (affine invariance) and fully
exposed to this one. A search can be immune to one and exposed to the other. Do
not let this be absorbed into
active/per_parameter_step_scaling.md.Suggested shape of the work
parameter under a narrow
UniformPriorand show samples outside the box. Ifit cannot be reproduced, say so — the mechanism would still be worth closing,
but the framing changes.
before/after) versus a
Clipper-style opt-in for the NumPy searches versusmaking
LBFGSdefault to a real clipper. These are not equivalent and thechoice is a human one.
-inf"sentence — it is cited elsewhere as a reason the clipper was scoped narrowly.
Out of scope
active/per_parameter_step_scaling.md).Priorclasses in a way that alters the nested samplers, where thehard box currently works correctly.