Gaussian posterior#

These examples walk through Bayesian inference with Sequince’s standard Sequential Monte Carlo (SMC) sampler. Start with gaussian.py to learn the workflow, then run rosenbrock.py to explore a curved posterior and customize mutation and resampling. Each script contains its own prior, likelihood, and sampler setup. Edit the parameters near the top of the script to experiment.

Setup and running#

Use Python 3.11 or later and install Sequince with the optional plotting and HDF5 dependencies:

python -m pip install 'sequince[corner,hdf5]'

For installation options, including using a development checkout, see “Getting started” in the main Sequince documentation.

Run the script from this directory:

python gaussian.py

The script writes to outdir/gaussian within this directory. Running a script again replaces its previous outputs.

1. Define the target distribution#

Sequince needs functions that evaluate the log prior and log likelihood. Both receive a batch of particles with shape (n_particles, n_dimensions) and a StepContext, and return one value per particle, with shape (n_particles,). Sequince passes the context when evaluating the target. Use context.shared to read static model parameters supplied to smc.run through shared_context. For example, both scripts define the prior as:

def log_prior(particles, context):
    lower_bound = context.shared["lower_bound"]
    upper_bound = context.shared["upper_bound"]
    inside = np.all(
        (particles >= lower_bound) & (particles <= upper_bound), axis=-1
    )
    log_density = -particles.shape[1] * np.log(upper_bound - lower_bound)
    return np.where(inside, log_density, -np.inf)

The Gaussian likelihood reads mean and sigma from the same shared mapping; the Rosenbrock likelihood reads its coefficients a and b. This makes the target functions independent of module-level model settings.

For a parameter vector \(\boldsymbol{\theta}\) with \(d\) dimensions, the examples use independent uniform priors between -5 and 5:

\[\begin{split}\log p(\boldsymbol{\theta}) = \begin{cases} -d \log 10, & \boldsymbol{\theta} \in [-5, 5]^d, \\ -\infty, & \text{otherwise}. \end{cases}\end{split}\]

Including the prior normalization is necessary for a correct evidence estimate.

In gaussian.py, the likelihood is a standard multivariate Gaussian, including its normalization constant (the defaults are mean=0 and sigma=1). The resulting posterior is a standard Gaussian restricted to the prior box. Its mean is zero, and its covariance is very close to the identity for these wide bounds. The script also computes the analytic evidence, including the Gaussian probability mass inside the box.

2. Initialize particles from the prior#

Create a seeded NumPy generator, draw particles from the prior, and put them in a ParticleState:

rng = np.random.default_rng(seed)
initial_state = ParticleState(
    particles=rng.uniform(
        lower_bound, upper_bound, size=(n_particles, n_dimensions)
    ),
)

When log_weights is omitted, ParticleState supplies equal normalized weights, each with log weight \(\log w_i = -\log N\), where \(N\) is the number of particles (n_particles). This is appropriate here because the particles are drawn from the prior, the initial target distribution. There is no need to set the weights explicitly. Supply log_weights when constructing a state that already has nonuniform importance weights.

Keep using the same generator for the sampler. Changing the initialization to a different distribution requires accounting for that proposal; see the main API reference for make_standard_bayesian_smc and its log_proposal option.

3. Configure the sampler#

Use the prebuilt helper to assemble the standard Bayesian SMC workflow:

smc, posterior, metadata = make_standard_bayesian_smc(
    log_prior=log_prior,
    log_likelihood=log_likelihood,
    target_ess_ratio=0.8,
    mutation="minipcn",
    mutation_n_steps=16,
)

The sampler moves through tempered distributions

\[\pi_\beta(\boldsymbol{\theta}) \propto p(\boldsymbol{\theta})\,L(\boldsymbol{\theta})^\beta, \qquad 0 \leq \beta \leq 1,\]

where \(p\) is the prior and \(L\) is the likelihood. It starts at the prior with \(\beta=0\) and finishes at the posterior with \(\beta=1\). Adaptive tempering chooses increments using an effective sample size (ESS) criterion. Resampling concentrates particles in regions of higher weight, and mutation moves help the population explore each intermediate distribution.

The helper returns the SMC runner, a tempered posterior object, and initial metadata. These scripts use the runner and metadata directly; the posterior object is also available for further sampling with draw_additional_samples. See “Configuring the runner” in the main User guide and the helper’s API reference for resampling thresholds, mutation choices, and other options.

4. Run SMC#

Pass the initial population, generator, and helper metadata to the runner:

result = smc.run(
    initial_state,
    rng=rng,
    shared_context={
        "lower_bound": lower_bound,
        "upper_bound": upper_bound,
        "mean": mean,
        "sigma": sigma,
    },
    metadata=metadata,
)

The runner makes shared_context available as context.shared in both target functions. The Gaussian example supplies the bounds, mean, and standard deviation shown above; the Rosenbrock example supplies bounds and its two coefficients. Treat this mapping as read-only during the run.

The helper’s metadata is available as context.metadata and carries evolving information such as the tempering parameter \(\beta\) (context.metadata["beta"]). Sequince’s tempered posterior applies \(\beta\) to the likelihood; the likelihood functions return their ordinary log likelihood, without applying \(\beta\) themselves. context.t identifies the current SMC iteration. The configured sampler stops when it reaches the posterior. Logging shows progress and the running evidence estimate.

See “Meta data & shared context” in the main Getting started guide and “Working with particle state” in the User guide for these interfaces.

5. Inspect and save the result#

result.final_state contains the final particles, their weights, and the log evidence estimate. result.weighted_mean() and result.weighted_covariance() compute posterior summaries using those weights. For the Gaussian, compare the summaries and evidence with the known values. Monte Carlo estimates fluctuate; agreement improves with adequate particles and mixing, rather than being exact for a single run.

Each script saves:

  • corner.png: marginal and joint posterior plots.

  • history.png: the tempering parameter and log evidence over iterations.

  • posterior.npz: particles, normalized weights, and the log evidence estimate.

  • result.h5: the SMC result saved using result.save_to_file.

The arrays can be loaded with numpy.load. Retain the weights when analyzing the particles. See the main API reference for SMCResult for posterior sample extraction, diagnostics, plotting, and result persistence.

Gaussian results#

Download script.

Weighted marginal and joint posterior distributions for the Gaussian target.

Weighted marginal and joint posterior distributions for the Gaussian target.#

Adaptive temperature and accumulated log evidence at each SMC stage.

Adaptive temperature and accumulated log evidence at each SMC stage.#

Run provenance.

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

Continue with Rosenbrock posterior to explore a curved posterior and conditional resampling.