Repository navigation
dynesty acceptance-walk: hitting maxmcmc is silent and hidden in the stats plot #1162
Description
With sample="acceptance-walk", bilby adapts walks towards naccept accepted steps, and LivePointSampler.update_user clips it at maxmcmc. When the clip binds, new live points take far fewer than naccept accepted steps, which correlates the live points and can bias the evidence and the posterior. Nothing reports this:
- No warning. The clip is silent. The "Hit maximum number of walks" warning exists only for
sample="rwalk". - The
Ncpanel hides it.maxmcmclimits proposals, butNccounts likelihood calls. A proposal that leaves the unit cube uses up a step without calling the likelihood, soNcsits belowmaxmcmceven when every walk is capped. - The
Scalepanel shows it, but it is hard to read. Underbound="live",Scaleis the accepted-step count, but it has no reference line atnacceptand its label makes it look like a proposal scale.
How often this happens in GW runs
In five GW examples (BBH and BNS, nlive=1000, naccept=60, maxmcmc=5000), 54–70% of iterations were capped, rising to 58–100% in the second half. In a GW150914-like run, every walk from mid-run onwards was capped at about 8 accepted steps, while Nc peaked around 2500. The run finished without a message and its lifetime KS test passed (p = 0.51).
MWE
This runs in about 15 s with dynesty 3.1.0. It uses a small maxmcmc so that the cap binds quickly.
import bilby
import numpy as np
from bilby.core.sampler.dynesty import Dynesty, dynesty_stats_plot
from scipy.stats import multivariate_normal
ndim, naccept, maxmcmc = 8, 60, 30
mean = np.full(ndim, 0.9) # peak near the prior edge
cov = 0.01 * (0.9 * np.ones((ndim, ndim)) + 0.1 * np.eye(ndim))
names = [f"x{ii}" for ii in range(ndim)]
class Gaussian(bilby.Likelihood):
def log_likelihood(self, parameters=None):
x = [parameters[name] for name in names]
return multivariate_normal.logpdf(x, mean=mean, cov=cov)
priors = bilby.core.prior.PriorDict(
{name: bilby.core.prior.Uniform(0, 1, name) for name in names}
)
sampler = Dynesty(
Gaussian(), priors, outdir="outdir", label="mwe", nlive=300,
sample="acceptance-walk", bound="live", naccept=naccept, maxmcmc=maxmcmc,
npool=1, check_point=False, plot=False,
)
sampler.run_sampler()
data = sampler.sampler.saved_run.D
keep = [bool(s) and "accept" in s for s in data["proposal_stats"]]
accept = np.array([s["accept"] for s, k in zip(data["proposal_stats"], keep) if k])
walks = accept + np.array([s["reject"] for s, k in zip(data["proposal_stats"], keep) if k])
nc = np.asarray(data["nc"])[keep]
print(f"capped: {np.mean(walks >= maxmcmc):.0%}")
print(f"median accepted steps: {np.median(accept):.0f} (naccept={naccept})")
print(f"median Nc: {np.median(nc):.0f} (maxmcmc={maxmcmc})")
dynesty_stats_plot(sampler.sampler)[0].savefig("outdir/mwe_stats.png")Output:
capped: 100%
median accepted steps: 6 (naccept=60)
median Nc: 22 (maxmcmc=30)
The run logs no warning. In the stats plot below, I added red lines at maxmcmc (top) and naccept (second panel); bilby does not draw them. Nc sits below the cap, and Scale (accepted steps) sits at about 6 against a target of 60. The KS test still passes.
Rerunning the same Gaussian with the default maxmcmc=5000 gives 0% capped and a median of 61 accepted steps. The ln Z values are 0.07 ± 0.27 capped and −0.31 ± 0.27 uncapped, against a true value of −0.34. That comes from one seed each, so it hints at a bias rather than proving one.
Proposed changes
Everything below can come from the existing proposal_stats (accept and reject per iteration).
- Stats plot: add a walk-length panel (
accept + reject) with a line atmaxmcmc. RelabelScaleas "Accepted steps" underlive/live-multiand draw a line atnaccept. RelabelNcas "Likelihood calls". - Warning: at each checkpoint and at the end of the run, warn if more than about 5% of iterations since the last check were capped with
accept < naccept. The warning would give that fraction and the median accept count, and suggest raisingmaxmcmc. Warning once per checkpoint, rather than once per walk, keeps the log readable. - Metadata: store the capped fraction and the median accept count in
result.meta_data["run_statistics"].
Relation to #1154
#1154 discards walks that are too short before reaching maxmcmc, but it logs capped walks only at debug level, so this case stays silent. Separately, when walks < naccept, binom(n=walks, p=naccept / walks) has p > 1, so ppf returns nan and the check never fires. In the MWE above, for example, the check is skipped on every iteration.
Activity
GregoryAshton commentedon Oct 20, 2026
Here is the stats plot with maxmcmc=30
GregoryAshton commentedon Oct 20, 2026
I can provide a PR to fix this, but I wanted to first check with @ColmTalbot that I wasn't misunderstanding something.
scttfrdmn commentedon Oct 24, 2026
A data point from a GW150914-style run (bilby 2.8.2, dynesty acceptance-walk, naccept=60, maxmcmc=5000): every walk sat at the 5000 cap from about the 1,500th chain onward, and the mean accepted steps per chain fell from 38 to 15, against the naccept=60 target. Doubling naccept to 120 changed almost nothing (20.53M vs 20.46M likelihood calls), because the cap, not naccept, was binding, and nothing in the logs at default level said so.