Skip to content

Commit 2b6714b

Browse files
authored
Merge pull request #54 from dwhswenson/wizard-rewrite
Wizard rewrite
2 parents f8ed965 + 1399860 commit 2b6714b

32 files changed

Lines changed: 2341 additions & 449 deletions

‎paths_cli/commands/wizard.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import click
2+
from paths_cli.wizard.plugin_registration import register_installed_plugins
23
from paths_cli.wizard.two_state_tps import TWO_STATE_TPS_WIZARD
4+
from paths_cli import OPSCommandPlugin
35

46
@click.command(
57
'wizard',
68
short_help="run wizard for setting up simulations",
79
)
810
def wizard(): # no-cov
11+
register_installed_plugins()
912
TWO_STATE_TPS_WIZARD.run_wizard()
1013

11-
CLI = wizard
12-
SECTION = "Simulation setup"
14+
15+
PLUGIN = OPSCommandPlugin(
16+
command=wizard,
17+
section="Simulation setup",
18+
requires_ops=(1, 0),
19+
requires_cli=(0, 3)
20+
)

‎paths_cli/compiling/volumes.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ def cv_volume_build_func(**dct):
1313
builder = paths.CVDefinedVolume
1414
if cv.period_min is not None or cv.period_max is not None:
1515
builder = paths.PeriodicCVDefinedVolume
16+
dct['period_min'] = cv.period_min
17+
dct['period_max'] = cv.period_max
1618

1719
dct['collectivevariable'] = dct.pop('cv')
1820
# TODO: wrap this with some logging

‎paths_cli/plugin_management.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
class PluginRegistrationError(RuntimeError):
88
pass
99

10-
# TODO: make more generic than OPS (requires_ops => requires_lib)
1110
class Plugin(object):
1211
"""Generic plugin object
1312

‎paths_cli/tests/compiling/test_volumes.py‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
from paths_cli.tests.compiling.utils import mock_compiler
44

55
import yaml
6+
import numpy as np
67
import openpathsampling as paths
8+
from openpathsampling.experimental.storage.collective_variables import \
9+
CoordinateFunctionCV
710
from openpathsampling.tests.test_helpers import make_1d_traj
811

912
from paths_cli.compiling.volumes import *
@@ -13,7 +16,6 @@ def setup(self):
1316
self.yml = "\n".join(["type: cv-volume", "cv: {func}",
1417
"lambda_min: 0", "lambda_max: 1"])
1518

16-
self.mock_cv = mock.Mock(return_value=0.5)
1719
self.named_objs_dict = {
1820
'foo': {'name': 'foo',
1921
'type': 'bar',
@@ -40,16 +42,29 @@ def test_build_cv_volume(self, inline, periodic):
4042
self.set_periodic(periodic)
4143
yml = self.yml.format(func=self.func[inline])
4244
dct = yaml.load(yml, Loader=yaml.FullLoader)
45+
period_min, period_max = {'periodic': (-np.pi, np.pi),
46+
'nonperiodic': (None, None)}[periodic]
47+
mock_cv = CoordinateFunctionCV(lambda s: s.xyz[0][0],
48+
period_min=period_min,
49+
period_max=period_max).named('foo')
4350
if inline =='external':
4451
patch_loc = 'paths_cli.compiling.root_compiler._COMPILERS'
4552
compilers = {
46-
'cv': mock_compiler('cv', named_objs={'foo': self.mock_cv})
53+
'cv': mock_compiler('cv', named_objs={'foo': mock_cv})
4754
}
4855
with mock.patch.dict(patch_loc, compilers):
4956
vol = build_cv_volume(dct)
5057
elif inline == 'internal':
5158
vol = build_cv_volume(dct)
52-
assert vol.collectivevariable(1) == 0.5
59+
60+
in_state = make_1d_traj([0.5])[0]
61+
assert vol.collectivevariable(in_state) == 0.5
62+
assert vol(in_state)
63+
out_state = make_1d_traj([2.0])[0]
64+
assert vol.collectivevariable(out_state) == 2.0
65+
assert not vol(out_state)
66+
if_periodic = make_1d_traj([7.0])[0]
67+
assert (vol(if_periodic) == (periodic == 'periodic'))
5368
expected_class = {
5469
'nonperiodic': paths.CVDefinedVolume,
5570
'periodic': paths.PeriodicCVDefinedVolume

‎paths_cli/tests/wizard/test_cvs.py‎

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from paths_cli.tests.wizard.mock_wizard import mock_wizard
88

99
from paths_cli.wizard.cvs import (
10-
_get_topology, _get_atom_indices, distance, angle, dihedral, rmsd,
11-
coordinate, cvs, SUPPORTED_CVS
10+
TOPOLOGY_CV_PREREQ, _get_atom_indices, MDTRAJ_DISTANCE, MDTRAJ_ANGLE,
11+
MDTRAJ_DIHEDRAL, COORDINATE_CV
1212
)
1313

1414
import openpathsampling as paths
@@ -17,7 +17,7 @@
1717
from openpathsampling.tests.test_helpers import make_1d_traj
1818

1919
@pytest.mark.parametrize('n_engines', [0, 1, 2])
20-
def test_get_topology(ad_engine, n_engines):
20+
def test_TOPOLOGY_CV_PREREQ(ad_engine, n_engines):
2121
inputs = {0: [], 1: [], 2: ['1']}[n_engines]
2222
wizard = mock_wizard(inputs)
2323
engines = [ad_engine, paths.engines.NoEngine(None).named('foo')]
@@ -27,9 +27,10 @@ def mock_register(obj, obj_type, store_name):
2727
wizard.register = mock_register
2828

2929
mock_engines = mock.Mock(return_value=ad_engine)
30-
patch_loc = 'paths_cli.wizard.engines.engines'
30+
patch_loc = 'paths_cli.wizard.cvs.TOPOLOGY_CV_PREREQ.create_func'
3131
with mock.patch(patch_loc, new=mock_engines):
32-
topology = _get_topology(wizard)
32+
topology_result = TOPOLOGY_CV_PREREQ(wizard)
33+
topology = topology_result['topology'][0]
3334
assert isinstance(topology, paths.engines.MDTrajTopology)
3435

3536
@pytest.mark.parametrize('inputs', [
@@ -58,37 +59,31 @@ def test_distance(ad_openmm, ad_engine):
5859
md = pytest.importorskip('mdtraj')
5960
wizard = mock_wizard(['0, 1'])
6061
md_func = partial(md.compute_distances, atom_pairs=[[0, 1]])
61-
_mdtraj_function_test(wizard, distance, md_func, ad_openmm, ad_engine)
62+
_mdtraj_function_test(wizard, MDTRAJ_DISTANCE, md_func, ad_openmm,
63+
ad_engine)
6264

6365
def test_angle(ad_openmm, ad_engine):
6466
md = pytest.importorskip('mdtraj')
6567
wizard = mock_wizard(['0, 1, 2'])
6668
md_func = partial(md.compute_angles, angle_indices=[[0, 1, 2]])
67-
_mdtraj_function_test(wizard, angle, md_func, ad_openmm, ad_engine)
69+
_mdtraj_function_test(wizard, MDTRAJ_ANGLE, md_func, ad_openmm, ad_engine)
6870

6971
def test_dihedral(ad_openmm, ad_engine):
7072
md = pytest.importorskip('mdtraj')
7173
wizard = mock_wizard(['0, 1, 2, 3'])
7274
md_func = partial(md.compute_dihedrals, indices=[[0, 1, 2, 3]])
73-
_mdtraj_function_test(wizard, dihedral, md_func, ad_openmm, ad_engine)
75+
_mdtraj_function_test(wizard, MDTRAJ_DIHEDRAL, md_func, ad_openmm,
76+
ad_engine)
7477

7578
@pytest.mark.parametrize('inputs', [
7679
(['0', 'x']), ('foo', '0', 'x'), (['0', 'q', 'x'])
7780
])
7881
def test_coordinate(inputs):
7982
wizard = mock_wizard(inputs)
80-
cv = coordinate(wizard)
83+
cv = COORDINATE_CV(wizard)
8184
traj = make_1d_traj([5.0])
8285
assert cv(traj[0]) == 5.0
8386
if 'foo' in inputs:
8487
assert "I can't make an atom index" in wizard.console.log_text
8588
if 'q' in inputs:
8689
assert "Please select one of" in wizard.console.log_text
87-
88-
@pytest.mark.parametrize('inputs', [(['foo', 'Distance']), (['Distance'])])
89-
def test_cvs(inputs):
90-
wizard = mock_wizard(inputs)
91-
say_hello = mock.Mock(return_value="hello")
92-
with mock.patch.dict(SUPPORTED_CVS, {'Distance': say_hello}):
93-
assert cvs(wizard) == "hello"
94-
assert wizard.console.input_call_count == len(inputs)

‎paths_cli/tests/wizard/test_engines.py‎

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import pytest
2+
3+
from paths_cli.wizard.helper import *
4+
from paths_cli.wizard.helper import _LONG_EVAL_HELP
5+
6+
def test_raise_quit():
7+
with pytest.raises(QuitWizard):
8+
raise_quit("foo", None)
9+
10+
def test_restart():
11+
with pytest.raises(RestartObjectException):
12+
raise_restart("foo", None)
13+
14+
def test_force_exit():
15+
with pytest.raises(SystemExit):
16+
force_exit("foo", None)
17+
18+
class TestEvalHelperFunc:
19+
def setup(self):
20+
self.param_helper = {
21+
'str': "help_string",
22+
'method': lambda help_args, context: f"help_{help_args}"
23+
}
24+
self.expected = {
25+
'str': "help_string",
26+
'method': "help_foo"
27+
}
28+
29+
@pytest.mark.parametrize('helper_type', ['str', 'method'])
30+
def test_call(self, helper_type):
31+
help_func = EvalHelperFunc(self.param_helper[helper_type])
32+
help_str = help_func("foo")
33+
assert self.expected[helper_type] in help_str
34+
assert "ask for help with '?eval'" in help_str
35+
36+
@pytest.mark.parametrize('helper_type', ['str', 'method'])
37+
def test_call_eval(self, helper_type):
38+
help_func = EvalHelperFunc(self.param_helper[helper_type])
39+
assert help_func("eval") == _LONG_EVAL_HELP
40+
41+
class TestHelper:
42+
def setup(self):
43+
self.helper = Helper(help_func=lambda s, ctx: s)
44+
45+
def test_help_string(self):
46+
# if the helper "function" is a string, return that string whether
47+
# or not additional arguments to help are given
48+
helper = Helper(help_func="a string")
49+
assert helper("?") == "a string"
50+
assert helper("?foo") == "a string"
51+
52+
def test_help_with_args(self):
53+
# if the helper can process arguments, do so (our example return the
54+
# input)
55+
assert self.helper("?foo") == "foo"
56+
57+
def test_command_help_empty(self):
58+
# when calling help with "?!", get the list of commands
59+
assert "The following commands can be used" in self.helper("?!")
60+
61+
@pytest.mark.parametrize('inp', ["?!q", "?!quit"])
62+
def test_command_help_for_command(self, inp):
63+
# when calling help on a specific command, get the help for that
64+
# command
65+
assert "The !quit command" in self.helper(inp)
66+
67+
def test_command_help_no_such_command(self):
68+
# if asking for help on an unknown command, report that there is no
69+
# help available for it
70+
assert "No help for" in self.helper("?!foo")
71+
72+
@pytest.mark.parametrize('inp', ["!q", "!quit"])
73+
def test_run_command_quit(self, inp):
74+
# run the quit command when asked to
75+
with pytest.raises(QuitWizard):
76+
self.helper(inp)
77+
78+
@pytest.mark.parametrize('inp', ["!!q", "!!quit"])
79+
def test_run_command_force_quit(self, inp):
80+
# run the force quit command when asked to
81+
with pytest.raises(SystemExit):
82+
self.helper(inp)
83+
84+
def test_run_command_restart(self):
85+
# run the restart command when asked to
86+
with pytest.raises(RestartObjectException):
87+
self.helper("!restart")
88+
89+
def test_run_command_unknown(self):
90+
# unknown command names should just report an unknown command
91+
assert "Unknown command" in self.helper("!foo")
92+
93+
def test_run_command_no_argument(self):
94+
assert "Please provide a command. The following" in self.helper("!")
95+
96+
def test_empty_helper(self):
97+
empty = Helper(help_func=None)
98+
assert "no help available" in empty("?")
99+
assert "no help available" in empty("?foo")

‎paths_cli/tests/wizard/test_load_from_ops.py‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,11 @@ def ops_file_fixture():
4545
return storage
4646

4747
def test_named_objs_helper(ops_file_fixture):
48-
wizard = mock_wizard([])
4948
helper_func = named_objs_helper(ops_file_fixture, 'foo')
50-
helper_func(wizard, 'any')
51-
assert "what I found" in wizard.console.log_text
52-
assert "bar" in wizard.console.log_text
53-
assert "baz" in wizard.console.log_text
49+
result = helper_func('any')
50+
assert "what I found" in result
51+
assert "bar" in result
52+
assert "baz" in result
5453

5554
@pytest.mark.parametrize('with_failure', [False, True])
5655
def test_get_ops_storage(tmpdir, with_failure):

‎paths_cli/tests/wizard/test_openmm.py‎

Lines changed: 3 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,18 @@
55
from paths_cli.tests.utils import assert_url
66

77
from paths_cli.wizard.openmm import (
8-
_load_openmm_xml, _load_topology, openmm, OPENMM_SERIALIZATION_URL
8+
OPENMM_PLUGIN, OPENMM_SERIALIZATION_URL
99
)
1010

1111
from paths_cli.compat.openmm import mm, HAS_OPENMM
1212

1313
def test_helper_url():
1414
assert_url(OPENMM_SERIALIZATION_URL)
1515

16-
@pytest.mark.parametrize('obj_type', ['system', 'integrator', 'foo'])
17-
def test_load_openmm_xml(ad_openmm, obj_type):
18-
# switch back to importorskip when we drop OpenMM < 7.6
19-
if not HAS_OPENMM:
20-
pytest.skip("could not import openmm")
21-
# mm = pytest.importorskip("simtk.openmm")
22-
filename = f"{obj_type}.xml"
23-
inputs = [filename]
24-
expected_count = 1
25-
if obj_type == 'foo':
26-
inputs.append('integrator.xml')
27-
expected_count = 2
28-
29-
wizard = mock_wizard(inputs)
30-
superclass = {'integrator': mm.CustomIntegrator,
31-
'system': mm.System,
32-
'foo': mm.CustomIntegrator}[obj_type]
33-
with ad_openmm.as_cwd():
34-
obj = _load_openmm_xml(wizard, obj_type)
35-
assert isinstance(obj, superclass)
36-
37-
assert wizard.console.input_call_count == expected_count
38-
39-
@pytest.mark.parametrize('setup', ['normal', 'bad_filetype', 'no_file'])
40-
def test_load_topology(ad_openmm, setup):
41-
import openpathsampling as paths
42-
inputs = {'normal': [],
43-
'bad_filetype': ['foo.bar'],
44-
'no_file': ['foo.pdb']}[setup]
45-
expected_text = {'normal': "PDB file",
46-
'bad_filetype': 'trr',
47-
'no_file': 'No such file'}[setup]
48-
inputs += ['ad.pdb']
49-
wizard = mock_wizard(inputs)
50-
with ad_openmm.as_cwd():
51-
top = _load_topology(wizard)
52-
53-
assert isinstance(top, paths.engines.MDTrajTopology)
54-
assert wizard.console.input_call_count == len(inputs)
55-
assert expected_text in wizard.console.log_text
56-
5716
def test_openmm(ad_openmm):
58-
inputs = ['system.xml', 'integrator.xml', 'ad.pdb', '10', '10000']
17+
inputs = ['ad.pdb', 'integrator.xml', 'system.xml', '10', '10000']
5918
wizard = mock_wizard(inputs)
6019
with ad_openmm.as_cwd():
61-
engine = openmm(wizard)
20+
engine = OPENMM_PLUGIN(wizard)
6221
assert engine.n_frames_max == 10000
6322
assert engine.n_steps_per_frame == 10

0 commit comments

Comments
 (0)