Skip to content

dynesty acceptance-walk: hitting maxmcmc is silent and hidden in the stats plot #1162

Open

Description

@GregoryAshton

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 Nc panel hides it. maxmcmc limits proposals, but Nc counts likelihood calls. A proposal that leaves the unit cube uses up a step without calling the likelihood, so Nc sits below maxmcmc even when every walk is capped.
  • The Scale panel shows it, but it is hard to read. Under bound="live", Scale is the accepted-step count, but it has no reference line at naccept and 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).

  1. Stats plot: add a walk-length panel (accept + reject) with a line at maxmcmc. Relabel Scale as "Accepted steps" under live/live-multi and draw a line at naccept. Relabel Nc as "Likelihood calls".
  2. 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 raising maxmcmc. Warning once per checkpoint, rather than once per walk, keeps the log readable.
  3. 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

  1. GregoryAshton commented on Oct 20, 2026

    @GregoryAshton
    CollaboratorAuthor

    Here is the stats plot with maxmcmc=30

    Image
  2. GregoryAshton commented on Oct 20, 2026

    @GregoryAshton
    CollaboratorAuthor

    I can provide a PR to fix this, but I wanted to first check with @ColmTalbot that I wasn't misunderstanding something.

  3. scttfrdmn commented on Oct 24, 2026

    @scttfrdmn

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions