Skip to content

Reweighting an adaptive ensemble

Adaptive sampling deliberately over-samples sparse regions. The configurations an exploration campaign produces are therefore not Boltzmann distributed, and a histogram of them is not a free-energy surface. Something has to put the weights back.

Trails-MD offers two routes, and they fail in different ways — which is the reason for keeping both.

MSM reweighting RiteWeight
Assumption Markovian at the chosen lag, on the chosen discretisation none of either
Needs a lag time yes — and the answer depends on it no
Discretisation error enters through the clustering averaged away by re-randomising it
Cost one clustering + one eigenproblem one clustering + one eigenproblem per iteration

Neither fixes mis-coverage. A basin that was never visited cannot be reweighted into existence, and no diagnostic in either method will tell you one is missing.

MSM reweighting

The standard route: discretise, count transitions at lag $\tau$, solve for the stationary distribution $\pi$, and give each frame weight $\pi_I / N_I$ where $I$ is its state and $N_I$ that state's frame count. See MSM & kinetic seeding.

The catch is the lag. Too short and the cluster-level dynamics are not Markovian, so $\pi$ is wrong; too long and the transition counts run out. Trails-MD reports an implied-timescale diagnostic precisely so this can be checked rather than assumed.

RiteWeight

trails_md.analysis.riteweight implements the randomized iterative trajectory reweighting scheme of Kania, Webber, Simpson, Aristoff and Zuckerman, PNAS 123, e2529246123 (2026). Short trajectory segments are reweighted iteratively until their weighted distribution is self-consistent with the stationary distribution of a transition matrix built from those same weights.

The key device is that the clustering is re-randomised at every iteration. Segments that shared a cluster in one iteration are separated in the next, so the fixed point is governed by the underlying microstate dynamics rather than by any particular discretisation. In practice the answer should not depend on the cluster count — a property worth checking, and one the test suite asserts.

import numpy as np
from trails_md.analysis import riteweight

# One (start, end) pair per short segment. Features must be invariant to rotation
# and translation: pairwise distances, torsion sin/cos, a learned projection, ...
result = riteweight(start_features, end_features, n_clusters=150,
                    n_iterations=2000, average_last=400, seed=0)

print(result.converged)          # heuristic: late drift << early drift, and small
weights = result.weights         # one weight per segment, summing to 1

H, xe, ye = np.histogram2d(cv[:, 0], cv[:, 1], bins=60, weights=weights)
F = -kT * np.log(H / H.sum())

Forming the segment pairs

Both configurations of a pair must come from the same continuous stretch of unbiased dynamics. Trails-MD applies no biasing forces, but exploration mode redraws velocities at every respawn, so a pair must never straddle a respawn boundary — the same constraint that governs the time-lagged CV estimators:

starts, ends = [], []
for iteration in iterations:
    cv = np.load(iteration / "cvs.npz")["cvs"]
    per_walker = len(cv) // n_walkers
    for w in range(n_walkers):
        seg = cv[w * per_walker:(w + 1) * per_walker]
        starts.append(seg[:-lag])
        ends.append(seg[lag:])

lag here only sets which pairs are formed; unlike an MSM, the fixed point is not a function of it. Checking that the answer is flat across several lags is a cheap and informative diagnostic.

Convergence

RiteWeightResult.weight_drift records the per-iteration $L_1$ change in the weight vector. A settled run shows it falling and then fluctuating about a small value. result.converged applies a heuristic to that trace; it is reported rather than enforced, so you can judge convergence instead of assuming it.

Cost

Each iteration re-clusters every pooled point, so cost grows as n_iterations × n_points. The nearest-centre assignment uses a KD-tree over the (few) cluster centres, which is exact and turns an otherwise prohibitive $O(N k)$ scan into roughly $O(N \log k)$ — the difference between feasible and not for a campaign with $10^6$ pooled frames.

Attribution

This is an independent implementation written from the published algorithm. The authors' reference code (github.com/ZuckermanLab/rite_weight) carries no licence statement and was deliberately not copied or vendored. If you use this feature, cite the PNAS paper.