Data-driven CVs with SPIB
June 15, 2026 · View on GitHub
Instead of a hand-crafted progress variable, PathGennie can learn one on the fly
with SPIB (State Predictive Information Bottleneck; Wang & Tiwary, 2021),
which jointly learns a low-dimensional CV and an emergent set of metastable
states from the frames a run visits. Requires the ml extra (pip install -e .[ml], i.e. PyTorch).
How SPIB works
A predictive information bottleneck:
- an encoder
q(z|x)maps a featurized configuration to a Gaussian latentz(the CV is its mean); - a classifier
p(y|z)predicts, fromz_t, the future metastable-state labely_{t+dt}; - training minimises
CE(p(y|z_t), y_{t+dt}) + β·KL(q(z|x)||prior)— the IB trade-off between being predictive and being compressed; - state labels are refined self-consistently: after each round every frame is
relabelled by
argmax p(y|z), empty states are dropped, and the number of metastable states emerges from the data.
Featurization (pathgennie/cv/features.py, NumPy)
from pathgennie.cv.features import Featurizer, pairwise_distances, contact_features, dihedral_features
feat = Featurizer(funcs=[
lambda c: pairwise_distances(c, [[4, 6], [6, 8]]),
lambda c: dihedral_features(c, [[4, 6, 8, 14]]),
], standardize=True)
feat.fit(frames) # accumulate online mean/std
X = feat.transform_batch(frames)
Featurizer with no funcs uses the flattened coordinates (handy for the toy
system). Featurization is pure NumPy and has no heavy dependency.
Training a model directly
from pathgennie.cv.spib import train_spib
result = train_spib(features, # (n_frames, n_features), time-ordered
dt=1, n_states_init=6, latent_dim=2, beta=1e-3,
epochs=60, n_refine=6, seed=0)
result.model # SPIB encoder/classifier
result.labels # per-frame metastable-state ids
result.n_states # emergent number of states
Driving PathGennie with a learned CV
SPIBProgress is an adaptive ProgressVariable: it bootstraps from a coarse
geometric CV, buffers the path the driver visits, retrains periodically (the
"iterative path-learning cycle"), then steers using the learned latent.
from pathgennie.cv.features import Featurizer
from pathgennie.cv.spib import SPIBProgress
from pathgennie.core.progress import TargetMetric
bootstrap = TargetMetric(my_geometric_cv, target_cv=target)
progress = SPIBProgress(
Featurizer(funcs=my_feature_fns),
bootstrap,
mode="target", target_coords=target_coords,
refresh_every=50, min_frames=40, dt=1,
train_kwargs=dict(n_states_init=6, latent_dim=2, epochs=40),
)
# pass `progress` to PathGennieDriver as usual
The driver calls progress.observe(coords, cycle) once per cycle (a no-op for
static CVs), which is how SPIB accumulates frames and retrains. After a refresh,
progress.n_states and progress.state_labels expose the metastable states —
these feed the roadmap graph.
Trajectory length matters
A learned CV needs segments long enough to contain real relaxation. If you are in
the ultrashort discovery regime, see the guard in
strategy-profiles.md; prefer the sampling profile (or
longer tau1/tau2) when learning a CV for quantitative work.