Skip to content

Commit 871ae8e

Browse files
committed
initial CLI setup
1 parent cd8980b commit 871ae8e

File tree

8 files changed

+673
-0
lines changed

8 files changed

+673
-0
lines changed

paths_cli/__init__.py

Whitespace-only changes.

paths_cli/cli.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# builds off the example of MultiCommand in click's docs
2+
import collections
3+
import click
4+
import os
5+
6+
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
7+
8+
plugin_folder = os.path.join(os.path.dirname(__file__), 'commands')
9+
10+
class OpenPathSamplingCLI(click.MultiCommand):
11+
def __init__(self, *args, **kwargs):
12+
self.plugin_folders = [
13+
os.path.join(os.path.dirname(__file__), 'commands'),
14+
os.path.join(click.get_app_dir("OpenPathSampling",
15+
force_posix=True),
16+
'cli-plugins')
17+
]
18+
self._get_command = {}
19+
self._sections = collections.defaultdict(list)
20+
plugin_files, command_list = self._list_plugins()
21+
for cmd, plugin_file in zip(command_list, plugin_files):
22+
command, section = self._load_plugin(plugin_file)
23+
self._get_command[cmd] = command
24+
self._sections[section].append(cmd)
25+
super(OpenPathSamplingCLI, self).__init__(*args, **kwargs)
26+
27+
def _load_plugin(self, name):
28+
ns = {}
29+
fn = os.path.join(plugin_folder, name + '.py')
30+
with open(fn) as f:
31+
code = compile(f.read(), fn, 'exec')
32+
eval(code, ns, ns)
33+
return ns['CLI'], ns['SECTION']
34+
35+
def _list_plugins(self):
36+
files = []
37+
commands = []
38+
for filename in os.listdir(plugin_folder):
39+
if filename.endswith('.py'):
40+
command = filename.replace('_', '-')
41+
files.append(filename[:-3])
42+
commands.append(command[:-3])
43+
return files, commands
44+
45+
46+
def list_commands(self, ctx):
47+
return list(self._get_command.keys())
48+
49+
def get_command(self, ctx, name):
50+
name = name.replace('_', '-') # auto alias to allow - or _
51+
return self._get_command.get(name)
52+
53+
def format_commands(self, ctx, formatter):
54+
sec_order = ['Simulation', 'Analysis', 'Miscellaneous']
55+
for sec in sec_order:
56+
cmds = self._sections.get(sec, [])
57+
rows = []
58+
for cmd in cmds:
59+
command = self.get_command(ctx, cmd)
60+
if command is None:
61+
continue
62+
rows.append((cmd, command.short_help or ''))
63+
64+
if rows:
65+
with formatter.section(sec + " Commands"):
66+
formatter.write_dl(rows)
67+
68+
69+
main_help="""
70+
OpenPathSampling is a Python library for path sampling simulations. This
71+
command line tool facilitates common tasks when working with
72+
OpenPathSampling. To use it, use one of the subcommands below. For example,
73+
you can get more information about the strip-snapshots (filesize reduction)
74+
tool with:
75+
76+
openpathsampling strip-snapshots --help
77+
"""
78+
79+
def main():
80+
cli = OpenPathSamplingCLI(
81+
name="openpathsampling",
82+
help=main_help,
83+
context_settings=CONTEXT_SETTINGS
84+
)
85+
cli()
86+
87+
if __name__ == '__main__':
88+
main()
89+
# print("list commands:", cli.list_commands())

paths_cli/commands/contents.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import click
2+
from paths_cli.parameters import INPUT_FILE
3+
4+
@click.command(
5+
'nclist',
6+
short_help="list named objects from an OPS .nc file",
7+
)
8+
@INPUT_FILE.clicked(required=True)
9+
def nclist(input_file):
10+
"""List the names of named objects in an OPS .nc file.
11+
12+
This is particularly useful when getting ready to use one of simulation
13+
scripts (i.e., to identify exactly how a state or engine is named.)
14+
"""
15+
storage = INPUT_FILE.get(input_file)
16+
print(storage)
17+
store_section_mapping = {
18+
'CVs': storage.cvs, 'Volumes': storage.volumes,
19+
'Engines': storage.engines, 'Networks': storage.networks,
20+
'Move Schemes': storage.schemes,
21+
'Simulations': storage.pathsimulators,
22+
}
23+
for section, store in store_section_mapping.items():
24+
print(get_section_string_nameable(section, store,
25+
_get_named_namedobj))
26+
print(get_section_string_nameable('Tags', storage.tags, _get_named_tags))
27+
28+
print("\nData Objects:")
29+
unnamed_sections = {
30+
'Steps': storage.steps, 'Move Changes': storage.movechanges,
31+
'SampleSets': storage.samplesets,
32+
'Trajectories': storage.trajectories, 'Snapshots': storage.snapshots
33+
}
34+
for section, store in unnamed_sections.items():
35+
print(get_unnamed_section_string(section, store))
36+
37+
def _item_or_items(count):
38+
return "item" if count == 1 else "items"
39+
40+
def get_unnamed_section_string(section, store):
41+
len_store = len(store)
42+
return (section + ": " + str(len_store) + " unnamed "
43+
+ _item_or_items(len_store))
44+
45+
def _get_named_namedobj(store):
46+
return [item.name for item in store if item.is_named]
47+
48+
def _get_named_tags(store):
49+
return list(store.keys())
50+
51+
def get_section_string_nameable(section, store, get_named):
52+
out_str = ""
53+
len_store = len(store)
54+
out_str += (section + ": " + str(len_store) + " "
55+
+ _item_or_items(len_store))
56+
named = get_named(store)
57+
n_unnamed = len_store - len(named)
58+
for name in named:
59+
out_str += "\n* " + name
60+
if n_unnamed > 0:
61+
prefix = "plus " if named else ""
62+
out_str += ("\n* " + prefix + str(n_unnamed) + " unnamed "
63+
+ _item_or_items(n_unnamed))
64+
return out_str
65+
66+
CLI = nclist
67+
SECTION = "Miscellaneous"

paths_cli/commands/strip_snapshots.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import click
2+
from paths_cli.parameters import (
3+
INPUT_FILE, OUTPUT_FILE, CVS
4+
)
5+
6+
from tqdm.auto import tqdm
7+
8+
@click.command(
9+
'strip-snapshots',
10+
short_help="Remove coordinates/velocities from an OPS storage"
11+
)
12+
@INPUT_FILE.clicked(required=True)
13+
@OUTPUT_FILE.clicked(required=True)
14+
@CVS.clicked(required=False)
15+
@click.option('--blocksize', type=int, default=10,
16+
help="block size for precomputing CVs")
17+
def strip_snapshots(input_file, output_file, cv, blocksize):
18+
"""
19+
Remove snapshot information (coordinates, velocities) from INPUT_FILE.
20+
21+
By giving the --cv option (once for each CV), you can select to only
22+
save certain CVs. If you do not give that option, all CVs will be saved.
23+
"""
24+
loaded_cvs = [CVS.get(cv) for cv in cv]
25+
return strip_snapshots_main(
26+
input_storage=INPUT_FILE.get(input_file),
27+
output_storage=OUTPUT_FILE.get(output_file),
28+
cvs=loaded_cvs,
29+
blocksize=blocksize
30+
)
31+
32+
33+
def block_precompute_cvs(cvs, proxy_snaps, blocksize):
34+
n_snaps = len(proxy_snaps)
35+
n_blocks = (n_snaps // blocksize) +1
36+
37+
blocks = [proxy_snaps[i*blocksize: min((i+1)*blocksize, n_snaps)]
38+
for i in range(n_blocks)]
39+
40+
for block_num in tqdm(range(n_blocks), leave=False):
41+
block = blocks[block_num]
42+
for cv in cvs:
43+
cv.enable_diskcache()
44+
_ = cv(block)
45+
46+
47+
def strip_snapshots_main(input_storage, output_storage, cvs, blocksize):
48+
if not cvs:
49+
cvs = list(input_storage.cvs)
50+
51+
# save template
52+
output_storage.save(input_storage.snapshots[0])
53+
54+
snapshot_proxies = input_storage.snapshots.all().as_proxies()
55+
stages = tqdm(['precompute', 'cvs', 'snapshots', 'trajectories',
56+
'steps'])
57+
for stage in stages:
58+
stages.set_description("%s" % stage)
59+
if stage == 'precompute':
60+
block_precompute_cvs(cvs, snapshot_proxies, blocksize)
61+
else:
62+
store_func, inputs = {
63+
'cvs': (output_storage.cvs.save, input_storage.cvs),
64+
'snapshots': (output_storage.snapshots.mention,
65+
snapshot_proxies),
66+
'trajectories': (output_storage.trajectories.mention,
67+
input_storage.trajectories),
68+
'steps': (output_storage.steps.save, input_storage.steps)
69+
}[stage]
70+
for obj in tqdm(inputs):
71+
store_func(obj)
72+
73+
74+
CLI = strip_snapshots
75+
SECTION = "Miscellaneous"

paths_cli/parameters.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import click
2+
import openpathsampling as paths
3+
4+
class AbstractParameter(object):
5+
def __init__(self, *args, **kwargs):
6+
self.args = args
7+
if 'required' in kwargs:
8+
raise ValueError("Can't set required status now")
9+
self.kwargs = kwargs
10+
11+
def clicked(self, required=False):
12+
raise NotImplementedError()
13+
14+
class Option(AbstractParameter):
15+
def clicked(self, required=False):
16+
return click.option(*self.args, **self.kwargs, required=required)
17+
18+
class Argument(AbstractParameter):
19+
def clicked(self, required=False):
20+
return click.argument(*self.args, **self.kwargs, required=required)
21+
22+
23+
class AbstractLoader(object):
24+
def __init__(self, param):
25+
self.param = param
26+
27+
@property
28+
def clicked(self):
29+
return self.param.clicked
30+
31+
def get(self, *args, **kwargs):
32+
return NotImplementedError()
33+
34+
35+
class StorageLoader(AbstractLoader):
36+
def __init__(self, param, mode):
37+
super(StorageLoader, self).__init__(param)
38+
self.mode = mode
39+
40+
def get(self, name):
41+
return paths.Storage(name, mode=self.mode)
42+
43+
44+
class OPSStorageLoadNames(AbstractLoader):
45+
def __init__(self, param, store):
46+
super(OPSStorageLoadNames, self).__init__(param)
47+
self.store = store
48+
49+
def get(self, storage, name):
50+
return getattr(storage, self.store)[name]
51+
52+
53+
class OPSStorageLoadSingle(OPSStorageLoadNames):
54+
def __init__(self, param, store, fallback=None, num_store=None):
55+
super(OPSStorageLoadSingle, self).__init__(param, store)
56+
self.fallback = fallback
57+
if num_store is None:
58+
num_store = store
59+
self.num_store = num_store
60+
61+
def get(self, storage, name):
62+
store = getattr(storage, self.store)
63+
64+
result = None
65+
# if the we can get by name/number, do it
66+
if name is not None:
67+
result = store[name]
68+
69+
if result is None:
70+
try:
71+
num = int(name)
72+
except ValueError:
73+
pass
74+
else:
75+
num_store = getattr(storage, self.num_store)
76+
result = num_store[num]
77+
78+
if result is not None:
79+
return result
80+
81+
# if there's only one of them, take that
82+
if len(store) == 1:
83+
return store[0]
84+
85+
# if only one is named, take it
86+
named_things = [o for o in store if o.name]
87+
if len(named_things) == 1:
88+
return named_things[0]
89+
90+
if self.fallback:
91+
result = self.fallback(self, storage, name)
92+
93+
if result is None:
94+
raise RuntimeError("Couldn't find %s", name)
95+
96+
return result
97+
98+
ENGINE = OPSStorageLoadSingle(
99+
param=Option('-e', '--engine', help="identifer for the engine"),
100+
store='engines',
101+
fallback=None # for now... I'll add more tricks later
102+
)
103+
104+
SCHEME = OPSStorageLoadSingle(
105+
param=Option('-m', '--scheme', help="identifier for the move scheme"),
106+
store='schemes',
107+
fallback=None
108+
)
109+
110+
INIT_TRAJ = OPSStorageLoadSingle(
111+
param=Option('-t', '--init-traj',
112+
help="identifier for initial trajectory"),
113+
store='tags',
114+
num_store='trajectories',
115+
fallback=None # for now
116+
)
117+
118+
CVS = OPSStorageLoadNames(
119+
param=Option('--cv', type=str, multiple=True,
120+
help='name of CV; may select more than once'),
121+
store='cvs'
122+
)
123+
124+
STATES = OPSStorageLoadNames(
125+
param=Option('-s', '--state', multiple=True,
126+
help='name of state; may select more than once'),
127+
store='volumes'
128+
)
129+
130+
INPUT_FILE = StorageLoader(
131+
param=Argument('input_file', type=str),
132+
mode='r'
133+
)
134+
135+
OUTPUT_FILE = StorageLoader(
136+
param=Option('-o', '--output-file', type=str, help="output ncfile"),
137+
mode='w'
138+
)
139+
140+
N_STEPS_MC = click.option('-n', '--nsteps', type=int,
141+
help="number of Monte Carlo trials to run")

0 commit comments

Comments
 (0)