Skip to content

Commit b18f499

Browse files
committed
Merge branch 'main' into cmd-bootstrap-init
2 parents e773c70 + 6f0e2d6 commit b18f499

File tree

6 files changed

+201
-11
lines changed

6 files changed

+201
-11
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import click
2+
3+
import paths_cli.utils
4+
from paths_cli.parameters import APPEND_FILE, ENGINE, MULTI_TAG
5+
from paths_cli import OPSCommandPlugin
6+
7+
8+
@click.command(
9+
"load-trajectory",
10+
short_help="Load an external trajectory file",
11+
)
12+
@click.argument('traj_file')
13+
@click.option(
14+
'--top',
15+
help=(
16+
"Topology file (typically PDB). Only for required "
17+
"formats."
18+
),
19+
default=None,
20+
)
21+
@APPEND_FILE.clicked(required=True)
22+
@MULTI_TAG.clicked()
23+
def load_trajectory(traj_file, top, append_file, tag):
24+
"""Load a trajectory from a file.
25+
26+
This uses MDTraj under the hood, and can load any file format that
27+
MDTraj can. NB: This stores in a format based on OpenMM snapshots.
28+
Trajectories loaded this way will work with engines compatible with
29+
that input (e.g., GROMACS).
30+
"""
31+
from openpathsampling.engines.openmm.tools import ops_load_trajectory
32+
if top:
33+
traj = ops_load_trajectory(traj_file, top=top)
34+
else:
35+
traj = ops_load_trajectory(traj_file)
36+
37+
storage = APPEND_FILE.get(append_file)
38+
storage.save(traj)
39+
for tag_name in tag:
40+
storage.tags[tag_name] = traj
41+
42+
43+
PLUGIN = OPSCommandPlugin(
44+
command=load_trajectory,
45+
section="Miscellaneous",
46+
requires_ops=(1, 6),
47+
requires_cli=(0, 4),
48+
)
49+
50+
if __name__ == "__main__": # -no-cov-
51+
load_trajectory()

paths_cli/parameters.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,29 @@
1818
store='schemes',
1919
)
2020

21-
INIT_CONDS = OPSStorageLoadMultiple(
21+
class InitCondsLoader(OPSStorageLoadMultiple):
22+
def _extract_trajectories(self, obj):
23+
import openpathsampling as paths
24+
if isinstance(obj, paths.SampleSet):
25+
yield from (s.trajectory for s in obj)
26+
elif isinstance(obj, paths.Sample):
27+
yield obj.trajectory
28+
elif isinstance(obj, paths.Trajectory):
29+
yield obj
30+
elif isinstance(obj, list):
31+
for o in obj:
32+
yield from self._extract_trajectories(o)
33+
else:
34+
raise RuntimeError("Unknown initial conditions type: "
35+
f"{obj} (type: {type(obj)}")
36+
37+
def get(self, storage, names):
38+
results = super().get(storage, names)
39+
final_results = list(self._extract_trajectories(results))
40+
return final_results
41+
42+
43+
INIT_CONDS = InitCondsLoader(
2244
param=Option('-t', '--init-conds', multiple=True,
2345
help=("identifier for initial conditions "
2446
+ "(sample set or trajectory)" + HELP_MULTIPLE)),

paths_cli/tests/commands/test_equilibrate.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
def print_test(output_storage, scheme, init_conds, multiplier, extra_steps):
1212
print(isinstance(output_storage, paths.Storage))
1313
print(scheme.__uuid__)
14-
print(init_conds.__uuid__)
14+
print([o.__uuid__ for o in init_conds])
1515
print(multiplier, extra_steps)
1616

1717

@@ -31,8 +31,10 @@ def test_equilibrate(tps_fixture):
3131
["setup.nc", "-o", "foo.nc"]
3232
)
3333
out_str = "True\n{schemeid}\n{condsid}\n1 0\n"
34-
expected_output = out_str.format(schemeid=scheme.__uuid__,
35-
condsid=init_conds.__uuid__)
34+
expected_output = out_str.format(
35+
schemeid=scheme.__uuid__,
36+
condsid=[o.trajectory.__uuid__ for o in init_conds],
37+
)
3638
assert results.exit_code == 0
3739
assert results.output == expected_output
3840

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from click.testing import CliRunner
2+
from contextlib import contextmanager
3+
import pytest
4+
from importlib import resources
5+
from openpathsampling.tests.test_helpers import data_filename
6+
import openpathsampling as paths
7+
8+
from paths_cli.commands.load_trajectory import *
9+
10+
11+
@contextmanager
12+
def run_load_trajectory(args):
13+
runner = CliRunner()
14+
with runner.isolated_filesystem():
15+
storage = paths.Storage("setup.nc", 'w')
16+
storage.close()
17+
results = runner.invoke(
18+
load_trajectory,
19+
args
20+
)
21+
assert results.exit_code == 0
22+
st = paths.Storage("setup.nc", mode='r')
23+
assert len(st.trajectories) == 1
24+
yield st
25+
26+
27+
@pytest.mark.parametrize("with_top", [True, False])
28+
@pytest.mark.parametrize("with_tag", [True, False])
29+
def test_load_trajectory_pdb(with_top, with_tag):
30+
# test that we can load a PDB file with or without topology; also tests
31+
# that the taging works correctly
32+
pytest.importorskip("openmm")
33+
pytest.importorskip("mdtraj")
34+
pdb_path = data_filename("ala_small_traj.pdb")
35+
out_file = "setup.nc"
36+
args = [
37+
pdb_path,
38+
'--append-file', out_file,
39+
]
40+
if with_top:
41+
args.extend(['--top', pdb_path])
42+
43+
if with_tag:
44+
args.extend(['--tag', 'init_snap'])
45+
46+
with run_load_trajectory(args) as st:
47+
traj = st.trajectories[0]
48+
assert len(traj) == 10
49+
if with_tag:
50+
tagged = st.tags['init_snap']
51+
assert tagged == traj
52+
53+
def test_load_trajectory_trr():
54+
pytest.importorskip("openmm")
55+
pytest.importorskip("mdtraj")
56+
trr = data_filename("gromacs_engine/project_trr/0000000.trr")
57+
gro = data_filename("gromacs_engine/conf.gro")
58+
out_file = "setup.nc"
59+
args = [
60+
trr,
61+
'--append-file', out_file,
62+
'--top', gro,
63+
]
64+
with run_load_trajectory(args) as st:
65+
traj = st.trajectories[0]
66+
assert len(traj) == 4
67+
68+
def test_load_trajectory_bad_topology():
69+
pytest.importorskip("openmm")
70+
pytest.importorskip("mdtraj")
71+
trr = data_filename("gromacs_engine/project_trr/0000000.trr")
72+
pdb = data_filename("tip4p_water.pdb")
73+
out_file = "setup.nc"
74+
args = [
75+
trr,
76+
'--append-file', out_file,
77+
'--top', pdb,
78+
]
79+
runner = CliRunner()
80+
with runner.isolated_filesystem():
81+
storage = paths.Storage("setup.nc", 'w')
82+
storage.close()
83+
result = runner.invoke(
84+
load_trajectory,
85+
args
86+
)
87+
assert result.exit_code == 1
88+
assert "topology" in str(result.exception)
89+
assert "same atoms" in str(result.exception)

paths_cli/tests/commands/test_pathsampling.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
def print_test(output_storage, scheme, init_conds, n_steps):
1212
print(isinstance(output_storage, paths.Storage))
1313
print(scheme.__uuid__)
14-
print(init_conds.__uuid__)
14+
print([traj.__uuid__ for traj in init_conds])
1515
print(n_steps)
1616

1717
@patch('paths_cli.commands.pathsampling.pathsampling_main', print_test)
@@ -26,7 +26,8 @@ def test_pathsampling(tps_fixture):
2626

2727
results = runner.invoke(pathsampling, ['setup.nc', '-o', 'foo.nc',
2828
'-n', '1000'])
29-
expected_output = (f"True\n{scheme.__uuid__}\n{init_conds.__uuid__}"
29+
initcondsid = [samp.trajectory.__uuid__ for samp in init_conds]
30+
expected_output = (f"True\n{scheme.__uuid__}\n{initcondsid}"
3031
"\n1000\n")
3132

3233
assert results.output == expected_output

paths_cli/tests/test_parameters.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,8 +214,12 @@ def create_file(self, getter):
214214
get_type, getter_style = self._parse_getter(getter)
215215
main, other = {
216216
'traj': (self.traj, self.other_traj),
217-
'sset': (self.sample_set, self.other_sample_set)
217+
'sset': (self.sample_set, self.other_sample_set),
218+
'samp': (self.sample_set[0], self.other_sample_set[0]),
218219
}[get_type]
220+
if get_type == 'samp':
221+
storage.save(main)
222+
storage.save(other)
219223
if get_type == 'sset':
220224
storage.save(self.sample_set)
221225
storage.save(self.other_sample_set)
@@ -231,20 +235,23 @@ def create_file(self, getter):
231235

232236
if other_tag:
233237
storage.tags[other_tag] = other
238+
234239
storage.close()
235240
return filename
236241

237242
@pytest.mark.parametrize("getter", [
238243
'name-traj', 'number-traj', 'tag-final-traj', 'tag-initial-traj',
239-
'name-sset', 'number-sset', 'tag-final-sset', 'tag-initial-sset'
244+
'name-sset', 'number-sset', 'tag-final-sset', 'tag-initial-sset',
245+
'name-samp', 'number-samp',
240246
])
241247
def test_get(self, getter):
242248
filename = self.create_file(getter)
243249
storage = paths.Storage(filename, mode='r')
244250
get_type, getter_style = self._parse_getter(getter)
245251
expected = {
246-
'sset': self.sample_set,
247-
'traj': self.traj
252+
'sset': [s.trajectory for s in self.sample_set],
253+
'traj': [self.traj],
254+
'samp': [self.sample_set[0].trajectory],
248255
}[get_type]
249256
get_arg = {
250257
'name': 'traj',
@@ -277,7 +284,13 @@ def test_get_none(self, num_in_file):
277284

278285
st = paths.Storage(filename, mode='r')
279286
obj = INIT_CONDS.get(st, None)
280-
assert obj == stored_things[num_in_file - 1]
287+
expected = [
288+
[self.traj],
289+
[s.trajectory for s in self.sample_set],
290+
[s.trajectory for s in self.other_sample_set],
291+
[s.trajectory for s in self.other_sample_set],
292+
]
293+
assert obj == expected[num_in_file - 1]
281294

282295
def test_get_multiple(self):
283296
filename = self.create_file('number-traj')
@@ -297,6 +310,18 @@ def test_cannot_guess(self):
297310
with pytest.raises(RuntimeError):
298311
self.PARAMETER.get(storage, None)
299312

313+
def test_get_bad_name(self):
314+
filename = self._filename("bad_tag")
315+
storage = paths.Storage(filename, 'w')
316+
storage.save(self.traj)
317+
storage.save(self.other_traj)
318+
storage.tags['bad_tag'] = "foo"
319+
storage.close()
320+
321+
storage = paths.Storage(filename, 'r')
322+
with pytest.raises(RuntimeError, match="initial conditions type"):
323+
self.PARAMETER.get(storage, "bad_tag")
324+
300325

301326
class TestINIT_SNAP(ParamInstanceTest):
302327
PARAMETER = INIT_SNAP

0 commit comments

Comments
 (0)