mcmckit¶
mcmckit is a minimal, plug-and-play set of MCMC samplers for Python, built for Bayesian model updating and inverse problems in engineering: structural health monitoring, finite element model updating, structural dynamics.
The expensive part of model updating is your forward model. mcmckit is designed to stay out of its way.
You own the loop¶
Every sampler is a plain function that advances the chain by one step. State goes in as arguments and comes back as return values. Nothing is hidden on an object, so the recursion is yours to write, stop, inspect and modify.
Start with the simplest sampler, Metropolis-Hastings, which has a fixed proposal and no adaptation state at all:
import numpy as np
from mcmckit import mh_step
def log_post(theta): # your model goes here
return -0.5 * np.sum(theta**2) # a standard normal, for illustration
x = np.zeros(2) # current position
logp = log_post(x) # its log posterior
cov = np.eye(2) * 0.5 # proposal covariance
chain = [] # a list, so the run can be any length
n_accepted = 0
for i in range(10_000):
x, logp, accepted = mh_step(log_post, x, logp, cov)
chain.append(x)
n_accepted += accepted
chain = np.array(chain) # (n_iterations, n_parameters)
posterior = chain[1000:] # burn-in is just a slice
print(posterior.mean(0), posterior.std(0))
print(f"acceptance rate: {n_accepted / len(chain):.2f}")
mh_step proposes a move, accepts or rejects it, and hands back the new
position, its log posterior, and whether the move was taken. Everything else
in this package is a variation on that.
Because the chain is a plain list you append to, nothing needs to know the
run length in advance. Break out of the loop whenever your own criterion says
so, and np.array whatever you collected.
The acceptance rate is your tuning signal. This one prints about 0.67,
which is too high: the proposal is too small, so the chain accepts nearly
everything and inches along. Widening cov would fix it. Roughly 0.2 to 0.4
is healthy for a random walk.
Plain MH needs a good cov, which you rarely know in advance. RAM learns one
while sampling; the only change is that its adaptation state S is threaded
through too, and it needs the step number:
from mcmckit import ram_step
x = np.zeros(2)
logp = log_post(x)
S = np.linalg.cholesky(np.eye(2) * 0.1**2) # a rough guess is fine
chain = []
n_accepted = 0
for i in range(1, 10_001): # 1-indexed: drives adaptation
x, logp, S, accepted = ram_step(log_post, x, logp, S, i)
chain.append(x)
n_accepted += accepted
if i % 1000 == 0: # your convergence check
print(i, x, logp)
chain = np.array(chain)
print(f"acceptance rate: {n_accepted / len(chain):.2f}")
Same shape, one extra threaded value. This prints about 0.25, against RAM's 0.234 target: starting from a proposal 5x too small, it found a sensible one on its own.
A caveat worth knowing early: a high acceptance rate is not a good sign. It usually means the steps are too small and the chain is crawling. Judge a run by effective sample size, not acceptance alone. Choosing a sampler shows what that looks like.
Everything is a plain module-level function, so import what you use and call it bare, or keep the package namespace if you prefer. Both are the same call:
| Function | Threaded state | Returns |
|---|---|---|
mh_step |
— (fixed cov) |
x, logp, accepted |
ram_step |
S, step index i |
x, logp, S, accepted |
dram_step |
DRAMState |
x, logp, state, accepted |
mala_step |
grad |
x, logp, grad, accepted |
adaptive_mala_step |
grad, log_step, i |
x, logp, grad, log_step, accepted |
gibbs_step |
— (blocks, proposal_std) |
x, logp, accepted_per_block |
See Step functions for the full reference, and Your own loop for a worked structural example.
Or hand over the loop¶
When you do not need control of the recursion, the full-run helpers are thin loops over exactly the same step functions:
from mcmckit import ram
result = ram(log_post, x0=[0.0, 0.0], n_samples=10_000)
print(result.mean(), result.std())
result.discard(1000).plot_corner()
metropolis, ram, dram, mala, adaptive_mala and gibbs all follow
this shape and return a Result with statistics and plots.
Samplers¶
| Step function | Full run | Method | Notes |
|---|---|---|---|
mh_step |
metropolis |
Random-walk MH | Fixed proposal covariance |
mala_step |
mala |
Langevin | Needs the gradient |
ram_step |
ram |
Robust Adaptive MH | Self-tunes covariance (Vihola 2012) |
dram_step |
dram |
Delayed Rejection + AM | Best general-purpose adaptive sampler |
adaptive_mala_step |
adaptive_mala |
Adaptive Langevin | Log-space step-size tuning |
gibbs_step |
gibbs |
Metropolis-within-Gibbs | Block updates, per-block rates |
| — | TMCMC |
Transitional MCMC | Prior→posterior bridge, log-evidence |
TMCMC advances a whole population of particles per stage rather than a single chain position, so it has no single-step form and stays a class.
Not sure which to use? Choosing a sampler compares them all on the same problem, with traces, effective sample sizes and a hard target that separates them. Plots shows everything the package can draw.
Installation¶
For development:
Package layout¶
mcmckit/
├── steps.py ← single-step functions: you own the loop
├── runners.py ← full-run helpers: thin loops over steps.py
├── core/
│ ├── problem.py ← Problem (prior + likelihood interface)
│ ├── result.py ← Result (samples, plots, statistics)
│ └── noise.py ← GaussianNoiseLikelihood
└── samplers/ ← stateful sampler classes, and TMCMC