REST2 — replica exchange with solute tempering¶
Hamiltonian replica exchange at one physical temperature. The rungs of the ladder differ by how much of the solute's Hamiltonian is scaled down, not by how hot the thermostat is [@wang2011rest2]. That is the whole point: only the solute's internal and solute–environment terms are softened, so the cost of the ladder does not grow with the number of solvent molecules the way temperature REMD's does.
The two example files beside this README¶
| file | what it is |
|---|---|
example.config |
what you hand to md-openmm build-md |
example.in |
what md-run then reads — the ladder's production stage |
build-md generates the .in from the .config; you do not normally write one by
hand. It is shipped here because it is the file the run actually reads, and because
every setting in it carries the schema's own description as a comment — so the meaning
of a key can be looked up where it is used rather than in the source.
resolved.config, written beside the .in at run time, stays AUTHORITATIVE: the .in
is resolved into it, and that resolved document is what the run reads.
Both are checked by tests/test_method_example_inputs.py, which regenerates the .in
from the .config and fails if they have drifted — an example that no longer matches
the engine is worse than none.
Ensemble¶
NVT. The runtime installs no barostat and a requested pressure is refused. Exchanging complete configurations under NPT would also have to exchange volumes and carry the pV work; that is deliberately not implemented rather than approximated.
Every replica runs at the same thermostat temperature and the same β, so an exchange never rescales velocities. Velocity rescaling belongs to temperature REMD, where the rungs differ in β; here it would inject or remove energy at every accepted swap. A test asserts that no runtime module does it.
What is scaled, and what is not¶
τ is the only persisted scaling coordinate. Everything else is derived from it:
| term | factor |
|---|---|
| solute–solute nonbonded, 1-4, solute torsions, CMAP | (1 − τ)² |
| solute–environment nonbonded | (1 − τ) |
| generalized Born (implicit) | (1 − τ) |
| environment–environment | 1 — untouched |
| bonds and angles | 1 — untouched |
| unscaled torsions: every proper torsion across an ordinary amide C–N (ω) of a protein or peptide-like residue, an aromatic ring bond or another double bond; every improper | 1 — unscaled by convention |
Leaving these unscaled is this repository's convention (v4, rest2-unscaled-torsions), not
textbook REST2. A softened ω barrier lets the backbone sample cis amides the force field was never
fit to describe; a softened ring or double-bond torsion lets a hot state bend a planar group, and a
softened improper lets it invert a centre. In every case the hot states would explore geometry
state 0 never visits, and exchanges would stop being useful.
The ω protection belongs to peptides, not to ligands (v4, decided 2026-10-12). The argument
above is about a backbone whose reference ensemble never visits cis. A small-molecule ligand has no
such reference: a tertiary amide has two real rotamers behind a barrier the cold run does not cross,
which is what REST2 is for. So an amide inside a non-protein residue is scaled and recorded
under ligand_amide_scaled_bonds; a protein residue, and a residue named in the scaler's
peptide_like_residues (the residue of a solute.kind: peptide-like build), keep it unscaled. A
bond between two residues is a peptide bond whatever either residue is called, so a modified
residue in a protein chain is still read from bond orders or refused, never silently scaled. To
protect one ligand amide anyway, list its central bond in unscaled_list.
A v3 ladder is continued, not re-derived
v3 → v4 changes which bonds build-top --rest2-scaler protects when it writes the states. A
ladder built under v3 runs from those files, so it resumes and extends under v4 and keeps
recording v3. The states of a new build differ from a v3 build of the same ligand wherever it
has an amide, so the two are two Hamiltonians with two digests. The exclusion is expressed as
central bonds: every proper torsion term across such a bond is left unscaled, so no term is
missed because it was enumerated differently. The ARG guanidinium counts as double bonds, and so does the guanidinium of a peptide-like residue (all three C–N bonds). A ligand's guanidine or guanidinium C–N bonds are scaled, all three alike (v4, decided 2026-10-13), under guanidinium_scaled_bonds: the three bonds are partly double whichever one the SMILES writes as the double bond, so that notation must not decide what REST2 heats.
How a bond is classified. Protein residues from a residue table (PHE, TYR, TRP, HIS rings; ARG
NE–CZ, CZ–NH1, CZ–NH2). A small molecule from its bond orders, read from an SDF (see
the scaler). A torsion is proper or improper by the
bond graph: a bonded chain is proper; one atom bonded to the other three is improper. A torsion
that cannot be classified is refused, never silently scaled. A ring-locked amide nitrogen
(proline, or a ring of at most max_proline_ring_size atoms) keeps its ω eligible for scaling.
The scaled states: build-top --rest2-scaler¶
The ladder scales nothing. Its states are built ONCE, as files, before any run:
md-openmm build-top --rest2-scaler -s build/built.xml -p build/built.pdb --config build/scaler.config
# build/scaler.config
method: REST2 # REST2 (a ladder), cMD (one hot state) or AIS (the V0 end state)
schedule:
kind: linear
n_states: 4
tau_min: 0.0 # default
tau_max: 0.5
# unscaled_torsions: true # default; false scales every solute torsion
# sdf_filelist: {MO1: MO1.sdf, MO2: MO2.sdf}
# peptide_like_residues: [CYC] # unset: read from build/built.log (peptide-like -> its residue)
A cyclic peptide built as kind: peptide-like keeps its backbone ω protected without a key: the
scaler reads solute.kind from the build record beside the System (built.log) and, for
peptide-like, treats the solute's non-protein residues as peptide-like. Any other kind names none,
so a ligand's amide is scaled. State the key to override — even as [], which says "none" about a
peptide-like build — and the record is then not consulted. A System built by hand has no record to
read; the answer is "none", and the scaler's ligand amides log line says where it came from.
It writes build/REST2/:
build/REST2/
├── system_state0.xml … system_state3.xml one scaled System per state, tau 0 … 0.5
├── scaler.yaml the record: source System sha256, every state's tau and sha256, the solute,
│ every unscaled central bond and improper, the SDFs used
├── scaler.log
└── <RESNAME>-unscaled.png each small molecule, its unscaled torsions' bonds in red
A small molecule's bond orders come from sdf_filelist, else <RESNAME>.sdf beside the System,
else built.sdf when it is the only non-standard residue; with several and no mapping, the scaler
refuses rather than guesses. The directory appears complete or not at all, and --overwrite moves
the previous set aside rather than deleting it.
build-md refuses a REST2 configuration until build/REST2/scaler.yaml exists, was built from
this built.xml, and holds exactly the configured taus. Every run integrates those files as they
are; a state is never scaled a second time.
Selective REST2: choosing the hot region (0.6.1, in development)¶
Not in a release. This section describes branch
0.6.1work.
With no selector, the hot region is the whole solute: every atom that is not solvent or a
counter-ion. The states are byte-identical to 0.6.0's. Three optional scaler.config keys choose
a region instead:
backbone_scaling_list: ":45,46,59" # quoted AMBER residue mask
sidechain_scaling_list: ":45-50"
ligand_scaling_dict:
L01: # a label, not an identity
mask: ":201" # exactly ONE residue: this copy, not its twins
torsion_exclusions: auto # or an md-tools-torsion-exclusions/1 file
A complete, commented example for ACE-ALA-NME is
selective-scaler.config beside this page. It is resolved, and states
are built from it, by a test.
The region is chosen in scaler.config, and only there. In a build-md REST2 configuration
the same three keys, under rest2:, are claims about the saved states, not settings. They
work the way rest2.number_of_replicas and rest2.tau_max already do:
- A claim is resolved by the same resolver and compared with the region recorded in
build/REST2/scaler.yaml. The comparison is of resolved regions, not text, so":2,3"and":2-3"agree. Mask spelling, ligand labels and file paths are never compared. A disagreement is refused, with thebuild-top --rest2-scalercommand that would rebuild the states. - No claim accepts the recorded region, and
build-md.logprints it, so a selective ladder never runs unannounced. - A verified claim is not carried into the run. It is reset to null before
resolved.configand the.infiles are written, likecollective_variables.generate. An.infile naming a claim key is refused bymd-run. - Another protocol that sets the keys is refused by name, and so is the compact ligand form
L01: <path>.
# REST2.config (build-md): a claim about build/REST2/, checked, never applied
rest2:
number_of_replicas: 4
tau_max: 0.5
backbone_scaling_list: ":2"
sidechain_scaling_list: ":2"
If any selector is present, the selection is explicit. Only the named categories and instances
are hot. An omitted category means none, not all. An explicit region that heats nothing is
refused. Selective scaling is explicit solvent only: any selector on an implicit (GB) System is
refused, because a generalised-Born energy is not separable per atom. unscaled_torsions: false
is refused with a selector, because an improper has no central bond for a region to own.
Masks use a documented subset of the AMBER grammar (md-tools-residue-mask/1): ":45",
":45,46,59", ":45-50", ":45,48-52". The numbers are one-based topology residue indices,
counted across every chain without a reset. They are not PDB author numbers and not Python
indices. Everything else is refused by name, with the supported subset printed: atom selectors
(@), chain and molecule selectors (::, ^), operators, wildcards, residue names, zero, and
descending ranges. Chain qualifiers are not supported. scaler.log prints the resolved residue
map (index, name, chain, author id, role), so you can see what each number matched.
Two questions, answered separately (policy md-tools-selective-rest2/1):
| question | answer |
|---|---|
which atoms carry (1−τ) / (1−τ)² nonbonded |
backbone atoms of backbone-selected residues, sidechain atoms of sidechain-selected residues, every atom of a selected ligand instance |
which torsions carry (1−τ)² |
every eligible proper term across a selected central bond |
A central bond is selected when the region that owns it is selected:
| central bond | owner |
|---|---|
| N–CA (φ), CA–C (ψ) | the residue's backbone |
| any bond with a sidechain atom, one residue (CA–CB is χ1) | the residue's sidechain |
| peptide C(i)–N(i+1) (ω_i) | residue i's backbone |
| disulfide SG–SG | both sidechains jointly. Scaled only when both are selected; reported when one is |
| inside a ligand | that instance |
| anything else between residues | undefined, and refused when either side is selected |
The four atoms of a scaled torsion need not all be hot. χ1 (N–CA–CB–CG) is scaled by a sidechain selection although N and CA are backbone atoms it does not heat. φ reaches into the previous residue's C. Ordinary amide ω of a protein residue, aromatic ring and double-bond torsions, and every improper, stay unscaled whoever owns them. A proline-like ω is scaled when residue i's backbone is selected.
Choosing the residues around a ligand¶
The configuration always carries an explicit mask. To find out which residues line a site, ask, and paste the answer:
It prints the criterion it used, the structure and its digest, a table (index, chain, author
residue id, name, minimum distance) and a ready-to-paste mask. The criterion
(md-tools-pocket-selection/1) is: heavy atoms only on both sides, the minimum distance
over all pairs rather than a centroid distance, strictly less than the cutoff (a residue
exactly at it is out — and the report lists what lies just beyond, with its distance, so a
borderline residue is visible), the minimum image when the topology has a box, and no
solvent or ions unless --include-solvent is given.
It resolves nothing at run time. A pocket that were re-derived when a run started would mean the record said "whatever was within 5 Å on the day"; the mask you paste says which residues were hot.
Which sidechain bonds are scalable¶
The amino acids are a small fixed set, so this is a table, not a perception problem:
md_tools.rest2.sidechains (md-tools-sidechain-rotatability/1) lists, per residue, every
central bond inside the sidechain — each chi with its torsion, each terminal rotation (methyl,
hydroxyl, thiol, ammonium), and each bond that is never scaled with its class:
| never scaled | where | class |
|---|---|---|
| the sidechain amide C–N | ASN, GLN | amide_omega |
| the guanidinium, three bonds | ARG | double_bond |
| the aromatic ring | PHE, TYR, TRP, HIS (HID/HIE/HIP) | aromatic_ring |
A carboxylate (ASP, GLU) is not a central bond at all: both oxygens are terminal, so no torsion
runs across it. CYX's chi2 exists only once its disulfide does, and the SG–SG bond itself is owned
by both sidechains. python -c "from md_tools.rest2.sidechains import describe_residue;
print(describe_residue('TYR'))" prints one residue.
The table and the torsion classifier must agree, and a region resolution checks it: if they disagreed about a selected sidechain, the build is refused rather than scaling a region nobody predicted. A residue the table does not define — a modified or unsupported one — is refused by name.
Membership. Backbone is N, H, H1–H3, CA, HA, HA2, HA3, C, O and OXT. Everything else in a supported protein residue is sidechain. So glycine has no sidechain, proline's ring (CB, CG, CD) is sidechain, a CYX's SG is sidechain, and a cap (ACE, NME, NHE, NMA) is backbone throughout. Nucleic acids and residues outside the supported protein set are refused by name.
CMAP. A CMAP term is scaled when both its φ and ψ central bonds are selected, and left unchanged and reported when only one is. Under the ownership above, a standard residue's φ and ψ belong to the same backbone, so a mask cannot produce a mixed term; the rule matters for records written by hand. A map shared with an unselected residue is duplicated, never scaled in place. Note that ff19SB has no periodic term across N–CA or CA–C: a backbone selection there scales its CMAP and nonbonded terms only. Under ff14SB, φ and ψ torsions are scaled directly.
Ligand instances. mask names one residue, and two copies of a compound are two entries. The
compact form L01: <file> is refused for now: the ligand mapping record has no instance
alias, and a residue name cannot say which copy is meant (deferred to the coordinator). With
torsion_exclusions: auto, the instance is classified from its bond orders like any
non-standard residue. A file adds protected central bonds and never un-protects one:
format: md-tools-torsion-exclusions/1
parameters: CHEMBL112/param_0123456789ab # required: the package the names belong to
residue_name: TYL # optional; checked when present
central_bonds:
- [C4, N1] # package-local atom names
The file is refused when:
- the instance has no recorded package in
ligand_mapping.json; parametersdiffers from the instance's package;- it names an atom the residue does not have;
- a listed bond is not the central bond of a proper torsion.
Its contents and sha256 are saved in the record, and so is each named bond's topology-index pair.
A later edit to the file does not change a built state. It does change the next resolution, and
md_tools.rest2.regions.exclusion_file_changes reports it.
The record. scaler.yaml gains the following, while solute stays the whole solute that
trajectories and restraints are about:
selection:md-tools-solute-selection/2.0, carrying the mode, the masks as written, the residue map, the nonbonded atoms, the owned central bonds, per-term CMAP decisions, and the ligand instances;selection_sha256: the identity, over only what determines the Hamiltonian;selection_provenance_sha256: over the whole record, provenance included;scaler_arguments: exactly the argumentsbuild_scaled_systemtakes besides τ, so every state rebuilds from the record alone.
Hamiltonian identity is md-tools-hamiltonian-identity/v4. Its selection_sha256 hashes only
what determines the Hamiltonian: the hot atoms, the scaled and protected torsion bonds, each CMAP
decision, the improper policy and rule versions, and each ligand instance's residue, package and
resolved exclusions. Mask spelling, instance labels, file paths and an exclusion file's comments
are provenance. They are recorded, but they never make a run unresumable. v4 is v3 with
detector_policy_version moved out of the projection for exactly that reason, which is why a
0.6.1/0.6.2/0.6.3 (v3) record is accepted on sight of a matching v3_selection_sha256: the energy
did not move, so refusing it would strand an in-flight ladder over a stamp. A 0.6.0 (v2) identity
is still accepted too, but only for a legacy selection and only when every v2 field matches.
Both bridges live in ONE function, rest2.identity.require_same_hamiltonian, and every caller
that compares a hamiltonian entry — the ladder's compare_identity included — must go through
it rather than deciding for itself which formats are comparable. A caller that compares the entry
whole breaks on the next format bump, silently and for no physical reason.
What MD-tools implements¶
- a linear τ ladder from 0 to
tau_max, one Context per thermodynamic state; - neighbouring-pair exchange with a Metropolis criterion evaluated by recomputing both Hamiltonians, never by comparing two stored claims;
- one trajectory per fixed state (
solute_state0_prod1.nc,solute_state1_prod1.nc, …, pluswhole_state<i>_prod<N>.ncwhen a whole-system cadence is set), never per walker; - an Amber-format
rem.log, parseable bycpptrajas type Hamiltonian; - four independent schedules — exchange, whole-system output, solute output, checkpointing;
- checkpoint, restart and out-of-place extension.
What it does not implement¶
- no NPT ladder (see Ensemble above);
- no temperature REMD, and no velocity rescaling on exchange;
- no automatic ladder optimisation —
number_of_replicasandtau_maxare yours; - OpenMMTools is not imported by generated production code. It remains an optional oracle in tests, and the package works without it.
Required inputs¶
built.pdb, built.xml, the saved states in build/REST2/, and an equilibrated state to start
every rung from — normally eq_npt_free.xml from the cMD chain that build-md generates alongside
the ladder.
Minimal sequence¶
md-openmm build-top -i ALA.pdb \
-os build/built.xml -op build/built.pdb -log build/built.log
md-openmm build-top --rest2-scaler -s build/built.xml -p build/built.pdb \
--config build/scaler.config
md-openmm build-md -odir ./REST2-run1 --config example.config
cd REST2-run1 && ./run.sh
The ladder itself, driven directly:
One process holds every state and lets OpenMM choose the device. Under MPI, one rank per state:
mpiexec -n 4 python REST2.py -p ../build/built.pdb -ng 4 \
--groupfile remd_groupfile.1 -log REST2.log
# or, the Amber-like way -- one rank per thermodynamic state, `-ng` checked against both the
# configured replica count and the MPI world size:
mpirun -n 4 md-openmm md-run -ng 4 -i ../input/REST2.in -p ../build/built.pdb \
--groupfile remd_groupfile.1 -odir . \
-o remd_records/REST2_prod1.out -log remd_records/REST2_prod1.log \
-r remd_records/restart_prod1.json
A ladder reads -s only from its group file. Every line names one saved state,
-s ../build/REST2/system_state<i>.xml, state i on line i, all from one scaler.yaml whose taus
are the ladder's. There is no single -s for the launch to carry — one would claim one Hamiltonian
for every rung — so -s on the command line is refused by name, and so is a group file naming
anything other than saved states.
--groupfile always comes with -ng, and the group file also carries each rung's own -c, so
none is passed on the command line. -ng states how many groups the file is expected to hold: a
truncated group file would otherwise run a shorter ladder silently. A run with no group file at all
is refused — there is no second way to describe a coordinated run.
A world size that is neither 1 nor exactly the number of states is refused.
Equilibrating every rung under its own tau¶
By default the equilibration chain runs once, at τ = 0, and every rung starts from its end state.
rest2.equilibration_per_tau: true (off by default, REST2 only) runs the
equilibration on every rung, under that rung's own τ, instead:
| solvent | the τ = 0 chain (run.sh) |
the ladder's -c |
then, on every rung including τ = 0 |
|---|---|---|---|
| implicit | min only |
min.xml |
eq_nvt_posres, eq_nvt_posres_2, eq_nvt_free |
| explicit | min, eq_nvt_posres, eq_npt_posres, eq_npt_free — the NPT stages fix the box |
eq_npt_free.xml |
the same three, at that box |
The per-rung stages are the fixed-volume stages a scaled run gets — a scaled Hamiltonian is NVT
throughout, and a ladder's rungs share one volume — with the lengths stages.restrained_nvt_steps,
stages.restrained_npt_steps and stages.unrestrained_npt_steps; a stage of 0 steps is skipped
and all three at 0 is refused. Each is done as the stage chain does it: a fresh integrator seeded
derive_seed(seed, stage, "state<i>"), the previous stage's positions, velocities and box
installed (velocities are carried, never redrawn), then the restraint strength set; the restraint
is the chain's own Force, on the solute, towards the topology's coordinates, on a copy of the
rung — the rung Systems the ladder propagates never carry it.
Order: these stages, then rest2.equilibration_steps (if any), then the step-0 observation and
the first exchange. None of it is production. Each rung's end state is kept as
per_tau_state<i>.xml, described (seeds, stages, digests) by per_tau_equilibration.json and
recorded in restart.json under per_tau_equilibration; no trajectory or CV series is written
for it. A ladder interrupted during it has taken no exchange step and has no checkpoint, so
--resume refuses it by name — rerun with --overwrite; the equilibration is deterministic from
the recorded seeds. An exported reference bundle performs it too, with the same code.
Restraining every rung¶
A ladder may carry torsion restraints: umbrella.file names a restraint definition, exactly as
protocol: umbrella does, and collective_variables.file must name the definition those
restraints resolve their torsions against.
collective_variables: {file: cv.yaml, interval_steps: 500}
umbrella: {file: umbrella.yaml} # the SAME restraints on every rung
The same restraints on every rung, and that is the point. The exchange criterion compares
u_i(x) and u_j(x), each evaluated by installing a configuration in that rung's own Context, so
a bias W(x) present identically on both rungs enters both reduced potentials and cancels from
log alpha exactly. The ladder therefore samples the restrained ensemble at every rung with an
acceptance probability that is the unrestrained one. Per-rung variation is not offered: a bias that
differed between rungs would enter the acceptance probability and tilt the ladder towards whichever
rung restrains least, silently and with every output still well formed.
The restraint is added after the REST2 scaling and is never scaled by τ — it is not part of the
molecular Hamiltonian REST2 weakens, and a τ-dependent bias would not cancel. Each restraint's
force constant rides on its own per-torsion scale, so one definition may mix strengths and forms
(harmonic, flat_bottom) freely; the force's global parameter defaults to 1.0, so a rung is
biased from its first step without a runtime call. restart.json records the definition file and
every resolved restraint — which four atoms, which centre, which constant — under
scientific_identity.torsion_restraints, and the rank log prints them under "Torsion restraints".
Tested by arithmetic rather than by sampling: tests/test_ladder_torsion_restraints.py computes
log alpha with and without the bias at the same configurations and requires equality to 1e-6
kJ/mol, with a counter-example showing a bias that differs between rungs does not cancel;
tests/test_hprest2_gpu_evidence.py repeats the identity on a real CUDA ladder's own states.
Two consequences worth stating. A restrained and an unrestrained ladder from the same seed do not
accept the same exchanges — the bias changes the forces, so their trajectories diverge from the
first propagation; what is identical is the criterion at the same configurations. And a restrained
ladder cannot be exported as a reference bundle: verify_rungs.py rebuilds each rung from rung 0
by scaling, which is not how a restraint is applied, so export-reference refuses it by name.
Generated files¶
build/, min/ and input/ belong to the SYSTEM and are shared by every run beside them; only
REST2-run1/ belongs to this run. See the layout.
ALA/ the dataset root -- this is what you register
├── build/ built.xml built.pdb built.log
│ └── REST2/ system_state<n>.xml scaler.yaml <RESNAME>-unscaled.png
├── min/ the minimised structure, shared
├── input/ min.in eq_1.in eq_2.in eq_3.in REST2.in
└── REST2-run1/
├── resolved.config authoritative run.config the seed
├── REST2.py the compact ladder entry point
├── run.sh build-md.log
├── eq/ this run's equilibration: eq_<k>.{py,xml,out,log}
│ solute_eq_<k>.nc mdout_eq_<k>.csv
├── remd0/ remd1/ … ONE DIRECTORY PER THERMODYNAMIC STATE
│ remd_state<n>_prod<x>.nc whole-system trajectory, segment x
│ solute_state<n>_prod<x>.nc solute trajectory, segment x
├── remd_records/ the ladder's own records, ONE SET PER SEGMENT:
│ REST2_prod<x>.{out,log} restart_prod<x>.json rem_prod<x>.log
│ exchange_prod<x>.csv ledger_prod<x>.nc
├── cv_state<i>.csv the CV series, one per STATE, with its cv_state<i>.json sidecar
├── remd_groupfile.1 one line per state, naming that state's saved System
├── rank/ per-PROCESS reports
└── solute.yaml _protocol.py written by rank 0, verified by every rank
The index is the STATE's, never the walker's. After an accepted exchange the configuration in
remd2/ belongs to a different walker, and that is the point: MD-tools writes the state-sorted
file directly, which is why cpptraj needs remdtrajtemp and GROMACS ships demux.pl and this
does not. Files are grouped by state so that remd0/*.nc concatenates one state across every
segment in order; the per-segment records are grouped the other way, in remd_records/.
Restart and continuation¶
--resume continues from the checkpoint; the run state records interrupted or failed with a
reason when it does not finish, and only a run that did finish writes a completion manifest.
A resume reproduces the trajectory, not merely a valid one. Positions, velocities and box are
the complete physical state of a Langevin walker and are stored unconditionally, which is what
keeps a checkpoint readable on any device. They are not sufficient to continue a trajectory: the
integrator's pseudo-random stream has a position within it that coordinates do not carry, so a
walker resumed from them alone draws different noise from that point on — correct, statistically
exact, and a different trajectory. So each rung's OpenMM context checkpoint is stored alongside
the coordinates, with the platform and precision that produced it. A continuation that finds them
and is running on that platform restores them and reproduces an uninterrupted run exactly; one on
a different device ignores them and falls back to coordinates, as before. Which path was taken is
printed as # continuation : and recorded, never inferred. The cost is checkpoint size: one
context checkpoint per rung beside the coordinates, on a file that is rewritten whole rather than
accumulated.
One caveat belongs with this: OpenMM's CPU platform sums force reductions in
thread-completion order and so is reproducible only at a fixed thread count. Two replicate CPU
ladder runs with the same pinned seed diverge by the first observation when the pool differs.
That is a property of the platform, not of the ladder, and the run record carries cpu_threads
so two runs can be told apart from two thread counts.
Extension is out of place. --extend-from writes a new directory holding only the new segment
and pins the parent by content. The parent is left byte-for-byte unchanged, so a chain is a
sequence of immutable segments rather than a file that grows and loses its own history.
An extension segment is atomic: an interrupted one is REDONE, not resumed. There is no
mid-extension restart. --resume with --extend-from is refused as two different operations, and
--resume alone on the segment's own directory is worse than refused — it would physically
continue the run and write a restart.json with no extends block at all, so the segment would
finish looking like an ordinary run with its parent pinning, its segment-local counts and its
chain accounting silently gone. Nothing refuses that: an extension is atomic by omission,
because no resume path knows extends exists. Re-running into the partial directory is refused
three separate ways (--force is refused with --extend-from, --overwrite does not apply, and
the existing -x/-r/checkpoint collide), so the answer is a fresh directory and the whole
segment re-run. A partial segment costs what it cost; chunk a long chain into several smaller
--extend segments, each extending the previous completed one, so an interruption loses one
small segment rather than a long one. The chunk's own generated script is never the right entry
point either — the helpers in an extension directory are written and digest-verified by whichever
generated script runs with -odir, which is the parent's.
Storage, restart and validation¶
Four records, four jobs:
| record | written | job |
|---|---|---|
analysis NetCDF (-x) |
every committed iteration | the authoritative history, plus the scientific identity written before propagation begins |
| checkpoint NetCDF | every whole-output interval | configurations, mapping, RNG states, rule state, iteration and budget |
<stem>.runstate.json |
atomically, on transition | initialized → running → completed/interrupted/failed; never claims completion |
restart.json (-r) |
atomically, at the end | evidence of completion; never a precondition for resuming an in-place run — but an out-of-place extension has no resume at all, so for a segment its absence means the segment is redone |
Each stream's marker is written after every array for that row, so an interrupted write leaves the counter on the previous complete row. A marker may lag its data but never lead it, and one that leads is refused as a file disagreeing with itself.
A resume continues from the last checkpoint, not the last committed row: the exchange stream commits every attempt while checkpoints are written less often, so an interruption leaves committed rows with no configurations behind them. Those rows are rewound. Before anything is opened for writing the stored output is read and validated — markers within their rows, steps strictly increasing in every stream, and the stored exchange steps equal to the scheduled ones, because a dropped attempt in the middle still leaves steps increasing and can still reach the budget.
--resume needs no restart.json. --extend N requires a run that reached its budget and adds N
attempts to the budget that run reached, not to the one in the configuration — a run already
extended stores a larger budget than the configuration describes.
Both of those are about a run continuing in place. The auto-continue contract and every
sentence above about resuming cover in-place runs only: an interrupted out-of-place extension
(--extend-from) is re-run as a whole segment into a fresh directory, as described under
Extension is out of place. The interrupted run-state note and the executor's message say
which of the two you are holding, so the file itself answers the question.
An interruption is neither success nor failure and exits with its own status (130). Verification opens and reads the files: a file that exists is what a crashed run leaves behind, so existence is never treated as completion.
The selection file¶
solute.yaml, written beside the run by rank 0, records what the ladder integrated: the solute
atom indices, the unscaled central bonds and impropers with readable residue and atom labels, the
topology digest, and rest2.scaled_states — the scaler.yaml it came from (relative to the run)
and every state's sha256. It is a record, not an input: the selection is made once, by
build-top --rest2-scaler, and the ladder classifies nothing itself.
Configuration fields that matter¶
| field | default | why you would change it |
|---|---|---|
rest2.number_of_replicas |
4 | the number of states. Too few and neighbouring rungs never exchange |
rest2.tau_max |
0.5 | the top rung. τ = 0.5 scales solute–solute terms by 0.25 |
rest2.exchange_interval_steps |
5000 | steps between exchange attempts |
rest2.number_of_exchanges |
500 | the length of the run, in exchanges |
rest2.equilibration_steps |
0 | a free relaxation of every rung under its own τ before the first exchange |
rest2.equilibration_per_tau |
false | the whole equilibration chain on every rung under its own τ; see above |
rest2.state_trajectory |
true | one trajectory per state |
rest2.rem_log |
true | the Amber-format exchange log |
Reporting and registration¶
REST2.log is the machine record. The per-state trajectories and rem.log are what an analysis
reads; cpptraj parses both. Register the finished tree with md-openmm data-register — see
data registration.
Limitations¶
- The ladder is linear in τ. Nothing here optimises the spacing for a target acceptance ratio.
- Acceptance is reported, not enforced. A ladder that never exchanges completes and says so; it is not automatically repaired.
- NVT only.
- The ω convention above is a deliberate deviation from textbook REST2 and must be stated in any write-up that uses this implementation.
- Implicit-solvent ligand REST2 remains scientifically unvalidated.
--extendand--resumeare coordinated across ranks at an event boundary; only rank 0 opens the analysis storage for writing, in any mode.- Smoke runs are picoseconds and validate neither ladder quality nor convergence.
- Datasets registered before v0.5.0 were produced by a retired generator. Their provenance records that and is not rewritten: only the way a ladder is generated changed, not the Hamiltonian it runs.
References¶
REST2 [@wang2011rest2]; the force fields and integrator settings in scientific defaults.