Rosenbrock posterior#

Start with Gaussian posterior for the prior, particle initialization, and standard SMC workflow. This example keeps the same uniform prior and uses a curved likelihood with differential-evolution mutation and conditional resampling.

Run from this directory with the same dependencies as the Gaussian example:

python rosenbrock.py

Outputs are written to outdir/rosenbrock.

Define the likelihood#

rosenbrock.py follows the same workflow with a Rosenbrock log likelihood:

\[\log L(\boldsymbol{\theta}) = -\sum_{j=1}^{d-1} \left[b\left(\theta_{j+1} - \theta_j^2\right)^2 + \left(a - \theta_j\right)^2\right].\]

The likelihood reads \(a\) and \(b\) from context.shared. With the default \(a=1\) and \(b=100\), the two-dimensional likelihood concentrates around a narrow curved valley near \(\theta_2 = \theta_1^2\), with a maximum at \((\theta_1, \theta_2) = (1, 1)\). Inspect the corner plot for this shape and the history plot for how tempering progresses. This likelihood is unnormalized: the reported evidence uses exactly the convention above, and the script provides no analytic reference value.

Configure mutation and resampling#

The Rosenbrock example also demonstrates more options in the prebuilt helper:

smc, posterior, metadata = make_standard_bayesian_smc(
    log_prior=log_prior,
    log_likelihood=log_likelihood,
    weighting_criterion="cess",
    target_ess_ratio=0.8,
    resampling="stratified",
    always_resample=False,
    resampling_ess_threshold=0.5,
    mutation="differential-evolution",
    mutation_n_steps=32,
    mutation_kwargs={"gamma_one_probability": 0.1},
)

mutation="differential-evolution" proposes moves using scaled differences between other particles in the ensemble. mutation_kwargs passes options to the mutation implementation: here gamma_one_probability=0.1 gives each proposal a 10% chance of using a unit scale instead of the usual dimension-dependent scale. Each mutation stage takes 32 steps.

resampling="stratified" changes the resampling scheme from the Gaussian example’s default systematic scheme. With always_resample=False, resampling occurs when the population ESS falls below \(0.5N\). By default, resample_at_end=True also resamples at the final posterior, even if that threshold has not been crossed. When omitted, always_mutate follows the resolved always_resample value, so mutation runs only on iterations that resample here. Set always_mutate=True to run mutation every iteration.

Use weighting_criterion="cess" with conditional resampling. Conditional ESS (CESS) measures the effect of a proposed temperature increment given the current weighted population; target_ess_ratio=0.8 controls that increment. The separate resampling_ess_threshold=0.5 controls when to resample using the population ESS. Sequince’s ordinary "ess" tempering criterion requires resampling every iteration, so simply setting always_resample=False with that criterion is invalid.

See the main API reference for make_standard_bayesian_smc and DifferentialEvolutionMutation for the available options.

Try changing n_particles, mutation_n_steps, and the seed to assess the stability of the posterior summaries. Increasing target_ess_ratio usually produces smaller tempering increments and more iterations. Increasing particle count or mutation steps increases computational cost. Compare several runs before judging whether a setting explores the posterior adequately.

Rosenbrock results#

Download script.

Weighted posterior samples along the curved Rosenbrock valley.

Weighted posterior samples along the curved Rosenbrock valley.#

Temperature and log evidence for the unnormalized Rosenbrock likelihood.

Temperature and log evidence for the unnormalized Rosenbrock likelihood.#

Run provenance.

  1"""Sample a Rosenbrock posterior using Sequince. See README.rst."""
  2
  3import logging
  4from pathlib import Path
  5
  6import matplotlib.pyplot as plt
  7import numpy as np
  8from sequince import ParticleState, make_standard_bayesian_smc
  9from sequince.log import configure_logging
 10from sequince.steps import StepContext
 11
 12# Edit these parameters to explore the example.
 13n_dimensions = 2
 14n_particles = 2000
 15lower_bound, upper_bound = -5.0, 5.0
 16a, b = 1.0, 100.0
 17seed = 42
 18outdir = Path("outdir/rosenbrock")
 19
 20
 21def log_prior(particles: np.ndarray, context: StepContext) -> np.ndarray:
 22    """Evaluate a normalized uniform prior within the bounds.
 23
 24    Parameters
 25    ----------
 26    particles : numpy.ndarray, shape (n_particles, n_dimensions)
 27        Batch of parameter vectors.
 28    context : StepContext
 29        Runtime context whose shared mapping contains the model parameters.
 30
 31    Returns
 32    -------
 33    log_density : numpy.ndarray, shape (n_particles,)
 34        Log prior density, with negative infinity outside the bounds.
 35    """
 36    lower_bound = context.shared["lower_bound"]
 37    upper_bound = context.shared["upper_bound"]
 38    inside = np.all(
 39        (particles >= lower_bound) & (particles <= upper_bound), axis=-1
 40    )
 41    log_density = -particles.shape[1] * np.log(upper_bound - lower_bound)
 42    return np.where(inside, log_density, -np.inf)
 43
 44
 45def log_likelihood(particles: np.ndarray, context: StepContext) -> np.ndarray:
 46    """Evaluate an unnormalized Rosenbrock likelihood.
 47
 48    Parameters
 49    ----------
 50    particles : numpy.ndarray, shape (n_particles, n_dimensions)
 51        Batch of parameter vectors, with at least two dimensions.
 52    context : StepContext
 53        Runtime context containing Rosenbrock coefficients ``a`` and ``b``.
 54
 55    Returns
 56    -------
 57    log_density : numpy.ndarray, shape (n_particles,)
 58        Negative Rosenbrock function values. No likelihood normalization
 59        constant is included, so evidence refers to this convention.
 60    """
 61    a = context.shared["a"]
 62    b = context.shared["b"]
 63    return -np.sum(
 64        b * (particles[:, 1:] - particles[:, :-1] ** 2) ** 2
 65        + (a - particles[:, :-1]) ** 2,
 66        axis=-1,
 67    )
 68
 69
 70def main() -> None:
 71    """Sample the posterior and save particles, plots, and the SMC result."""
 72    configure_logging(level=logging.INFO)
 73    rng = np.random.default_rng(seed)
 74    outdir.mkdir(parents=True, exist_ok=True)
 75
 76    # ParticleState supplies equal weights, appropriate for prior draws.
 77    initial_state = ParticleState(
 78        particles=rng.uniform(
 79            lower_bound, upper_bound, size=(n_particles, n_dimensions)
 80        ),
 81    )
 82
 83    # CESS-based tempering allows weights to accumulate between resampling.
 84    smc, posterior, metadata = make_standard_bayesian_smc(
 85        log_prior=log_prior,
 86        log_likelihood=log_likelihood,
 87        weighting_criterion="cess",
 88        target_ess_ratio=0.8,
 89        resampling="stratified",
 90        always_resample=False,
 91        resampling_ess_threshold=0.5,
 92        mutation="differential-evolution",
 93        mutation_n_steps=32,
 94        mutation_kwargs={"gamma_one_probability": 0.1},
 95    )
 96    result = smc.run(
 97        initial_state,
 98        rng=rng,
 99        shared_context={
100            "lower_bound": lower_bound,
101            "upper_bound": upper_bound,
102            "a": a,
103            "b": b,
104        },
105        metadata=metadata,
106    )
107
108    state = result.final_state
109    print(f"Estimated log evidence: {state.log_evidence:.4f}")
110
111    print(f"Posterior mean: {result.weighted_mean()}")
112    print(f"Posterior covariance:\n{result.weighted_covariance()}")
113
114    fig = result.plot_corner()
115    fig.savefig(outdir / "corner.png")
116    plt.close(fig)
117
118    fig, axes = plt.subplots(2, 1, figsize=(6, 6))
119    result.plot_metadata_history("beta", ax=axes[0])
120    axes[0].set_ylabel("Tempering parameter beta")
121    result.plot_log_evidence(ax=axes[1])
122    fig.tight_layout()
123    fig.savefig(outdir / "history.png")
124    plt.close(fig)
125
126    # Keep weights with the particles so subsequent analysis remains valid.
127    np.savez(
128        outdir / "posterior.npz",
129        particles=state.particles,
130        weights=state.weights,
131        log_evidence=state.log_evidence,
132    )
133    result.save_to_file(outdir / "result.h5")
134
135
136if __name__ == "__main__":
137    main()