Source code for topo.mdrun.protocol
"""Temperature protocol for the runner: equilibrium and annealing/quenching.
A *protocol* is a list of ``(temperature, n_steps)`` stages. The runner builds up
to two of them and runs each into its own output files:
* :func:`quench_schedule` -- the **quench phase** (only when ``anneal = yes``):
hold at ``t_high`` and, for a linear ramp, cool down to ``ref_t``. Written to
``<outname>_quench.dcd`` / ``.log``. Sums to ``cfg.quench_steps()``.
* :func:`production_schedule` -- the **production phase**: ``md_steps`` at
``ref_t``. Written to the usual ``<outname>.dcd`` / ``.log``. This is also the
whole schedule for a plain equilibrium run.
``anneal_steps`` is therefore *separate* from ``md_steps`` (the grand total is
``quench_steps() + md_steps``), and ``ref_t`` is always the low / refold
temperature -- there is no separate ``t_low`` key.
:func:`run_protocol` drives an existing ``Simulation`` through one schedule,
setting the integrator temperature per stage, and can resume a restart partway
through a stage.
"""
from openmm import unit
[docs]
def quench_schedule(cfg):
"""Stages for the quench phase, summing to ``cfg.quench_steps()``.
Returns ``[]`` when annealing is off. Otherwise:
* ``anneal_ramp = jump`` -> ``[(t_high, anneal_steps)]``. The actual quench
to ``ref_t`` happens at the phase boundary, when production starts, so the
delta T-jump lands exactly between the ``_quench`` and production files.
* ``anneal_ramp = linear`` -> the hold at ``t_high`` followed by
``anneal_ramp_increments`` discrete cooling stages spanning
``anneal_ramp_steps`` (T decreasing ``t_high -> ref_t``).
"""
if not cfg.anneal:
return []
stages = [(cfg.t_high, cfg.anneal_steps)]
if cfg.anneal_ramp == 'linear' and cfg.anneal_ramp_steps > 0:
n_inc = max(1, cfg.anneal_ramp_increments)
t_high = cfg.t_high.value_in_unit(unit.kelvin)
t_low = cfg.ref_t.value_in_unit(unit.kelvin)
base = cfg.anneal_ramp_steps // n_inc
for i in range(1, n_inc + 1):
# Temperature at the END of increment i (so the final increment lands
# exactly on ref_t). Put the division remainder on the last increment.
frac = i / n_inc
temperature = (t_high + (t_low - t_high) * frac) * unit.kelvin
n = base if i < n_inc else cfg.anneal_ramp_steps - base * (n_inc - 1)
if n > 0:
stages.append((temperature, n))
return stages
[docs]
def production_schedule(cfg):
"""The production phase: a single constant-``ref_t`` stage of ``md_steps``."""
return [(cfg.ref_t, cfg.md_steps)]
[docs]
def describe_schedule(schedule):
"""One-line human summary, e.g. ``600 K x 100000 -> 300 K x 900000``."""
parts = []
for temperature, n in schedule:
try:
t = f"{temperature.value_in_unit(unit.kelvin):g} K"
except AttributeError:
t = f"{temperature} K"
parts.append(f"{t} x {n}")
return " -> ".join(parts) if parts else "(none)"
[docs]
def run_protocol(simulation, schedule, done_steps=0):
"""Step ``simulation`` through ``schedule``, one stage at a time.
``done_steps`` is measured relative to the start of this schedule. It lets a
restart resume mid-schedule: stages already completed are skipped, and a
stage that was partially done runs only its remaining steps. For a single
stage this reduces to ``simulation.step(n - done_steps)``.
"""
consumed = 0
for temperature, n in schedule:
if n <= 0:
continue
stage_end = consumed + n
if stage_end <= done_steps:
consumed = stage_end
continue
run = stage_end - max(consumed, done_steps)
simulation.integrator.setTemperature(temperature)
simulation.step(run)
consumed = stage_end