diff --git a/.gitignore b/.gitignore index 04b033c..8b79034 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ # generated files +psi.*.clean +out.csv /timer.dat /resid_fixed.pdb /resid.pdb @@ -111,3 +113,7 @@ ENV/ # profraw files from LLVM? Unclear exactly what triggers this # There are reports this comes from LLVM profiling, but also Xcode 9. *profraw + +# MDSAPT-generated files +timer.dat +input.yaml diff --git a/docs/index.rst b/docs/index.rst index bde05c2..9ddb991 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,4 +24,3 @@ For the poster we presented at ACS Spring 2022, `click here <./_static/mdsapt_po optimizer reader viewer - scripts diff --git a/docs/install.rst b/docs/install.rst index f319a4a..ac9eb91 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,16 +1,35 @@ Installation ============ -MD-SAPT can be installed by cloning the GitHub repository. +MD-SAPT can be installed from the psi4 Conda repo like so: + +.. code-block:: bash + + conda install -c psi4/label/dev -c conda-forge mdsapt + +Alternatively, it can be installed by cloning the GitHub repository. .. code-block:: bash git clone https://github.com/calpolyccg/MDSAPT.git pip install ./MDSAPT -MD-SAPT can also be installed from conda. +To ensure it's been installed correctly, run `mdsapt` or `python3 -m mdsapt`. -.. code-block:: bash +.. code-block:: + Warning: importing 'simtk.openmm' is deprecated. Import 'openmm' instead. + 2022-03-30 09:32:50,071 mdsapt INFO MDSAPT 1.2.0 starting + 2022-03-30 09:32:50,071 mdsapt INFO Copyright (c) 2021 Alia Lescoulie, Astrid Yu, and Ashley Ringer McDonald + 2022-03-30 09:32:50,071 mdsapt INFO Released under GPLv3 License + Usage: python -m mdsapt [OPTIONS] COMMAND [ARGS]... + + MDSAPT - Molecular Dynamics Symmetry-Adapted Perturbation Theory + + This command-line interface lets you easily do common MDSAPT-related tasks. - conda install -c psi4 MDSAPT + Options: + --help Show this message and exit. + Commands: + generate Generate a template input file at filename. + run Run a SAPT calculation using the configuration in in_file. diff --git a/docs/quick.rst b/docs/quick.rst index 80d0a61..32f62b9 100644 --- a/docs/quick.rst +++ b/docs/quick.rst @@ -1,21 +1,25 @@ Quickstart Guide ================ -MD-SAPT simplifies for the calculation of SAPT interaction energies from MD -trajectories. Running it just requires MD simulation files. +MD-SAPT simplifies the calculation of SAPT interaction energies between selected residue pairs in MD trajectories. Running it just requires MD simulation files. -Basic work flow -=============== +Prerequisites +_____________ -MD-SAPT can be run as a script or in a notebook. It gives the SAPT energy -between the specified residue pairs. +Ensure that you have the following things set up: -Setup Process -_____________ + - You have existing MD trajectory and topology files in any form that `MDAnalysis `_ for a bigger example. - SAPT_run.results.to_csv('results.csv') \ No newline at end of file diff --git a/docs/scripts.rst b/docs/scripts.rst deleted file mode 100644 index d921bdf..0000000 --- a/docs/scripts.rst +++ /dev/null @@ -1,23 +0,0 @@ -MD-SAPT Scripts -=============== - -MD-SAPT is packaged with some scripts to aid users. - -`mdsapt_get_runinput` -_____________________ - -Usage - -.. code-block:: bash - - python mdsapt_get_runinput.py filename - -`mdsapt_run_sapt` -_________________ - -Usage - -.. code-block:: bash - - python mdsapt_run_sapt.py input.yaml results.out - diff --git a/environment.yml b/environment.yml index c278c7c..c8e0617 100644 --- a/environment.yml +++ b/environment.yml @@ -10,6 +10,7 @@ dependencies: - psi4>=1.6.1,<1.7 - mdanalysis>=2.2.0,<2.3 + - click - numpy - openmm - pandas diff --git a/mdsapt/__init__.py b/mdsapt/__init__.py index 1fa5e22..c459b40 100644 --- a/mdsapt/__init__.py +++ b/mdsapt/__init__.py @@ -8,6 +8,8 @@ from .config import Config, load_from_yaml_file from .sapt import TrajectorySAPT, DockingSAPT +from .cli import cli + # Handle versioneer from ._version import get_versions versions = get_versions() diff --git a/mdsapt/__main__.py b/mdsapt/__main__.py new file mode 100755 index 0000000..ba7de58 --- /dev/null +++ b/mdsapt/__main__.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +""" +Main entrypoint for the CLI. +""" + +# Note that we import directly from the CLI instead of the full package. This is an optimization +# since some CLI tasks do not need the entire package. +from .cli import cli + + +if __name__ == '__main__': + cli() diff --git a/mdsapt/cli.py b/mdsapt/cli.py new file mode 100644 index 0000000..b58564c --- /dev/null +++ b/mdsapt/cli.py @@ -0,0 +1,109 @@ +import logging +import os +import sys + +import click +from mdsapt.config import RangeFrameSelection + +# Note that we do not import MDSAPT. This is a speed optimization; it is imported later. + + +logger = logging.getLogger(__name__) + +_dir_path = os.path.dirname(os.path.realpath(__file__)) +"""Location of the mdsapt package to be used in resolving templates.""" + + +@click.group() +def cli(): + """ + MDSAPT - Molecular Dynamics Symmetry-Adapted Perturbation Theory, by Alia Lescoulie, Astrid Yu, and Ashley Ringer McDonald. + + This command-line interface lets you easily do common MDSAPT-related tasks. + """ + + +@cli.command() +@click.argument( + 'filename', + default='input.yaml', +) +@click.option( + '-t', '--template', 'template', + help="Template to generate from. By default, trajectory.", + type=click.Choice(['trajectory', 'docking'], case_sensitive=False), + default='trajectory', +) +@click.option( + '-f', '--force', + help="If provided, overwrites existing files.", + is_flag=True, +) +def generate(filename: str, template: str, force: bool): + """ + Generate a template input file at filename. + """ + ensure_safe_to_overwrite(filename, force) + + # TODO: make a wizard for these templates + template_path = os.path.join(_dir_path, 'data', f'{template}_template.yaml') + + with open(template_path, 'r') as template: + template_data = template.read() + with open(filename, 'w') as new_file: + new_file.write(template_data) + + logger.info(f'Generated template input file {filename}') + + +@cli.command() +@click.argument( + 'in_file', + default='input.yaml', +) +@click.argument( + 'out_file', + default='out.csv', +) +@click.option( + '-f', '--force', + help="If provided, overwrites existing files.", + is_flag=True, +) +def run(in_file: str, out_file: str, force: bool): + """ + Run a SAPT calculation using the configuration in in_file. Outputs will be written to + out_file. + """ + import mdsapt + + ensure_safe_to_overwrite(out_file, force) + + config = mdsapt.load_from_yaml_file(in_file) + if isinstance(config.analysis, mdsapt.config.TrajectoryAnalysisConfig): + sapt = mdsapt.TrajectorySAPT(config) + frames = config.analysis.frames + sapt.run(frames.start, frames.stop, frames.step) + elif isinstance(config.analysis, mdsapt.config.DockingAnalysisConfig): + sapt = mdsapt.DockingSAPT(config) + sapt.run() + + logger.info('saving results to CSV') + sapt.results.to_csv(out_file) + + +def ensure_safe_to_overwrite(path: str, force: bool): + """ + Helper function to ensure that it's safe to overwrite the given file, and + halts the program if not. + """ + if not os.path.exists(path): + return + + if force: + logger.warning("will overwrite existing CSV %s", path) + return + + logger.error("Halting, file already exists: %s", path) + logger.error("If you want to overwrite that file, add the -f flag") + sys.exit(-1) diff --git a/mdsapt/config.py b/mdsapt/config.py index 0053c03..09e4768 100644 --- a/mdsapt/config.py +++ b/mdsapt/config.py @@ -140,7 +140,7 @@ class RangeFrameSelection(BaseModel): """ start: Optional[conint(ge=0)] stop: Optional[conint(ge=0)] - step: Optional[conint(ge=1)] + step: Optional[conint(ge=1)] = 1 @root_validator() def _check_start_before_stop(cls, values: Dict[str, int]) -> Dict[str, int]: @@ -165,20 +165,14 @@ class TrajectoryAnalysisConfig(BaseModel): frames: A selection of frames to analyze. - This may either be a :obj:`RangeFrameSelection` with start/stop/step, - or a list of frame numbers. - - Serialization behavior - ---------------------- - If this value is a range, it will be serialized using start/stop/step. - Otherwise, it will be serialized into a List[int]. + This must be a :obj:`RangeFrameSelection` with start/stop/step. output: A file to write an output CSV to. """ type: Literal['trajectory'] topology: TopologySelection trajectories: List[FilePath] pairs: List[Tuple[conint(ge=0), conint(ge=0)]] - frames: Union[List[int], RangeFrameSelection] + frames: RangeFrameSelection output: str # noinspection PyMethodParameters @@ -192,7 +186,7 @@ def check_valid_md_system(cls, values: Dict[str, Any]) -> Dict[str, Any]: topology: TopologySelection = values['topology'] trajectories: List[FilePath] = values['trajectories'] ag_pair: List[Tuple[conint(ge=0), conint(ge=0)]] = values['pairs'] - frames: Union[List[int], RangeFrameSelection] = values['frames'] + frames: RangeFrameSelection = values['frames'] try: unv = topology.create_universe([str(p) for p in trajectories]) @@ -204,14 +198,8 @@ def check_valid_md_system(cls, values: Dict[str, Any]) -> Dict[str, Any]: errors.append(f'Selected residues are missing from topology: {missing_selections}') trajlen: int = len(unv.trajectory) - if isinstance(frames, RangeFrameSelection): - if trajlen <= frames.stop: - errors.append(f'Stop {frames.stop} exceeds trajectory length {trajlen}.') - else: - frames: List[int] - for frame in frames: - if frame >= trajlen: - errors.append(f'Frame {frame} exceeds trajectory length {trajlen}') + if trajlen <= frames.stop: + errors.append(f'Stop {frames.stop} exceeds trajectory length {trajlen}.') if len(errors) > 0: raise ValidationError([errors], cls) diff --git a/mdsapt/data/trajectory_template.yaml b/mdsapt/data/trajectory_template.yaml index dca8ee3..ddc1889 100644 --- a/mdsapt/data/trajectory_template.yaml +++ b/mdsapt/data/trajectory_template.yaml @@ -28,15 +28,9 @@ analysis: - [132, 152] - [34, 152] frames: - # To select a set of specific frames: - - 1 - - 4 - - 8 - - # # Alternatively, to select a range of frames: (these are mutually exclusive) - # start: - # stop: - # step: + start: 10 + stop: 30 + #step: 2 output: 'output.csv' diff --git a/mdsapt/tests/test_config.py b/mdsapt/tests/test_config.py index 0ca7118..ae4197b 100644 --- a/mdsapt/tests/test_config.py +++ b/mdsapt/tests/test_config.py @@ -32,7 +32,6 @@ def test_frame_range_selection() -> None: @pytest.mark.parametrize('key,var', [ ('trajectories', [f'{resources_dir}/test_read_error.dcd']), ('frames', {'start': 1, 'stop': 120}), - ('frames', [1, 4, 6, 120]), ('pairs', [(250, 251)]) ]) def test_traj_analysis_config(key: str, var: Any) -> None: @@ -44,7 +43,7 @@ def test_traj_analysis_config(key: str, var: Any) -> None: topology=f'{resources_dir}/testtop.psf', trajectories=[f'{resources_dir}/testtraj.dcd'], pairs=[(132, 152), (34, 152)], - frames=[1, 4, 6], + frames={'start': 1, 'stop': 4}, output=True ) @@ -63,7 +62,7 @@ def test_traj_sel() -> None: topology=f'{resources_dir}/testtop.psf', trajectories=[f'{resources_dir}/testtraj.dcd'], pairs=[(132, 152), (34, 152)], - frames=[1, 4, 6], + frames={'start': 1, 'stop': 4}, output=True) cfg: TrajectoryAnalysisConfig = TrajectoryAnalysisConfig(**traj_analysis_dict) diff --git a/mdsapt/tests/testing_resources/test_input.yaml b/mdsapt/tests/testing_resources/test_input.yaml index fb8b79e..fc21b4e 100644 --- a/mdsapt/tests/testing_resources/test_input.yaml +++ b/mdsapt/tests/testing_resources/test_input.yaml @@ -27,14 +27,9 @@ analysis: pairs: - [11, 119] frames: - # To select a set of specific frames: - - 1 - - 2 - - # # Alternatively, to select a range of frames: (these are mutually exclusive) - # start: - # stop: - # step: + start: 1 + stop: 2 + #step: 3 output: 'output.csv' diff --git a/scripts/mdsapt_get_runinput.py b/scripts/mdsapt_get_runinput.py deleted file mode 100644 index 8433344..0000000 --- a/scripts/mdsapt_get_runinput.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python -# pylint: skip-file -# type: ignore - -from argparse import ArgumentParser - -import logging - -logger = logging.getLogger('mdsapt.get_runinput') - -if __name__ == '__main__': - parser = ArgumentParser(usage=__doc__) - parser.add_argument('filename', metavar='F', type=str, nargs='?', - default='input.yaml', - help='Filename for generated input file input.yaml') - - filename = vars(parser.parse_args())['filename'] - - with open('../mdsapt/data/template_input.yaml', 'r') as template: - template_data = template.read() - - with open(f'{filename}.yaml', 'w+') as new_file: - new_file.write(template_data) - logger.info(f'Generated template input file {filename}') diff --git a/scripts/mdsapt_run_sapt.py b/scripts/mdsapt_run_sapt.py deleted file mode 100644 index 40cf656..0000000 --- a/scripts/mdsapt_run_sapt.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python -# pylint: skip-file -# type: ignore - -from argparse import ArgumentParser - -import logging - -logger = logging.getLogger('mdsapt') - -import mdsapt - -if __name__ == '__main__': - parser = ArgumentParser(usage=__doc__) - parser.add_argument('filename', metavar='I', type=str, nargs='?', - default='input.yaml', - help='Filename for input yaml file') - parser.add_argument('outfile', metavar='O', type=str, nargs='?', - default='out.csv', - help='Name for output csv file') - - filename = vars(parser.parse_args())['filename'] - - settings = mdsapt.InputReader(filename) - - optimizer = mdsapt.Optimizer(settings) - - if optimizer.num_failed_residue != 0: - logger.error('optimization failed see log for list of failed residues') - - sapt = mdsapt.TrajectorySAPT(settings, optimizer).run(settings.start, settings.stop, settings.step) - - logger.info('saving results to CSV') - sapt.results.to_csv(filename) diff --git a/setup.py b/setup.py index 88df039..a1df560 100755 --- a/setup.py +++ b/setup.py @@ -32,6 +32,11 @@ cmdclass=versioneer.get_cmdclass(), license='GPL-3.0', + # Register the CLI entrypoint + entry_points = { + 'console_scripts': ['mdsapt=mdsapt.cli:cli'], + }, + # Which Python importable modules should be included when your package is installed # Handled automatically by setuptools. Use 'exclude' to prevent some specific # subpackage(s) from being added, if needed