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:
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#
Weighted posterior samples along the curved Rosenbrock valley.#
Temperature and log evidence for the unnormalized Rosenbrock likelihood.#
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()