Overview
UniformPrior.log_prior_from_value short-circuits to return 0.0 on the NumPy path (if xp is np:) without evaluating the bound, while the JAX path immediately below returns -inf outside [lower_limit, upper_limit]. For every search with fom_is_log_likelihood=False on the NumPy path (Emcee, Zeus, Drawer, and LBFGS/BFGS under the default ClipperNone), the declared UniformPrior box is therefore not enforced anywhere in the objective — the sampled posterior may not be the declared model.
Reproduced. This issue includes the reproduction the Mind prompt asked for as step 1, and the effect is worse than "slow diffusion to the walls": with an unconstrained parameter under UniformPrior(-0.1, 0.1), a 30-walker × 600-step Emcee fit had 100% of accepted samples outside the box, with the parameter running away to |offset| ≈ 1e14 (max-likelihood sample at offset ≈ 2.1e10). Emcee's affine-invariant stretch move scales proposals by the walker spread, so an unpenalised flat direction grows exponentially, not as a random walk.
Plan
Reproduce on clean main — done (evidence below), on main @ fe9f813.
- Human decision (required before any code): pick the fix shape — (A) strict NumPy path mirroring JAX, (B) clipper-style opt-in for the NumPy searches, (C) default
LBFGS/BFGS to ClipperPriorBox. Recommendation below: A (+C as an orthogonal follow-up); B rejected for MCMC.
- Implement the chosen fix with a measured before/after on a reference Emcee fit (chains, acceptance rate) — this changes behaviour for every existing Emcee/Zeus/LBFGS run and is a deliberate, measured change.
- Correct the false phase-1 record: "the MCMC samplers reject
-inf proposals so the walker simply stays put" — there is no -inf to reject on the NumPy path. The sentence lives in two places: PyAutoMind/complete/2026/08/prior-support-clipper.md (~line 290) and the autofit/non_linear/clipper.py module docstring (lines 20–23).
- Unit + regression tests pinning bound enforcement on both paths.
Reproduction evidence
Environment: fresh clone of main @ fe9f813 (autofit 2026.8.17.1), Python 3.12, NumPy path throughout.
Part A — the objective directly (Fitness(model, analysis, fom_is_log_likelihood=False), the Emcee convention), 4-parameter 1D Gaussian model plus an offset parameter the likelihood ignores, declared UniformPrior(-0.1, 0.1):
log posterior, offset= 0.05 (in bounds) : 101.836275
log posterior, offset= 5.00 (OUT of box): 101.836275
identical (no penalty applied) : True
out-of-box penalised with -inf? : False
log_prior_list_from_vector(out-of-box) : [0.0, 0.0, 0.0, 0.0] (sum=0.0)
instance_from_vector raises? : no (offset=5.0)
Part B — end-to-end Emcee (af.Emcee(nwalkers=30, nsteps=600), walkers initialised in-box from the priors):
total accepted samples : 450
samples with offset OUTSIDE [-0.1, 0.1] : 450 (100.0%)
min / max sampled offset : -3.3e+14 / 4.6e+14
max-likelihood offset : 2.1e+10
Reproduction script (self-contained, runs on clean main)
import numpy as np
import autofit as af
from autofit.non_linear.fitness import Fitness
class GaussianWithOffset:
def __init__(self, centre=50.0, normalization=1.0, sigma=5.0, offset=0.0):
self.centre = centre
self.normalization = normalization
self.sigma = sigma
self.offset = offset # ignored by the likelihood: unconstrained
def model_data_from(self, xvalues):
transformed = xvalues - self.centre
return (
self.normalization
/ (self.sigma * np.sqrt(2.0 * np.pi))
* np.exp(-0.5 * (transformed / self.sigma) ** 2)
)
class AnalysisIgnoringOffset(af.Analysis):
_use_jax = False # NumPy path
def __init__(self, data, noise_map):
self.data = data
self.noise_map = noise_map
self.xvalues = np.arange(data.shape[0], dtype=float)
def log_likelihood_function(self, instance):
model_data = instance.model_data_from(self.xvalues)
residual_map = self.data - model_data
chi_squared = float(np.sum((residual_map / self.noise_map) ** 2.0))
noise_normalization = float(np.sum(np.log(2 * np.pi * self.noise_map**2.0)))
return -0.5 * (chi_squared + noise_normalization)
rng = np.random.default_rng(1)
xvalues = np.arange(100, dtype=float)
truth = GaussianWithOffset(centre=50.0, normalization=25.0, sigma=5.0)
noise = 0.1
data = truth.model_data_from(xvalues) + rng.normal(0.0, noise, size=xvalues.shape)
noise_map = np.full(xvalues.shape, noise)
model = af.Model(GaussianWithOffset)
model.centre = af.UniformPrior(lower_limit=0.0, upper_limit=100.0)
model.normalization = af.UniformPrior(lower_limit=1e-2, upper_limit=1e2)
model.sigma = af.UniformPrior(lower_limit=0.1, upper_limit=25.0)
model.offset = af.UniformPrior(lower_limit=-0.1, upper_limit=0.1) # the narrow box
analysis = AnalysisIgnoringOffset(data=data, noise_map=noise_map)
# Part A: direct objective check (Emcee convention: fom_is_log_likelihood=False)
fitness = Fitness(model=model, analysis=analysis, paths=None, fom_is_log_likelihood=False)
in_bounds = [50.0, 25.0, 5.0, 0.05]
out_bounds = [50.0, 25.0, 5.0, 5.0] # offset 50x outside the box
fom_in = fitness(parameters=in_bounds)
fom_out = fitness(parameters=out_bounds)
print(fom_in, fom_out, fom_in == fom_out, np.isneginf(fom_out))
print(model.log_prior_list_from_vector(vector=out_bounds, xp=np))
# Part B: end-to-end Emcee fit
search = af.Emcee(
name="repro_uniform_bounds",
nwalkers=30,
nsteps=600,
iterations_per_full_update=1_000_000,
number_of_cores=1,
)
result = search.fit(model=model, analysis=analysis)
params = np.asarray(result.samples.parameter_lists)
offset_samples = params[:, 3]
outside = (offset_samples < -0.1) | (offset_samples > 0.1)
print(offset_samples.size, int(outside.sum()), offset_samples.min(), offset_samples.max())
The fix decision (human-required)
Three shapes were scoped in the Mind prompt; they are not equivalent:
A. Strict NumPy path (recommended). Make UniformPrior.log_prior_from_value evaluate the bound on the NumPy path exactly as the JAX path does (0.0 in bounds, -inf outside), and close the same gap in LogUniformPrior above upper_limit (its NumPy path currently returns a finite -log(value) there; below 0 it is already -inf). Emcee and Zeus natively treat a -inf log-probability as a rejected proposal, so this makes the phase-1 record's rejection sentence true going forward — rejection is precisely the restoring mechanism MCMC should have, per clipper.py's own doctrine. This is a behaviour change for every existing Emcee/Zeus/Drawer/LBFGS run (chains, acceptance rates and stored results all move), hence the measured before/after. Two knock-ons to handle:
Fitness.log_likelihood_from inverts fom - sum(log_prior); with both -inf that is NaN. Guard the inversion so bookkeeping (history, quick-update, resume sanity check) stays finite.
- For the gradient searches a
-inf objective re-creates the dead-lane failure the phase-1 clipper work characterised — which is exactly what ClipperPriorBox exists for; that pairs with C rather than blocking A.
B. Clipper-style opt-in for the NumPy MCMC searches (rejected). Projecting MCMC proposals onto the box breaks detailed balance (probability mass piles up on the boundary) — the sampled distribution would still not be the declared model, just wrong in a subtler way. Rejection (A) is the correct MCMC mechanism; Clipper remains the right tool for the gradient searches only.
C. Default LBFGS/BFGS to ClipperPriorBox (orthogonal; can land with A or separately). The default ClipperNone passes bounds=None to scipy, so the box is unenforced there too. Making ClipperPriorBox the default gives scipy declarative bounds with no change to the objective. Interacts with A: with a strict prior and no clipper, an LBFGS line-search step that leaves the box sees an infinite objective — C prevents that class of failure.
Detailed implementation plan
Click to expand
Affected Repositories
- PyAutoFit (primary)
- PyAutoMind (phase-1 record correction —
complete/2026/08/prior-support-clipper.md)
Branch Survey (web session — fresh clones)
| Repository |
Current Branch |
Dirty? |
| PyAutoFit |
main @ fe9f813 |
clean |
Suggested branch: feature/uniform-prior-bounds-numpy-path
Blocked: PyAutoFit is currently claimed by stored-sample-reconstruction-guard (feature/stored-sample-reconstruction-guard) and version-stamp-sync-guards (feature/version-stamp-sync-guards) — this task is registered in PyAutoMind/planned.md and cannot start until those ship.
Implementation Steps (written for option A + C; adjust to the decision)
autofit/mapper/prior/uniform.py — log_prior_from_value: evaluate the bound on the NumPy path (0.0 if lower_limit <= value <= upper_limit else -np.inf; scalar-friendly), keep the xp.where JAX branch. Update the docstring ("always zero, provided the value is between the lower and upper limit" — make the proviso real).
autofit/mapper/prior/log_uniform.py — enforce upper_limit on the NumPy path (currently finite -log(value) above it); rewrite the docstring that documents the asymmetry as intended behaviour.
autofit/non_linear/fitness.py — log_likelihood_from: guard the -inf - (-inf) = NaN inversion (e.g. only subtract the prior where the prior sum is finite; a -inf FoM with -inf prior is a rejected point, not a likelihood).
autofit/non_linear/clipper.py — correct the module docstring (lines 20–23): under the strict path the MCMC rejection sentence becomes true; rephrase to describe the actual mechanism, citing this issue.
- (Option C)
autofit/non_linear/search/mle/bfgs/search.py — default clipper=ClipperPriorBox() for LBFGS (and decide for plain BFGS, which ignores bounds — warn per phase-1 finding).
- Tests (
test_autofit): unit tests for log_prior_from_value in/out of bounds on the NumPy path for UniformPrior + LogUniformPrior (parity with JAX path where jax is installed); a seeded fast Emcee regression asserting all accepted samples of an unconstrained parameter stay inside the declared box.
- Before/after measurement on a reference Emcee fit (acceptance fraction, posterior on constrained parameters) recorded in the PR body.
- PyAutoMind: correct the sentence in
complete/2026/08/prior-support-clipper.md (record-correction commit referencing this issue), per the prompt's step 3.
Key Files
autofit/mapper/prior/uniform.py — the unguarded NumPy branch (the bug).
autofit/mapper/prior/log_uniform.py — same gap above upper_limit.
autofit/non_linear/fitness.py — objective assembly (fom_is_log_likelihood=False) + log_likelihood_from inversion.
autofit/non_linear/clipper.py — phase-1 doctrine + the false rejection sentence; ClipperPriorBox for option C.
autofit/non_linear/search/mle/bfgs/search.py — LBFGS bounds plumbing (option C).
PyAutoMind/complete/2026/08/prior-support-clipper.md — the record to correct.
Out of scope (per the Mind prompt)
- Per-parameter step scaling (
active/per_parameter_step_scaling.md) — Emcee is immune to that and fully exposed to this; keep them straight.
- Changing
Prior classes in ways that alter the nested samplers (Nautilus/Dynesty propose in the unit cube and are unaffected; log_prior_from_value does not enter their objective, but any wider Prior surgery is off the table).
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.0on the NumPy path (if xp is np:) without evaluating the bound, while the JAX path immediately below returns-infoutside[lower_limit, upper_limit]. For every search withfom_is_log_likelihood=Falseon the NumPy path (Emcee, Zeus, Drawer, and LBFGS/BFGS under the defaultClipperNone), the declaredUniformPriorbox is therefore not enforced anywhere in the objective — the sampled posterior may not be the declared model.Reproduced. This issue includes the reproduction the Mind prompt asked for as step 1, and the effect is worse than "slow diffusion to the walls": with an unconstrained parameter under
UniformPrior(-0.1, 0.1), a 30-walker × 600-step Emcee fit had 100% of accepted samples outside the box, with the parameter running away to |offset| ≈ 1e14 (max-likelihood sample at offset ≈ 2.1e10). Emcee's affine-invariant stretch move scales proposals by the walker spread, so an unpenalised flat direction grows exponentially, not as a random walk.Plan
Reproduce on clean— done (evidence below), onmainmain@fe9f813.LBFGS/BFGStoClipperPriorBox. Recommendation below: A (+C as an orthogonal follow-up); B rejected for MCMC.-infproposals so the walker simply stays put" — there is no-infto reject on the NumPy path. The sentence lives in two places:PyAutoMind/complete/2026/08/prior-support-clipper.md(~line 290) and theautofit/non_linear/clipper.pymodule docstring (lines 20–23).Reproduction evidence
Environment: fresh clone of
main@fe9f813(autofit 2026.8.17.1), Python 3.12, NumPy path throughout.Part A — the objective directly (
Fitness(model, analysis, fom_is_log_likelihood=False), the Emcee convention), 4-parameter 1D Gaussian model plus anoffsetparameter the likelihood ignores, declaredUniformPrior(-0.1, 0.1):Part B — end-to-end Emcee (
af.Emcee(nwalkers=30, nsteps=600), walkers initialised in-box from the priors):Reproduction script (self-contained, runs on clean main)
The fix decision (human-required)
Three shapes were scoped in the Mind prompt; they are not equivalent:
A. Strict NumPy path (recommended). Make
UniformPrior.log_prior_from_valueevaluate the bound on the NumPy path exactly as the JAX path does (0.0in bounds,-infoutside), and close the same gap inLogUniformPrioraboveupper_limit(its NumPy path currently returns a finite-log(value)there; below 0 it is already-inf). Emcee and Zeus natively treat a-inflog-probability as a rejected proposal, so this makes the phase-1 record's rejection sentence true going forward — rejection is precisely the restoring mechanism MCMC should have, perclipper.py's own doctrine. This is a behaviour change for every existing Emcee/Zeus/Drawer/LBFGS run (chains, acceptance rates and stored results all move), hence the measured before/after. Two knock-ons to handle:Fitness.log_likelihood_frominvertsfom - sum(log_prior); with both-infthat isNaN. Guard the inversion so bookkeeping (history, quick-update, resume sanity check) stays finite.-infobjective re-creates the dead-lane failure the phase-1 clipper work characterised — which is exactly whatClipperPriorBoxexists for; that pairs with C rather than blocking A.B. Clipper-style opt-in for the NumPy MCMC searches (rejected). Projecting MCMC proposals onto the box breaks detailed balance (probability mass piles up on the boundary) — the sampled distribution would still not be the declared model, just wrong in a subtler way. Rejection (A) is the correct MCMC mechanism;
Clipperremains the right tool for the gradient searches only.C. Default
LBFGS/BFGStoClipperPriorBox(orthogonal; can land with A or separately). The defaultClipperNonepassesbounds=Noneto scipy, so the box is unenforced there too. MakingClipperPriorBoxthe default gives scipy declarative bounds with no change to the objective. Interacts with A: with a strict prior and no clipper, an LBFGS line-search step that leaves the box sees an infinite objective — C prevents that class of failure.Detailed implementation plan
Click to expand
Affected Repositories
complete/2026/08/prior-support-clipper.md)Branch Survey (web session — fresh clones)
Suggested branch:
feature/uniform-prior-bounds-numpy-pathBlocked: PyAutoFit is currently claimed by
stored-sample-reconstruction-guard(feature/stored-sample-reconstruction-guard) andversion-stamp-sync-guards(feature/version-stamp-sync-guards) — this task is registered inPyAutoMind/planned.mdand cannot start until those ship.Implementation Steps (written for option A + C; adjust to the decision)
autofit/mapper/prior/uniform.py—log_prior_from_value: evaluate the bound on the NumPy path (0.0iflower_limit <= value <= upper_limitelse-np.inf; scalar-friendly), keep thexp.whereJAX branch. Update the docstring ("always zero, provided the value is between the lower and upper limit" — make the proviso real).autofit/mapper/prior/log_uniform.py— enforceupper_limiton the NumPy path (currently finite-log(value)above it); rewrite the docstring that documents the asymmetry as intended behaviour.autofit/non_linear/fitness.py—log_likelihood_from: guard the-inf - (-inf) = NaNinversion (e.g. only subtract the prior where the prior sum is finite; a-infFoM with-infprior is a rejected point, not a likelihood).autofit/non_linear/clipper.py— correct the module docstring (lines 20–23): under the strict path the MCMC rejection sentence becomes true; rephrase to describe the actual mechanism, citing this issue.autofit/non_linear/search/mle/bfgs/search.py— defaultclipper=ClipperPriorBox()forLBFGS(and decide for plainBFGS, which ignores bounds — warn per phase-1 finding).test_autofit): unit tests forlog_prior_from_valuein/out of bounds on the NumPy path forUniformPrior+LogUniformPrior(parity with JAX path where jax is installed); a seeded fast Emcee regression asserting all accepted samples of an unconstrained parameter stay inside the declared box.complete/2026/08/prior-support-clipper.md(record-correction commit referencing this issue), per the prompt's step 3.Key Files
autofit/mapper/prior/uniform.py— the unguarded NumPy branch (the bug).autofit/mapper/prior/log_uniform.py— same gap aboveupper_limit.autofit/non_linear/fitness.py— objective assembly (fom_is_log_likelihood=False) +log_likelihood_frominversion.autofit/non_linear/clipper.py— phase-1 doctrine + the false rejection sentence;ClipperPriorBoxfor option C.autofit/non_linear/search/mle/bfgs/search.py— LBFGS bounds plumbing (option C).PyAutoMind/complete/2026/08/prior-support-clipper.md— the record to correct.Out of scope (per the Mind prompt)
active/per_parameter_step_scaling.md) — Emcee is immune to that and fully exposed to this; keep them straight.Priorclasses in ways that alter the nested samplers (Nautilus/Dynesty propose in the unit cube and are unaffected;log_prior_from_valuedoes not enter their objective, but any widerPriorsurgery is off the table).Original 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.