Skip to content

Commit 29cd0d0

Browse files
authored
Merge pull request #35 from dwhswenson/md
New command: `md`
2 parents 10dcb1b + 8e9fa46 commit 29cd0d0

File tree

6 files changed

+417
-3
lines changed

6 files changed

+417
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ miscellaneous operations on OPS output files.
2222
**Simulation Commands:**
2323

2424
* `visit-all`: Run MD to generate initial trajectories
25+
* `md`: Run MD for fixed time or until a given ensemble is satisfied
2526
* `equilibrate`: Run equilibration for path sampling
2627
* `pathsampling`: Run any path sampling simulation, including TIS variants
2728

paths_cli/commands/md.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import click
2+
3+
import paths_cli.utils
4+
from paths_cli.parameters import (INPUT_FILE, OUTPUT_FILE, ENGINE,
5+
MULTI_ENSEMBLE, INIT_SNAP)
6+
7+
import logging
8+
logger = logging.getLogger(__name__)
9+
10+
@click.command(
11+
"md",
12+
short_help=("Run MD for fixed time or until a given ensemble is "
13+
"satisfied"),
14+
)
15+
@INPUT_FILE.clicked(required=True)
16+
@OUTPUT_FILE.clicked(required=True)
17+
@ENGINE.clicked(required=False)
18+
@MULTI_ENSEMBLE.clicked(required=False)
19+
@click.option('-n', '--nsteps', type=int,
20+
help="number of MD steps to run")
21+
@INIT_SNAP.clicked(required=False)
22+
def md(input_file, output_file, engine, ensemble, nsteps, init_frame):
23+
"""Run MD for for time of steps or until ensembles are satisfied.
24+
25+
This can either take a --nsteps or --ensemble, but not both. If the
26+
--ensemble option is specfied more than once, then this will attempt to
27+
run until all ensembles are satisfied by a subtrajectory.
28+
29+
This still respects the maximum number of frames as set in the engine,
30+
and will terminate if the trajectory gets longer than that.
31+
"""
32+
storage = INPUT_FILE.get(input_file)
33+
md_main(
34+
output_storage=OUTPUT_FILE.get(output_file),
35+
engine=ENGINE.get(storage, engine),
36+
ensembles=MULTI_ENSEMBLE.get(storage, ensemble),
37+
nsteps=nsteps,
38+
initial_frame=INIT_SNAP.get(storage, init_frame)
39+
)
40+
41+
class ProgressReporter(object):
42+
"""Generic class for a callable that reports progress.
43+
44+
Base class for ends-with-ensemble and fixed-length tricks.
45+
46+
Parameters
47+
----------
48+
timestep : Any
49+
timestep, optionally with units
50+
update_freq : int
51+
how often to report updates
52+
"""
53+
def __init__(self, timestep, update_freq):
54+
self.timestep = timestep
55+
self.update_freq = update_freq
56+
57+
def steps_progress_string(self, n_steps):
58+
"""Return string for number of frames run and time elapsed
59+
60+
Not newline-terminated.
61+
"""
62+
report_str = "Ran {n_steps} frames"
63+
if self.timestep is not None:
64+
report_str += " [{}]".format(str(n_steps * self.timestep))
65+
report_str += '.'
66+
return report_str.format(n_steps=n_steps)
67+
68+
def progress_string(self, n_steps):
69+
"""Return the progress string. Subclasses may override.
70+
"""
71+
report_str = self.steps_progress_string(n_steps) + "\n"
72+
return report_str.format(n_steps=n_steps)
73+
74+
def report_progress(self, n_steps, force=False):
75+
"""Report the progress to the terminal.
76+
"""
77+
import openpathsampling as paths
78+
if (n_steps % self.update_freq == 0) or force:
79+
string = self.progress_string(n_steps)
80+
paths.tools.refresh_output(string)
81+
82+
def __call__(self, trajectory, trusted=False):
83+
raise NotImplementedError()
84+
85+
86+
class EnsembleSatisfiedContinueConditions(ProgressReporter):
87+
"""Continuation condition for including subtrajs for each ensemble.
88+
89+
This object creates a continuation condition (a callable) analogous with
90+
the ensemble ``can_append`` method. This will tell the trajectory to
91+
keep running until, for each of the given ensembles, a subtrajectory has
92+
been found that will satisfy the ensemble.
93+
94+
Parameters
95+
----------
96+
ensembles: List[:class:`openpathsampling.Ensemble`]
97+
the ensembles to satisfy
98+
timestep : Any
99+
timestep, optionally with units
100+
update_freq : int
101+
how often to report updates
102+
"""
103+
def __init__(self, ensembles, timestep=None, update_freq=10):
104+
super().__init__(timestep, update_freq)
105+
self.satisfied = {ens: False for ens in ensembles}
106+
107+
def progress_string(self, n_steps):
108+
report_str = self.steps_progress_string(n_steps)
109+
report_str += (" Found ensembles [{found}]. "
110+
"Looking for [{missing}].\n")
111+
found = [ens.name for ens, done in self.satisfied.items() if done]
112+
missing = [ens.name for ens, done in self.satisfied.items()
113+
if not done]
114+
found_str = ",".join(found)
115+
missing_str = ",".join(missing)
116+
return report_str.format(n_steps=n_steps,
117+
found=found_str,
118+
missing=missing_str)
119+
120+
121+
def _check_previous_frame(self, trajectory, start, unsatisfied):
122+
if -start > len(trajectory):
123+
# we've done the whole traj; don't keep going
124+
return False
125+
subtraj = trajectory[start:]
126+
logger.debug(str(subtraj) + "/" + str(trajectory))
127+
for ens in unsatisfied:
128+
if not ens.strict_can_prepend(subtraj, trusted=True):
129+
# test if we can't prepend because we satsify
130+
self.satisfied[ens] = ens(subtraj) or ens(subtraj[1:])
131+
unsatisfied.remove(ens)
132+
return bool(unsatisfied)
133+
134+
def _call_untrusted(self, trajectory):
135+
self.satisfied = {ens: False for ens in self.satisfied}
136+
for i in range(1, len(trajectory)):
137+
keep_going = self(trajectory[:i], trusted=True)
138+
if not keep_going:
139+
return False
140+
return self(trajectory, trusted=True)
141+
142+
def __call__(self, trajectory, trusted=False):
143+
if not trusted:
144+
return self._call_untrusted(trajectory)
145+
146+
# below here, trusted is True
147+
self.report_progress(len(trajectory) - 1)
148+
149+
unsatisfied = [ens for ens, done in self.satisfied.items()
150+
if not done]
151+
# TODO: update on how many ensembles left, what frame number we are
152+
153+
start = -1
154+
while self._check_previous_frame(trajectory, start, unsatisfied):
155+
start -= 1
156+
157+
return not all(self.satisfied.values())
158+
159+
160+
class FixedLengthContinueCondition(ProgressReporter):
161+
"""Continuation condition for fixed-length runs.
162+
163+
Parameters
164+
----------
165+
length : int
166+
final length of the trajectory in frames
167+
timestep : Any
168+
timestep, optionally with units
169+
update_freq : int
170+
how often to report updates
171+
"""
172+
def __init__(self, length, timestep=None, update_freq=10):
173+
super().__init__(timestep, update_freq)
174+
self.length = length
175+
176+
def __call__(self, trajectory, trusted=False):
177+
len_traj = len(trajectory)
178+
self.report_progress(len_traj - 1)
179+
return len_traj < self.length
180+
181+
182+
183+
def md_main(output_storage, engine, ensembles, nsteps, initial_frame):
184+
import openpathsampling as paths
185+
if nsteps is not None and ensembles:
186+
raise RuntimeError("Options --ensemble and --nsteps cannot both be"
187+
" used at once.")
188+
189+
if ensembles:
190+
continue_cond = EnsembleSatisfiedContinueConditions(ensembles)
191+
else:
192+
continue_cond = FixedLengthContinueCondition(nsteps)
193+
194+
trajectory = engine.generate(initial_frame, running=continue_cond)
195+
continue_cond.report_progress(len(trajectory) - 1, force=True)
196+
paths_cli.utils.tag_final_result(trajectory, output_storage,
197+
'final_conditions')
198+
return trajectory, None
199+
200+
CLI = md
201+
SECTION = "Simulation"
202+
REQUIRES_OPS = (1, 0)
203+

paths_cli/commands/visit_all.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import click
22

3+
import paths_cli.utils
34
from paths_cli.parameters import (INPUT_FILE, OUTPUT_FILE, ENGINE, STATES,
45
INIT_SNAP)
56

@@ -34,9 +35,8 @@ def visit_all_main(output_storage, states, engine, initial_frame):
3435
timestep = getattr(engine, 'timestep', None)
3536
visit_all_ens = paths.VisitAllStatesEnsemble(states, timestep=timestep)
3637
trajectory = engine.generate(initial_frame, [visit_all_ens.can_append])
37-
if output_storage is not None:
38-
output_storage.save(trajectory)
39-
output_storage.tags['final_conditions'] = trajectory
38+
paths_cli.utils.tag_final_result(trajectory, output_storage,
39+
'final_conditions')
4040

4141
return trajectory, None # no simulation object to return here
4242

paths_cli/parameters.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@
5757
store='engines'
5858
)
5959

60+
MULTI_ENSEMBLE = OPSStorageLoadNames(
61+
param=Option('--ensemble', type=str, multiple=True,
62+
help='name of index of ensemble' + HELP_MULTIPLE),
63+
store='ensembles'
64+
)
65+
6066
STATES = OPSStorageLoadNames(
6167
param=Option('-s', '--state', type=str, multiple=True,
6268
help='name of state' + HELP_MULTIPLE),

0 commit comments

Comments
 (0)