Sampling
One-step Markov chain Monte Carlo moves, importance and rejection sampling, variance reduction, particle filtering and stick-breaking priors, driven by draws you supply.
Prelude.Sampling provides the moves of the common Monte Carlo samplers. Metropolis–Hastings, MALA,
Hamiltonian Monte Carlo, slice sampling, Gibbs sampling, parallel tempering and simulated annealing
are all the same loop with a different proposal: carry the state, read a proposal draw and an
acceptance draw, and accept by a 0/1 mask rather than a branch. Each definition here is one step
of that loop. It also covers rejection and importance sampling with the effective sample size,
antithetic and control variates, the pieces of a bootstrap particle filter, and the stick-breaking
construction behind Dirichlet-process and related nonparametric priors.
Every random number is an argument. A step takes, for example, a standard normal draw z and a
uniform draw u in [0, 1), and returns the next state; you generate the draws in C# and run the
chain by calling the step repeatedly (or wrap it in a recursive definition of your own). Every
definition works elementwise, so passing vectors of states and draws advances many independent chains
in one call.
The important limitation: the target is a Gaussian. This language has no function-valued
parameters, so the samplers cannot take an arbitrary density. Each one targets the normal distribution
\(N(\mu, \sigma^2)\) whose mean mu and standard deviation sigma are arguments (Gibbs targets a
standard bivariate normal with correlation rho). That is enough to learn, test and calibrate the
samplers against a known answer; it is not a way to sample an arbitrary posterior.
Notation used in the statements below. Unless a definition sums over a vector, it acts entrywise, so
each statement is written for real scalars and applies to every entry of a vector argument. Many
divisors pass through the guard nonzero of Prelude.Chains,
which leaves every divisor of magnitude at least \(10^{-300}\) unchanged. For the Gaussian target we write \(\ell(x) = \operatorname{logTarget}(x, \mu, \sigma)\) and \(g(x) = \operatorname{slopeOf}(x, \mu, \sigma)\).
open Prelude.Sampling
The target and its gradient
The target’s log density, up to an additive constant, and its derivative:
\[\log \pi(x) = -\frac{(x - \mu)^2}{2\sigma^2}, \qquad \frac{d}{dx}\log \pi(x) = \frac{\mu - x}{\sigma^2}\]A standard deviation of zero is nudged to a tiny positive value rather than dividing by zero.
logTarget
logTarget(x, mu, sigma) · x state scalar or vector · mu mean scalar · sigma standard deviation scalar → same shape as x
Let \(x \in \mathbb{R}\) be a state, \(\mu \in \mathbb{R}\) the mean and \(\sigma > 0\) the standard deviation. Then
\[\operatorname{logTarget}(x, \mu, \sigma) = \ell(x) = -\frac{(x - \mu)^2}{\operatorname{nonzero}(2\sigma^2)} .\]The unnormalised log density \(\log \pi(x)\) of \(N(\mu, \sigma^2)\). Its maximum is 0, at \(x = \mu\).
Intuition MCMC never needs a density’s normalising constant, only how much more likely one point is than another. Working in logs turns those ratios into differences and avoids underflow far in the tails; dropping the constant makes the peak exactly 0, so values read as “log-likelihood relative to the best point”.
Use it when you compute an acceptance ratio by hand, a tempered density with tempered, or a Gaussian likelihood (it is exactly what particleWeights exponentiates). It is the only target the samplers here know, so it is most useful for testing a sampler against a known answer rather than for real posteriors.
density-evaluationsampler-validation
def logTarget(x, mu, sigma) ≔ 0 − (x − mu) · (x − mu) / nonzero(2 · sigma · sigma)
slopeOf
slopeOf(x, mu, sigma) · x state scalar or vector · mu mean scalar · sigma standard deviation scalar → same shape as x
Let \(x \in \mathbb{R}\), \(\mu \in \mathbb{R}\) and \(\sigma > 0\). Then
\[\operatorname{slopeOf}(x, \mu, \sigma) = g(x) = \frac{\mu - x}{\operatorname{nonzero}(\sigma^2)} .\]The gradient of the log density, \((\mu - x)/\sigma^2\), which MALA and Hamiltonian Monte Carlo follow uphill.
Intuition An arrow pointing toward higher probability, longer the further you are from the mode and the narrower the target. For a Gaussian it is a spring pulling back to \(\mu\) with stiffness \(1/\sigma^2\).
Use it when you want proposals that head where the probability is instead of wandering blindly — it is the force in malaNext and hmcNext — or when you check a gradient-based sampler’s arithmetic against a closed form. Random-walk hastingsNext and sliceNext do not need it.
gradient-based-samplingsampler-validation
def slopeOf(x, mu, sigma) ≔ (mu − x) / nonzero(sigma · sigma)
keeps
keeps(logRatio, u) · logRatio log acceptance ratio scalar or vector · u uniform draw in [0, 1) → mask
Let \(\lambda \in \mathbb{R}\) (the argument logRatio) be a log acceptance ratio and let
\(u \in [0, 1)\) be a uniform draw. Then
The Metropolis acceptance test: 1 when \(u < \min(1, e^{\text{logRatio}})\), 0 otherwise. Every sampler below accepts through it.
Intuition Always take a move that is at least as good; take a worse one with probability equal to how much worse it is. That balance — every step into a region matched by steps out in proportion to its probability — is what makes the chain spend time exactly according to the target. Returning a 0/1 mask lets a vector of chains accept or reject independently without a branch.
Use it when you write any Metropolis-style step, including ones not on this page: pass the log ratio you computed (with any Hastings correction) and a fresh uniform. It also accepts parallel-tempering swaps (swapRatio). For a cost to minimise at a temperature use annealAccepts.
bayesian-posterior-samplingaccept-reject-decisions
def keeps(logRatio, u) ≔ u < exp(min(logRatio, 0))
Metropolis–Hastings
Random-walk Metropolis proposes \(x' = x + \text{step}\cdot z\) and accepts with probability \(\min(1, \pi(x')/\pi(x))\). The proposal is symmetric, so the Hastings correction cancels and the ratio is the target alone.
hastingsNext
hastingsNext(x, step, mu, sigma, z, u) · x current state · step proposal scale scalar · mu mean · sigma standard deviation · z standard normal draw · u uniform draw → next state
Let \(x \in \mathbb{R}\) be the current state, \(h > 0\) (the argument step) the proposal scale,
\(\mu \in \mathbb{R}\) and \(\sigma > 0\), let \(z \in \mathbb{R}\) be a standard normal draw and
\(u \in [0, 1)\) a uniform draw, and let \(x' = x + h z\). Then
One random-walk Metropolis step: the proposal if it is accepted, otherwise x unchanged. Shapes
follow x; pass vectors of x, z and u to step many chains at once.
Intuition A blind hiker who steps in a random direction and keeps the step if it goes uphill, or sometimes if it goes downhill. Only the target’s ratio is used, so it works where nothing but the (unnormalised) density can be evaluated.
Use it when you want the simplest, most robust MCMC baseline, or a reference to compare other samplers against. Tune step so that roughly a quarter to a half of proposals are accepted; in one dimension about \(2.4\sigma\) is near optimal. Too small and the chain crawls, too large and it rarely moves. It mixes slowly in many dimensions, where malaNext or hmcNext use the gradient to do better.
bayesian-posterior-samplingmonte-carlo-estimationstep-size-tuning
def hastingsNext(x, step, mu, sigma, z, u) ≔ x + (walkProposal(x, step, z) − x) · keeps(hastingsRatio(x, walkProposal(x, step, z), mu, sigma), u)
Building blocks
walkProposal(x, step, z)— Let \(x \in \mathbb{R}\), \(h > 0\) (the argumentstep) and \(z \in \mathbb{R}\) a standard normal draw. Then \(\operatorname{walkProposal}(x, h, z) = x + h z\). The random-walk proposal,xplussteptimes a normal draw.hastingsRatio(x, y, mu, sigma)— Let \(x, y \in \mathbb{R}\), \(\mu \in \mathbb{R}\) and \(\sigma > 0\). Then \(\operatorname{hastingsRatio}(x, y, \mu, \sigma) = \ell(y) - \ell(x)\). The log acceptance ratio of moving fromxtoy.
MALA
The Metropolis-adjusted Langevin algorithm drifts the proposal uphill along the gradient, so more proposals are worth keeping:
\[x' = x + \frac{h}{2} \nabla \log \pi(x) + \sqrt{h}\, z\]with step size \(h\). This proposal is not symmetric, so the Hastings correction \(q(x \mid x') / q(x' \mid x)\) must be carried. Leaving it out is the classic MALA mistake: the chain still moves and looks plausible, but converges to the wrong distribution.
malaNext
malaNext(x, step, mu, sigma, z, u) · x current state · step step size h scalar · mu mean · sigma standard deviation · z standard normal draw · u uniform draw → next state
Let \(x \in \mathbb{R}\) be the current state, \(h > 0\) (the argument step) the step size,
\(\mu \in \mathbb{R}\) and \(\sigma > 0\), let \(z \in \mathbb{R}\) be a standard normal draw and
\(u \in [0, 1)\) a uniform draw. Let \(m(a) = a + \tfrac{h}{2} g(a)\),
\(\log q(b \mid a) = -\dfrac{(b - m(a))^2}{\operatorname{nonzero}(2h)}\), and \(x' = m(x) + \sqrt{h}\, z\). Then
One MALA step with the full Hastings correction: the drifted proposal if accepted, otherwise x.
Intuition A random walk with a tilt: first slide downhill in energy (uphill in probability) along the gradient, then add noise. That is a discretised Langevin diffusion, whose stationary distribution is the target; the Metropolis test with the Hastings correction repairs the error the discretisation introduces.
Use it when the gradient is available and a random walk mixes too slowly, especially as dimension grows. Aim for an acceptance rate around 50–60% and shrink step if it falls well below. The step is a variance, so it scales like \(\sigma^2\), not \(\sigma\). For longer moves per step use hmcNext; if no gradient exists use hastingsNext or sliceNext.
bayesian-posterior-samplinggradient-based-samplingstep-size-tuning
def malaNext(x, step, mu, sigma, z, u) ≔ x + (malaProposal(x, step, mu, sigma, z) − x) · keeps(malaRatio(x, malaProposal(x, step, mu, sigma, z), step, mu, sigma), u)
Building blocks
malaMean(x, step, mu, sigma)— Let \(x \in \mathbb{R}\), \(h > 0\) (the argumentstep), \(\mu \in \mathbb{R}\) and \(\sigma > 0\). Then \(\operatorname{malaMean}(x, h, \mu, \sigma) = m(x) = x + \tfrac{h}{2} g(x)\). The centre of the proposal,xmoved half a step along the gradient.malaProposal(x, step, mu, sigma, z)— Let \(x\), \(h\), \(\mu\), \(\sigma\) be as formalaMeanand \(z \in \mathbb{R}\) a standard normal draw. Then \(\operatorname{malaProposal}(x, h, \mu, \sigma, z) = m(x) + \sqrt{h}\, z\). The drifted proposal, the centre plussqrt(step)times a normal draw.malaLogQ(from, to, step, mu, sigma)— Let \(a, b \in \mathbb{R}\) (the argumentsfrom,to), \(h > 0\), \(\mu \in \mathbb{R}\) and \(\sigma > 0\). Then \(\operatorname{malaLogQ}(a, b, h, \mu, \sigma) = \log q(b \mid a) = -(b - m(a))^2 / \operatorname{nonzero}(2h)\). The unnormalised log density of proposingtofromfrom.malaRatio(x, y, step, mu, sigma)— Let \(x, y \in \mathbb{R}\), \(h > 0\), \(\mu \in \mathbb{R}\) and \(\sigma > 0\). Then \(\operatorname{malaRatio}(x, y, h, \mu, \sigma) = \ell(y) - \ell(x) + \log q(x \mid y) - \log q(y \mid x)\). The log acceptance ratio including the Hastings correction.
Hamiltonian Monte Carlo
Hamiltonian Monte Carlo gives the state a momentum \(p\) and simulates the dynamics of the energy
\[H(x, p) = -\log \pi(x) + \tfrac{1}{2} p^2\]with the leapfrog integrator (half kick, drift, half kick), then accepts with probability
\(\min(1, e^{H(x,p) - H(x',p')})\). Each call takes one leapfrog step from a momentum draw you
supply (a fresh standard normal per call). Leapfrog is symplectic, so for a sensible step size the
energy wanders but does not drift; a step that is too large makes it diverge, which a good acceptance
rate alone can hide. Use diverged to detect that.
In this section let \(x \in \mathbb{R}\) be the position, \(p \in \mathbb{R}\) the momentum,
\(h > 0\) (the argument step) the leapfrog step size, \(\mu \in \mathbb{R}\) and \(\sigma > 0\), and
write one leapfrog step as
energyOf
energyOf(x, p, mu, sigma) · x position · p momentum · mu mean · sigma standard deviation → energy
Let \(x, p \in \mathbb{R}\), \(\mu \in \mathbb{R}\) and \(\sigma > 0\). Then
\[\operatorname{energyOf}(x, p, \mu, \sigma) = H(x, p) = -\ell(x) + \tfrac{1}{2} p^2 .\]The total energy \(H(x, p)\): potential \(-\log \pi(x)\) plus kinetic \(p^2/2\).
Intuition Picture a frictionless puck on a bowl whose height is \(-\log \pi\): low where probability is high. Its speed trades height for motion, and the total energy is conserved by exact dynamics. Any change in \(H\) after a leapfrog step is purely integration error, which is why the acceptance test is built from it.
Use it when you diagnose Hamiltonian Monte Carlo: track the energy change across steps (small and unbiased means the step size is fine), or compute the acceptance ratio for a longer trajectory you integrate yourself. diverged wraps the check for one step.
sampler-validationgradient-based-sampling
def energyOf(x, p, mu, sigma) ≔ 0 − logTarget(x, mu, sigma) + p · p / 2
hmcNext
hmcNext(x, p, step, mu, sigma, u) · x current position · p momentum, a standard normal draw · step leapfrog step size scalar · mu mean · sigma standard deviation · u uniform draw → next position
Let \(x\), \(p\), \(h\), \(\mu\), \(\sigma\) and \(x', p'\) be as above, with \(p\) a standard normal draw, and let \(u \in [0, 1)\) be a uniform draw. Then
\[\operatorname{hmcNext}(x, p, h, \mu, \sigma, u) = x + (x' - x) \, \big[\, u < e^{\min(H(x, p) - H(x', p'),\, 0)} \,\big] .\]One Hamiltonian Monte Carlo step with a single leapfrog step: the new position if the energy test
accepts it, otherwise x.
Intuition Give the state a random kick of momentum and let it coast along the bowl of \(-\log \pi\) for a moment. Physics carries it toward and across regions of high probability instead of diffusing, and the energy test corrects the integrator’s error.
Use it when you study or test HMC mechanics against a known Gaussian. Draw a fresh standard normal p every call. Note that with a single leapfrog step HMC is equivalent to MALA (with MALA step \(h^2\)), so the long, far-reaching moves that make HMC powerful need many leapfrog steps chained by your own code. Watch diverged when tuning step.
bayesian-posterior-samplinggradient-based-sampling
def hmcNext(x, p, step, mu, sigma, u) ≔ x + (leapX(x, p, step, mu, sigma) − x) · keeps(hmcRatio(x, p, step, mu, sigma), u)
diverged
diverged(x, p, step, mu, sigma) · x position · p momentum · step step size · mu mean · sigma standard deviation → mask
Let \(x\), \(p\), \(h\), \(\mu\), \(\sigma\) and \(x', p'\) be as above. Then
\[\operatorname{diverged}(x, p, h, \mu, \sigma) = \big[\, \lvert H(x, p) - H(x', p') \rvert > 10 \,\big] .\]1 when the leapfrog step changes the energy by more than 10, the sign of a divergent step size. Count the ones across chains or steps to diagnose a step that is too large.
Intuition Exact dynamics conserve energy, so a large energy jump means the integrator has stopped following the physics. An error of 10 corresponds to an acceptance probability of about \(e^{-10}\) when energy rises, and a step producing such errors is unstable, not merely inefficient.
Use it when you tune the HMC step size or check a run’s trustworthiness: a nonzero share of divergences means the step is too large for some region of the target, and the samples there are unreliable. Shrink step until the count is zero. The threshold 10 is fixed; compute energyOf differences yourself for a different cutoff.
sampler-validationstep-size-tuning
def diverged(x, p, step, mu, sigma) ≔ abs(hmcRatio(x, p, step, mu, sigma)) > 10
Building blocks
kickMomentum(p, x, step, mu, sigma)— Let \(p, x \in \mathbb{R}\), \(h > 0\) (the argumentstep), \(\mu \in \mathbb{R}\) and \(\sigma > 0\). Then \(\operatorname{kickMomentum}(p, x, h, \mu, \sigma) = p + \tfrac{h}{2} g(x)\). A half-step momentum update along the gradient.driftPosition(x, p, step)— Let \(x, p \in \mathbb{R}\) and \(h > 0\). Then \(\operatorname{driftPosition}(x, p, h) = x + h p\). A full-step position update along the momentum.leapX(x, p, step, mu, sigma)— Let \(x\), \(p\), \(h\), \(\mu\), \(\sigma\) be as above. Then \(\operatorname{leapX}(x, p, h, \mu, \sigma) = x' = x + h\big(p + \tfrac{h}{2} g(x)\big)\). The position after one leapfrog step.leapP(x, p, step, mu, sigma)— Let \(x\), \(p\), \(h\), \(\mu\), \(\sigma\) be as above. Then \(\operatorname{leapP}(x, p, h, \mu, \sigma) = p' = p + \tfrac{h}{2} g(x) + \tfrac{h}{2} g(x')\). The momentum after one leapfrog step.hmcRatio(x, p, step, mu, sigma)— Let \(x\), \(p\), \(h\), \(\mu\), \(\sigma\) be as above. Then \(\operatorname{hmcRatio}(x, p, h, \mu, \sigma) = H(x, p) - H(x', p')\). The energy before the leapfrog step minus the energy after it: the log acceptance ratio.
Slice sampling
Slice sampling has no acceptance rate and no step size to tune. Draw a height uniformly under the density at the current point, then draw uniformly from the set of points where the density is above that height. For a Gaussian that set is an interval with a closed form,
\[\mu \pm \sigma \sqrt{-2 \log y}\]where \(\log y = \log \pi(x) + \log u\), so no stepping-out procedure is needed.
sliceNext
sliceNext(x, mu, sigma, u, v) · x current state · mu mean · sigma standard deviation · u uniform draw for the height · v uniform draw along the slice → next state
Let \(x \in \mathbb{R}\) be the current state, \(\mu \in \mathbb{R}\) and \(\sigma > 0\), and let \(u, v \in [0, 1)\) be independent uniform draws. Let \(y = \ell(x) + \ln \operatorname{nonzero}(u)\) and \(w = \sigma \sqrt{\max(-2y, 0)}\). Then
\[\operatorname{sliceNext}(x, \mu, \sigma, u, v) = \mu - w + 2 w v .\]One slice-sampling step. Every step moves; there is nothing to reject.
Intuition Sampling from a density is the same as sampling a point uniformly from the area under its curve. Alternate: pick a random height below the curve at the current point, then pick uniformly among all points where the curve is higher than that. The slice adapts its own width to the target, so there is no step size.
Use it when you want a sampler with nothing to tune and no wasted proposals. Here the slice is exact because the target is Gaussian; for general densities the slice must be found by stepping out and shrinking, which this definition does not do. Successive states are still correlated through the height draw, so thin or run several chains as for any MCMC.
bayesian-posterior-samplingsampler-validation
def sliceNext(x, mu, sigma, u, v) ≔ mu − sliceHalfWidth(sliceHeight(x, mu, sigma, u), sigma) + 2 · sliceHalfWidth(sliceHeight(x, mu, sigma, u), sigma) · v
Building blocks
sliceHeight(x, mu, sigma, u)— Let \(x \in \mathbb{R}\), \(\mu \in \mathbb{R}\), \(\sigma > 0\) and \(u \in [0, 1)\) a uniform draw. Then \(\operatorname{sliceHeight}(x, \mu, \sigma, u) = \ell(x) + \ln \operatorname{nonzero}(u)\). The log height of the slice, drawn under the density atx.sliceHalfWidth(h, sigma)— Let \(y \in \mathbb{R}\) (the argumenth) be a log height and \(\sigma > 0\). Then \(\operatorname{sliceHalfWidth}(y, \sigma) = \sigma \sqrt{\max(-2y, 0)}\). Half the width of the slice at log heighth.
Gibbs sampling
A Gibbs move samples one coordinate exactly from its conditional given the others, so every draw is accepted. For a standard bivariate normal with correlation \(\rho\),
\[x \mid y \sim N(\rho y, \; 1 - \rho^2)\]A sweep draws the first coordinate given the second, then the second given the new first.
gibbsDraw
gibbsDraw(other, rho, z) · other current value of the other coordinate · rho correlation in (−1, 1) scalar · z standard normal draw → new value of this coordinate
Let \(y \in \mathbb{R}\) (the argument other) be the current value of the other coordinate, let
\(-1 < \rho < 1\) and let \(z \in \mathbb{R}\) be a standard normal draw. Then
One Gibbs update of a standard bivariate normal: \(\rho \cdot \text{other} + \sqrt{1 - \rho^2}\, z\).
Intuition A hard joint distribution broken into easy one-dimensional pieces: freeze one coordinate, draw the other exactly from its conditional, then swap roles. The chain moves only parallel to the axes, so when the variables are strongly correlated it zig-zags in tiny steps along a narrow diagonal ridge.
Use it when each conditional has a closed form — conjugate hierarchical models, latent-variable models — or to demonstrate how correlation slows mixing (compare \(\rho = 0.5\) with \(\rho = 0.99\)). This definition is only for zero means and unit variances; for other bivariate normals standardise first. When correlation is high, reparametrise or use a joint move such as hmcNext.
bayesian-posterior-samplingmultivariate-sampling
def gibbsDraw(other, rho, z) ≔ gibbsMean(other, rho) + gibbsSpread(rho) · z
Building blocks
gibbsMean(other, rho)— Let \(y \in \mathbb{R}\) (the argumentother) and \(-1 < \rho < 1\). Then \(\operatorname{gibbsMean}(y, \rho) = \rho y\). The conditional mean,rhotimes the other coordinate.gibbsSpread(rho)— Let \(-1 < \rho < 1\). Then \(\operatorname{gibbsSpread}(\rho) = \sqrt{\max(1 - \rho^2, 0)}\). The conditional standard deviation, the square root of 1 −rhosquared.
Tempering and annealing
Raising the temperature \(T\) flattens a target to \(\pi(x)^{1/T}\), so a hot chain wanders freely between modes that a cold chain cannot cross. Parallel tempering runs chains at several temperatures and proposes swapping their states; the swap has its own Metropolis ratio, which keeps every chain correct:
\[\log \alpha = \Big(\frac{1}{T_c} - \frac{1}{T_h}\Big)\big(\log \pi(x_h) - \log \pi(x_c)\big)\]Simulated annealing is tempering with a falling temperature and no swap: always accept a move to a lower cost, accept a move to a higher cost sometimes, and less often as it cools. A schedule that cools too fast freezes in the first basin it finds.
tempered
tempered(logp, temperature) · logp log density · temperature T → log density
Let \(\lambda \in \mathbb{R}\) (the argument logp) be a log density value and \(T > 0\) the temperature. Then
The tempered log density \(\log \pi / T\).
Intuition Heat melts the landscape: dividing the log density by \(T > 1\) shrinks every height difference, so peaks get lower and valleys shallower and a chain can walk between modes. \(T < 1\) sharpens the peaks instead, and \(T \to 0\) concentrates everything on the maximum.
Use it when you build a ladder of chains for parallel tempering, anneal toward a mode, or flatten a posterior during warm-up. For a Gaussian, temperature \(T\) is the same as scaling \(\sigma\) by \(\sqrt{T}\). The swap test between two tempered chains is swapRatio.
multimodal-samplingglobal-optimisation
def tempered(logp, temperature) ≔ logp / nonzero(temperature)
swapRatio
swapRatio(xCold, xHot, mu, sigma, cold, hot) · xCold state of the cold chain · xHot state of the hot chain · mu mean · sigma standard deviation · cold cold temperature · hot hot temperature → log acceptance ratio
Let \(x_c, x_h \in \mathbb{R}\) (the arguments xCold, xHot) be the states of the two chains,
\(\mu \in \mathbb{R}\) and \(\sigma > 0\), and let \(T_c, T_h > 0\) (the arguments cold, hot) be their
temperatures. Then
with each temperature passed through \(\operatorname{nonzero}\).
The log acceptance ratio for exchanging the two chains’ states. Accept the swap with
keeps(swapRatio(…), u).
Intuition The hot chain scouts, the cold chain exploits. A swap hands a good state found by the hot chain down to the cold one, and it is accepted readily when the hot chain’s state is more probable than the cold chain’s. Because the swap is itself a Metropolis move on the joint system, every chain keeps its own correct distribution.
Use it when the target has several separated modes and a single chain gets stuck in one. Space temperatures so neighbouring chains accept swaps a reasonable share of the time (roughly 20–40%); if swaps almost never succeed, add intermediate temperatures. Only samples of the chain at \(T = 1\) belong to the target.
multimodal-samplingbayesian-posterior-sampling
def swapRatio(xCold, xHot, mu, sigma, cold, hot) ≔ (tempered(logTarget(xHot, mu, sigma), cold) + tempered(logTarget(xCold, mu, sigma), hot)) − (tempered(logTarget(xCold, mu, sigma), cold) + tempered(logTarget(xHot, mu, sigma), hot))
coolingAt
coolingAt(start, rate, k) · start initial temperature · rate cooling rate · k step number → temperature
Let \(T_0 > 0\) (the argument start) be the initial temperature, \(r \ge 0\) (the argument rate)
the cooling rate and \(k \ge 0\) the step number. Then
The exponential cooling schedule \(T_k = T_0 \, e^{-\text{rate}\cdot k}\).
Intuition Start hot enough to roam freely, then cool by the same fraction every step, so the search spends as many steps going from \(T_0\) to \(T_0/10\) as from \(T_0/10\) to \(T_0/100\). It is the same shape as decayed in Policies, written with the rate in the exponent.
Use it when you schedule simulated annealing (feed it to annealAccepts). Choose \(T_0\) comparable to typical cost differences so most uphill moves are accepted at first, and a rate so the temperature is small relative to cost differences by the last step, about \(\ln(T_0/T_{\text{end}})/k_{\max}\). Cooling too fast freezes in the first basin found; the logarithmic schedules with convergence guarantees are far slower.
global-optimisationhyperparameter-choice
def coolingAt(start, rate, k) ≔ start · exp(0 − rate · k)
annealAccepts
annealAccepts(here, there, temperature, u) · here cost of the current state · there cost of the proposed state · temperature current temperature · u uniform draw → mask
Let \(c, c' \in \mathbb{R}\) (the arguments here, there) be the costs of the current and proposed
states, \(T > 0\) the temperature and \(u \in [0, 1)\) a uniform draw. Then
The annealing acceptance test for a cost to be minimised: 1 when the proposal is no worse, otherwise 1 with probability \(e^{-(\text{there} - \text{here})/T}\).
Intuition Greedy descent that is allowed to climb out of a hole. At high temperature a step uphill in cost is nearly free, so the search crosses ridges; as the temperature falls it becomes greedy and settles into a deep basin. Temperature is measured in the units of the cost.
Use it when you minimise a cost with many local minima and no useful gradient — scheduling, routing, layout, combinatorial assignment. Propose a neighbour in C#, test it with this mask at coolingAt(start, rate, k), and keep the best state ever seen as well as the current one. It does not sample a distribution; for that use keeps with a log-density ratio.
global-optimisationcombinatorial-optimisation
def annealAccepts(here, there, temperature, u) ≔ u < exp(min((here − there) / nonzero(temperature), 0))
Rejection and importance sampling
Rejection sampling is exact and wasteful: it keeps a draw when a uniform falls under the ratio of the target to an envelope, so the survivors come exactly from the target and the price is everything thrown away. Importance sampling is approximate and keeps everything: each draw is weighted by \(\pi(x)/q(x)\) instead. An importance estimate is only as good as its weights are even, and the effective sample size
\[\mathrm{ESS} = \frac{\left(\sum_i w_i\right)^2}{\sum_i w_i^2}\]says how many equally weighted draws it is worth: \(n\) when the weights are equal, and 1 when a single draw carries everything.
underEnvelope
underEnvelope(x, mu, sigma, bound, u) · x proposed points vector · mu mean · sigma standard deviation · bound envelope height scalar · u uniform draws vector → mask vector
Let \(x \in \mathbb{R}^n\) be proposed points, \(\mu \in \mathbb{R}\), \(\sigma > 0\), let \(B \ge 1\)
(the argument bound) be the envelope height and \(u \in [0, 1)^n\) independent uniform draws. Then
Rejection sampling under a flat envelope of height bound: 1 for each proposal to keep, when
\(u \cdot \text{bound} < \pi(x)\). The target is the unnormalised Gaussian whose peak is 1, so the
envelope needs bound of at least 1; draw x uniformly over the region of interest.
Intuition Throw darts uniformly at a box that contains the curve and keep those that land under it. The survivors’ positions are distributed exactly like the density, and the empty space in the box is the price.
Use it when you need independent, exact draws in low dimension, or a ground truth to test an MCMC sampler against. Keep bound as close to 1 as possible and the region only as wide as the target’s support you care about: mass outside the region is never proposed, so a region of \(\mu \pm 5\sigma\) loses almost nothing. In more than a few dimensions nearly every dart misses; use MCMC or importanceWeight there. acceptedShare measures the waste.
random-variate-generationsampler-validation
def underEnvelope(x, mu, sigma, bound, u) ≔ u · bound < exp(logTarget(x, mu, sigma))
acceptedShare
acceptedShare(x, mu, sigma, bound, u) · arguments as for underEnvelope → scalar
Let \(x\), \(\mu\), \(\sigma\), \(B\) and \(u \in [0, 1)^n\) be as for underEnvelope. Then
The fraction of proposals accepted, a measure of how tight the envelope is.
Intuition The share of darts under the curve equals the area under the curve divided by the area of the box. So it is both an efficiency score and a Monte Carlo estimate of an integral.
Use it when you tune the envelope (the share falls as bound or the region grows, so keep both as small as correctness allows), budget how many proposals are needed for a given number of samples (divide by the share), or estimate an unnormalised density’s integral over a region of length \(L\) as share × bound × \(L\) (hit-or-miss integration).
sampler-validationmonte-carlo-estimationnormalising-constants
def acceptedShare(x, mu, sigma, bound, u) ≔ einsum("i->", underEnvelope(x, mu, sigma, bound, u)) / count(u)
importanceWeight
importanceWeight(x, mu, sigma, proposalSpread) · x draws from the proposal · mu target mean · sigma target standard deviation · proposalSpread proposal standard deviation → weights
Let \(x \in \mathbb{R}\) be a draw from the proposal, \(\mu \in \mathbb{R}\) and \(\sigma > 0\) the target’s
mean and standard deviation, and \(\tau > 0\) (the argument proposalSpread) the proposal’s standard
deviation. Then
Importance weights for draws from the proposal \(N(0, \text{proposalSpread}^2)\) (centred at zero)
towards the target \(N(\mu, \sigma^2)\). Both densities are unnormalised, so the weights are correct
up to one common factor; that factor cancels in weightedMeanOf and effectiveSize.
Intuition You sampled from the wrong distribution, so correct for it: each draw counts in proportion to how much more (or less) often the target would have produced it than the proposal did. Draws the proposal over-produced are down-weighted, under-produced ones up-weighted.
Use it when sampling the target is hard but a simple proposal is easy — estimating expectations, reweighting simulation results to new parameters, or off-policy evaluation. The proposal here is fixed at mean 0; make it wider than the target: with \(\tau\) below \(\sigma/\sqrt{2}\) the weights have infinite variance and a few huge ones dominate. Always check effectiveSize. When you need unweighted samples, use underEnvelope or MCMC.
importance-weightingmonte-carlo-estimation
def importanceWeight(x, mu, sigma, proposalSpread) ≔ exp(logTarget(x, mu, sigma) − logTarget(x, 0, proposalSpread))
weightedMeanOf
weightedMeanOf(x, w) · x values vector · w weights vector → scalar
Let \(x \in \mathbb{R}^n\) be values and \(w \in [0, \infty)^n\) weights. Then
\[\operatorname{weightedMeanOf}(x, w) = \frac{\sum_{i=1}^{n} x_i w_i}{\operatorname{nonzero}\big(\sum_{i=1}^{n} w_i\big)} .\]The self-normalised weighted mean \(\sum_i x_i w_i / \sum_i w_i\).
Intuition A centre of mass: each value pulls on the average with its own weight. Dividing by the total weight means the weights need only be right up to a common factor, which is all unnormalised densities can give.
Use it when you turn importance weights into an estimate of an expectation, average particle states in a filter, or compute any weighted average. Self-normalising adds a small bias that vanishes as the sample grows, in exchange for not needing normalising constants. Pass \(f(x)\) rather than \(x\) to estimate \(\mathbb{E}[f]\). Its reliability is only as good as effectiveSize.
importance-weightingmonte-carlo-estimation
def weightedMeanOf(x, w) ≔ einsum("i,i->", x, w) / nonzero(einsum("i->", w))
effectiveSize
effectiveSize(w) · w weights vector → scalar
Let \(w \in [0, \infty)^n\) be weights. Then
\[\operatorname{effectiveSize}(w) = \frac{\big(\sum_{i=1}^{n} w_i\big)^2}{\operatorname{nonzero}\big(\sum_{i=1}^{n} w_i^2\big)} .\]The effective sample size of a set of weights. It does not depend on their scale.
Intuition How many equally weighted draws your weighted sample is really worth. A thousand draws where three carry nearly all the weight behave like about three: the estimate has the noise of three samples, whatever the count says.
Use it when you judge an importance-sampling estimate, choose a proposal (larger ESS is better), or decide when a particle filter must resample (degenerate is a fixed-threshold version). Report it next to any weighted estimate. A high ESS does not prove the proposal covers the target: regions the proposal never reaches leave no trace in the weights.
sampler-validationimportance-weighting
def effectiveSize(w) ≔ einsum("i->", w) · einsum("i->", w) / nonzero(einsum("i,i->", w, w))
degenerate
degenerate(w) · w weights vector → 1 or 0
Let \(w \in [0, \infty)^n\) be weights. Then
\[\operatorname{degenerate}(w) = \Big[\, \operatorname{effectiveSize}(w) < \frac{n}{10} \,\Big] .\]1 when the effective sample size has fallen below a tenth of the number of draws: a weighted estimate that is effectively a handful of samples and should not be trusted (or a particle cloud that needs resampling).
Intuition An alarm on weight collapse: it fires when fewer than one in ten draws is effectively contributing. Past that point the estimate is driven by a handful of lucky samples.
Use it when you want a yes/no trigger — resample a particle cloud only when it fires, reject a proposal distribution, or flag an importance estimate as unreliable. The threshold \(n/10\) is fixed; many particle filters resample earlier, at \(\mathrm{ESS} < n/2\), which you can test directly with effectiveSize.
sampler-validationstate-estimation
def degenerate(w) ≔ effectiveSize(w) < count(w) / 10
Monte Carlo estimators
Two classical ways to lower the variance of a Monte Carlo average. Antithetic variates pair each
uniform draw with its mirror \(1 - u\), so the errors of a monotone integrand cancel instead of
accumulating. Control variates subtract a correlated quantity \(c\) whose mean is known,
\(y - \beta (c - \mathbb{E}[c])\). reduction reports the variance ratio actually achieved: a value
at or above one means the pairing or control was badly chosen.
antithetic
antithetic(u) · u uniform draws → mirrored draws
Let \(u \in [0, 1)\) be a uniform draw. Then
\[\operatorname{antithetic}(u) = 1 - u .\]The antithetic partner \(1 - u\) of each uniform draw.
Intuition Balance every draw with its mirror image. If a draw near 0 gives a result that is too low, its partner near 1 gives one that is too high, and their average lands close to the truth, so pairs disagree less than independent draws would.
Use it when you estimate an expectation of a monotone function of a uniform — option prices, inverse-CDF simulations, anything that increases with the draw. Evaluate \(f(u)\) and \(f(1-u)\), average each pair, and check with reduction against an equal number of independent evaluations. For a symmetric function such as \((u - 0.5)^2\) the pair gives identical values and the trick is worse than useless.
variance-reductionmonte-carlo-estimation
def antithetic(u) ≔ 1 − u
controlled
controlled(y, c, cmean, beta) · y samples of the quantity · c samples of the control · cmean known mean of the control · beta coefficient → adjusted samples
Let \(y, c \in \mathbb{R}\) be paired samples of the quantity and the control, \(\bar{c} = \mathbb{E}[c]\)
(the argument cmean) the control’s known mean and \(\beta \in \mathbb{R}\) the coefficient. Then
The control-variate adjusted samples \(y - \beta (c - \mathbb{E}[c])\). Their mean estimates the mean
of y.
Intuition If a quantity you can simulate moves together with one whose mean you already know, the known one tells you how lucky this particular sample was. Subtract that luck, scaled by \(\beta\), and the noise shared by the two cancels while the mean of y is untouched.
Use it when you have a cheap, correlated quantity with a known expectation — the analytic price of a simpler option, the input’s own mean, a linearised model. The best \(\beta\) is \(\operatorname{Cov}(y, c)/\operatorname{Var}(c)\), which cuts the variance by a factor \(1 - \rho^2\); estimating it from the same samples adds a small bias. A weakly correlated control helps little, so confirm with reduction.
variance-reductionmonte-carlo-estimation
def controlled(y, c, cmean, beta) ≔ y − beta · (c − cmean)
varianceOf
varianceOf(v) · v samples vector → scalar
Let \(v \in \mathbb{R}^n\) be samples and \(\bar{v} = \frac{1}{n} \sum_i v_i\). Then
\[\operatorname{varianceOf}(v) = \frac{\sum_{i=1}^{n} (v_i - \bar{v})^2}{\operatorname{nonzero}(n - 1)} .\]The sample variance, with the \(n - 1\) divisor.
Intuition The average squared distance from the sample mean, corrected for the fact that the mean was fitted to these same points and so sits closer to them than the true mean does. Dividing by \(n - 1\) instead of \(n\) makes the estimate unbiased.
Use it when you need the spread of Monte Carlo samples: the standard error of their mean is \(\sqrt{\text{varianceOf}(v)/n}\), which tells you how many more samples a target accuracy needs. It assumes independent samples; for correlated MCMC output it understates the error of the mean. To compare two estimators use reduction.
uncertainty-quantificationmonte-carlo-estimation
def varianceOf(v) ≔ einsum("i,i->", v − mean(v), v − mean(v)) / nonzero(count(v) − 1)
reduction
reduction(plain, improved) · plain samples from the plain estimator vector · improved samples from the variance-reduced estimator vector → scalar
Let \(a \in \mathbb{R}^n\) (the argument plain) and \(b \in \mathbb{R}^{n'}\) (the argument improved)
be samples from the two estimators. Then
The ratio of sample variances, improved over plain. Below one is an improvement.
Intuition How many times fewer samples the improved estimator needs for the same accuracy: a ratio of 0.1 means one tenth. It measures what a variance-reduction trick actually achieved, not what it promised.
Use it when you validate antithetic pairs, controlled samples or a new proposal against the plain estimator. Compare at equal cost: an antithetic pair uses two function evaluations, so set its variance against plain samples that are averages of two independent draws. With few samples the ratio is itself noisy.
variance-reductionestimator-comparison
def reduction(plain, improved) ≔ varianceOf(improved) / nonzero(varianceOf(plain))
The bootstrap particle filter
A particle filter tracks a state through a model that need not be linear or Gaussian, where the Kalman filter cannot go (on a linear-Gaussian model the two should agree). Each step weights the particles by how well they explain the observation, accumulates the evidence from the average weight, and resamples. Resampling here is systematic: one uniform per step lays a comb of \(n\) evenly spaced teeth \((k + u)/n\) over the cumulative weights, which has lower variance than drawing \(n\) independent indices.
particleWeights
particleWeights(cloud, observed, noise) · cloud particle states vector · observed observation scalar · noise observation noise standard deviation scalar → weights vector
Let \(x \in \mathbb{R}^n\) (the argument cloud) be the particle states, \(y \in \mathbb{R}\) (the
argument observed) the observation and \(\sigma > 0\) (the argument noise) the observation noise.
Then
The unnormalised Gaussian likelihood \(\exp(-(x_i - y)^2 / 2\sigma^2)\) of the observation under each particle.
Intuition Each particle is a guess of the hidden state; its weight is how well that guess explains what the sensor just reported. Guesses near the observation score close to 1, guesses many noise-widths away almost 0.
Use it when you track a hidden quantity from noisy measurements — position, level, a drifting parameter — with a particle filter. The observation model is fixed: the sensor reads the state directly plus Gaussian noise of standard deviation noise. Propagate the particles through your dynamics in C#, weight them here, then feed the weights to logEvidence and resample. Too small a noise collapses the weights onto one particle; check effectiveSize. For linear-Gaussian models a Kalman filter is exact and cheaper.
state-estimationnonlinear-filtering
def particleWeights(cloud, observed, noise) ≔ exp(logTarget(cloud, observed, noise))
drawnAgain
drawnAgain(cloud, w, u) · cloud particle states vector n · w weights vector n · u one uniform draw scalar → resampled states vector n
Let \(x \in \mathbb{R}^n\) (the argument cloud) be the particle states, \(w \in [0, \infty)^n\) the
weights and \(u \in [0, 1)\) one uniform draw; with positions \(j = 0, 1, \dots, n-1\) let
\(\bar{w} = \operatorname{normalised}(w)\), \(W_j = \sum_{j' \le j} \bar{w}_{j'}\) and \(c_j = (j + u)/n\).
Then, as the code computes it, for every \(i\)
which does not depend on \(i\). Systematic resampling would instead set entry \(k\) to \(x_j\) for the one \(j\) with \(W_j - \bar{w}_j < c_k \le W_j\): the code compares tooth \(j\) of the comb with particle \(j\) rather than every tooth with every particle.
Known issue: in this release the result is not a resampled cloud. Every entry comes back equal to
the same sum of particles (for example, particles [10, 20, 30, 40] with equal weights and u = 0.5 return
[100, 100, 100, 100], where [10, 20, 30, 40] is expected). Do not rely on drawnAgain until this
is corrected; resample in C# from particleWeights instead.
Intuition Resampling kills particles that explain the data poorly and copies those that explain it well, so effort is not wasted tracking hopeless guesses. Systematic resampling lays \(n\) evenly spaced teeth over the cumulative weights with one random offset: a particle with weight \(\bar w\) receives almost exactly \(n \bar w\) copies, with less randomness than \(n\) independent draws.
Use it when — not in this release: as the known issue above states, it does not return a resampled cloud. Implement systematic resampling in C# from normalised particleWeights, and resample only when degenerate (or your own ESS threshold) fires, since resampling itself adds noise.
state-estimationparticle-degeneracy
def drawnAgain(cloud, w, u) ≔ einsum("ij,j->i", (runningTotal(normalised(w)) ≥ laid(comb(w, u))) · (runningTotal(normalised(w)) − normalised(w) < laid(comb(w, u))), cloud)
logEvidence
logEvidence(w) · w unnormalised weights of one step vector → scalar
Let \(w \in [0, \infty)^n\) be the unnormalised weights of one step. Then
\[\operatorname{logEvidence}(w) = \ln \operatorname{nonzero}\Big( \frac{1}{n} \sum_{i=1}^{n} w_i \Big) .\]The log of the average weight, this step’s contribution to the log marginal likelihood of the
observations; sum it over steps. With weights from particleWeights, which omit the Gaussian’s
normalising constant, add \(-\log(\sigma\sqrt{2\pi})\) per observation for the true value.
Intuition The average particle weight estimates how probable this observation was given everything seen before. Multiplying those across steps — adding their logs — gives how probable the whole observation sequence is under the model: a score for the model itself, not for any one state.
Use it when you compare models or parameter settings (noise levels, dynamics) on the same data — higher is better — or drive parameter estimation such as particle MCMC. The plain average assumes the particles carried equal weight before this step, that is, you resampled; otherwise use the weighted average. Add the normalising constant when comparing different noise values, since it changes with \(\sigma\).
model-comparisonstate-estimation
def logEvidence(w) ≔ ln(nonzero(einsum("i->", w) / count(w)))
Building blocks
normalised(w)— Let \(w \in [0, \infty)^n\). Then \(\operatorname{normalised}(w)_i = w_i / \operatorname{nonzero}\big(\sum_{j} w_j\big)\). The weights divided by their sum.comb(w, u)— Let \(w \in \mathbb{R}^n\) and \(u \in [0, 1)\), with positions \(k = 0, 1, \dots, n-1\). Then \(\operatorname{comb}(w, u)_k = (k + u)/n\). The systematic resampling comb, positions(k + u)/nfor k = 0 … n−1.
Stick breaking and nonparametric priors
A Dirichlet process is a stick broken forever: break off a Beta(1, α) fraction of what remains, again and again. The pieces are the mixture weights,
\[\pi_k = v_k \prod_{j<k} (1 - v_j), \qquad v_k \sim \mathrm{Beta}(1, \alpha)\]and a small concentration α breaks off large pieces early, so fewer clusters are effectively in play. Beta(1, α) has a closed-form inverse CDF, \(v = 1 - (1 - u)^{1/\alpha}\), so each break comes exactly from a uniform draw, with no rejection.
The expected number of clusters grows like \(\alpha \log n\) under the Chinese restaurant process and like \(n^d\) under the Pitman–Yor process with discount \(d\) (the power law that real vocabularies follow). The formulas below are the standard large-\(n\) approximations, not exact expectations.
In this section \(\alpha > 0\) is the argument concentration, \(n \ge 0\) the argument customers,
and positions of a vector of \(K\) sticks are counted \(k = 0, 1, \dots, K-1\).
breakAt
breakAt(u, concentration) · u uniform draws · concentration α > 0 → break fractions
Let \(u \in [0, 1)\) be a uniform draw and \(\alpha > 0\). Then
\[\operatorname{breakAt}(u, \alpha) = 1 - (1 - u)^{1/\operatorname{nonzero}(\alpha)} .\]Converts uniform draws into Beta(1, α) draws by the inverse CDF.
Intuition How much of the remaining stick to snap off. The average fraction is \(1/(1+\alpha)\): with \(\alpha\) small most of the stick goes at once (a few big clusters), with \(\alpha\) large each break is a sliver (many small clusters).
Use it when you need Beta(1, α) variates exactly from uniforms — the building block of stickWeights, or any model with such a prior. Because it is a monotone transform of \(u\), seeded draws give reproducible breaks and pair well with antithetic. For a vector of weights call stickWeights rather than chaining breaks by hand.
random-variate-generationmixture-modelling
def breakAt(u, concentration) ≔ 1 − (1 − u) ^ (1 / nonzero(concentration))
stickWeights
stickWeights(u, concentration) · u uniform draws, one per stick vector K · concentration α → weights vector K
Let \(u \in [0, 1)^K\) be independent uniform draws, one per stick, and \(\alpha > 0\), and let \(v_k = \operatorname{breakAt}(u_k, \alpha)\). Then
\[\operatorname{stickWeights}(u, \alpha)_k = v_k \prod_{j<k} (1 - v_j), \qquad k = 0, 1, \dots, K-1,\]where the product is computed as \(\exp\big(\sum_{j \le k} \ln \operatorname{nonzero}(1 - v_j) - \ln \operatorname{nonzero}(1 - v_k)\big)\).
The first \(K\) stick-breaking weights of a Dirichlet process, from \(K\) uniform draws.
Intuition Mixture weights for an unbounded number of clusters: take a fraction of a unit stick for cluster 0, a fraction of what is left for cluster 1, and so on. Early clusters tend to be large and later ones ever smaller, so a finite number carry almost all the mass without fixing that number in advance.
Use it when you simulate from or initialise a Dirichlet-process mixture, a truncated variational approximation, or any model where the number of components should be learned. Choose \(K\) large enough that the leftover mass is negligible (check with stickMass). The weights come out stochastically decreasing, not sorted, and \(K\) weights sum to slightly less than one.
mixture-modellingunknown-cluster-countrandom-variate-generation
def stickWeights(u, concentration) ≔ breakAt(u, concentration) · unbroken(breakAt(u, concentration))
stickMass
stickMass(u, concentration) · u uniform draws vector K · concentration α → scalar
Let \(u \in [0, 1)^K\) be independent uniform draws and \(\alpha > 0\). Then
\[\operatorname{stickMass}(u, \alpha) = \sum_{k=0}^{K-1} \operatorname{stickWeights}(u, \alpha)_k .\]The total of the first \(K\) weights. One minus it is the mass still left on the stick, a check on whether \(K\) is large enough.
Intuition What the truncation has captured. Whatever remains, \(1 - \text{stickMass}\), is the probability of a cluster beyond the first \(K\) — the part of the infinite model you have thrown away.
Use it when you pick the truncation level of a stick-breaking prior: raise \(K\) until the leftover mass is below your tolerance for typical draws. On average the leftover is \((\alpha/(1+\alpha))^K\), so larger \(\alpha\) needs a larger \(K\). You can also put the leftover on a final “everything else” component so the weights sum to one.
truncation-errormixture-modelling
def stickMass(u, concentration) ≔ einsum("i->", stickWeights(u, concentration))
crpTables
crpTables(concentration, customers) · concentration α · customers n → scalar
Let \(\alpha > 0\) and \(n \ge 0\). Then
\[\operatorname{crpTables}(\alpha, n) = \alpha \ln\Big(1 + \frac{n}{\operatorname{nonzero}(\alpha)}\Big) .\]The approximate expected number of occupied tables in a Chinese restaurant process after \(n\) customers, \(\alpha \log(1 + n/\alpha)\).
Intuition Each new customer joins an occupied table in proportion to its size or opens a new one with weight \(\alpha\). New tables keep appearing, but ever more rarely, so their number grows only logarithmically: doubling the data adds about \(\alpha \ln 2\) clusters.
Use it when you choose the concentration \(\alpha\) of a Dirichlet-process prior: solve for the \(\alpha\) that gives the number of clusters you expect for your data size, or check whether a fitted model’s cluster count is plausible under its prior. It is an approximation, not the exact expectation. If clusters in your data keep appearing like a power law (words, species), use pitmanTables.
prior-elicitationunknown-cluster-count
def crpTables(concentration, customers) ≔ concentration · ln(1 + customers / nonzero(concentration))
pitmanTables
pitmanTables(concentration, discount, customers) · concentration α · discount d in (0, 1) · customers n → scalar
Let \(\alpha > 0\), \(0 < d < 1\) (the argument discount) and \(n \ge 0\). Then
The power-law growth of the number of tables in a Pitman–Yor process, \(\alpha n^d / d\).
Intuition The discount \(d\) takes a little from every occupied table and hands it to new ones, so new clusters keep arriving much faster than in the Chinese restaurant process: their number grows like \(n^d\) rather than \(\log n\). That heavy tail of rare clusters is what word frequencies and species counts look like.
Use it when you set the discount of a Pitman–Yor prior for data with a power-law vocabulary (language models, topic models, ecology): fit \(d\) to how the number of distinct items grows with sample size. Treat it as the growth rate; the constant in front is a rough scale. \(d\) close to 0 behaves like crpTables.
prior-elicitationpower-law-modellingunknown-cluster-count
def pitmanTables(concentration, discount, customers) ≔ concentration · customers ^ discount / nonzero(discount)
buffetFeatures
buffetFeatures(concentration, customers) · concentration α · customers n → scalar
Let \(\alpha > 0\) and \(n \ge 0\). Then
\[\operatorname{buffetFeatures}(\alpha, n) = \alpha \ln(1 + n) .\]The approximate expected total number of features (dishes) in an Indian buffet process after \(n\) customers, \(\alpha \log(1 + n)\).
Intuition In the Indian buffet each object can have many features at once. Customer \(i\) tries popular dishes and about \(\alpha/i\) new ones, so the total number of distinct features grows like a harmonic sum — logarithmically in the number of objects.
Use it when you choose \(\alpha\) for a latent-feature model (for example overlapping topics, hidden causes, binary factor analysis) so the prior’s feature count fits your data size, or sanity-check a fitted model’s number of features. Together with buffetOwned it fixes both the total and the per-object number of features. For exclusive cluster membership use crpTables instead.
latent-feature-modellingprior-elicitation
def buffetFeatures(concentration, customers) ≔ concentration · ln(1 + customers)
buffetOwned
buffetOwned(concentration) · concentration α → scalar
Let \(\alpha > 0\). Then
\[\operatorname{buffetOwned}(\alpha) = \alpha .\]The expected number of features each customer takes in an Indian buffet process, which is α.
Intuition The same \(\alpha\) that controls how many features exist overall is also how many each object has on average, however many objects there are. It is the density of each row of the binary feature matrix.
Use it when you set \(\alpha\) from domain knowledge about how many features one object typically has (“each document touches about three topics”), then read the implied total from buffetFeatures. It is a single parameter doing two jobs; if the two counts you expect disagree, the one-parameter buffet prior cannot fit both.
latent-feature-modellingprior-elicitation
def buffetOwned(concentration) ≔ concentration
Building blocks
unbroken(v)— Let \(v \in [0, 1)^K\), with positions \(k = 0, 1, \dots, K-1\). Then \(\operatorname{unbroken}(v)_k = \exp\big(\sum_{j \le k} \ln \operatorname{nonzero}(1 - v_j) - \ln \operatorname{nonzero}(1 - v_k)\big) = \prod_{j<k} (1 - v_j)\) (the last equality whenever every \(1 - v_j \ge 10^{-300}\)). The length of stick left before each break, the running product of 1 −vover earlier breaks.
Examples
All examples use one file, Chainsteps.hb, so the C# class is Chainsteps. Vectors are passed as
Tensor so the general overload is called.
Advance two Metropolis chains by one step
Both chains start at 0 targeting \(N(0, 1)\) with step 1 and the same normal draw 0.5, so both propose 0.5, whose log ratio is \(-0.125\) (acceptance probability 0.8825). The first chain’s uniform draw 0.9 rejects; the second’s 0.5 accepts.
open Prelude.Sampling
fn metropolisStep(x, step, mu, sigma, z, u) ↦ hastingsNext(x, step, mu, sigma, z, u)
using MLambda.Hilbert.Runtime;
using Examples;
var x = (Tensor)new[] { 0d, 0d };
var z = (Tensor)new[] { 0.5, 0.5 };
var u = (Tensor)new[] { 0.9, 0.5 };
var next = Chainsteps.MetropolisStep(x, 1.0, 0.0, 1.0, z, u);
Output (verified):
hastingsNext = [0, 0.5]
Take gradient-guided steps with MALA and HMC
Target \(N(0, 1)\). MALA from 2 with step 0.5 and draw 0 proposes \(2 + 0.25 \cdot (-2) = 1.5\), and the corrected ratio accepts it. HMC from 1 with momentum 0.5 and step 0.1 kicks the momentum to 0.45 and drifts to 1.045, which is accepted.
open Prelude.Sampling
fn malaStep(x, step, mu, sigma, z, u) ↦ malaNext(x, step, mu, sigma, z, u)
fn hamiltonStep(x, p, step, mu, sigma, u) ↦ hmcNext(x, p, step, mu, sigma, u)
using Examples;
var mala = Chainsteps.MalaStep(2.0, 0.5, 0.0, 1.0, 0.0, 0.5);
var hmc = Chainsteps.HamiltonStep(1.0, 0.5, 0.1, 0.0, 1.0, 0.5);
Output (verified):
malaNext = 1.5
hmcNext = 1.045
Draw with a slice sampler and a Gibbs sweep
Slice: from 0 on \(N(0, 1)\) with height draw 0.5 the slice is \(\pm\sqrt{2 \ln 2} = \pm 1.17741\), and the draw 0.75 lands three quarters of the way across it. Gibbs: with correlation 0.6, the other coordinate at 1 and a normal draw of 1, the update is \(0.6 + 0.8 = 1.4\).
open Prelude.Sampling
fn sliceStep(x, mu, sigma, u, v) ↦ sliceNext(x, mu, sigma, u, v)
fn gibbsStep(other, rho, z) ↦ gibbsDraw(other, rho, z)
using Examples;
var slice = Chainsteps.SliceStep(0.0, 0.0, 1.0, 0.5, 0.75);
var gibbs = Chainsteps.GibbsStep(1.0, 0.6, 1.0);
Output (verified, rounded to 6 significant digits):
sliceNext = 0.588705
gibbsDraw = 1.4
Judge importance weights
Equal weights are worth all four draws; a single dominant weight is worth one. A weighted mean of 1, 2 and 3 with weights 1, 1 and 2 is \(9/4\).
open Prelude.Sampling
fn essOf(w) ↦ effectiveSize(w)
fn weightedAverage(x, w) ↦ weightedMeanOf(x, w)
using MLambda.Hilbert.Runtime;
using Examples;
var even = Chainsteps.EssOf((Tensor)new[] { 1d, 1d, 1d, 1d });
var one = Chainsteps.EssOf((Tensor)new[] { 1d, 0d, 0d, 0d });
var avg = Chainsteps.WeightedAverage((Tensor)new[] { 1d, 2d, 3d }, (Tensor)new[] { 1d, 1d, 2d });
Output (verified):
effectiveSize (even) = 4
effectiveSize (one) = 1
weightedMeanOf = 2.25
Break a stick for a Dirichlet process
With α = 1 a uniform draw of 0.5 breaks off exactly half of what remains. The expected number of tables for 100 customers at α = 1 is \(\ln 101\).
open Prelude.Sampling
fn sticks(u, concentration) ↦ stickWeights(u, concentration)
fn stickTotal(u, concentration) ↦ stickMass(u, concentration)
fn tablesExpected(concentration, customers) ↦ crpTables(concentration, customers)
using MLambda.Hilbert.Runtime;
using Examples;
var u = (Tensor)new[] { 0.5, 0.5, 0.5 };
var weights = Chainsteps.Sticks(u, 1.0);
var mass = Chainsteps.StickTotal(u, 1.0);
var tables = Chainsteps.TablesExpected(1.0, 100.0);
Output (verified, rounded to 6 significant digits):
stickWeights = [0.5, 0.25, 0.125]
stickMass = 0.875
crpTables = 4.61512
Compare the variance of two estimators
A plain estimator’s samples [1, 3, 5, 7] have variance 20/3; an improved estimator’s [3, 4, 4, 5]
have variance 2/3, a tenfold reduction. The same file also gives a cooling schedule: starting at 10
with rate 0.5, the temperature after 2 steps is \(10/e\).
open Prelude.Sampling
fn varianceRatio(plain, improved) ↦ reduction(plain, improved)
fn temperatureAt(start, rate, k) ↦ coolingAt(start, rate, k)
using MLambda.Hilbert.Runtime;
using Examples;
var ratio = Chainsteps.VarianceRatio((Tensor)new[] { 1d, 3d, 5d, 7d }, (Tensor)new[] { 3d, 4d, 4d, 5d });
var temp = Chainsteps.TemperatureAt(10.0, 0.5, 2.0);
Output (verified, rounded to 6 significant digits):
reduction = 0.1
coolingAt = 3.67879