From 5882cf1c1235ff44f069e1b8bfc6dc9e2bd405e4 Mon Sep 17 00:00:00 2001 From: Nathan Lim Date: Tue, 18 Jun 2019 09:48:45 -0700 Subject: [PATCH 1/9] Changes for v0.2.5 --- .codecov.yml | 4 - .readthedocs.yml | 2 +- .travis.yml | 19 +- README.md | 94 +- blues/__init__.py | 3 - blues/example.py | 66 - blues/formats.py | 407 +---- blues/integrators.py | 14 +- blues/logging.yml | 37 + blues/moves.py | 1314 ---------------- blues/ncmc.py | 824 ++++++++++ blues/posedart.py | 503 ------ blues/settings.py | 321 ---- blues/simulation.py | 1332 ---------------- blues/smartdart.py | 693 --------- blues/{reporters.py => storage.py} | 678 +++----- blues/switching.py | 1360 ----------------- blues/systemfactory.py | 303 ++++ ...t_simulation.py => simulation_test_old.py} | 220 +-- blues/tests/test_ethylene.py | 141 +- blues/tests/test_randomrotation.py | 65 - blues/tests/test_sidechain.py | 84 - blues/tests/test_storage.py | 176 +++ blues/tests/test_utilities.py | 211 +++ blues/utils.py | 404 ++--- devdocs/README.md | 134 -- devdocs/class-diagram.html | 11 - devdocs/class-diagram.png | Bin 116965 -> 0 bytes devtools/conda-recipe/meta.yaml | 8 +- docs/BLUES_tutorial.ipynb | 1012 ------------ docs/conf.py | 4 +- docs/devdoc.rst | 286 ++++ docs/environment.yml | 5 +- docs/index.rst | 4 +- docs/installation.rst | 6 +- docs/intro.rst | 26 +- docs/module_doc.rst | 71 +- docs/tutorial.rst | 18 - docs/usage.rst | 55 + examples/example_rotmove.py | 33 - examples/example_sidechain.py | 36 - examples/rotmove_cuda.yml | 102 -- examples/sidechain_cuda.yml | 95 -- images/ct-2018-01018f_0014.jpeg | Bin 0 -> 173663 bytes images/uml.png | Bin 0 -> 153339 bytes images/uml.xml | 1 + 46 files changed, 2486 insertions(+), 8696 deletions(-) delete mode 100644 blues/example.py create mode 100644 blues/logging.yml delete mode 100644 blues/moves.py create mode 100644 blues/ncmc.py delete mode 100755 blues/posedart.py delete mode 100644 blues/settings.py delete mode 100644 blues/simulation.py delete mode 100644 blues/smartdart.py rename blues/{reporters.py => storage.py} (51%) delete mode 100644 blues/switching.py create mode 100644 blues/systemfactory.py rename blues/tests/{test_simulation.py => simulation_test_old.py} (53%) delete mode 100644 blues/tests/test_randomrotation.py delete mode 100644 blues/tests/test_sidechain.py create mode 100644 blues/tests/test_storage.py create mode 100644 blues/tests/test_utilities.py delete mode 100644 devdocs/README.md delete mode 100644 devdocs/class-diagram.html delete mode 100644 devdocs/class-diagram.png delete mode 100644 docs/BLUES_tutorial.ipynb create mode 100644 docs/devdoc.rst delete mode 100644 docs/tutorial.rst create mode 100644 docs/usage.rst delete mode 100644 examples/example_rotmove.py delete mode 100644 examples/example_sidechain.py delete mode 100644 examples/rotmove_cuda.yml delete mode 100644 examples/sidechain_cuda.yml create mode 100644 images/ct-2018-01018f_0014.jpeg create mode 100644 images/uml.png create mode 100644 images/uml.xml diff --git a/.codecov.yml b/.codecov.yml index f262e76f..6b065cc2 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -30,10 +30,6 @@ ignore: - setup.py - blues/tests/* - blues/_version.py - - blues/*dart.py - - blues/switching.py - - blues/formats.py - - blues/example.py diff --git a/.readthedocs.yml b/.readthedocs.yml index 18589c80..b4203d75 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,7 +3,7 @@ conda: file: docs/environment.yml python: - version: 3.5 + version: 3.6 setup_py_install: true extra_requirements: - tests diff --git a/.travis.yml b/.travis.yml index 3869bc55..862d94a6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,9 +9,9 @@ branches: env: global: - ORGNAME="omnia" - - USERNAME="mobleylab" + - USERNAME="nathanmlim" - PKG_NAME="blues" - - RELEASE=true + - RELEASE=false #- RELEASE=false - CONDA_ENV="${PKG_NAME}-${TRAVIS_OS_NAME}" - OE_LICENSE="$HOME/oe_license.txt" @@ -35,11 +35,6 @@ matrix: # - CONDA_PY="37" # - PYTHON_VER="3.7" - - os: linux - python: 3.5 - env: - - CONDA_PY="35" - - PYTHON_VER="3.5" - os: linux python: 3.6 env: @@ -51,7 +46,6 @@ matrix: - CONDA_PY="37" - PYTHON_VER="3.7" - before_install: # Additional info about the build - uname -a @@ -61,8 +55,8 @@ before_install: - python -V # Unpack encrypted OpenEye license file - - if [ "$TRAVIS_SECURE_ENV_VARS" == true ]; then openssl aes-256-cbc -K $encrypted_7751cf1f2f9d_key -iv $encrypted_7751cf1f2f9d_iv -in oe_license.txt.enc -out oe_license.txt -d; fi - - if [ "$TRAVIS_SECURE_ENV_VARS" == false ]; then echo "OpenEye license will not be installed in forks."; fi + #- if [ "$TRAVIS_SECURE_ENV_VARS" == true ]; then openssl aes-256-cbc -K $encrypted_7751cf1f2f9d_key -iv $encrypted_7751cf1f2f9d_iv -in oe_license.txt.enc -out oe_license.txt -d; fi + #- if [ "$TRAVIS_SECURE_ENV_VARS" == false ]; then echo "OpenEye license will not be installed in forks."; fi - conda update --yes -q conda # Turn on always yes @@ -80,20 +74,17 @@ install: - echo ${PYTHON_VER} ${CONDA_PY} - conda create -n ${CONDA_ENV} python=${PYTHON_VER} pip pytest pytest-cov conda-verify - conda activate ${CONDA_ENV} + # Install pip only modules - pip install codecov - # Install OpenEye dependencies # Use beta version for partial bond orders - conda install -c openeye/label/beta openeye-toolkits && python -c "import openeye; print(openeye.__version__)" - conda install -q -c openeye/label/Orion -c omnia oeommtools packmol - - conda info oeommtools - - conda info numexpr=2.6.6 # Build and install package - conda build -q --python=${PYTHON_VER} devtools/conda-recipe - - conda info blues - conda install --use-local -q ${PKG_NAME} - conda list diff --git a/README.md b/README.md index 8552f539..62233a46 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,26 @@ This package takes advantage of non-equilibrium candidate Monte Carlo moves (NCMC) to help sample between different ligand binding modes. + + +Latest release: +[![Build Status](https://travis-ci.com/nathanmlim/blues.svg?branch=master)](https://travis-ci.com/nathanmlim/blues) +[![Documentation Status](https://readthedocs.org/projects/blues-fork/badge/?version=master)](https://blues-fork.readthedocs.io/en/master/?badge=master) +[![codecov](https://codecov.io/gh/nathanmlim/blues/branch/master/graph/badge.svg)](https://codecov.io/gh/nathanmlim/blues) +[![Anaconda-Server Badge](https://anaconda.org/nathanmlim/blues/badges/version.svg)](https://anaconda.org/nathanmlim/blues) + [![DOI](https://zenodo.org/badge/62096511.svg)](https://zenodo.org/badge/latestdoi/62096511) ## Citations -#### Publication +#### Publications - [Gill, S; Lim, N. M.; Grinaway, P.; Rustenburg, A. S.; Fass, J.; Ross, G.; Chodera, J. D.; Mobley, D. L. “Binding Modes of Ligands Using Enhanced Sampling (BLUES): Rapid Decorrelation of Ligand Binding Modes Using Nonequilibrium Candidate Monte Carlo”](https://pubs.acs.org/doi/abs/10.1021/acs.jpcb.7b11820) - Journal of Physical Chemistry B. February 27, 2018 +- [Burley, K. H., Gill, S. C., Lim, N. M., & Mobley, D. L. "Enhancing Sidechain Rotamer Sampling Using Non-Equilibrium Candidate Monte Carlo"](https://pubs.acs.org/doi/abs/10.1021/acs.jctc.8b01018) - Journal of Chemical Theory and Computation. January 24, 2019 #### Preprints - [BLUES v1](https://chemrxiv.org/articles/Binding_Modes_of_Ligands_Using_Enhanced_Sampling_BLUES_Rapid_Decorrelation_of_Ligand_Binding_Modes_Using_Nonequilibrium_Candidate_Monte_Carlo/5406907) - ChemRxiv September 19, 2017 @@ -20,13 +30,12 @@ Latest release: ## Manifest * `blues/` - Source code and example scripts for BLUES toolkit -* `devdocs/` - Class diagrams for developers * `devtools/` - Developer tools and documentation for conda, travis, and issuing a release +* `docs/` - Documentation * `images/` - Images/logo for repository -* `notebooks` - Jupyter notebooks for testing/development ## Prerequisites -BLUES is compatible with MacOSX/Linux with Python>=3.5 (blues<=1.1 still works with Python 2.7) +BLUES is compatible with MacOSX/Linux with Python>=3.6 (blues<=1.1 still works with Python 2.7) Install [miniconda](http://conda.pydata.org/miniconda.html) according to your system. ## Requirements @@ -57,7 +66,7 @@ Install from source (NOT RECOMMENDED) git clone git@github.com:MobleyLab/blues.git # Install some dependencies -conda install -c omnia -c conda-forge openmmtools=0.15.0 openmm=7.2.2 numpy cython +conda install -c omnia -c conda-forge openmmtools openmm pymbar numpy cython # Install BLUES package from the top directory pip install -e . @@ -69,40 +78,61 @@ pytest -v -s ## Documentation For documentation on the BLUES modules see [ReadTheDocs: Modules](https://mobleylab-blues.readthedocs.io/en/latest/module_doc.html) -For a tutorial on how to use BLUES see [ReadTheDocs: Tutorial](https://mobleylab-blues.readthedocs.io/en/latest/tutorial.html) -### BLUES using NCMC -This package takes advantage of non-equilibrium candidate Monte Carlo moves (NCMC) to help sample between different ligand binding modes using the OpenMM simulation package. One goal for this package is to allow for easy additions of other moves of interest, which will be covered below. +### Usage +This package takes advantage of non-equilibrium candidate Monte Carlo moves (NCMC) to help sample between different ligand binding modes using the OpenMM simulation package. One goal for this package is to allow for easy additions of other moves of interest, which will be covered below. -### Example Use -An example of how to set up a simulation sampling the binding modes of toluene bound to T4 lysozyme using NCMC and a rotational move can be found in `examples/example_rotmove.py` +The integrator of `BLUES` contains the framework necessary for NCMC. Specifically, the integrator class calculates the work done during a NCMC move. It also controls the lambda scaling of parameters. The integrator that BLUES uses inherits from `openmmtools.integrators.AlchemicalNonequilibriumLangevinIntegrator` to keep track of the work done outside integration steps, allowing Monte Carlo (MC) moves to be incorporated together with the NCMC thermodynamic perturbation protocol. Currently, the `openmmtools.alchemy` package is used to generate the lambda parameters for the ligand, allowing alchemical modification of the sterics and electrostatics of the system. -### Actually using BLUES -The integrator of `BLUES` contains the framework necessary for NCMC. Specifically, the integrator class calculates the work done during a NCMC move. It also controls the lambda scaling of parameters. The integrator that BLUES uses inherits from `openmmtools.integrators.AlchemicalNonequilibriumLangevinIntegrator` to keep track of the work done outside integration steps, allowing Monte Carlo (MC) moves to be incorporated together with the NCMC thermodynamic perturbation protocol. Currently the `openmmtools.alchemy` package is used to generate the lambda parameters for the ligand, allowing alchemical modification of the sterics and electrostatics of the system. -The `Simulation` class in `blues/simulation.py` serves as a wrapper for running NCMC simulations. +The `BLUESSampler` class in `ncmc.py` serves as a wrapper for running NCMC+MD simulations. To run the hybrid simulation, the `BLUESSampler` class requires defining two moves for running the (1) MD simulation and (2) the NCMC protcol. These moves are defined in the `ncmc.py` module. A simple example is provided below. -### Implementing Custom Moves -Users can implement their own MC moves into NCMC by inheriting from an appropriate `blues.moves.Move` class and constructing a custom `move()` method that only takes in an Openmm context object as a parameter. The `move()` method will then access the positions of that context, change those positions, then update the positions of that context. For example if you would like to add a move that randomly translates a set of coordinates the code would look similar to this pseudocode: +#### Example +Using the BLUES framework requires the use of a **ThermodynamicState** and **SamplerState** from `openmmtools` which we import from `openmmtools.states`: ```python -from blues.moves import Move -class TranslationMove(Move): - def __init__(self, atom_indices): - self.atom_indices = atom_indices - def move(context): - """pseudocode for move""" - positions = context.context.getState(getPositions=True).getPositions(asNumpy=True) - #get positions from context - #use some function that translates atom_indices - newPositions = RandomTranslation(positions[self.atom_indices]) - context.setPositions(newPositions) - return context +from openmmtools.states import ThermodynamicState, SamplerState +from openmmtools.testsystems import TolueneVacuum +from blues.ncmc import * +from simtk import unit ``` -### Combining Moves -**Note: This feature has not been tested, use at your own risk.** -If you're interested in combining moves together sequentially–say you'd like to perform a rotation and translation move together–instead of coding up a new `Move` class that performs that, you can instead leverage the functionality of existing `Move`s using the `CombinationMove` class. `CombinationMove` takes in a list of instantiated `Move` objects. The `CombinationMove`'s `move()` method perfroms the moves in either listed or reverse order. Replicating a rotation and translation move on t, then, can effectively be done by passing in an instantiated TranslationMove (from the pseudocode example above) and RandomLigandRotation. -One important non-obvious thing to note about the CombinationMove class is that to ensure detailed balance is maintained, moves are done half the time in listed order and half the time in the reverse order. +Create the states for a toluene molecule in vacuum. +```python +tol = TolueneVacuum() +thermodynamic_state = ThermodynamicState(tol.system, temperature=300*unit.kelvin) +sampler_state = SamplerState(positions=tol.positions) +``` + +Define our langevin dynamics move for the MD simulation portion and then our NCMC move which performs a random rotation. Here, we use a customized LangevinDynamicsMove which allows us to store information from the MD simulation portion. + +```python +dynamics_move = ReportLangevinDynamicsMove(n_steps=10) +ncmc_move = RandomLigandRotationMove(n_steps=10, atom_subset=list(range(15))) +``` + +Provide the `BLUESSampler` class with an `openmm.Topology` and these objects to run the NCMC+MD simulation. +```python +sampler = BLUESSampler(thermodynamic_state=thermodynamic_state, + sampler_state=sampler_state, + dynamics_move=dynamics_move, + ncmc_move=ncmc_move, + topology=tol.topology) +sampler.run(n_iterations=1) +``` + +### Implementing custom NCMC moves +Users can implement their own MC moves into NCMC by inheriting from an appropriate `blues.ncmc.NCMCMove` class and overriding the `_propose_positions()` method that only takes in and returns a positions array. +Updating the positions in the context is handled by the `BLUESSampler` class. + +With blues>=0.2.5, the API has been redesigned to allow compatibility with moves implemented in [`openmmtools.mcmc`](https://openmmtools.readthedocs.io/en/0.18.1/mcmc.html#mcmc-move-types). Users can take MCMC moves and turn them into NCMC moves without having to write new code. Simply, override the `_get_integrator()` method to use the `blues.integrator.AlchemicalExternalLangevinIntegrator` provided in this module. For example: + +```python +from blues.ncmc import NCMCMove +from openmmtools.mcmc import MCDisplacementMove +class NCMCDisplacementMove(MCDisplacementMove, NCMCMove): + def _get_integrator(self, thermodynamic_state): + return NCMCMove._get_integrator(self,thermodynamic_state) +``` ## Versions: - Version 0.0.1: Basic BLUES functionality/package @@ -117,6 +147,8 @@ One important non-obvious thing to note about the CombinationMove class is that - [Version 0.2.1](https://doi.org/10.5281/zenodo.1288925): Bug fix in alchemical correction term - [Version 0.2.2](https://doi.org/10.5281/zenodo.1324415): Bug fixes for OpenEye tests and restarting from the YAML; enhancements to the Logger and package installation. - [Version 0.2.3](https://zenodo.org/badge/latestdoi/62096511): Improvements to Travis CI, fix in velocity synicng, and add tests for checking freezing selection. +- [Version 0.2.4](): Addition of a simple test that can run on CPU. +- [Version 0.2.5](): API redesign for compatibility with `openmmtools` ## Acknowledgements We would like to thank Patrick Grinaway and John Chodera for their basic code framework for NCMC in OpenMM (see https://github.com/choderalab/perses/tree/master/perses/annihilation), and John Chodera and Christopher Bayly for their helpful discussions. diff --git a/blues/__init__.py b/blues/__init__.py index f1d7e582..366b4edd 100755 --- a/blues/__init__.py +++ b/blues/__init__.py @@ -2,9 +2,6 @@ """ BLUES """ -# Add imports here -from blues import integrators, moves, reporters, settings, simulation, utils - # Handle versioneer from ._version import get_versions versions = get_versions() diff --git a/blues/example.py b/blues/example.py deleted file mode 100644 index 190a916a..00000000 --- a/blues/example.py +++ /dev/null @@ -1,66 +0,0 @@ -from blues.moves import MoveEngine, RandomLigandRotationMove, SideChainMove -from blues.settings import Settings -from blues.simulation import * -from blues.utils import get_data_filename - - -def ligrot_example(yaml_file): - # Parse a YAML configuration, return as Dict - cfg = Settings(yaml_file).asDict() - structure = cfg['Structure'] - - #Select move type - ligand = RandomLigandRotationMove(structure, 'LIG') - #Iniitialize object that selects movestep - ligand_mover = MoveEngine(ligand) - - #Generate the openmm.Systems outside SimulationFactory to allow modifications - systems = SystemFactory(structure, ligand.atom_indices, cfg['system']) - - #Freeze atoms in the alchemical system to speed up alchemical calculation - systems.alch = systems.freeze_radius(structure, systems.alch, **cfg['freeze']) - - #Generate the OpenMM Simulations - simulations = SimulationFactory(systems, ligand_mover, cfg['simulation'], cfg['md_reporters'], - cfg['ncmc_reporters']) - - # Run BLUES Simulation - blues = BLUESSimulation(simulations, cfg['simulation']) - blues.run() - - -def sidechain_example(yaml_file): - # Parse a YAML configuration, return as Dict - cfg = Settings(yaml_file).asDict() - structure = cfg['Structure'] - - #Select move type - sidechain = SideChainMove(structure, [1]) - #Iniitialize object that selects movestep - sidechain_mover = MoveEngine(sidechain) - - #Generate the openmm.Systems outside SimulationFactory to allow modifications - systems = SystemFactory(structure, sidechain.atom_indices, cfg['system']) - - #Generate the OpenMM Simulations - simulations = SimulationFactory(systems, sidechain_mover, cfg['simulation'], cfg['md_reporters'], - cfg['ncmc_reporters']) - - # Run BLUES Simulation - blues = BLUESSimulation(simulations, cfg['simulation']) - blues.run() - - #Analysis - import mdtraj as md - import numpy as np - - traj = md.load_netcdf('vacDivaline-test/vacDivaline.nc', top='tests/data/vacDivaline.prmtop') - indicies = np.array([[0, 4, 6, 8]]) - dihedraldata = md.compute_dihedrals(traj, indicies) - with open("vacDivaline-test/dihedrals.txt", 'w') as output: - for value in dihedraldata: - output.write("%s\n" % str(value)[1:-1]) - - -ligrot_example(get_data_filename('blues', '../examples/rotmove_cuda.yml')) -#sidechain_example(get_data_filename('blues', '../examples/sidechain_cuda.yml')) diff --git a/blues/formats.py b/blues/formats.py index d3e4af46..7ca141aa 100644 --- a/blues/formats.py +++ b/blues/formats.py @@ -12,7 +12,7 @@ from mdtraj.utils import ensure_type, in_units_of from parmed.amber.netcdffiles import NetCDFTraj -from blues import reporters +from blues import storage ###################### @@ -51,7 +51,7 @@ class LoggerFormatter(logging.Formatter): def __init__(self): super().__init__(fmt="%(levelname)s: %(msg)s", datefmt="%H:%M:%S", style='%') - reporters.addLoggingLevel('REPORT', logging.WARNING - 5) + storage.addLoggingLevel('REPORT', logging.WARNING - 5) def format(self, record): @@ -84,395 +84,6 @@ def format(self, record): return result -class BLUESHDF5TrajectoryFile(HDF5TrajectoryFile): - """ - Extension of the `mdtraj.formats.hdf5.HDF5TrajectoryFile` class which - handles the writing of the trajectory data to the HDF5 file format. - Additional features include writing NCMC related data to the HDF5 file. - - Parameters - ---------- - filename : str - The filename for the HDF5 file. - mode : str, default='r' - The mode to open the HDF5 file in. - force_overwrite : bool, default=True - If True, overwrite the file if it already exists - compression : str, default='zlib' - Valid choices are ['zlib', 'lzo', 'bzip2', 'blosc'] - - """ - - def __init__(self, filename, mode='r', force_overwrite=True, compression='zlib'): - super(BLUESHDF5TrajectoryFile, self).__init__(filename, mode, force_overwrite, compression) - - def write(self, - coordinates, - parameters=None, - environment=None, - time=None, - cell_lengths=None, - cell_angles=None, - velocities=None, - kineticEnergy=None, - potentialEnergy=None, - temperature=None, - alchemicalLambda=None, - protocolWork=None, - title=None): - """Write one or more frames of data to the file - This method saves data that is associated with one or more simulation - frames. Note that all of the arguments can either be raw numpy arrays - or unitted arrays (with simtk.unit.Quantity). If the arrays are unittted, - a unit conversion will be automatically done from the supplied units - into the proper units for saving on disk. You won't have to worry about - it. - - Furthermore, if you wish to save a single frame of simulation data, you - can do so naturally, for instance by supplying a 2d array for the - coordinates and a single float for the time. This "shape deficiency" - will be recognized, and handled appropriately. - - Parameters - ---------- - coordinates : np.ndarray, shape=(n_frames, n_atoms, 3) - The cartesian coordinates of the atoms to write. By convention, the - lengths should be in units of nanometers. - time : np.ndarray, shape=(n_frames,), optional - You may optionally specify the simulation time, in picoseconds - corresponding to each frame. - cell_lengths : np.ndarray, shape=(n_frames, 3), dtype=float32, optional - You may optionally specify the unitcell lengths. - The length of the periodic box in each frame, in each direction, - `a`, `b`, `c`. By convention the lengths should be in units - of angstroms. - cell_angles : np.ndarray, shape=(n_frames, 3), dtype=float32, optional - You may optionally specify the unitcell angles in each frame. - Organized analogously to cell_lengths. Gives the alpha, beta and - gamma angles respectively. By convention, the angles should be - in units of degrees. - velocities : np.ndarray, shape=(n_frames, n_atoms, 3), optional - You may optionally specify the cartesian components of the velocity - for each atom in each frame. By convention, the velocities - should be in units of nanometers / picosecond. - kineticEnergy : np.ndarray, shape=(n_frames,), optional - You may optionally specify the kinetic energy in each frame. By - convention the kinetic energies should b in units of kilojoules per - mole. - potentialEnergy : np.ndarray, shape=(n_frames,), optional - You may optionally specify the potential energy in each frame. By - convention the kinetic energies should b in units of kilojoules per - mole. - temperature : np.ndarray, shape=(n_frames,), optional - You may optionally specify the temperature in each frame. By - convention the temperatures should b in units of Kelvin. - alchemicalLambda : np.ndarray, shape=(n_frames,), optional - You may optionally specify the alchemicalLambda in each frame. These - have no units, but are generally between zero and one. - protocolWork : np.ndarray, shape=(n_frames,), optional - You may optionally specify the protocolWork in each frame. These - are in reduced units of kT but are stored dimensionless - title : str - Title of the HDF5 trajectory file - """ - _check_mode(self.mode, ('w', 'a')) - - # these must be either both present or both absent. since - # we're going to throw an error if one is present w/o the other, - # lets do it now. - if cell_lengths is None and cell_angles is not None: - raise ValueError('cell_lengths were given, but no cell_angles') - if cell_lengths is not None and cell_angles is None: - raise ValueError('cell_angles were given, but no cell_lengths') - - # if the input arrays are simtk.unit.Quantities, convert them - # into md units. Note that this acts as a no-op if the user doesn't - # have simtk.unit installed (e.g. they didn't install OpenMM) - coordinates = in_units_of(coordinates, None, 'nanometers') - time = in_units_of(time, None, 'picoseconds') - cell_lengths = in_units_of(cell_lengths, None, 'nanometers') - cell_angles = in_units_of(cell_angles, None, 'degrees') - velocities = in_units_of(velocities, None, 'nanometers/picosecond') - kineticEnergy = in_units_of(kineticEnergy, None, 'kilojoules_per_mole') - potentialEnergy = in_units_of(potentialEnergy, None, 'kilojoules_per_mole') - temperature = in_units_of(temperature, None, 'kelvin') - alchemicalLambda = in_units_of(alchemicalLambda, None, 'dimensionless') - protocolWork = in_units_of(protocolWork, None, 'kT') - - # do typechecking and shapechecking on the arrays - # this ensure_type method has a lot of options, but basically it lets - # us validate most aspects of the array. Also, we can upconvert - # on defficent ndim, which means that if the user sends in a single - # frame of data (i.e. coordinates is shape=(n_atoms, 3)), we can - # realize that. obviously the default mode is that they want to - # write multiple frames at a time, so the coordinate shape is - # (n_frames, n_atoms, 3) - coordinates = ensure_type( - coordinates, - dtype=np.float32, - ndim=3, - name='coordinates', - shape=(None, None, 3), - can_be_none=False, - warn_on_cast=False, - add_newaxis_on_deficient_ndim=True) - n_frames, n_atoms, = coordinates.shape[0:2] - time = ensure_type( - time, - dtype=np.float32, - ndim=1, - name='time', - shape=(n_frames, ), - can_be_none=True, - warn_on_cast=False, - add_newaxis_on_deficient_ndim=True) - cell_lengths = ensure_type( - cell_lengths, - dtype=np.float32, - ndim=2, - name='cell_lengths', - shape=(n_frames, 3), - can_be_none=True, - warn_on_cast=False, - add_newaxis_on_deficient_ndim=True) - cell_angles = ensure_type( - cell_angles, - dtype=np.float32, - ndim=2, - name='cell_angles', - shape=(n_frames, 3), - can_be_none=True, - warn_on_cast=False, - add_newaxis_on_deficient_ndim=True) - velocities = ensure_type( - velocities, - dtype=np.float32, - ndim=3, - name='velocoties', - shape=(n_frames, n_atoms, 3), - can_be_none=True, - warn_on_cast=False, - add_newaxis_on_deficient_ndim=True) - kineticEnergy = ensure_type( - kineticEnergy, - dtype=np.float32, - ndim=1, - name='kineticEnergy', - shape=(n_frames, ), - can_be_none=True, - warn_on_cast=False, - add_newaxis_on_deficient_ndim=True) - potentialEnergy = ensure_type( - potentialEnergy, - dtype=np.float32, - ndim=1, - name='potentialEnergy', - shape=(n_frames, ), - can_be_none=True, - warn_on_cast=False, - add_newaxis_on_deficient_ndim=True) - temperature = ensure_type( - temperature, - dtype=np.float32, - ndim=1, - name='temperature', - shape=(n_frames, ), - can_be_none=True, - warn_on_cast=False, - add_newaxis_on_deficient_ndim=True) - alchemicalLambda = ensure_type( - alchemicalLambda, - dtype=np.float32, - ndim=1, - name='alchemicalLambda', - shape=(n_frames, ), - can_be_none=True, - warn_on_cast=False, - add_newaxis_on_deficient_ndim=True) - protocolWork = ensure_type( - protocolWork, - dtype=np.float32, - ndim=1, - name='protocolWork', - shape=(n_frames, ), - can_be_none=True, - warn_on_cast=False, - add_newaxis_on_deficient_ndim=True) - - # if this is our first call to write(), we need to create the headers - # and the arrays in the underlying HDF5 file - if self._needs_initialization: - self._initialize_headers( - n_atoms=n_atoms, - title=title, - parameters=parameters, - set_environment=(environment is not None), - set_coordinates=True, - set_time=(time is not None), - set_cell=(cell_lengths is not None or cell_angles is not None), - set_velocities=(velocities is not None), - set_kineticEnergy=(kineticEnergy is not None), - set_potentialEnergy=(potentialEnergy is not None), - set_temperature=(temperature is not None), - set_alchemicalLambda=(alchemicalLambda is not None), - set_protocolWork=(protocolWork is not None)) - self._needs_initialization = False - - # we need to check that that the entries that the user is trying - # to save are actually fields in OUR file - - try: - # try to get the nodes for all of the fields that we have - # which are not None - for name in [ - 'coordinates', 'time', 'cell_angles', 'cell_lengths', 'velocities', 'kineticEnergy', - 'potentialEnergy', 'temperature', 'protocolWork', 'alchemicalLambda' - ]: - contents = locals()[name] - if contents is not None: - self._get_node(where='/', name=name).append(contents) - if contents is None: - # for each attribute that they're not saving, we want - # to make sure the file doesn't explect it - try: - self._get_node(where='/', name=name) - raise AssertionError() - except self.tables.NoSuchNodeError: - pass - - except self.tables.NoSuchNodeError: - raise ValueError("The file that you're trying to save to doesn't " - "contain the field %s. You can always save a new trajectory " - "and have it contain this information, but I don't allow 'ragged' " - "arrays. If one frame is going to have %s information, then I expect " - "all of them to. So I can't save it for just these frames. Sorry " - "about that :)" % (name, name)) - except AssertionError: - raise ValueError("The file that you're saving to expects each frame " - "to contain %s information, but you did not supply it." - "I don't allow 'ragged' arrays. If one frame is going " - "to have %s information, then I expect all of them to. " % (name, name)) - - self._frame_index += n_frames - self.flush() - - def _encodeStringForPyTables(self, string, name, where='/', complevel=1, complib='zlib', shuffle=True): - """ - Encode a given string into a character array (PyTables) - - Parameters: - ----------- - string : str, input string to be encoded - name : str, title or name of table for character array - where : str, filepath where character array will be stored - By default ('/') the character array will be stored at the root level. - complevel : int, compression level - complib : str, default='zlib' - Valid choices are ['zlib', 'lzo', 'bzip2', 'blosc'] - shuffle : bool, default=True - Whether or not to use the Shuffle filter in the HDF5 library. This is normally used to improve the compression ratio. A false value disables shuffling and a true one enables it. The default value depends on whether compression is enabled or not; if compression is enabled, shuffling defaults to be enabled, else shuffling is disabled. Shuffling can only be used when compression is enabled. - - """ - bytestring = np.fromstring(string.encode('utf-8'), np.uint8) - atom = self.tables.UInt8Atom() - filters = self.tables.Filters(complevel, complib, shuffle) - if self.tables.__version__ >= '3.0.0': - self._handle.create_carray(where=where, name=name, obj=bytestring, atom=atom, filters=filters) - else: - self._handle.createCArray(where=where, name=name, obj=bytestring, atom=atom, filters=filters) - - def _initialize_headers(self, n_atoms, title, parameters, set_environment, set_coordinates, set_time, set_cell, - set_velocities, set_kineticEnergy, set_potentialEnergy, set_temperature, - set_alchemicalLambda, set_protocolWork): - """ - Function that initializes the tables for storing data from the simulation - and writes metadata at the root level of the HDF5 file. - - Parameters - ----------- - n_atoms : int - Number of atoms in system - title : str - Title for the root level data table - parameters : dict - Arguments/parameters used for the simulation. - set_* : bool - Parameters that begin with set_*. If True, the corresponding data - will be written to the HDF5 file. - - """ - self._n_atoms = n_atoms - self._parameters = parameters - self._handle.root._v_attrs.title = str(title) - self._handle.root._v_attrs.conventions = str('Pande') - self._handle.root._v_attrs.conventionVersion = str('1.1') - self._handle.root._v_attrs.program = str('MDTraj') - self._handle.root._v_attrs.programVersion = str(mdtraj.version.full_version) - self._handle.root._v_attrs.method = str('BLUES') - self._handle.root._v_attrs.methodVersion = str(blues.__version__) - self._handle.root._v_attrs.reference = str('DOI: 10.1021/acs.jpcb.7b11820') - - if not hasattr(self._handle.root._v_attrs, 'application'): - self._handle.root._v_attrs.application = str('OpenMM') - self._handle.root._v_attrs.applicationVersion = str(simtk.openmm.version.full_version) - - # create arrays that store frame level informat - if set_coordinates: - self._create_earray( - where='/', name='coordinates', atom=self.tables.Float32Atom(), shape=(0, self._n_atoms, 3)) - self._handle.root.coordinates.attrs['units'] = str('nanometers') - - if set_time: - self._create_earray(where='/', name='time', atom=self.tables.Float32Atom(), shape=(0, )) - self._handle.root.time.attrs['units'] = str('picoseconds') - - if set_cell: - self._create_earray(where='/', name='cell_lengths', atom=self.tables.Float32Atom(), shape=(0, 3)) - self._create_earray(where='/', name='cell_angles', atom=self.tables.Float32Atom(), shape=(0, 3)) - self._handle.root.cell_lengths.attrs['units'] = str('nanometers') - self._handle.root.cell_angles.attrs['units'] = str('degrees') - - if set_velocities: - self._create_earray( - where='/', name='velocities', atom=self.tables.Float32Atom(), shape=(0, self._n_atoms, 3)) - self._handle.root.velocities.attrs['units'] = str('nanometers/picosecond') - - if set_kineticEnergy: - self._create_earray(where='/', name='kineticEnergy', atom=self.tables.Float32Atom(), shape=(0, )) - self._handle.root.kineticEnergy.attrs['units'] = str('kilojoules_per_mole') - - if set_potentialEnergy: - self._create_earray(where='/', name='potentialEnergy', atom=self.tables.Float32Atom(), shape=(0, )) - self._handle.root.potentialEnergy.attrs['units'] = str('kilojoules_per_mole') - - if set_temperature: - self._create_earray(where='/', name='temperature', atom=self.tables.Float32Atom(), shape=(0, )) - self._handle.root.temperature.attrs['units'] = str('kelvin') - - #Add another portion akin to this if you want to store more data in the h5 file - if set_alchemicalLambda: - self._create_earray(where='/', name='alchemicalLambda', atom=self.tables.Float32Atom(), shape=(0, )) - self._handle.root.alchemicalLambda.attrs['units'] = str('dimensionless') - - if set_protocolWork: - self._create_earray(where='/', name='protocolWork', atom=self.tables.Float32Atom(), shape=(0, )) - self._handle.root.protocolWork.attrs['units'] = str('kT') - - if parameters: - if 'Logger' in self._parameters: self._parameters.pop('Logger') - paramjson = json.dumps(self._parameters) - self._encodeStringForPyTables(string=paramjson, name='parameters') - - if set_environment: - try: - envout = subprocess.check_output('conda env export --no-builds', shell=True, stderr=subprocess.STDOUT) - envjson = json.dumps(yaml.load(envout), sort_keys=True, indent=2) - self._encodeStringForPyTables(envjson, name='environment') - except Exception as e: - print(e) - pass - - class NetCDF4Traj(NetCDFTraj): """Extension of `parmed.amber.netcdffiles.NetCDFTraj` to allow proper file flushing. Requires the netcdf4 library (not scipy), install with @@ -593,17 +204,17 @@ def open_new(cls, ncfile.createDimension('label', 5) inst.cell_spatial, inst.cell_angular, inst.label = 3, 3, 5 # Create the variables and assign units and scaling factors - v = ncfile.createVariable('spatial', 'c', ('spatial', )) + v = ncfile.createVariable('spatial', 'c', ('spatial',)) v[:] = np.asarray(list('xyz')) if inst.hasbox: - v = ncfile.createVariable('cell_spatial', 'c', ('cell_spatial', )) + v = ncfile.createVariable('cell_spatial', 'c', ('cell_spatial',)) v[:] = np.asarray(list('abc')) v = ncfile.createVariable('cell_angular', 'c', ( 'cell_angular', 'label', )) v[:] = np.asarray([list('alpha'), list('beta '), list('gamma')]) - v = ncfile.createVariable('time', 'f', ('frame', )) + v = ncfile.createVariable('time', 'f', ('frame',)) v.units = 'picosecond' if inst.hascrds: v = ncfile.createVariable('coordinates', 'f', ('frame', 'atom', 'spatial')) @@ -627,23 +238,23 @@ def open_new(cls, v.units = 'degree' inst._last_box_frame = 0 if inst.remd == 'TEMPERATURE': - v = ncfile.createVariable('temp0', 'd', ('frame', )) + v = ncfile.createVariable('temp0', 'd', ('frame',)) v.units = 'kelvin' inst._last_remd_frame = 0 elif inst.remd == 'MULTI': ncfile.createVariable('remd_indices', 'i', ('frame', 'remd_dimension')) - ncfile.createVariable('remd_dimtype', 'i', ('remd_dimension', )) + ncfile.createVariable('remd_dimtype', 'i', ('remd_dimension',)) inst._last_remd_frame = 0 inst._last_time_frame = 0 if inst.hasprotocolWork: - v = ncfile.createVariable('protocolWork', 'f', ('frame', )) + v = ncfile.createVariable('protocolWork', 'f', ('frame',)) v.units = 'kT' inst._last_protocolWork_frame = 0 if inst.hasalchemicalLambda: - v = ncfile.createVariable('alchemicalLambda', 'f', ('frame', )) + v = ncfile.createVariable('alchemicalLambda', 'f', ('frame',)) v.units = 'unitless' inst._last_alchemicalLambda_frame = 0 diff --git a/blues/integrators.py b/blues/integrators.py index 501dfc19..cef49631 100644 --- a/blues/integrators.py +++ b/blues/integrators.py @@ -106,7 +106,7 @@ def __init__(self, measure_heat=True, nsteps_neq=100, nprop=1, - prop_lambda=0.3, + propLambda=0.3, *args, **kwargs): # call the base class constructor @@ -121,7 +121,7 @@ def __init__(self, measure_heat=measure_heat, nsteps_neq=nsteps_neq) - self._prop_lambda = self._get_prop_lambda(prop_lambda) + self._propLambda = self._get_propLambda(propLambda) # add some global variables relevant to the integrator kB = simtk.unit.BOLTZMANN_CONSTANT_kB * simtk.unit.AVOGADRO_CONSTANT_NA @@ -131,8 +131,8 @@ def __init__(self, self.addGlobalVariable("first_step", 0) self.addGlobalVariable("nprop", nprop) self.addGlobalVariable("prop", 1) - self.addGlobalVariable("prop_lambda_min", self._prop_lambda[0]) - self.addGlobalVariable("prop_lambda_max", self._prop_lambda[1]) + self.addGlobalVariable("prop_lambda_min", self._propLambda[0]) + self.addGlobalVariable("prop_lambda_max", self._propLambda[1]) # Behavior changed in https://github.com/choderalab/openmmtools/commit/7c2630050631e126d61b67f56e941de429b2d643#diff-5ce4bc8893e544833c827299a5d48b0d self._step_dispatch_table['H'] = (self._add_alchemical_perturbation_step, False) #$self._registered_step_types['H'] = ( @@ -144,9 +144,9 @@ def __init__(self, except: self.addGlobalVariable('shadow_work', 0) - def _get_prop_lambda(self, prop_lambda): - prop_lambda_max = round(prop_lambda + 0.5, 4) - prop_lambda_min = round(0.5 - prop_lambda, 4) + def _get_propLambda(self, propLambda): + prop_lambda_max = round(propLambda + 0.5, 4) + prop_lambda_min = round(0.5 - propLambda, 4) prop_range = prop_lambda_max - prop_lambda_min #Set values to outside [0, 1.0] to skip IfBlock diff --git a/blues/logging.yml b/blues/logging.yml new file mode 100644 index 00000000..21d79180 --- /dev/null +++ b/blues/logging.yml @@ -0,0 +1,37 @@ +version: 1 +disable_existing_loggers: False +formatters: + debug: + format: "%(levelname)s: [%(module)s.%(funcName)s] %(message)s" + info: + format: "%(message)s" + +handlers: + console: + class: logging.StreamHandler + level: DEBUG + formatter: info + stream: ext://sys.stdout + + file_handler: + class: logging.handlers.RotatingFileHandler + level: DEBUG + formatter: info + filename: blues.log + maxBytes: 10485760 # 10MB + backupCount: 20 + encoding: utf8 + +# loggers: +# blues.ncmc: +# level: DEBUG +# handlers: [console] +# propagate: no +# blues.storage: +# level: INFO +# handlers: [console] +# propagate: no + +root: + level: DEBUG + handlers: [console, file_handler] diff --git a/blues/moves.py b/blues/moves.py deleted file mode 100644 index 7322173f..00000000 --- a/blues/moves.py +++ /dev/null @@ -1,1314 +0,0 @@ -""" -Provides the main Move class which allows definition of moves -which alter the positions of subsets of atoms in a context during a BLUES -simulation, in order to increase sampling. -Also provides functionality for CombinationMove definitions which consist of -a combination of other pre-defined moves such as via instances of Move. - -Authors: Samuel C. Gill - -Contributors: Nathan M. Lim, Kalistyn Burley, David L. Mobley -""" - -import copy -import math -import random -import sys -import traceback - -import mdtraj -import numpy -import parmed -from simtk import unit - -try: - import openeye.oechem as oechem - if not oechem.OEChemIsLicensed(): - print('ImportError: Need License for OEChem! SideChainMove class will be unavailable.') - try: - import oeommtools.utils as oeommtools - except ImportError: - print('ImportError: Could not import oeommtools. SideChainMove class will be unavailable.') -except ImportError: - print('ImportError: Could not import openeye-toolkits. SideChainMove class will be unavailable.') - - -class Move(object): - """This is the base Move class. Move provides methods for calculating properties - and applying the move on the set of atoms being perturbed in the NCMC simulation. - """ - - def __init__(self): - """Initialize the Move object - Currently empy. - """ - - def initializeSystem(self, system, integrator): - """If the system or integrator needs to be modified to perform the move - (ex. adding a force) this method is called during the start - of the simulation to change the system or integrator to accomodate that. - - Parameters - ---------- - system : openmm.System - System to be modified. - integrator : openmm.Integrator - Integrator to be modified. - - Returns - ------- - system : openmm.System - The modified System object. - integrator : openmm.Integrator - The modified Integrator object. - - """ - new_sys = system - new_int = integrator - return new_sys, new_int - - def beforeMove(self, context): - """This method is called at the start of the NCMC portion if the - context needs to be checked or modified before performing the move - at the halfway point. - - Parameters - ---------- - context : openmm.Context - Context containing the positions to be moved. - - Returns - ------- - context : openmm.Context - The same input context, but whose context were changed by this function. - - """ - return context - - def afterMove(self, context): - """This method is called at the end of the NCMC portion if the - context needs to be checked or modified before performing the move - at the halfway point. - - Parameters - ---------- - context : openmm.Context - Context containing the positions to be moved. - - Returns - ------- - context : openmm.Context - The same input context, but whose context were changed by this function. - - """ - - return context - - def _error(self, context): - """This method is called if running during NCMC portion results - in an error. This allows portions of the context, such as the - context parameters that would not be fixed by just reverting the - positions/velocities of the context. - - Parameters - ---------- - context : openmm.Context - Context containing the positions to be moved. - - Returns - ------- - context : openmm.Context - The same input context, but whose context were changed by this function. - - """ - - return context - - def move(self, context): - """This method is called at the end of the NCMC portion if the - context needs to be checked or modified before performing the move - at the halfway point. - - Parameters - ---------- - context : openmm.Context - Context containing the positions to be moved. - - Returns - ------- - context : openmm.Context - The same input context, but whose context were changed by this function. - """ - return context - - -class RandomLigandRotationMove(Move): - """RandomLightRotationMove that provides methods for calculating properties on the - object 'model' (i.e ligand) being perturbed in the NCMC simulation. - Current methods calculate the object's atomic masses and center of masss. - Calculating the object's center of mass will get the positions - and total mass. - - Parameters - ---------- - resname : str - String specifying the residue name of the ligand. - structure: parmed.Structure - ParmEd Structure object of the relevant system to be moved. - random_state : integer or numpy.RandomState, optional - The generator used for random numbers. If an integer is given, it fixes the seed. Defaults to the global numpy random number generator. - - Attributes - ---------- - structure : parmed.Structure - The structure of the ligand or selected atoms to be rotated. - resname : str, default='LIG' - The residue name of the ligand or selected atoms to be rotated. - topology : openmm.Topology - The topology of the ligand or selected atoms to be rotated. - atom_indices : list - Atom indicies of the ligand. - masses : list - Particle masses of the ligand with units. - totalmass : int - Total mass of the ligand. - center_of_mass : numpy.array - Calculated center of mass of the ligand in XYZ coordinates. This should - be updated every iteration. - positions : numpy.array - Ligands positions in XYZ coordinates. This should be updated - every iteration. - - Examples - -------- - >>> from blues.move import RandomLigandRotationMove - >>> ligand = RandomLigandRotationMove(structure, 'LIG') - >>> ligand.resname - 'LIG' - """ - - def __init__(self, structure, resname='LIG', random_state=None): - self.structure = structure - self.resname = resname - self.random_state = random_state - self.atom_indices = self.getAtomIndices(structure, self.resname) - self.topology = structure[self.atom_indices].topology - self.totalmass = 0 - self.masses = [] - - self.center_of_mass = None - self.positions = structure[self.atom_indices].positions - self._calculateProperties() - - def getAtomIndices(self, structure, resname): - """ - Get atom indices of a ligand from ParmEd Structure. - - Parameters - ---------- - resname : str - String specifying the residue name of the ligand. - structure: parmed.Structure - ParmEd Structure object of the atoms to be moved. - - Returns - ------- - atom_indices : list of ints - list of atoms in the coordinate file matching lig_resname - """ - # TODO: Add option for resnum to better select residue names - atom_indices = [] - topology = structure.topology - for atom in topology.atoms(): - if str(resname) in atom.residue.name: - atom_indices.append(atom.index) - return atom_indices - - def getMasses(self, topology): - """ - Returns a list of masses of the specified ligand atoms. - - Parameters - ---------- - topology: parmed.Topology - ParmEd topology object containing atoms of the system. - - Returns - ------- - masses: 1xn numpy.array * simtk.unit.dalton - array of masses of len(self.atom_indices), denoting - the masses of the atoms in self.atom_indices - totalmass: float * simtk.unit.dalton - The sum of the mass found in masses - """ - masses = unit.Quantity(numpy.zeros([int(topology.getNumAtoms()), 1], numpy.float32), unit.dalton) - for idx, atom in enumerate(topology.atoms()): - masses[idx] = atom.element._mass - totalmass = masses.sum() - return masses, totalmass - - def getCenterOfMass(self, positions, masses): - """Returns the calculated center of mass of the ligand as a numpy.array - - Parameters - ---------- - positions: nx3 numpy array * simtk.unit compatible with simtk.unit.nanometers - ParmEd positions of the atoms to be moved. - masses : numpy.array - numpy.array of particle masses - - Returns - ------- - center_of_mass: numpy array * simtk.unit compatible with simtk.unit.nanometers - 1x3 numpy.array of the center of mass of the given positions - """ - coordinates = numpy.asarray(positions._value, numpy.float32) - center_of_mass = parmed.geometry.center_of_mass(coordinates, masses) * positions.unit - return center_of_mass - - def _calculateProperties(self): - """Calculate the masses and center of mass for the object. This function - is called upon initailization of the class.""" - self.masses, self.totalmass = self.getMasses(self.topology) - self.center_of_mass = self.getCenterOfMass(self.positions, self.masses) - - def move(self, context): - """Function that performs a random rotation about the - center of mass of the ligand. - - Parameters - ---------- - context: simtk.openmm.Context object - Context containing the positions to be moved. - - Returns - ------- - context: simtk.openmm.Context object - The same input context, but whose positions were changed by this function. - """ - positions = context.getState(getPositions=True).getPositions(asNumpy=True) - - self.positions = positions[self.atom_indices] - self.center_of_mass = self.getCenterOfMass(self.positions, self.masses) - reduced_pos = self.positions - self.center_of_mass - - # Define random rotational move on the ligand - rand_quat = mdtraj.utils.uniform_quaternion(size=None, random_state=self.random_state) - rand_rotation_matrix = mdtraj.utils.rotation_matrix_from_quaternion(rand_quat) - #multiply lig coordinates by rot matrix and add back COM translation from origin - rot_move = numpy.dot(reduced_pos, rand_rotation_matrix) * positions.unit + self.center_of_mass - - # Update ligand positions in nc_sim - for index, atomidx in enumerate(self.atom_indices): - positions[atomidx] = rot_move[index] - context.setPositions(positions) - positions = context.getState(getPositions=True).getPositions(asNumpy=True) - self.positions = positions[self.atom_indices] - return context - - -class MoveEngine(object): - """MoveEngine provides perturbation functions for the context during the NCMC - simulation. - - Parameters - ---------- - moves : blues.move object or list of N blues.move objects - Specifies the possible moves to be performed. - - probabilities: list of floats, optional, default=None - A list of N probabilities, where probabilities[i] corresponds to the - probaility of moves[i] being selected to perform its associated - move() method. If None, uniform probabilities are assigned. - - Attributes - ---------- - moves : blues.move or list of N blues.move objects - Possible moves to be performed. - probabilities : list of floats - Normalized probabilities for each move. - selected_move : blues.move - Selected move to be performed. - move_name : str - Name of the selected move to be performed - - Examples - -------- - Load a parmed.Structure, list of moves with probabilities, initialize - the MoveEngine class, and select a move from our list. - - >>> import parmed - >>> from blues.moves import * - >>> structure = parmed.load_file('tests/data/eqToluene.prmtop', xyz='tests/data/eqToluene.inpcrd') - >>> probabilities = [0.25, 0.75] - >>> moves = [SideChainMove(structure, [111]),RandomLigandRotationMove(structure, 'LIG')] - >>> mover = MoveEngine(moves, probabilities) - >>> mover.moves - [, - ] - >>> mover.selectMove() - >>> mover.selected_move - - """ - - def __init__(self, moves, probabilities=None): - #make a list from moves if not a list - if isinstance(moves, list): - self.moves = moves - else: - self.moves = [moves] - #normalize probabilities - if probabilities is None: - single_prob = 1. / len(self.moves) - self.probabilities = [single_prob for x in (self.moves)] - else: - prob_sum = float(sum(probabilities)) - self.probabilities = [x / prob_sum for x in probabilities] - #if move and probabilitiy lists are different lengths throw error - if len(self.moves) != len(self.probabilities): - print('moves and probability list lengths need to match') - raise IndexError - #use index in selecting move - self.selected_move = None - - def selectMove(self): - """Chooses the move which will be selected for a given NCMC - iteration - """ - rand_num = numpy.random.choice(len(self.probabilities), p=self.probabilities) - self.selected_move = self.moves[rand_num] - self.move_name = self.selected_move.__class__.__name__ - - def runEngine(self, context): - """Selects a random Move object based on its - assigned probability and and performs its move() function - on a context. - - Parameters - ---------- - context : openmm.Context - OpenMM context whose positions should be moved. - - Returns - ------- - context : openmm.Context - OpenMM context whose positions have been moved. - """ - try: - new_context = self.selected_move.move(context) - except Exception as e: - #In case the move isn't properly implemented, print out useful info - print('Error: move not implemented correctly, printing traceback:') - ex_type, ex, tb = sys.exc_info() - traceback.print_tb(tb) - print(e) - raise SystemExit - - return new_context - - -######################## -## UNDER DEVELOPMENT ### -######################## - - -class SideChainMove(Move): - """**NOTE:** Usage of this class requires a valid OpenEye license. - - SideChainMove provides methods for calculating properties needed to - rotate a sidechain residue given a parmed.Structure. Calculated properties - include: backbone atom indicies, atom pointers and indicies of the residue - sidechain, bond pointers and indices for rotatable heavy bonds in - the sidechain, and atom indices upstream of selected bond. - - The class contains functions to randomly select a bond and angle to be rotated - and applies a rotation matrix to the target atoms to update their coordinates on the - object 'model' (i.e sidechain) being perturbed in the NCMC simulation. - - Parameters - ---------- - structure : parmed.Structure - The structure of the entire system to be simulated. - residue_list : list of int - List of the residue numbers of the sidechains to be rotated. - verbose : bool, default=False - Enable verbosity to print out detailed information of the rotation. - write_move : bool, default=False - If True, writes a PDB of the system after rotation. - - Attributes - ---------- - structure : parmed.Structure - The structure of the entire system to be simulated. - molecule : oechem.OEMolecule - The OEMolecule containing the sidechain(s) to be rotated. - residue_list : list of int - List containing the residue numbers of the sidechains to be rotated. - all_atoms : list of int - List containing the atom indicies of the sidechains to be rotated. - rot_atoms : dict - Dictionary of residues, bonds and atoms to be rotated - rot_bonds : dict of oechem.OEBondBase - Dictionary containing the bond pointers of the rotatable bonds. - qry_atoms : dict of oechem.OEAtomBase - Dictionary containing all the atom pointers (as OpenEye objects) that - make up the given residues. - - - Examples - -------- - >>> from blues.move import SideChainMove - >>> sidechain = SideChainMove(structure, [1]) - - """ - - def __init__(self, structure, residue_list, verbose=False, write_move=False): - self.structure = structure - self.molecule = self._pmdStructureToOEMol() - self.residue_list = residue_list - self.all_atoms = [atom.index for atom in self.structure.topology.atoms()] - self.rot_atoms, self.rot_bonds, self.qry_atoms = self.getRotBondAtoms() - self.atom_indices = self.rot_atoms - self.verbose = verbose - self.write_move = write_move - - def _pmdStructureToOEMol(self): - """Helper function for converting the parmed structure into an OEMolecule.""" - top = self.structure.topology - pos = self.structure.positions - molecule = oeommtools.openmmTop_to_oemol(top, pos, verbose=False) - oechem.OEPerceiveResidues(molecule) - oechem.OEFindRingAtomsAndBonds(molecule) - - return molecule - - def getBackboneAtoms(self, molecule): - """Takes an OpenEye Molecule, finds the backbone atoms and - returns the indicies of the backbone atoms. - - Parameters - ---------- - molecule : oechem.OEMolecule - The OEmolecule of the simulated system. - - Returns - ------- - backbone_atoms : list of int - List containing the atom indices of the backbone atoms. - - """ - - backbone_atoms = [] - pred = oechem.OEIsBackboneAtom() - for atom in molecule.GetAtoms(pred): - bb_atom_idx = atom.GetIdx() - backbone_atoms.append(bb_atom_idx) - - return backbone_atoms - - def getTargetAtoms(self, molecule, backbone_atoms, residue_list): - """Takes an OpenEye molecule and a list of residue numbers then - generates a dictionary containing all the atom pointers and indicies for the - non-backbone, atoms of those target residues, as well as a list of backbone atoms. - Note: The atom indicies start at 0 and are thus -1 from the PDB file indicies - - Parameters - ---------- - molecule : oechem.OEMolecule - The OEmolecule of the simulated system. - backbone_atoms : list of int - List containing the atom indices of the backbone atoms. - residue_list : list of int - List containing the residue numbers of the sidechains to be rotated. - - Returns - ------- - backbone_atoms : list of int - List containing the atom indices of the backbone atoms to be rotated. - qry_atoms : dict of oechem.OEAtomBase - Dictionary containing all the atom pointers (as OpenEye objects) that - make up the given residues. - - """ - - # create and clear dictionary to store atoms that make up residue list - qry_atoms = {} - qry_atoms.clear() - - reslib = [] - - #print('Searching residue list for atoms...') - # loop through all the atoms in the PDB OEGraphMol structure - for atom in molecule.GetAtoms(): - # check if the atom is in backbone - if atom.GetIdx() not in backbone_atoms: - # if heavy, find what residue it is associated with - myres = oechem.OEAtomGetResidue(atom) - # check if the residue number is amongst the list of residues - if myres.GetResidueNumber() in residue_list and myres.GetName() != "HOH": - # store the atom location in a query atom dict keyed by its atom index - qry_atoms.update({atom: atom.GetIdx()}) - #print('Found atom %s in residue number %i %s'%(atom,myres.GetResidueNumber(),myres.GetName())) - if myres not in reslib: - reslib.append(myres) - - return qry_atoms, backbone_atoms - - def findHeavyRotBonds(self, pdb_OEMol, qry_atoms): - """Takes in an OpenEye molecule as well as a dictionary of atom locations (keys) - and atom indicies. It loops over the query atoms and identifies any heavy bonds associated with each atom. - It stores and returns the bond indicies (keys) and the two atom indicies for each bond in a dictionary - Note: atom indicies start at 0, so are offset by 1 compared to pdb) - - Parameters - ---------- - pdb_OEMol : oechem.OEMolecule - The OEmolecule of the simulated system generated from a PDB file. - qry_atoms : dict of oechem.OEAtomBase - Dictionary containing all the atom pointers (as OpenEye objects) that - make up the given residues. - - Returns - ------- - rot_bonds : dict of oechem.OEBondBase - Dictionary containing the bond pointers of the rotatable bonds. - - - """ - # create and clear dictionary to store bond and atom indicies that are rotatable + heavy - rot_bonds = {} - rot_bonds.clear() - - for atom in qry_atoms.keys(): - myres = oechem.OEAtomGetResidue(atom) - for bond in atom.GetBonds(): - # retrieve the begnning and ending atoms - begatom = bond.GetBgn() - endatom = bond.GetEnd() - # if begnnning and ending atoms are not Hydrogen, and the bond is rotatable - if endatom.GetAtomicNum() > 1 and begatom.GetAtomicNum() > 1 and bond.IsRotor(): - # if the bond has not been added to dictionary already.. - # (as would happen if one of the atom pairs was previously looped over) - if bond not in rot_bonds: - #print('Bond number',bond, 'is rotatable, non-terminal, and contains only heavy atoms') - # store bond pointer (key) and atom indicies in dictionary if not already there - #rot_bonds.update({bond : {'AtomIdx_1' : bond.GetBgnIdx(), 'AtomIdx_2': bond.GetEndIdx()}}) - rot_bonds.update({bond: myres.GetResidueNumber()}) - - return rot_bonds - - def getRotAtoms(self, rotbonds, molecule, backbone_atoms): - """Function identifies and stores neighboring, upstream atoms for a given sidechain bond. - - Parameters - ---------- - rot_bonds : dict of oechem.OEBondBase - Dictionary containing the bond pointers of the rotatable bonds. - molecule : oechem.OEMolecule - The OEmolecule of the simulated system. - backbone_atoms : list of int - List containing the atom indices of the backbone atoms. - - - Returns - ------- - rot_atom_dict : dict of oechem.OEAtomBase - Dictionary containing the atom pointers for a given sidechain bond. - - """ - backbone = backbone_atoms - query_list = [] - idx_list = [] - rot_atom_dict = {} - rot_atom_dict.clear() - - for bond in rotbonds.keys(): - idx_list.clear() - query_list.clear() - resnum = (rotbonds[bond]) - thisbond = bond - ax1 = bond.GetBgn() - ax2 = bond.GetEnd() - - if resnum in rot_atom_dict.keys(): - rot_atom_dict[resnum].update({thisbond: []}) - else: - rot_atom_dict.update({resnum: {thisbond: []}}) - - idx_list.append(ax1.GetIdx()) - idx_list.append(ax2.GetIdx()) - - if ax1 not in query_list and ax1.GetIdx() not in backbone_atoms: - query_list.append(ax1) - if ax2 not in query_list and ax2.GetIdx() not in backbone_atoms: - query_list.append(ax2) - - for atom in query_list: - checklist = atom.GetAtoms() - for candidate in checklist: - if candidate not in query_list and candidate.GetIdx() not in backbone and candidate != ax2: - query_list.append(candidate) - if candidate.GetAtomicNum() > 1: - can_nbors = candidate.GetAtoms() - for can_nbor in can_nbors: - if can_nbor not in query_list and candidate.GetIdx( - ) not in backbone and candidate != ax2: - query_list.append(can_nbor) - - for atm in query_list: - y = atm.GetIdx() - if y not in idx_list: - idx_list.append(y) - - rot_atom_dict[resnum].update({thisbond: list(idx_list)}) - #print("Moving these atoms:", idx_list) - - return rot_atom_dict - - def getRotBondAtoms(self): - """This function is called on class initialization. - - Takes in a PDB filename (as a string) and list of residue numbers. Returns - a nested dictionary of rotatable bonds (containing only heavy atoms), that are keyed by residue number, - then keyed by bond pointer, containing values of atom indicies [axis1, axis2, atoms to be rotated] - Note: The atom indicies start at 0, and are offset by -1 from the PDB file indicies - - Returns - ------- - rot_atoms : dict - Dictionary of residues, bonds and atoms to be rotated - rot_bonds : dict of oechem.OEBondBase - Dictionary containing the bond pointers of the rotatable bonds. - qry_atoms : dict of oechem.OEAtomBase - Dictionary containing all the atom pointers (as OpenEye objects) that - make up the given residues. - - """ - backbone_atoms = self.getBackboneAtoms(self.molecule) - - # Generate dictionary containing locations and indicies of heavy residue atoms - #print('Dictionary of all query atoms generated from residue list\n') - qry_atoms, backbone_atoms = self.getTargetAtoms(self.molecule, backbone_atoms, self.residue_list) - - # Identify bonds containing query atoms and return dictionary of indicies - rot_bonds = self.findHeavyRotBonds(self.molecule, qry_atoms) - - # Generate dictionary of residues, bonds and atoms to be rotated - rot_atoms = self.getRotAtoms(rot_bonds, self.molecule, backbone_atoms) - return rot_atoms, rot_bonds, qry_atoms - - def chooseBondandTheta(self): - """This function is called on class initialization. - - Takes a dictionary containing nested dictionary, keyed by res#, - then keyed by bond_ptrs, containing a list of atoms to move, randomly selects a bond, - and generates a random angle (radians). It returns the atoms associated with the - the selected bond, the pointer for the selected bond and the randomly generated angle - - - Returns - ------- - theta_ran : - - targetatoms : - - res_choice : - - bond_choice : - - """ - - res_choice = random.choice(list(self.rot_atoms.keys())) - bond_choice = random.choice(list(self.rot_atoms[res_choice].keys())) - targetatoms = self.rot_atoms[res_choice][bond_choice] - theta_ran = random.random() * 2 * math.pi - - return theta_ran, targetatoms, res_choice, bond_choice - - def rotation_matrix(self, axis, theta): - """Function returns the rotation matrix associated with counterclockwise rotation - about the given axis by theta radians. - - Parameters - ---------- - axis : - - theta : float - The angle of rotation in radians. - """ - axis = numpy.asarray(axis) - axis = axis / math.sqrt(numpy.dot(axis, axis)) - a = math.cos(theta / 2.0) - b, c, d = -axis * math.sin(theta / 2.0) - aa, bb, cc, dd = a * a, b * b, c * c, d * d - bc, ad, ac, ab, bd, cd = b * c, a * d, a * c, a * b, b * d, c * d - return numpy.array([[aa + bb - cc - dd, 2 * (bc + ad), - 2 * (bd - ac)], [2 * (bc - ad), aa + cc - bb - dd, 2 * (cd + ab)], - [2 * (bd + ac), 2 * (cd - ab), aa + dd - bb - cc]]) - - def move(self, context, verbose=False): - """Rotates the target atoms around a selected bond by angle theta and updates - the atom coordinates in the parmed structure as well as the ncmc context object - - - Parameters - ---------- - context: simtk.openmm.Context object - Context containing the positions to be moved. - verbose : bool, default=False - Enable verbosity to print out detailed information of the rotation. - - Returns - ------- - context: simtk.openmm.Context object - The same input context, but whose positions were changed by this function. - - """ - - # determine the axis, theta, residue, and bond + atoms to be rotated - theta, target_atoms, res, bond = self.chooseBondandTheta() - print('Rotating bond: %s in resnum: %s by %.2f radians' % (bond, res, theta)) - - #retrieve the current positions - initial_positions = context.getState(getPositions=True).getPositions(asNumpy=True) - nc_positions = copy.deepcopy(initial_positions) - - model = copy.copy(self.structure) - - # set the parmed model to the same coordinates as the context - for idx, atom in enumerate(self.all_atoms): - if self.verbose: - print('Before:') - print(atom, idx) - print(nc_positions[atom], model.positions[atom]) - - model.atoms[atom].xx = nc_positions[atom][0].value_in_unit(unit.angstroms) - model.atoms[atom].xy = nc_positions[atom][1].value_in_unit(unit.angstroms) - model.atoms[atom].xz = nc_positions[atom][2].value_in_unit(unit.angstroms) - - if self.verbose: - print('After:') - print(nc_positions[atom], model.positions[atom]) - - positions = model.positions - - # find the rotation axis using the updated positions - axis1 = target_atoms[0] - axis2 = target_atoms[1] - rot_axis = (positions[axis1] - positions[axis2]) / positions.unit - - #calculate the rotation matrix - rot_matrix = self.rotation_matrix(rot_axis, theta) - - # apply the rotation matrix to the target atoms - for idx, atom in enumerate(target_atoms): - - my_position = positions[atom] - - if self.verbose: - print('The current position for %i is: %s' % (atom, my_position)) - - # find the reduced position (substract out axis) - red_position = (my_position - model.positions[axis2])._value - # find the new positions by multiplying by rot matrix - new_position = numpy.dot(rot_matrix, red_position) * positions.unit + positions[axis2] - - if self.verbose: print("The new position should be:", new_position) - - positions[atom] = new_position - # Update the parmed model with the new positions - model.atoms[atom].xx = new_position[0] / positions.unit - model.atoms[atom].xy = new_position[1] / positions.unit - model.atoms[atom].xz = new_position[2] / positions.unit - - #update the copied ncmc context array with the new positions - nc_positions[atom][0] = model.atoms[atom].xx * nc_positions.unit / 10 - nc_positions[atom][1] = model.atoms[atom].xy * nc_positions.unit / 10 - nc_positions[atom][2] = model.atoms[atom].xz * nc_positions.unit / 10 - - if self.verbose: - print('The updated position for this atom is:', model.positions[atom]) - - # update the actual ncmc context object with the new positions - context.setPositions(nc_positions) - - # update the class structure positions - self.structure.positions = model.positions - - if self.write_move: - filename = 'sc_move_%s_%s_%s.pdb' % (res, axis1, axis2) - mod_prot = model.save(filename, overwrite=True) - return context - - -class SmartDartMove(RandomLigandRotationMove): - """**WARNING:** This class has not been completely tested. Use at your own risk. - - Move object that allows center of mass smart darting moves to be performed on a ligand, - allowing translations of a ligand between pre-defined regions in space. The - `SmartDartMove.move()` method translates the ligand to the locations of the ligand - found in the coord_files. These locations are defined in terms of the basis_particles. - These locations are picked with a uniform probability. Based on Smart Darting Monte Carlo [smart-dart]_ - - Parameters - ---------- - structure: parmed.Structure - ParmEd Structure object of the relevant system to be moved. - basis_particles: list of 3 ints - Specifies the 3 indices of the protein whose coordinates will be used - to define a new set of basis vectors. - coord_files: list of str - List containing paths to coordinate files of the whole system for smart darting. - topology: str, optional, default=None - A path specifying a topology file matching the files in coord_files. Not - necessary if the coord_files already contain topologies (ex. PDBs). - dart_radius: simtk.unit float object compatible with simtk.unit.nanometers unit, - optional, default=0.2*simtk.unit.nanometers - The radius of the darting region around each dart. - self_dart: boolean, optional, default='False' - When performing the center of mass darting in `SmartDartMove.move()`,this - specifies whether or not to include the darting region where the center - of mass currently resides as an option to dart to. - resname : str, optional, default='LIG' - String specifying the residue name of the ligand. - - References - ---------- - .. [smart-dart] I. Andricioaei, J. E. Straub, and A. F. Voter, J. Chem. Phys. 114, 6994 (2001). - https://doi.org/10.1063/1.1358861 - - """ - - def __init__(self, - structure, - basis_particles, - coord_files, - topology=None, - dart_radius=0.2 * unit.nanometers, - self_dart=False, - resname='LIG'): - - super(SmartDartMove, self).__init__(structure, resname=resname) - - if len(coord_files) < 2: - raise ValueError('You should include at least two files in coord_files ' + - 'in order to benefit from smart darting') - self.dartboard = [] - self.n_dartboard = [] - self.particle_pairs = [] - self.particle_weights = [] - self.basis_particles = basis_particles - self.dart_radius = dart_radius - self.calculateProperties() - self.self_dart = self_dart - self.dartsFromParmEd(coord_files, topology) - - def dartsFromParmEd(self, coord_files, topology=None): - """ - Used to setup darts from a generic coordinate file, through MDtraj using the basis_particles to define - new basis vectors, which allows dart centers to remain consistant through a simulation. - This adds to the self.n_dartboard, which defines the centers used for smart darting. - - Parameters - ---------- - coord_files: list of str - List containing coordinate files of the whole system for smart darting. - topology: str, optional, default=None - A path specifying a topology file matching the files in coord_files. Not - necessary if the coord_files already contain topologies. - - """ - - n_dartboard = [] - dartboard = [] - #loop over specified files and generate parmed structures from each - #then the center of masses of the ligand in each structureare found - #finally those center of masses are added to the `self.dartboard`s to - #be used in the actual smart darting move to define darting regions - for coord_file in coord_files: - if topology == None: - #if coord_file contains topology info, just load coord file - temp_md = parmed.load_file(coord_file) - else: - #otherwise load file specified in topology - temp_md = parmed.load_file(topology, xyz=coord_file) - #get position values in terms of nanometers - context_pos = temp_md.positions.in_units_of(unit.nanometers) - lig_pos = numpy.asarray(context_pos._value)[self.atom_indices] * unit.nanometers - particle_pos = numpy.asarray(context_pos._value)[self.basis_particles] * unit.nanometers - #calculate center of mass of ligand - self.calculateProperties() - center_of_mass = self.getCenterOfMass(lig_pos, self.masses) - #get particle positions - new_coord = self._findNewCoord(particle_pos[0], particle_pos[1], particle_pos[2], center_of_mass) - #old_coord should be equal to com - old_coord = self._findOldCoord(particle_pos[0], particle_pos[1], particle_pos[2], new_coord) - numpy.testing.assert_almost_equal(old_coord._value, center_of_mass._value, decimal=1) - #add the center of mass in euclidian and new basis set (defined by the basis_particles) - n_dartboard.append(new_coord) - dartboard.append(old_coord) - self.n_dartboard = n_dartboard - self.dartboard = dartboard - - def move(self, context): - """ - Function for performing smart darting move with darts that - depend on particle positions in the system. - - Parameters - ---------- - context: simtk.openmm.Context object - Context containing the positions to be moved. - - Returns - ------- - context: simtk.openmm.Context object - The same input context, but whose positions were changed by this function. - - """ - - atom_indices = self.atom_indices - if len(self.n_dartboard) == 0: - raise ValueError('No darts are specified. Make sure you use ' + - 'SmartDartMove.dartsFromParmed() before using the move() function') - - #get state info from context - stateinfo = context.getState(True, True, False, True, True, False) - oldDartPos = stateinfo.getPositions(asNumpy=True) - #get the ligand positions - lig_pos = numpy.asarray(oldDartPos._value)[self.atom_indices] * unit.nanometers - #updates the darting regions based on the current position of the basis particles - self._findDart(context) - #find the ligand's current center of mass position - center = self.getCenterOfMass(lig_pos, self.masses) - #calculate the distance of the center of mass to the center of each darting region - selected_dart, changevec = self._calc_from_center(com=center) - #selected_dart is the selected darting region - - #if the center of mass was within one darting region, move the ligand to another region - if selected_dart != None: - newDartPos = numpy.copy(oldDartPos) - #find the center of mass in the new darting region - dart_switch = self._reDart(selected_dart, changevec) - #find the vector that will translate the ligand to the new darting region - vecMove = dart_switch - center - #apply that vector to the ligand to actually translate the coordinates - for atom in atom_indices: - newDartPos[atom] = newDartPos[atom] + vecMove._value - #set the positions after darting - context.setPositions(newDartPos) - - return context - - def _calc_from_center(self, com): - """ - Helper function that finds the distance of the current center of - mass to each dart center in self.dartboard - - Parameters - -------- - com: 1x3 numpy.array*simtk.unit.nanometers - Current center of mass coordinates of the ligand. - - Returns - ------- - selected_dart: simtk.unit.nanometers, or None - The distance of a dart to a center. Returns - None if the distance is greater than the darting region. - changevec: 1x3 numpy.array*simtk.unit.nanometers, - The vector from the ligand center of mass - to the center of a darting region. - - """ - - distList = [] - diffList = [] - indexList = [] - #Find the distances of the COM to each dart, appending - #the results to distList - for dart in self.dartboard: - diff = com - dart - dist = numpy.sqrt(numpy.sum((diff) * (diff))) * unit.nanometers - distList.append(dist) - diffList.append(diff) - selected_dart = [] - #Find the dart(s) less than self.dart_radius - for index, entry in enumerate(distList): - if entry <= self.dart_radius: - selected_dart.append(index) - diff = diffList[index] - indexList.append(index) - #Dart error checking - #to ensure reversibility the COM should only be - #within self.dart_radius of one dart - if len(selected_dart) == 1: - return selected_dart[0], diffList[indexList[0]] - elif len(selected_dart) == 0: - return None, diff - elif len(selected_dart) >= 2: - #COM should never be within two different darts - raise ValueError(' The spheres defining two darting regions have overlapped, ' + - 'which results in potential problems with detailed balance. ' + - 'We are terminating the simulation. Please check the size and ' + - 'identity of your darting regions defined by dart_radius.') - #TODO can treat cases using appropriate probablility correction - #see https://doi.org/10.1016/j.patcog.2011.02.006 - - def _findDart(self, context): - """ - Helper function to dynamically update dart positions based on the current positions - of the basis particles. - - Parameters - --------- - context: Context object from simtk.openmm - Context from the ncmc simulation. - - Returns - ------- - dart_list list of 1x3 numpy.arrays in units.nm - new dart positions calculated from the particle_pairs - and particle_weights. - - """ - - basis_particles = self.basis_particles - #make sure there's an equal number of particle pair lists - #and particle weight lists - dart_list = [] - state_info = context.getState(True, True, False, True, True, False) - temp_pos = state_info.getPositions(asNumpy=True) - part1 = temp_pos[basis_particles[0]] - part2 = temp_pos[basis_particles[1]] - part3 = temp_pos[basis_particles[2]] - for dart in self.n_dartboard: - old_center = self._findOldCoord(part1, part2, part3, dart) - dart_list.append(old_center) - self.dartboard = dart_list[:] - return dart_list - - def _reDart(self, selected_dart, changevec): - """ - Helper function to choose a random dart and determine the vector - that would translate the COM to that dart center + changevec. - This is called reDart in the sense that it helps to switch - the ligand to another darting region. - - Parameters - --------- - selected_dart : - changevec: 1x3 numpy.array * simtk.unit.nanometers - The vector difference of the ligand center of mass - to the closest dart center (if within the dart region). - - - Returns - ------- - dart_switch: 1x3 numpy.array * simtk.unit.nanometers - - """ - dartindex = list(range(len(self.dartboard))) - if self.self_dart == False: - dartindex.pop(selected_dart) - dartindex = numpy.random.choice(dartindex) - dvector = self.dartboard[dartindex] - dart_switch = dvector + changevec - return dart_switch - - def _changeBasis(self, a, b): - """ - Changes positions of a particle (b) in the regular basis set to - another basis set (a). Used to recalculate the center of mass - in terms of the local coordinates defined by self.basis_particles. - Used to change between the basis sets defined from the basis_particles - and the normal euclidian basis set. - - Parameters - ---------- - a: 3x3 numpy.array - Defines vectors that will create the new basis. - b: 1x3 numpy.array - Defines position of particle to be transformed into - new basis set. - - Returns - ------- - changed_coord: 1x3 numpy.array - Coordinates of b in new basis. - - """ - - ainv = numpy.linalg.inv(a.T) - changed_coord = numpy.dot(ainv, b.T) * unit.nanometers - return changed_coord - - def _undoBasis(self, a, b): - """ - Transforms positions in a transformed basis (b) to the regular - basis set. Used to transform the dart positions in the local - coordinate basis set to the cartesian basis set. - - Parameters - ---------- - a: 3x3 numpy.array - Defines vectors that defined the new basis. - b: 1x3 numpy.array - Defines position of particle to be transformed into - regular basis set. - - Returns - ------- - changed_coord: 1x3 numpy.array - Coordinates of b in new basis. - """ - - a = a.T - changed_coord = numpy.dot(a, b.T) * unit.nanometers - return changed_coord - - def _normalize(self, vector): - """Normalize a given vector - - Parameters - ---------- - vector: 1xn numpy.array - Vector to be normalized. - - Returns - ------- - unit_vec: 1xn numpy.array - Normalized vector. - - """ - - magnitude = numpy.sqrt(numpy.sum(vector * vector)) - unit_vec = vector / magnitude - return unit_vec - - def _localCoord(self, particle1, particle2, particle3): - """ - Defines a new coordinate system using 3 particles - returning the new basis set vectors - - Parameters - ---------- - particle1, particle2, particle3: 1x3 numpy.array - numpy.array corresponding to a given particle's positions - - Returns - ------- - vec1, vec2, vec3: 1x3 numpy.array - Basis vectors of the coordinate system defined - by particles1-3. - - """ - - part2 = particle2 - particle1 - part3 = particle3 - particle1 - vec1 = part2 - vec2 = part3 - vec3 = numpy.cross(vec1, vec2) * unit.nanometers - return vec1, vec2, vec3 - - def _findNewCoord(self, particle1, particle2, particle3, center): - """ - Finds the coordinates of a given center in the standard basis - in terms of a new basis defined by particles1-3 - - Parameters - ---------- - particle1, particle2, particle3: 1x3 numpy.array - numpy.array corresponding to a given particle's positions - center: 1x3 numpy.array * simtk.unit compatible with simtk.unit.nanometers - Coordinate of the center of mass in the standard basis set. - - Returns - ------- - new_coord : numpy.array - Updated coordinates in terms of new basis. - """ - - #calculate new basis set - vec1, vec2, vec3 = self._localCoord(particle1, particle2, particle3) - basis_set = numpy.zeros((3, 3)) * unit.nanometers - basis_set[0] = vec1 - basis_set[1] = vec2 - basis_set[2] = vec3 - #since the origin is centered at particle1 by convention - #subtract to account for this - recenter = center - particle1 - #find coordinate in new coordinate system - new_coord = self._changeBasis(basis_set, recenter) - return new_coord - - def _findOldCoord(self, particle1, particle2, particle3, center): - """ - Finds the coordinates of a given center (defined by a different basis - given by particles1-3) back in the euclidian coordinates - - Parameters - ---------- - particle1, particle2, particle3: 1x3 numpy.array - numpy.array corresponding to a given particle's positions - center: 1x3 numpy.array * simtk.unit compatible with simtk.unit.nanometers - Coordinate of the center of mass in the non-standard basis set. - - Returns - ------- - adjusted_center : numpy.array - Corrected coordinates of new center in euclidian coordinates. - - """ - - vec1, vec2, vec3 = self._localCoord(particle1, particle2, particle3) - basis_set = numpy.zeros((3, 3)) * unit.nanometers - basis_set[0] = vec1 - basis_set[1] = vec2 - basis_set[2] = vec3 - #since the origin is centered at particle1 by convention - #subtract to account for this - old_coord = self._undoBasis(basis_set, center) - adjusted_center = old_coord + particle1 - return adjusted_center - - -class CombinationMove(Move): - """**WARNING:** This class has not been completely tested. Use at your own risk. - - Move object that allows Move object moves to be performed according to - the order in move_list. To ensure detailed balance, the moves have an equal - chance to be performed in listed or reverse order. - - Parameters - ---------- - moves : list of blues.move.Move - - """ - - def __init__(self, moves): - self.moves = moves - - def move(self, context): - """Performs the move() functions of the Moves in move_list on - a context. - - Parameters - ---------- - context: simtk.openmm.Context object - Context containing the positions to be moved. - - Returns - ------- - context: simtk.openmm.Context object - The same input context, but whose positions were changed by this function. - - """ - rand = numpy.random.random() - #to maintain detailed balance this executes both - #the forward and reverse order moves with equal probability - if rand > 0.5: - for single_move in self.move_list: - single_move.move(context) - else: - for single_move in reverse(self.move_list): - single_move.move(context) diff --git a/blues/ncmc.py b/blues/ncmc.py new file mode 100644 index 00000000..81cee5a6 --- /dev/null +++ b/blues/ncmc.py @@ -0,0 +1,824 @@ +"""Provides moves and classes for running the BLUES simulation.""" + +import abc +import copy +import logging + +import mdtraj +import numpy +from openmmtools import alchemy, cache +from openmmtools.mcmc import LangevinDynamicsMove, MCMCMove +from openmmtools.states import CompoundThermodynamicState, ThermodynamicState +from simtk import openmm, unit + +from blues import utils +from blues.integrators import AlchemicalExternalLangevinIntegrator +from blues.systemfactory import generateAlchSystem +import traceback + +logger = logging.getLogger(__name__) + +class ReportLangevinDynamicsMove(object): + """Langevin dynamics segment as a (pseudo) Monte Carlo move. + + This move class allows the attachment of a reporter for storing the data from running this segment of dynamics. This move assigns a velocity from the Maxwell-Boltzmann distribution and executes a number of Maxwell-Boltzmann steps to propagate dynamics. This is not a *true* Monte Carlo move, in that the generation of the correct distribution is only exact in the limit of infinitely small timestep; in other words, the discretization error is assumed to be negligible. Use HybridMonteCarloMove instead to ensure the exact distribution is generated. + + .. warning:: + No Metropolization is used to ensure the correct phase space + distribution is sampled. This means that timestep-dependent errors + will remain uncorrected, and are amplified with larger timesteps. + Use this move at your own risk! + + Parameters + ---------- + n_steps : int, optional + The number of integration timesteps to take each time the + move is applied (default is 1000). + timestep : simtk.unit.Quantity, optional + The timestep to use for Langevin integration + (time units, default is 1*simtk.unit.femtosecond). + collision_rate : simtk.unit.Quantity, optional + The collision rate with fictitious bath particles + (1/time units, default is 10/simtk.unit.picoseconds). + reassign_velocities : bool, optional + If True, the velocities will be reassigned from the Maxwell-Boltzmann + distribution at the beginning of the move (default is False). + context_cache : openmmtools.cache.ContextCache, optional + The ContextCache to use for Context creation. If None, the global cache + openmmtools.cache.global_context_cache is used (default is None). + reporters : list + A list of the storage classes inteded for reporting the simulation data. + This can be either blues.storage.(NetCDF4Storage/BLUESStateDataStorage). + + Attributes + ---------- + n_steps : int + The number of integration timesteps to take each time the move + is applied. + timestep : simtk.unit.Quantity + The timestep to use for Langevin integration (time units). + collision_rate : simtk.unit.Quantity + The collision rate with fictitious bath particles (1/time units). + reassign_velocities : bool + If True, the velocities will be reassigned from the Maxwell-Boltzmann + distribution at the beginning of the move. + context_cache : openmmtools.cache.ContextCache + The ContextCache to use for Context creation. If None, the global + cache openmmtools.cache.global_context_cache is used. + reporters : list + A list of the storage classes inteded for reporting the simulation data. + This can be either blues.storage.(NetCDF4Storage/BLUESStateDataStorage). + + Examples + -------- + First we need to create the thermodynamic state and the sampler + state to propagate. Here we create an alanine dipeptide system + in vacuum. + + >>> from simtk import unit + >>> from openmmtools import testsystems + >>> from openmmtools.states import SamplerState, ThermodynamicState + >>> test = testsystems.AlanineDipeptideVacuum() + >>> sampler_state = SamplerState(positions=test.positions) + >>> thermodynamic_state = ThermodynamicState(system=test.system, temperature=298*unit.kelvin) + + Create reporters for storing our simulation data. + + >>> from blues.storage import NetCDF4Storage, BLUESStateDataStorage + nc_storage = NetCDF4Storage('test-md.nc', + reportInterval=5, + crds=True, vels=True, frcs=True) + state_storage = BLUESStateDataStorage('test.log', + reportInterval=5, + step=True, time=True, + potentialEnergy=True, + kineticEnergy=True, + totalEnergy=True, + temperature=True, + volume=True, + density=True, + progress=True, + remainingTime=True, + speed=True, + elapsedTime=True, + systemMass=True, + totalSteps=10) + + Create a Langevin move with default parameters + + >>> move = ReportLangevinDynamicsMove() + + or create a Langevin move with specified parameters. + + >>> move = ReportLangevinDynamicsMove(timestep=0.5*unit.femtoseconds, + collision_rate=20.0/unit.picoseconds, n_steps=10, + reporters=[nc_storage, state_storage]) + + Perform one update of the sampler state. The sampler state is updated + with the new state. + + >>> move.apply(thermodynamic_state, sampler_state) + >>> np.allclose(sampler_state.positions, test.positions) + False + + The same move can be applied to a different state, here an ideal gas. + + >>> test = testsystems.IdealGas() + >>> sampler_state = SamplerState(positions=test.positions) + >>> thermodynamic_state = ThermodynamicState(system=test.system, + ... temperature=298*unit.kelvin) + >>> move.apply(thermodynamic_state, sampler_state) + >>> np.allclose(sampler_state.positions, test.positions) + False + + """ + + def __init__(self, + n_steps=1000, + timestep=2.0 * unit.femtosecond, + collision_rate=1.0 / unit.picoseconds, + reassign_velocities=True, + context_cache=None, + reporters=[]): + self.n_steps = n_steps + self.timestep = timestep + self.collision_rate = collision_rate + self.reassign_velocities = reassign_velocities + self.context_cache = context_cache + self.reporters = list(reporters) + self.currentStep = 0 + + def _get_integrator(self, thermodynamic_state): + """ + Generates a LangevinIntegrator for the Simulations. + Parameters + ---------- + + Returns + ------- + integrator : openmm.LangevinIntegrator + The LangevinIntegrator object intended for the System. + """ + integrator = openmm.LangevinIntegrator(thermodynamic_state.temperature, + self.collision_rate, + self.timestep) + return integrator + + def _before_integration(self, context, thermodynamic_state): + """Execute code after Context creation and before integration.""" + context_state = context.getState( + getPositions=True, getVelocities=True, getEnergy=True, enforcePeriodicBox=thermodynamic_state.is_periodic) + self.initial_positions = context_state.getPositions(asNumpy=True) + self.initial_energy = thermodynamic_state.reduced_potential(context) + self._usesPBC = thermodynamic_state.is_periodic + + def _after_integration(self, context, thermodynamic_state): + """Execute code after integration. + + After this point there are no guarantees that the Context will still + exist, together with its bound integrator and system. + """ + context_state = context.getState( + getPositions=True, getVelocities=True, getEnergy=True, enforcePeriodicBox=thermodynamic_state.is_periodic) + + self.final_positions = context_state.getPositions(asNumpy=True) + self.final_energy = thermodynamic_state.reduced_potential(context) + + def apply(self, thermodynamic_state, sampler_state): + """Propagate the state through the integrator. + + This updates the SamplerState after the integration. + + Parameters + ---------- + thermodynamic_state : openmmtools.states.ThermodynamicState + The thermodynamic state to use to propagate dynamics. + sampler_state : openmmtools.states.SamplerState + The sampler state to apply the move to. This is modified. + """ + # Check if we have to use the global cache. + if self.context_cache is None: + context_cache = cache.global_context_cache + else: + context_cache = self.context_cache + + # Create integrator. + integrator = self._get_integrator(thermodynamic_state) + + # Create context. + context, integrator = context_cache.get_context(thermodynamic_state, integrator) + thermodynamic_state.apply_to_context(context) + + # If we reassign velocities, we can ignore the ones in sampler_state. + sampler_state.apply_to_context(context, ignore_velocities=self.reassign_velocities) + if self.reassign_velocities: + context.setVelocitiesToTemperature(thermodynamic_state.temperature) + + # Subclasses may implement _before_integration(). + self._before_integration(context, thermodynamic_state) + + try: + nextReport = [None] * len(self.reporters) + endStep = self.currentStep + self.n_steps + while self.currentStep < endStep: + nextSteps = endStep - self.currentStep + anyReport = False + for i, reporter in enumerate(self.reporters): + nextReport[i] = reporter.describeNextReport(self) + if nextReport[i][0] > 0 and nextReport[i][0] <= nextSteps: + nextSteps = nextReport[i][0] + anyReport = True + + stepsToGo = nextSteps + while stepsToGo > 10: + integrator.step(10) + stepsToGo -= 10 + integrator.step(stepsToGo) + self.currentStep += nextSteps + + if anyReport: + reports = [] + context_state = context.getState( + getPositions=True, + getVelocities=True, + getEnergy=True, + enforcePeriodicBox=thermodynamic_state.is_periodic) + + context_state.currentStep = self.currentStep + context_state.system = thermodynamic_state.get_system() + + for reporter, report in zip(self.reporters, nextReport): + reports.append((reporter, report)) + for reporter, next in reports: + reporter.report(context_state, integrator) + + except Exception as e: + logger.error(e) + else: + context_state = context.getState( + getPositions=True, + getVelocities=True, + getEnergy=True, + enforcePeriodicBox=thermodynamic_state.is_periodic) + + # Subclasses can read here info from the context to update internal statistics. + self._after_integration(context, thermodynamic_state) + + # Updated sampler state. + sampler_state.update_from_context( + context_state, ignore_positions=False, ignore_velocities=False, ignore_collective_variables=True) + +class NCMCMove(MCMCMove): + """A general NCMC move that applies an alchemical integrator. + + This class is intended to be inherited by NCMCMoves that need to alchemically modify and perturb part of the system. The child class has to implement the _propose_positions method. Reporters can be attached to report + data from the NCMC part of the simulation. + + You can decide to override _before_integration() and _after_integration() + to execute some code at specific points of the workflow, for example to + read data from the Context before the it is destroyed. + + Parameters + ---------- + n_steps : int, optional + The number of integration timesteps to take each time the + move is applied (default is 1000). + timestep : simtk.unit.Quantity, optional + The timestep to use for Langevin integration + (time units, default is 1*simtk.unit.femtosecond). + atom_subset : slice or list of int, optional + If specified, the move is applied only to those atoms specified by these + indices. If None, the move is applied to all atoms (default is None). + context_cache : openmmtools.cache.ContextCache, optional + The ContextCache to use for Context creation. If None, the global cache + openmmtools.cache.global_context_cache is used (default is None). + reporters : list + A list of the storage classes inteded for reporting the simulation data. + This can be either blues.storage.(NetCDF4Storage/BLUESStateDataStorage). + + Attributes + ---------- + n_steps : int + The number of integration timesteps to take each time the move + is applied. + timestep : simtk.unit.Quantity + The timestep to use for Langevin integration (time units). + atom_subset : slice or list of int, optional + If specified, the move is applied only to those atoms specified by these + indices. If None, the move is applied to all atoms (default is None). + context_cache : openmmtools.cache.ContextCache + The ContextCache to use for Context creation. If None, the global + cache openmmtools.cache.global_context_cache is used. + reporters : list + A list of the storage classes inteded for reporting the simulation data. + This can be either blues.storage.(NetCDF4Storage/BLUESStateDataStorage). + """ + + def __init__(self, + n_steps=1000, + timestep=2.0 * unit.femtosecond, + atom_subset=None, + context_cache=None, + nprop=1, + propLambda=0.3, + reporters=[]): + self.timestep = timestep + self.n_steps = n_steps + self.nprop = nprop + self.propLambda = propLambda + self.atom_subset = atom_subset + self.context_cache = context_cache + self.reporters = list(reporters) + + self.n_accepted = 0 + self.n_proposed = 0 + self.logp_accept = 0 + self.initial_energy = 0 + self.initial_positions = None + self.final_energy = 0 + self.final_positions = None + self.proposed_positions = None + self.currentStep = 0 + + @property + def statistics(self): + """Statistics as a dictionary.""" + return dict( + n_accepted=self.n_accepted, + n_proposed=self.n_proposed, + initial_energy=self.initial_energy, + initial_positions=self.initial_positions, + final_energy=self.final_energy, + proposed_positions=self.proposed_positions, + final_positions=self.final_positions, + logp_accept=self.logp_accept) + + @statistics.setter + def statistics(self, value): + self.n_accepted = value['n_accepted'] + self.n_proposed = value['n_proposed'] + self.initial_energy = value['initial_energy'] + self.initial_positions = value['initial_positions'] + self.final_energy = value['final_energy'] + self.proposed_positions = value['proposed_positions'] + self.final_positions = value['final_positions'] + self.logp_accept = value['logp_accept'] + + def _before_integration(self, context, thermodynamic_state): + """Execute code after Context creation and before integration.""" + context_state = context.getState( + getPositions=True, getVelocities=True, getEnergy=True, enforcePeriodicBox=thermodynamic_state.is_periodic) + + self.initial_positions = context_state.getPositions(asNumpy=True) + self.initial_box_vectors = context_state.getPeriodicBoxVectors() + self.initial_energy = thermodynamic_state.reduced_potential(context) + + def _after_integration(self, context, thermodynamic_state): + """Execute code after integration. + + After this point there are no guarantees that the Context will still + exist, together with its bound integrator and system. + """ + context_state = context.getState( + getPositions=True, getVelocities=True, getEnergy=True, enforcePeriodicBox=thermodynamic_state.is_periodic) + + self.final_positions = context_state.getPositions(asNumpy=True) + self.final_box_vectors = context_state.getPeriodicBoxVectors() + self.final_energy = thermodynamic_state.reduced_potential(context) + self.logp_accept = context._integrator.getLogAcceptanceProbability(context) + + def _get_integrator(self, thermodynamic_state): + return AlchemicalExternalLangevinIntegrator( + alchemical_functions={ + 'lambda_sterics': + 'min(1, (1/0.3)*abs(lambda-0.5))', + 'lambda_electrostatics': + 'step(0.2-lambda) - 1/0.2*lambda*step(0.2-lambda) + 1/0.2*(lambda-0.8)*step(lambda-0.8)' + }, + splitting="H V R O R V H", + temperature=thermodynamic_state.temperature, + nsteps_neq=self.n_steps, + timestep=self.timestep, + nprop=self.nprop, + propLambda=self.propLambda) + + def apply(self, thermodynamic_state, sampler_state): + """Apply a move to the sampler state. + + Parameters + ---------- + thermodynamic_state : openmmtools.states.ThermodynamicState + The thermodynamic state to use to apply the move. + sampler_state : openmmtools.states.SamplerState + The initial sampler state to apply the move to. This is modified. + + """ + # Check if we have to use the global cache. + if self.context_cache is None: + context_cache = cache.global_context_cache + else: + context_cache = self.context_cache + + # Create integrator + integrator = self._get_integrator(thermodynamic_state) + + # Create context + context, integrator = context_cache.get_context(thermodynamic_state, integrator) + + # Compute initial energy. We don't need to set velocities to compute the potential. + # TODO assume sampler_state.potential_energy is the correct potential if not None? + sampler_state.apply_to_context(context, ignore_velocities=False) + + self._before_integration(context, thermodynamic_state) + + try: + nextReport = [None] * len(self.reporters) + endStep = self.currentStep + self.n_steps + while self.currentStep < endStep: + nextSteps = endStep - self.currentStep + anyReport = False + for i, reporter in enumerate(self.reporters): + nextReport[i] = reporter.describeNextReport(self) + if nextReport[i][0] > 0 and nextReport[i][0] <= nextSteps: + nextSteps = nextReport[i][0] + anyReport = True + + alchLambda = integrator.getGlobalVariableByName('lambda') + if alchLambda == 0.5: + positions = context.getState(getPositions=True).getPositions(asNumpy=True) + proposed_positions = self._propose_positions(positions[self.atom_subset]) + for index, atomidx in enumerate(self.atom_subset): + positions[atomidx] = proposed_positions[index] + context.setPositions(positions) + + stepsToGo = nextSteps + while stepsToGo > 10: + integrator.step(10) + stepsToGo -= 10 + integrator.step(stepsToGo) + self.currentStep += nextSteps + + if anyReport: + context_state = context.getState( + getPositions=True, + getVelocities=True, + getEnergy=True, + enforcePeriodicBox=thermodynamic_state.is_periodic) + + context_state.currentStep = self.currentStep + context_state.system = thermodynamic_state.get_system() + + reports = [] + for reporter, report in zip(self.reporters, nextReport): + reports.append((reporter, report)) + for reporter, next in reports: + reporter.report(context_state, integrator) + + except Exception as e: + logger.error(e) + # Catches particle positions becoming nan during integration. + else: + context_state = context.getState( + getPositions=True, + getVelocities=True, + getEnergy=True, + enforcePeriodicBox=thermodynamic_state.is_periodic) + + self._after_integration(context, thermodynamic_state) + # Update everything but the collective variables from the State object + sampler_state.update_from_context( + context_state, ignore_positions=False, ignore_velocities=False, ignore_collective_variables=True) + + @abc.abstractmethod + def _propose_positions(self, positions): + """Return new proposed positions. + + These method must be implemented in subclasses. + + Parameters + ---------- + positions : nx3 numpy.ndarray + The original positions of the subset of atoms that these move + applied to. + + Returns + ------- + proposed_positions : nx3 numpy.ndarray + The new proposed positions. + + """ + pass + + +class RandomLigandRotationMove(NCMCMove): + """An NCMC move which proposes random rotations. + + This class will propose a random rotation (as a rigid body) using the center of mass of the selected atoms. This class does not metropolize the proposed moves. Reporters can be attached to record the ncmc simulation data, mostly useful for debugging by storing coordinates of the proposed moves or monitoring the ncmc simulation progression by attaching a state reporter. + + Parameters + ---------- + n_steps : int, optional + The number of integration timesteps to take each time the + move is applied (default is 1000). + timestep : simtk.unit.Quantity, optional + The timestep to use for Langevin integration + (time units, default is 1*simtk.unit.femtosecond). + atom_subset : slice or list of int, optional + If specified, the move is applied only to those atoms specified by these + indices. If None, the move is applied to all atoms (default is None). + context_cache : openmmtools.cache.ContextCache, optional + The ContextCache to use for Context creation. If None, the global cache + openmmtools.cache.global_context_cache is used (default is None). + reporters : list + A list of the storage classes inteded for reporting the simulation data. + This can be either blues.storage.(NetCDF4Storage/BLUESStateDataStorage). + + Attributes + ---------- + n_steps : int + The number of integration timesteps to take each time the move + is applied. + timestep : simtk.unit.Quantity + The timestep to use for Langevin integration (time units). + atom_subset : slice or list of int, optional + If specified, the move is applied only to those atoms specified by these + indices. If None, the move is applied to all atoms (default is None). + context_cache : openmmtools.cache.ContextCache + The ContextCache to use for Context creation. If None, the global + cache openmmtools.cache.global_context_cache is used. + reporters : list + A list of the storage classes inteded for reporting the simulation data. + This can be either blues.storage.(NetCDF4Storage/BLUESStateDataStorage). + + Examples + -------- + First we need to create the thermodynamic state, alchemical thermodynamic state, and the sampler state to propagate. Here we create a toy system of a charged ethylene molecule in between two charged particles. + + >>> from simtk import unit + >>> from openmmtools import testsystems, alchemy + >>> from openmmtools.states import SamplerState, ThermodynamicState + >>> from blues.systemfactories import generateAlchSystem + >>> from blues import utils + + >>> structure_pdb = utils.get_data_filename('blues', 'tests/data/ethylene_structure.pdb') + >>> structure = parmed.load_file(structure_pdb) + >>> system_xml = utils.get_data_filename('blues', 'tests/data/ethylene_system.xml') + with open(system_xml, 'r') as infile: + xml = infile.read() + system = openmm.XmlSerializer.deserialize(xml) + >>> thermodynamic_state = ThermodynamicState(system=system, temperature=200*unit.kelvin) + >>> sampler_state = SamplerState(positions=structure.positions.in_units_of(unit.nanometers)) + >>> alchemical_atoms = [2, 3, 4, 5, 6, 7] + >>> alch_system = generateAlchSystem(thermodynamic_state.get_system(), alchemical_atoms) + >>> alch_state = alchemy.AlchemicalState.from_system(alch_system) + >>> alch_thermodynamic_state = ThermodynamicState( + alch_system, thermodynamic_state.temperature) + >>> alch_thermodynamic_state = CompoundThermodynamicState( + alch_thermodynamic_state, composable_states=[alch_state]) + + Create reporters for storing our ncmc simulation data. + + >>> from blues.storage import NetCDF4Storage, BLUESStateDataStorage + nc_storage = NetCDF4Storage('test-ncmc.nc', + reportInterval=5, + crds=True, vels=True, frcs=True, + protocolWork=True, alchemicalLambda=True) + state_storage = BLUESStateDataStorage('test-ncmc.log', + reportInterval=5, + step=True, time=True, + potentialEnergy=True, + kineticEnergy=True, + totalEnergy=True, + temperature=True, + volume=True, + density=True, + progress=True, + remainingTime=True, + speed=True, + elapsedTime=True, + systemMass=True, + totalSteps=10, + protocolWork=True, + alchemicalLambda=True) + + Create a RandomLigandRotationMove move + + >>> rot_move = RandomLigandRotationMove(n_steps=5, + timestep=1*unit.femtoseconds, + atom_subset=alchemical_atoms, + reporters=[nc_storage, state_storage]) + + Perform one update of the sampler state. The sampler state is updated + with the new state. + + >>> move.apply(thermodynamic_state, sampler_state) + >>> np.allclose(sampler_state.positions, structure.positions) + False + """ + + def _before_integration(self, context, thermodynamic_state): + super(RandomLigandRotationMove, self)._before_integration(context, thermodynamic_state) + masses, totalmass = utils.getMasses(self.atom_subset, thermodynamic_state.topology) + self.masses = masses + + def _propose_positions(self, positions): + """Return new proposed positions. + + These method must be implemented in subclasses. + + Parameters + ---------- + positions : nx3 numpy.ndarray + The original positions of the subset of atoms that these move + applied to. + + Returns + ------- + proposed_positions : nx3 numpy.ndarray + The new proposed positions. + """ + # print('Proposing positions...') + # Calculate the center of mass + + center_of_mass = utils.getCenterOfMass(positions, self.masses) + reduced_pos = positions - center_of_mass + # Define random rotational move on the ligand + rand_quat = mdtraj.utils.uniform_quaternion(size=None) + rand_rotation_matrix = mdtraj.utils.rotation_matrix_from_quaternion(rand_quat) + # multiply lig coordinates by rot matrix and add back COM translation from origin + proposed_positions = numpy.dot(reduced_pos, rand_rotation_matrix) * positions.unit + center_of_mass + + return proposed_positions + + +# ============================================================================= +# NCMC+MD (BLUES) SAMPLER +# ============================================================================= +class BLUESSampler(object): + """BLUESSampler runs the NCMC+MD hybrid simulation. + + This class ties together the two moves classes to execute the NCMC+MD hybrid simulation. One move class is intended to carry out traditional MD and the other is intended carry out the NCMC move proposals which performs the alchemical transformation to given atom subset. This class handles proper metropolization of the NCMC move proposals, while correcting for the switch in integrators. + """ + + def __init__(self, + thermodynamic_state=None, + alch_thermodynamic_state=None, + sampler_state=None, + dynamics_move=None, + ncmc_move=None, + topology=None): + """Create an NCMC sampler. + + Parameters + ---------- + thermodynamic_state : ThermodynamicState + The thermodynamic state to simulate + alch_thermodynamic_state : CompoundThermodynamicState, optional + The alchemical thermodynamic state to simulate. If None, one is generated from the thermodynamic state using the default alchemical parameters. + sampler_state : SamplerState + The initial sampler state to simulate from. + dynamics_move : ReportLangevinDynamicsMove + The move class which propagates traditional dynamics. + ncmc_move : NCMCMove + The NCMCMove class which proposes perturbations to the selected atoms. + topology : openmm.Topology + A Topology of the system to be simulated. + """ + if thermodynamic_state is None: + raise Exception("'thermodynamic_state' must be specified") + if sampler_state is None: + raise Exception("'sampler_state' must be specified") + + self.sampler_state = sampler_state + self.ncmc_move = ncmc_move + self.dynamics_move = dynamics_move + # Make a deep copy of the state so that initial state is unchanged. + self.thermodynamic_state = copy.deepcopy(thermodynamic_state) + # Generate an alchemical thermodynamic state if none is provided + if alch_thermodynamic_state: + self.alch_thermodynamic_state = alch_thermodynamic_state + else: + self.alch_thermodynamic_state = self._get_alchemical_state(thermodynamic_state) + + # NML: Attach topology to thermodynamic_states + self.thermodynamic_state.topology = topology + self.alch_thermodynamic_state.topology = topology + + # Initialize + self.accept = False + self.iteration = 0 + self.n_accepted = 0 + + def _get_alchemical_state(self, thermodynamic_state): + alch_system = generateAlchSystem(thermodynamic_state.get_system(), self.ncmc_move.atom_subset) + alch_state = alchemy.AlchemicalState.from_system(alch_system) + alch_thermodynamic_state = ThermodynamicState(alch_system, thermodynamic_state.temperature) + alch_thermodynamic_state = CompoundThermodynamicState(alch_thermodynamic_state, composable_states=[alch_state]) + + return alch_thermodynamic_state + + def _printSimulationTiming(self, n_iterations): + """Prints the simulation timing and related information.""" + self.ncmc_move.totalSteps = int(self.ncmc_move.n_steps * n_iterations) + self.dynamics_move.totalSteps = int(self.dynamics_move.n_steps * n_iterations) + md_timestep = self.dynamics_move.timestep.value_in_unit(unit.picoseconds) + md_steps = self.dynamics_move.n_steps + ncmc_timestep = self.ncmc_move.timestep.value_in_unit(unit.picoseconds) + ncmc_steps = self.ncmc_move.n_steps + nprop = self.ncmc_move.nprop + propLambda = self.ncmc_move.propLambda + + force_eval = n_iterations * (ncmc_steps + md_steps) + time_ncmc_iter = ncmc_steps * ncmc_timestep + time_ncmc_total = time_ncmc_iter * n_iterations + time_md_iter = md_steps * md_timestep + time_md_total = time_md_iter * n_iterations + time_iter = time_ncmc_iter + time_md_iter + time_total = time_iter * n_iterations + + msg = 'Total BLUES Simulation Time = %s ps (%s ps/Iter)\n' % (time_total, time_iter) + msg += 'Total Force Evaluations = %s \n' % force_eval + msg += 'Total NCMC time = %s ps (%s ps/iter)\n' % (time_ncmc_total, time_ncmc_iter) + msg += 'Total MD time = %s ps (%s ps/iter)\n' % (time_md_total, time_md_iter) + + # Calculate number of lambda steps inside/outside region with extra propgation steps + #steps_in_prop = int(nprop * (2 * math.floor(propLambda * nstepsNC))) + #steps_out_prop = int((2 * math.ceil((0.5 - propLambda) * nstepsNC))) + + #prop_lambda_window = self._ncmc_sim.context._integrator._propLambda + # prop_lambda_window = round(prop_lambda_window[1] - prop_lambda_window[0], 4) + # if propSteps != nstepsNC: + # msg += '\t%s lambda switching steps within %s total propagation steps.\n' % (nstepsNC, propSteps) + # msg += '\tExtra propgation steps between lambda [%s, %s]\n' % (prop_lambda_window[0], + # prop_lambda_window[1]) + # msg += '\tLambda: 0.0 -> %s = %s propagation steps\n' % (prop_lambda_window[0], int(steps_out_prop / 2)) + # msg += '\tLambda: %s -> %s = %s propagation steps\n' % (prop_lambda_window[0], prop_lambda_window[1], + # steps_in_prop) + # msg += '\tLambda: %s -> 1.0 = %s propagation steps\n' % (prop_lambda_window[1], int(steps_out_prop / 2)) + logger.info(msg) + + def _computeAlchemicalCorrection(self): + # Create MD context with the final positions from NCMC simulation + integrator = self.dynamics_move._get_integrator(self.thermodynamic_state) + context, integrator = cache.global_context_cache.get_context(self.thermodynamic_state, integrator) + self.thermodynamic_state.apply_to_context(context) + self.sampler_state.apply_to_context(context, ignore_velocities=True) + alch_energy = self.thermodynamic_state.reduced_potential(context) + correction_factor = (self.ncmc_move.initial_energy - self.dynamics_move.final_energy + alch_energy - self.ncmc_move.final_energy) + return correction_factor + + def _acceptRejectMove(self): + logp_accept = self.ncmc_move.logp_accept + randnum = numpy.log(numpy.random.random()) + + correction_factor = self._computeAlchemicalCorrection() + logger.debug("logP {} + corr {}".format(logp_accept, correction_factor)) + logp_accept = logp_accept + correction_factor + + if (not numpy.isnan(logp_accept) and logp_accept > randnum): + logger.debug('NCMC MOVE ACCEPTED: logP {}'.format(logp_accept)) + self.n_accepted += 1 + else: + logger.debug('NCMC MOVE REJECTED: logP {}'.format(logp_accept)) + # Restore original positions & box vectors + self.sampler_state.positions = self.ncmc_move.initial_positions + self.sampler_state.box_vectors = self.ncmc_move.initial_box_vectors + + def equil(self, n_iterations=1): + """Equilibrate the system for N iterations.""" + # Set initial conditions by running 1 iteration of MD first + for iteration in range(n_iterations): + self.dynamics_move.apply(self.thermodynamic_state, self.sampler_state) + self.dynamics_move.currentStep = 0 + self.iteration += 1 + + def run(self, n_iterations=1): + """Run the sampler for the specified number of iterations. + + descriptive summary here + + Parameters + ---------- + niterations : int, optional, default=1 + Number of iterations to run the sampler for. + """ + context, integrator = cache.global_context_cache.get_context(self.thermodynamic_state) + utils.print_host_info(context) + self._printSimulationTiming(n_iterations) + if self.iteration == 0: + # Set initial conditions by running 1 iteration of MD first + self.equil(1) + + self.iteration = 0 + for iteration in range(n_iterations): + + self.ncmc_move.apply(self.alch_thermodynamic_state, self.sampler_state) + + self._acceptRejectMove() + + self.dynamics_move.apply(self.thermodynamic_state, self.sampler_state) + + self.iteration += 1 + + logger.info('n_accepted = {}'.format(self.n_accepted)) + logger.info('iterations = {}'.format(self.iteration)) diff --git a/blues/posedart.py b/blues/posedart.py deleted file mode 100755 index b0ff7b43..00000000 --- a/blues/posedart.py +++ /dev/null @@ -1,503 +0,0 @@ -""" -posedart.py: Provides the class for performing smart darting moves -during an NCMC simulation. - -Authors: Samuel C. Gill -Contributors: David L. Mobley -""" - -import mdtraj as md -import numpy as np -import simtk.unit as unit -from simtk.openmm import * -from simtk.openmm.app import * -from simtk.unit import * - -from blues.ncmc import SimNCMC, get_lig_residues - - -def zero_masses(system, firstres, lastres): - for index in range(firstres, lastres): - system.setParticleMass(index, 0 * daltons) - - -def beta(temperature): - kB = unit.BOLTZMANN_CONSTANT_kB * unit.AVOGADRO_CONSTANT_NA - kT = kB * temperature - beta = 1.0 / kT - return beta - - -def forcegroupify(system): - forcegroups = {} - for i in range(system.getNumForces()): - force = system.getForce(i) - force.setForceGroup(i) - forcegroups[force] = i - return forcegroups - - -def getEnergyDecomposition(context, forcegroups): - energies = {} - for f, i in forcegroups.items(): - energies[f] = context.getState(getEnergy=True, groups=2**i).getPotentialEnergy() - return energies - - -class PoseDart(SimNCMC): - """ - Class for performing smart darting moves during an NCMC simulation. - """ - - def __init__(self, pdb_files, fit_atoms, dart_size, **kwds): - super(PoseDart, self).__init__(**kwds) - self.dartboard = [] - self.dart_size = [] - print('initizalizing dart', dart_size._value, type(dart_size._value)) - print(self.residueList) - if type(dart_size._value) == list: - if len(dart_size) != len(residueList): - raise ValueError('mismatch between length of dart_size (%i) and residueList (%i)' % (len(dart_size), - len(residueList))) - self.dart_size = dart_size - elif type(dart_size._value) == int or type(dart_size._value) == float: - print('adding the same size darts') - for entry in self.residueList: - print('appending dart') - self.dart_size.append(dart_size.value_in_unit(unit.nanometers)) - self.dart_size = self.dart_size * unit.nanometers - - #self.dart_size = 0.2*unit.nanometers - self.binding_mode_traj = [] - self.fit_atoms = fit_atoms - self.ligand_pos = None - for pdb_file in pdb_files: - traj = md.load(pdb_file)[0] - self.binding_mode_traj.append(copy.deepcopy(traj)) - self.sim_traj = copy.deepcopy(self.binding_mode_traj[0]) - - def setDartUpdates(self, residueList): - self.residueList = residueList - - def defineLigandAtomsFromFile(lig_resname, coord_file, top_file=None): - self.residueList = get_lig_residues(lig_resname, coord_file, top_file) - - def add_dart(self, dart): - self.dartboard.append(dart) - - def dist_from_dart_center(self, sim_atom_pos, binding_mode_atom_pos): - - num_lig_atoms = len(self.residueList) - - dist_list = np.zeros((num_lig_atoms, 1)) - diff_list = np.zeros((num_lig_atoms, 3)) - indexList = [] - #Find the distances of the center to each dart, appending - #the results to dist_list - #TODO change to handle np.arrays instead - - for index, dart in enumerate(binding_mode_atom_pos): - diff = sim_atom_pos[index] - dart - dist = np.sqrt(np.sum((diff) * (diff))) - # dist = np.sqrt(np.sum((diff)*(diff)))*unit.nanometers - print('binding_mode_atom_pos', binding_mode_atom_pos) - print('sim_atom_pos', sim_atom_pos[index]) - print('dart', dart) - print('diff', diff) - diff_list[index] = diff - dist_list[index] = dist - print('diff_list', diff_list[index]) - print('dist_list', dist_list[index]) - - return dist_list, diff_list - - def poseDart(self, context=None, residueList=None): - """check whether molecule is within a pose, and - if it is, return the dart vectors for it's atoms - """ - if context == None: - context = self.nc_context - if residueList == None: - residueList = self.residueList - total_diff_list = [] - total_dist_list = [] - nc_pos = context.getState(getPositions=True).getPositions() - #update sim_traj positions for superposing binding modes - #might need to make self.sim_traj.xyz = nc_pos._value into - #self.sim_traj.xyz = [nc_pos._value] or np.array - self.sim_traj.xyz = nc_pos._value - #make a temp_pos to specify dart centers to compare - #distances between each dart and binding mode reference - temp_pos = [] - num_lig_atoms = len(self.residueList) - temp_pos = np.zeros((num_lig_atoms, 3)) - - for index, atom in enumerate(residueList): - print('temp_pos', temp_pos[index]) - print('nc_pos', nc_pos[atom]) - #keep track of units - temp_pos[index] = nc_pos[atom]._value - - #fit different binding modes to current protein - #to remove rotational changes - for pose in self.binding_mode_traj: - print('pose', pose.xyz) - pose = pose.superpose( - reference=self.sim_traj, atom_indices=self.fit_atoms, ref_atom_indices=self.fit_atoms) - # pose.save('temp.pdb') - pose_coord = pose.xyz[0] - print('pose_coord', pose.xyz[0]) - # help(pose.superpose) - binding_mode_pos = [] - #find the dart vectors and distances to each protein - #append the list to a storage list - temp_binding_mode_pos = np.zeros((num_lig_atoms, 3)) - temp_binding_mode_diff = np.zeros((num_lig_atoms, 3)) - temp_binding_mode_dist = np.zeros((num_lig_atoms, 1)) - - for index, atom in enumerate(residueList): - temp_binding_mode_pos[index] = pose_coord[atom] - temp_dist, temp_diff = self.dist_from_dart_center(temp_pos, temp_binding_mode_pos) - total_diff_list.append(temp_diff) - total_dist_list.append(temp_dist) - - print('total_diff_list', total_diff_list) - print('total_dist_list', total_dist_list) - print('self.dart_size', self.dart_size) - print('self.dart_size._value', self.dart_size._value) - selected = [] - #check to see which poses fall within the dart size - for index, single_pose in enumerate(total_dist_list): - counter = 0 - for atomnumber, dist in enumerate(single_pose): - print(self.dart_size) - print(self.dart_size[0]) - if dist <= self.dart_size[atomnumber]._value: - counter += 1 - print('counter for pose', index, 'is ', counter) - if counter == len(residueList): - selected.append(index) - if len(selected) == 1: - #returns binding mode index, and the diff_list - #diff_list will be used to dart - return selected[0], total_diff_list[selected[0]] - elif len(selected) == 0: - return None, total_diff_list - elif len(selected) >= 2: - print(selected) - #COM should never be within two different darts - raise ValueError('sphere size overlap, check darts') - - #use diff list to redart - - def poseRedart(self, changevec, binding_mode_pos, binding_mode_index, nc_pos, residueList=None): - """ - Helper function to choose a random pose and determine the vector - that would translate the current particles to that dart center - Arguments - ---------- - changevec: list - The change in vector that you want to apply, - typically supplied by poseDart - """ - if residueList == None: - residueList = self.residueList - changed_pos = copy.deepcopy(nc_pos) - rand_index = np.random.randint(len(self.binding_mode_traj)) - ###temp to encourage going to other binding modes - while rand_index == binding_mode_index: - rand_index = np.random.randint(len(self.binding_mode_traj)) - ### - - print('total residues', residueList) - for index, atom in enumerate(residueList): - #index refers to where in list - #atom refers to atom# - print('fitting atom', atom) - dartindex = binding_mode_index - print('binding_mode_pos', binding_mode_pos) - print('binding_mode_pos.xyz', (binding_mode_pos[dartindex].xyz)) - dart_origin = (binding_mode_pos[rand_index].xyz)[0][atom] - print('dart_origin', dart_origin) - print('changevec', changevec) - print('changevec[index]', changevec[index]) - dart_change = dart_origin + changevec[index] - changed_pos[atom] = dart_change * unit.nanometers - print('dart_change', dart_change) - print('dart_before', nc_pos[atom]) - print('dart_after', changed_pos[atom]) - - return changed_pos - - #select another binding pose and then for each atom - #use poseRedart() for each atom position - - def poseMove(self, context=None, residueList=None): - if residueList == None: - residueList = self.residueList - if context == None: - context = self.nc_context - stateinfo = context.getState(True, True, False, True, True, False) - oldEnergy = stateinfo.getPotentialEnergy() - oldDartPos = stateinfo.getPositions(asNumpy=True) - selected_pose, diff_list = self.poseDart() - if selected_pose == None: - print('no pose found') - else: - print('yes pose found') - new_pos = self.poseRedart( - changevec=diff_list, - binding_mode_pos=self.binding_mode_traj, - binding_mode_index=selected_pose, - nc_pos=oldDartPos) - context.setPositions(new_pos) - stateinfo = context.getState(True, True, False, True, True, False) - newEnergy = stateinfo.getPotentialEnergy() - print('oldEnergy', oldEnergy) - print('newEnergy', newEnergy) - old_md_state = self.md_simulation.context.getState(True, True, False, True, True, False) - print('md_oldEnergy', old_md_state.getPotentialEnergy()) - self.md_simulation.context.setPositions(new_pos) - new_md_state = self.md_simulation.context.getState(True, True, False, True, True, False) - print('md_newEnergy', new_md_state.getPotentialEnergy()) - - def findDart(self, particle_pairs=None, particle_weights=None): - """ - For dynamically updating dart positions based on positions - of other particles. - This takes the weighted average of the specified particles - and changes the dartboard of the object - - Arguments - --------- - particle_pairs: list of list of ints - each list defines the pairs to define darts - particle_weights: list of list of floats - each list defines the weights assigned to each particle positions - Returns - ------- - dart_list list of 1x3 np.arrays in units.nm - new dart positions calculated from the particle_pairs - and particle_weights - - """ - if particle_pairs == None: - particle_pairs = self.particle_pairs - if particle_weights == None: - particle_weights = self.particle_weights - #make sure there's an equal number of particle pair lists - #and particle weight lists - assert len(particle_pairs) == len(particle_weights) - - dart_list = [] - state_info = self.nc_context.getState(True, True, False, True, True, False) - temp_pos = state_info.getPositions(asNumpy=True) - #find particles positions and multiply by weights - for i, ppair in enumerate(particle_pairs): - temp_array = np.array([0, 0, 0]) * unit.nanometers - #weighted average - temp_wavg = 0 - for j, particle in enumerate(ppair): - print('temp_pos', particle, temp_pos[particle]) - temp_array += (temp_pos[particle] * float(particle_weights[i][j])) - temp_wavg += float(particle_weights[i][j]) - print(temp_array) - #divide by total number of particles in a list and append - #calculated postion to dart_list - dart_list.append(temp_array[:] / temp_wavg) - self.dartboard = dart_list[:] - return dart_list - - def virtualDart(self, virtual_particles=None): - """ - For dynamically updating dart positions based on positions - of other particles. - This takes the weighted average of the specified particles - and changes the dartboard of the object - - Arguments - --------- - virtual_particles: list of ints - Each int in the list specifies a particle - particle_weights: list of list of floats - each list defines the weights assigned to each particle positions - Returns - ------- - dart_list list of 1x3 np.arrays in units.nm - new dart positions calculated from the particle_pairs - and particle_weights - - """ - if virtual_particles == None: - virtual_particles = self.virtual_particles - - dart_list = [] - state_info = self.nc_context.getState(True, True, False, True, True, False) - temp_pos = state_info.getPositions(asNumpy=True) - #find virtual particles positions and add to dartboard - for particle in virtual_particles: - print('temp_pos', particle, temp_pos[particle]) - dart_list.append(temp_pos[particle]) - self.dartboard = dart_list[:] - return dart_list - - def reDart(self, changevec): - """ - Helper function to choose a random dart and determine the vector - that would translate the COM to that dart center - """ - dartindex = np.random.randint(len(self.dartboard)) - dart_origin = self.dartboard[dartindex] - chboard = dvector + changevec - print('chboard', chboard) - return chboard - - def dartmove(self, context=None, residueList=None): - """ - Obsolete function kept for reference. - """ - if residueList == None: - residueList = self.residueList - if context == None: - self.nc_context - - stateinfo = self.context.getState(True, True, False, True, True, False) - oldDartPos = stateinfo.getPositions(asNumpy=True) - oldDartPE = stateinfo.getPotentialEnergy() - center = self.calculate_com(oldDartPos) - selectedboard, changevec = self.calc_from_center(com=center) - print('changevec', changevec) - if selectedboard != None: - #notes - #comMove is where the com ends up after accounting from where - #it was from the original dart center - #basically it's final displacement location - newDartPos = copy.deepcopy(oldDartPos) - comMove = self.reDart(changevec) - vecMove = comMove - center - for residue in residueList: - newDartPos[residue] = newDartPos[residue] + vecMove - context.setPositions(newDartPos) - newDartInfo = context.getState(True, True, False, True, True, False) - newDartPE = newDartInfo.getPotentialEnergy() - logaccept = -1.0 * (newDartPE - oldDartPE) * self.beta - randnum = math.log(np.random.random()) - print('logaccept', logaccept, randnum) - print('old/newPE', oldDartPE, newDartPE) - if logaccept >= randnum: - print('move accepted!') - self.acceptance = self.acceptance + 1 - else: - print('rejected') - context.setPositions(oldDartPos) - dartInfo = context.getState(True, False, False, False, False, False) - - return newDartInfo.getPositions(asNumpy=True) - - def justdartmove(self, context=None, residueList=None): - """ - Function for performing smart darting move with fixed coordinate darts - """ - if residueList == None: - residueList = self.residueList - if context == None: - context = self.nc_context - - stateinfo = context.getState(True, True, False, True, True, False) - oldDartPos = stateinfo.getPositions(asNumpy=True) - oldDartPE = stateinfo.getPotentialEnergy() - center = self.calculate_com(oldDartPos) - selectedboard, changevec = self.calc_from_center(com=center) - print('selectedboard', selectedboard) - print('changevec', changevec) - print('centermass', center) - if selectedboard != None: - newDartPos = copy.deepcopy(oldDartPos) - comMove = self.reDart(changevec) - print('comMove', comMove) - print('center', center) - vecMove = comMove - center - print('vecMove', vecMove) - for residue in residueList: - newDartPos[residue] = newDartPos[residue] + vecMove - print('worked') - print(newDartPos) - context.setPositions(newDartPos) - newDartInfo = context.getState(True, True, False, True, True, False) - # newDartPE = newDartInfo.getPotentialEnergy() - - return newDartInfo.getPositions(asNumpy=True) - - def updateDartMove(self, context=None, residueList=None): - """ - Function for performing smart darting move with darts that - depend on particle positions in the system - """ - - if residueList == None: - residueList = self.residueList - if context == None: - context = self.nc_context - - stateinfo = context.getState(True, True, False, True, True, False) - oldDartPos = stateinfo.getPositions(asNumpy=True) - oldDartPE = stateinfo.getPotentialEnergy() - self.findDart() - center = self.calculate_com(oldDartPos) - selectedboard, changevec = self.calc_from_center(com=center) - print('selectedboard', selectedboard) - print('changevec', changevec) - print('centermass', center) - if selectedboard != None: - newDartPos = copy.deepcopy(oldDartPos) - comMove = self.reDart(changevec) - print('comMove', comMove) - print('center', center) - vecMove = comMove - center - print('vecMove', vecMove) - for residue in residueList: - newDartPos[residue] = newDartPos[residue] + vecMove - print('worked') - print(newDartPos) - context.setPositions(newDartPos) - newDartInfo = self.nc_context.getState(True, True, False, True, True, False) - # newDartPE = newDartInfo.getPotentialEnergy() - - return newDartInfo.getPositions(asNumpy=True) - - def virtualDartMove(self, context=None, residueList=None): - """ - Function for performing smart darting move with darts that - depend on particle positions in the system - """ - - if residueList == None: - residueList = self.residueList - if context == None: - context = self.nc_context - - stateinfo = context.getState(True, True, False, True, True, False) - oldDartPos = stateinfo.getPositions(asNumpy=True) - oldDartPE = stateinfo.getPotentialEnergy() - self.virtualDart() - center = self.calculate_com(oldDartPos) - selectedboard, changevec = self.calc_from_center(com=center) - print('selectedboard', selectedboard) - print('changevec', changevec) - print('centermass', center) - if selectedboard != None: - newDartPos = copy.deepcopy(oldDartPos) - comMove = self.reDart(changevec) - print('comMove', comMove) - print('center', center) - vecMove = comMove - center - print('vecMove', vecMove) - for residue in residueList: - newDartPos[residue] = newDartPos[residue] + vecMove - print('worked') - print(newDartPos) - context.setPositions(newDartPos) - newDartInfo = self.nc_context.getState(True, True, False, True, True, False) - # newDartPE = newDartInfo.getPotentialEnergy() - - return newDartInfo.getPositions(asNumpy=True) diff --git a/blues/settings.py b/blues/settings.py deleted file mode 100644 index a013cbbb..00000000 --- a/blues/settings.py +++ /dev/null @@ -1,321 +0,0 @@ -import json -import logging -import os - -import parmed -import yaml -from simtk import unit -from simtk.openmm import app - -from blues import reporters, utils - - -class Settings(object): - """ - Function that will parse the YAML configuration file for setup and running - BLUES simulations. - - Parameters - ---------- - yaml_config : filepath to YAML file (or JSON) - """ - - def __init__(self, config): - # Parse YAML or YAML docstr into dict - config = Settings.load_yaml(config) - - # Parse the config into dict - if type(config) is dict: - config = Settings.set_Parameters(config) - self.config = config - - @staticmethod - def load_yaml(yaml_config): - """ - Function that reads the YAML configuration file and parameters are - returned as a dict. - """ - # Parse input parameters from YAML - try: - if os.path.isfile(yaml_config): - with open(yaml_config, 'r') as stream: - config = yaml.safe_load(stream) - else: - config = yaml.safe_load(yaml_config) - except IOError as e: - print("Unable to open file:", yaml_config) - raise e - except yaml.YAMLError as e: - yaml_err = 'YAML parsing error in file: {}'.format(yaml_config) - if hasattr(e, 'problem_mark'): - mark = e.problem_mark - print(yaml_err + '\nError on Line:{} Column:{}' \ - .format(mark.line + 1, mark.column + 1)) - raise e - else: - return config - - @staticmethod - def set_Structure(config): - """ - Load the input/reference files (.prmtop, .inpcrd) into a parmed.Structure. If a `restart` (.rst7) - file is given, overwrite the reference positions, velocities, and box vectors on the Structure. - - Parameters - ----------- - filename: str, filepath to input (.prmtop) - restart: str, file path to Amber restart file (.rst7) - logger: logging.Logger object, records information - - Notes - ----- - Reference for parmed.load_Structure *args and **kwargs - https://parmed.github.io/ParmEd/html/structobj/parmed.formats.registry.load_file.html#parmed.formats.registry.load_file - """ - if 'restart' in config['structure'].keys(): - rst7 = config['structure']['restart'] - config['Logger'].info('Restarting simulation from {}'.format(rst7)) - restart = parmed.amber.Rst7(rst7) - config['structure'].pop('restart') - - structure = parmed.load_file(**config['structure']) - structure.positions = restart.positions - structure.velocities = restart.velocities - structure.box = restart.box - else: - structure = parmed.load_file(**config['structure']) - - config['Structure'] = structure - return config - - @staticmethod - def set_Output(config): - """ - Parses/updates the config (dict) with the given path for storing output files. - """ - # Set file paths - if 'output_dir' in config.keys(): - os.makedirs(config['output_dir'], exist_ok=True) - else: - output_dir = '.' - outfname = os.path.join(config['output_dir'], config['outfname']) - print(outfname) - config['outfname'] = outfname - config['simulation']['outfname'] = outfname - return config - - @staticmethod - def set_Logger(config): - """ - Initializes the logging.Logger modules and parses/updates the - config (dict) with the logger_level and the file path to store the .log file - """ - # Initialize root Logger module - #level = config['logger_level'].upper() - level = config['logger']['level'].upper() - stream = config['logger']['stream'] - - if 'filename' in config['logger'].keys(): - outfname = config['logger']['filename'] - else: - outfname = config['outfname'] - - if level == 'DEBUG': - # Add verbosity if logging is set to DEBUG - config['verbose'] = True - config['system']['verbose'] = True - config['simulation']['verbose'] = True - else: - config['verbose'] = False - config['system']['verbose'] = False - config['simulation']['verbose'] = False - logger_level = eval("logging.%s" % level) - logger = reporters.init_logger(logging.getLogger(), logger_level, stream, outfname) - config['Logger'] = logger - - return config - - @staticmethod - def set_Units(config): - """ - Parses/updates the config (dict) values with parameters that should have - units on them. If no unit is provided, the default units are assumed. - - Distances: unit.angstroms - Temperature: unit.kelvins - Masses: unit.daltons - Time: unit.picoseconds - Pressure: unit.atmospheres - Force: unit.kilocalories_per_mole/unit.angstroms**2 - """ - # Default parmed units. - default_units = { - 'nonbondedCutoff': unit.angstroms, - 'switchDistance': unit.angstroms, - 'implicitSolventKappa': unit.angstroms, - 'freeze_distance': unit.angstroms, - 'temperature': unit.kelvins, - 'hydrogenMass': unit.daltons, - 'dt': unit.picoseconds, - 'friction': 1 / unit.picoseconds, - 'pressure': unit.atmospheres, - 'implicitSolventSaltConc': unit.mole / unit.liters, - 'weight': unit.kilocalories_per_mole / unit.angstroms**2, - } - - # Loop over parameters which require units - for param, unit_type in default_units.items(): - # Check each nested subset of parameters - for setup_keys in ['system', 'simulation', 'freeze', 'restraints']: - # If the parameter requires units, cheeck if provided by user - try: - #print(param, config[setup_keys].keys()) - if str(param) in config[setup_keys].keys(): - user_input = config[setup_keys][param] - - if '*' in str(user_input): - config[setup_keys][param] = utils.parse_unit_quantity(user_input) - # If not provided, set default units - else: - config['Logger'].warn("Units for '{} = {}' not specified. Setting units to '{}'".format( - param, user_input, unit_type)) - config[setup_keys][param] = user_input * unit_type - - except: - pass - return config - - @staticmethod - def check_SystemModifications(config): - """ - Given a dict (config), check the parameters related to freezing or - restraining the system. Requires loading parmed.Structure from YAML. - """ - # Check Amber Selections - if 'freeze' in config.keys(): - freeze_keys = ['freeze_center', 'freeze_solvent', 'freeze_selection'] - for sel in freeze_keys: - if sel in config['freeze']: - utils.check_amber_selection(config['Structure'], config['freeze'][sel]) - - if 'restraints' in config.keys(): - utils.check_amber_selection(config['Structure'], config['restraints']['selection']) - - @staticmethod - def set_Apps(config): - """ - Check system parameters which require loading from the simtk.openmm.app namespace - - nonbondedMethod : ['NoCutoff', 'CutoffNonPeriodic', 'CutoffPeriodic', 'PME', 'Ewald'], - constraints : [None, 'HBonds', 'HAngles', 'AllBonds'], - implicitSolvent : ['HCT', 'OBC1', 'OBC2', 'GBn', 'GBn2'] - """ - - # System related parameters that require import from the simtk.openmm.app namesapce - valid_apps = { - 'nonbondedMethod': ['NoCutoff', 'CutoffNonPeriodic', 'CutoffPeriodic', 'PME', 'Ewald'], - 'constraints': [None, 'HBonds', 'HAngles', 'AllBonds'], - 'implicitSolvent': ['HCT', 'OBC1', 'OBC2', 'GBn', 'GBn2'] - } - - for method, app_type in valid_apps.items(): - if method in config['system']: - user_input = config['system'][method] - try: - config['system'][method] = eval("app.%s" % user_input) - except: - config['Logger'].exception("'{}' was not a valid option for '{}'. Valid options: {}".format( - user_input, method, app_type)) - return config - - @staticmethod - def set_ncmcSteps(config): - """ - Calculates the number of lambda switching steps and integrator steps - for the NCMC simulation. - """ - ncmc_parameters = utils.calculateNCMCSteps(**config['simulation']) - for k, v in ncmc_parameters.items(): - config['simulation'][k] = v - return config - - @staticmethod - def set_Reporters(config): - """ - Store the openmm.Reporters for the simulations to the configuration - """ - logger = config['Logger'] - outfname = config['outfname'] - nstepsNC = config['simulation']['nstepsNC'] - moveStep = config['simulation']['moveStep'] - - if 'md_reporters' in config.keys(): - # Returns a list of Reporter objects, overwrites the configuration parameters - md_reporter_cfg = reporters.ReporterConfig(outfname, config['md_reporters'], logger) - config['md_reporters'] = md_reporter_cfg.makeReporters() - if md_reporter_cfg.trajectory_interval: - config['simulation']['md_trajectory_interval'] = md_reporter_cfg.trajectory_interval - else: - logger.warn('Configuration for MD reporters were not set.') - - # Configure the NCMC simulation reporters - if 'ncmc_reporters' in config.keys(): - - #Update the reporter parameters with the proper NCMC steps - for rep in config['ncmc_reporters'].keys(): - - if 'totalSteps' in config['ncmc_reporters'][rep].keys(): - config['ncmc_reporters'][rep]['totalSteps'] = nstepsNC - - #If -1 is given in frame_indices, record at the last frame - #If 0.5 is given in frame_indices, record at the midpoint/movestep - if 'frame_indices' in config['ncmc_reporters'][rep].keys(): - frame_indices = config['ncmc_reporters'][rep]['frame_indices'] - frame_indices = [moveStep if x == 0.5 else x for x in frame_indices] - frame_indices = [nstepsNC if x == -1 else x for x in frame_indices] - config['ncmc_reporters'][rep]['frame_indices'] = frame_indices - - ncmc_reporter_cfg = reporters.ReporterConfig(outfname + '-ncmc', config['ncmc_reporters'], logger) - config['ncmc_reporters'] = ncmc_reporter_cfg.makeReporters() - else: - logger.warn('Configuration for NCMC reporters were not set.') - - return config - - @staticmethod - def set_Parameters(config): - """ - MAIN execution function for updating/correcting (placing units) in the config - """ - try: - # Set top level configuration parameters - config = Settings.set_Output(config) - config = Settings.set_Logger(config) - if 'structure' in config: - config = Settings.set_Structure(config) - Settings.check_SystemModifications(config) - config = Settings.set_Units(config) - config = Settings.set_Apps(config) - config = Settings.set_ncmcSteps(config) - config = Settings.set_Reporters(config) - - except Exception as e: - config['Logger'].exception(e) - raise e - - return config - - def asDict(self): - return self.config - - def asOrderedDict(self): - from collections import OrderedDict - return OrderedDict(sorted(self.config.items(), key=lambda t: t[0])) - - def asYAML(self): - return yaml.dump(self.config) - - def asJSON(self, pprint=False): - if pprint: - return json.dumps(self.config, sort_keys=True, indent=2, skipkeys=True, default=str) - return json.dumps(self.config, default=str) diff --git a/blues/simulation.py b/blues/simulation.py deleted file mode 100644 index ddab123d..00000000 --- a/blues/simulation.py +++ /dev/null @@ -1,1332 +0,0 @@ -""" -Provides classes for setting up and running the BLUES simulation. - -- `SystemFactory` : setup and modifying the OpenMM System prior to the simulation. -- `SimulationFactory` : generates the OpenMM Simulations from the System. -- `BLUESSimulation` : runs the NCMC+MD hybrid simulation. -- `MonteCarloSimulation` : runs a pure Monte Carlo simulation. - -Authors: Samuel C. Gill -Contributors: Nathan M. Lim, Meghan Osato, David L. Mobley -""" - -import logging -import math -import sys - -import numpy as np -import parmed -from openmmtools import alchemy -from simtk import openmm, unit -from simtk.openmm import app - -from blues import utils -from blues.integrators import AlchemicalExternalLangevinIntegrator - -finfo = np.finfo(np.float32) -rtol = finfo.precision -logger = logging.getLogger(__name__) - - -class SystemFactory(object): - """ - SystemFactory contains methods to generate/modify the OpenMM System object - required for generating the openmm.Simulation using a given - parmed.Structure() - - Examples - -------- - Load Parmed Structure, select move type, initialize `MoveEngine`, and - generate the openmm.Systems - - >>> structure = parmed.load_file('eqToluene.prmtop', xyz='eqToluene.inpcrd') - >>> ligand = RandomLigandRotationMove(structure, 'LIG') - >>> ligand_mover = MoveEngine(ligand) - >>> systems = SystemFactory(structure, ligand.atom_indices, config['system']) - - The MD and alchemical Systems are generated and stored as an attribute - - >>> systems.md - >>> systems.alch - - Freeze atoms in the alchemical system - - >>> systems.alch = SystemFactory.freeze_atoms(systems.alch, - freeze_distance=5.0, - freeze_center='LIG' - freeze_solvent='HOH,NA,CL') - - Parameters - ---------- - structure : parmed.Structure - A chemical structure composed of atoms, bonds, angles, torsions, and - other topological features. - atom_indices : list of int - Atom indicies of the move or designated for which the nonbonded forces - (both sterics and electrostatics components) have to be alchemically - modified. - config : dict, parameters for generating the `openmm.System` for the MD - and NCMC simulation. For complete parameters, see docs for `generateSystem` - and `generateAlchSystem` - """ - - def __init__(self, structure, atom_indices, config=None): - self.structure = structure - self.atom_indices = atom_indices - self._config = config - - #If parameters for generating the openmm.System is given, make them. - if self._config: - if 'alchemical' in self._config.keys(): - self.alch_config = self._config.pop('alchemical') - else: - #Use function defaults if none is provided - self.alch_config = {} - self.md = SystemFactory.generateSystem(self.structure, **self._config) - self.alch = SystemFactory.generateAlchSystem(self.md, self.atom_indices, **self.alch_config) - - @staticmethod - def amber_selection_to_atomidx(structure, selection): - """ - Converts AmberMask selection [amber-syntax]_ to list of atom indices. - - Parameters - ---------- - structure : parmed.Structure() - Structure of the system, used for atom selection. - selection : str - AmberMask selection that gets converted to a list of atom indices. - - Returns - ------- - mask_idx : list of int - List of atom indices. - - References - ---------- - .. [amber-syntax] J. Swails, ParmEd Documentation (2015). http://parmed.github.io/ParmEd/html/amber.html#amber-mask-syntax - - """ - mask = parmed.amber.AmberMask(structure, str(selection)) - mask_idx = [i for i in mask.Selected()] - return mask_idx - - @staticmethod - def atomidx_to_atomlist(structure, mask_idx): - """ - Goes through the structure and matches the previously selected atom - indices to the atom type. - - Parameters - ---------- - structure : parmed.Structure() - Structure of the system, used for atom selection. - mask_idx : list of int - List of atom indices. - - Returns - ------- - atom_list : list of atoms - The atoms that were previously selected in mask_idx. - """ - atom_list = [] - for i, at in enumerate(structure.atoms): - if i in mask_idx: - atom_list.append(structure.atoms[i]) - logger.debug('\nFreezing {}'.format(atom_list)) - return atom_list - - @classmethod - def generateSystem(cls, structure, **kwargs): - """ - Construct an OpenMM System representing the topology described by the - prmtop file. This function is just a wrapper for parmed Structure.createSystem(). - - Parameters - ---------- - structure : parmed.Structure() - The parmed.Structure of the molecular system to be simulated - nonbondedMethod : cutoff method - This is the cutoff method. It can be either the NoCutoff, - CutoffNonPeriodic, CutoffPeriodic, PME, or Ewald objects from the - simtk.openmm.app namespace - nonbondedCutoff : float or distance Quantity - The nonbonded cutoff must be either a floating point number - (interpreted as nanometers) or a Quantity with attached units. This - is ignored if nonbondedMethod is NoCutoff. - switchDistance : float or distance Quantity - The distance at which the switching function is turned on for van - der Waals interactions. This is ignored when no cutoff is used, and - no switch is used if switchDistance is 0, negative, or greater than - the cutoff - constraints : None, app.HBonds, app.HAngles, or app.AllBonds - Which type of constraints to add to the system (e.g., SHAKE). None - means no bonds are constrained. HBonds means bonds with hydrogen are - constrained - rigidWater : bool=True - If True, water is kept rigid regardless of the value of constraints. - A value of False is ignored if constraints is not None. - implicitSolvent : None, app.HCT, app.OBC1, app.OBC2, app.GBn, app.GBn2 - The Generalized Born implicit solvent model to use. - implicitSolventKappa : float or 1/distance Quantity = None - This is the Debye kappa property related to modeling saltwater - conditions in GB. It should have units of 1/distance (1/nanometers - is assumed if no units present). A value of None means that kappa - will be calculated from implicitSolventSaltConc (below) - implicitSolventSaltConc : float or amount/volume Quantity=0 moles/liter - If implicitSolventKappa is None, the kappa will be computed from the - salt concentration. It should have units compatible with mol/L - temperature : float or temperature Quantity = 298.15 kelvin - This is only used to compute kappa from implicitSolventSaltConc - soluteDielectric : float=1.0 - The dielectric constant of the protein interior used in GB - solventDielectric : float=78.5 - The dielectric constant of the water used in GB - useSASA : bool=False - If True, use the ACE non-polar solvation model. Otherwise, use no - SASA-based nonpolar solvation model. - removeCMMotion : bool=True - If True, the center-of-mass motion will be removed periodically - during the simulation. If False, it will not. - hydrogenMass : float or mass quantity = None - If not None, hydrogen masses will be changed to this mass and the - difference subtracted from the attached heavy atom (hydrogen mass - repartitioning) - ewaldErrorTolerance : float=0.0005 - When using PME or Ewald, the Ewald parameters will be calculated - from this value - flexibleConstraints : bool=True - If False, the energies and forces from the constrained degrees of - freedom will NOT be computed. If True, they will (but those degrees - of freedom will *still* be constrained). - verbose : bool=False - If True, the progress of this subroutine will be printed to stdout - splitDihedrals : bool=False - If True, the dihedrals will be split into two forces -- proper and - impropers. This is primarily useful for debugging torsion parameter - assignments. - - Returns - ------- - openmm.System - System formatted according to the prmtop file. - - Notes - ----- - This function calls prune_empty_terms if any Topology lists have - changed. - """ - return structure.createSystem(**kwargs) - - @classmethod - def generateAlchSystem(cls, - system, - atom_indices, - softcore_alpha=0.5, - softcore_a=1, - softcore_b=1, - softcore_c=6, - softcore_beta=0.0, - softcore_d=1, - softcore_e=1, - softcore_f=2, - annihilate_electrostatics=True, - annihilate_sterics=False, - disable_alchemical_dispersion_correction=True, - alchemical_pme_treatment='direct-space', - suppress_warnings=True, - **kwargs): - """Returns the OpenMM System for alchemical perturbations. - This function calls `openmmtools.alchemy.AbsoluteAlchemicalFactory` and - `openmmtools.alchemy.AlchemicalRegion` to generate the System for the - NCMC simulation. - - Parameters - ---------- - system : openmm.System - The OpenMM System object corresponding to the reference system. - atom_indices : list of int - Atom indicies of the move or designated for which the nonbonded forces - (both sterics and electrostatics components) have to be alchemically - modified. - annihilate_electrostatics : bool, optional - If True, electrostatics should be annihilated, rather than decoupled - (default is True). - annihilate_sterics : bool, optional - If True, sterics (Lennard-Jones or Halgren potential) will be annihilated, - rather than decoupled (default is False). - softcore_alpha : float, optional - Alchemical softcore parameter for Lennard-Jones (default is 0.5). - softcore_a, softcore_b, softcore_c : float, optional - Parameters modifying softcore Lennard-Jones form. Introduced in - Eq. 13 of Ref. [TTPham-JChemPhys135-2011]_ (default is 1). - softcore_beta : float, optional - Alchemical softcore parameter for electrostatics. Set this to zero - to recover standard electrostatic scaling (default is 0.0). - softcore_d, softcore_e, softcore_f : float, optional - Parameters modifying softcore electrostatics form (default is 1). - disable_alchemical_dispersion_correction : bool, optional, default=True - If True, the long-range dispersion correction will not be included for the alchemical - region to avoid the need to recompute the correction (a CPU operation that takes ~ 0.5 s) - every time 'lambda_sterics' is changed. If using nonequilibrium protocols, it is recommended - that this be set to True since this can lead to enormous (100x) slowdowns if the correction - must be recomputed every time step. - alchemical_pme_treatment : str, optional, default = 'direct-space' - Controls how alchemical region electrostatics are treated when PME is used. - Options are 'direct-space', 'coulomb', 'exact'. - - 'direct-space' only models the direct space contribution - - 'coulomb' includes switched Coulomb interaction - - 'exact' includes also the reciprocal space contribution, but it's - only possible to annihilate the charges and the softcore parameters - controlling the electrostatics are deactivated. Also, with this - method, modifying the global variable `lambda_electrostatics` is - not sufficient to control the charges. The recommended way to change - them is through the `AlchemicalState` class. - - Returns - ------- - alch_system : alchemical_system - System to be used for the NCMC simulation. - - References - ---------- - .. [TTPham-JChemPhys135-2011] T. T. Pham and M. R. Shirts, J. Chem. Phys 135, 034114 (2011). http://dx.doi.org/10.1063/1.3607597 - """ - if suppress_warnings: - #Lower logger level to suppress excess warnings - logging.getLogger("openmmtools.alchemy").setLevel(logging.ERROR) - - #Disabled correction term due to increased computational cost - factory = alchemy.AbsoluteAlchemicalFactory( - disable_alchemical_dispersion_correction=disable_alchemical_dispersion_correction, - alchemical_pme_treatment=alchemical_pme_treatment) - alch_region = alchemy.AlchemicalRegion( - alchemical_atoms=atom_indices, - softcore_alpha=softcore_alpha, - softcore_a=softcore_a, - softcore_b=softcore_b, - softcore_c=softcore_c, - softcore_beta=softcore_beta, - softcore_d=softcore_d, - softcore_e=softcore_e, - softcore_f=softcore_f, - annihilate_electrostatics=annihilate_electrostatics, - annihilate_sterics=annihilate_sterics) - - alch_system = factory.create_alchemical_system(system, alch_region) - return alch_system - - @classmethod - def restrain_positions(cls, structure, system, selection="(@CA,C,N)", weight=5.0, **kwargs): - """ - Applies positional restraints to atoms in the openmm.System - by the given parmed selection [amber-syntax]_. - - Parameters - ---------- - system : openmm.System - The OpenMM System object to be modified. - structure : parmed.Structure() - Structure of the system, used for atom selection. - selection : str, Default = "(@CA,C,N)" - AmberMask selection to apply positional restraints to - weight : float, Default = 5.0 - Restraint weight for xyz atom restraints in kcal/(mol A^2) - - Returns - ------- - system : openmm.System - Modified with positional restraints applied. - - """ - mask_idx = cls.amber_selection_to_atomidx(structure, selection) - - logger.info("{} positional restraints applied to selection: '{}' ({} atoms) on {}".format( - weight, selection, len(mask_idx), system)) - # define the custom force to restrain atoms to their starting positions - force = openmm.CustomExternalForce('k_restr*periodicdistance(x, y, z, x0, y0, z0)^2') - # Add the restraint weight as a global parameter in kcal/mol/A^2 - force.addGlobalParameter("k_restr", weight) - #force.addGlobalParameter("k_restr", weight*unit.kilocalories_per_mole/unit.angstroms**2) - # Define the target xyz coords for the restraint as per-atom (per-particle) parameters - force.addPerParticleParameter("x0") - force.addPerParticleParameter("y0") - force.addPerParticleParameter("z0") - - for i, atom_crd in enumerate(structure.positions): - if i in mask_idx: - logger.debug(i, structure.atoms[i]) - force.addParticle(i, atom_crd.value_in_unit(unit.nanometers)) - system.addForce(force) - - return system - - @classmethod - def freeze_atoms(cls, structure, system, freeze_selection=":LIG", **kwargs): - """ - Zeroes the masses of atoms from the given parmed selection [amber-syntax]_. - Massless atoms will be ignored by the integrator and will not change - positions. - - Parameters - ---------- - system : openmm.System - The OpenMM System object to be modified. - structure : parmed.Structure() - Structure of the system, used for atom selection. - freeze_selection : str, Default = ":LIG" - AmberMask selection for the center in which to select atoms for - zeroing their masses. - Defaults to freezing protein backbone atoms. - - Returns - ------- - system : openmm.System - The modified system with the selected atoms - """ - mask_idx = cls.amber_selection_to_atomidx(structure, freeze_selection) - logger.info("Freezing selection '{}' ({} atoms) on {}".format(freeze_selection, len(mask_idx), system)) - - cls.atomidx_to_atomlist(structure, mask_idx) - system = utils.zero_masses(system, mask_idx) - return system - - @classmethod - def freeze_radius(cls, - structure, - system, - freeze_distance=5.0 * unit.angstrom, - freeze_center=':LIG', - freeze_solvent=':HOH,NA,CL', - **kwargs): - """ - Zero the masses of atoms outside the given raidus of - the `freeze_center` parmed selection [amber-syntax]_. Massless atoms will be ignored by the - integrator and will not change positions.This is intended to freeze - the solvent and protein atoms around the ligand binding site. - - Parameters - ---------- - system : openmm.System - The OpenMM System object to be modified. - structure : parmed.Structure() - Structure of the system, used for atom selection. - freeze_distance : float, Default = 5.0 - Distance (angstroms) to select atoms for retaining their masses. - Atoms outside the set distance will have their masses set to 0.0. - freeze_center : str, Default = ":LIG" - AmberMask selection for the center in which to select atoms for - zeroing their masses. Default: LIG - freeze_solvent : str, Default = ":HOH,NA,CL" - AmberMask selection in which to select solvent atoms for zeroing - their masses. - - Returns - ------- - system : openmm.System - Modified system with masses outside the `freeze center` zeroed. - - """ - N_atoms = system.getNumParticles() - #Select the LIG and atoms within 5 angstroms, except for WAT or IONS (i.e. selects the binding site) - if hasattr(freeze_distance, '_value'): freeze_distance = freeze_distance._value - selection = "(%s<:%f)&!(%s)" % (freeze_center, freeze_distance, freeze_solvent) - logger.info('Inverting parmed selection for freezing: %s' % selection) - site_idx = cls.amber_selection_to_atomidx(structure, selection) - #Invert that selection to freeze everything but the binding site. - freeze_idx = set(range(N_atoms)) - set(site_idx) - - #Check if freeze selection has selected all atoms - if len(freeze_idx) == N_atoms: - err = 'All %i atoms appear to be selected for freezing. Check your atom selection.' % len(freeze_idx) - logger.error(err) - sys.exit(1) - - freeze_threshold = 0.98 - if len(freeze_idx) / N_atoms == freeze_threshold: - err = '%.0f%% of your system appears to be selected for freezing. Check your atom selection' % ( - 100 * freeze_threshold) - logger.error(err) - sys.exit(1) - - #Ensure that the freeze selection is larger than the center selection of atoms - center_idx = cls.amber_selection_to_atomidx(structure, freeze_center) - if len(site_idx) <= len(center_idx): - err = "%i unfrozen atoms is less than (or equal to) the number of atoms used as the selection center '%s' (%i atoms). Check your atom selection." % ( - len(site_idx), freeze_center, len(center_idx)) - logger.error(err) - sys.exit(1) - - freeze_warning = 0.80 - if len(freeze_idx) / N_atoms == freeze_warning: - warn = '%.0f%% of your system appears to be selected for freezing. This may cause unexpected behaviors.' % ( - 100 * freeze_warning) - logger.warm(warn) - sys.exit(1) - - #Ensure that the freeze selection is larger than the center selection point - center_idx = cls.amber_selection_to_atomidx(structure, freeze_center) - if len(site_idx) <= len(center_idx): - err = "%i unfrozen atoms is less than (or equal to) the number of atoms from the selection center '%s' (%i atoms). Check your atom selection." % ( - len(site_idx), freeze_center, len(center_idx)) - logger.error(err) - sys.exit(1) - - logger.info("Freezing {} atoms {} Angstroms from '{}' on {}".format( - len(freeze_idx), freeze_distance, freeze_center, system)) - - cls.atomidx_to_atomlist(structure, freeze_idx) - system = utils.zero_masses(system, freeze_idx) - return system - - -class SimulationFactory(object): - """SimulationFactory is used to generate the 3 required OpenMM Simulation - objects (MD, NCMC, ALCH) required for the BLUES run. This class can take a - list of reporters for the MD or NCMC simulation in the arguments - `md_reporters` or `ncmc_reporters`. - - Parameters - ---------- - systems : blues.simulation.SystemFactory object - The object containing the MD and alchemical openmm.Systems - move_engine : blues.moves.MoveEngine object - MoveEngine object which contains the list of moves performed - in the NCMC simulation. - config : dict - Simulation parameters which include: - nIter, nstepsNC, nstepsMD, nprop, propLambda, temperature, dt, propSteps, write_move - md_reporters : (optional) list of Reporter objects for the MD openmm.Simulation - ncmc_reporters : (optional) list of Reporter objects for the NCMC openmm.Simulation - - Examples - -------- - Load Parmed Structure from our input files, select the move type, - initialize the MoveEngine, and generate the openmm systems. - - >>> structure = parmed.load_file('eqToluene.prmtop', xyz='eqToluene.inpcrd') - >>> ligand = RandomLigandRotationMove(structure, 'LIG') - >>> ligand_mover = MoveEngine(ligand) - >>> systems = SystemFactory(structure, ligand.atom_indices, config['system']) - - Now, we can generate the Simulations from our openmm Systems using the - SimulationFactory class. If a configuration is provided at on initialization, - it will call `generateSimulationSet()` for convenience. Otherwise, the class can be - instantiated like a normal python class. - - - Below is an example of initializing the class like a normal python object. - - >>> simulations = SimulationFactory(systems, ligand_mover) - >>> hasattr(simulations, 'md') - False - >>> hasattr(simulations, 'ncmc') - False - - - Below, we provide a dict for configuring the Simulations and then - generate them by calling `simulations.generateSimulationSet()`. The MD/NCMC - simulation objects can be accessed separately as class attributes. - - >>> sim_cfg = { 'platform': 'OpenCL', - 'properties' : { 'OpenCLPrecision': 'single', - 'OpenCLDeviceIndex' : 2}, - 'nprop' : 1, - 'propLambda' : 0.3, - 'dt' : 0.001 * unit.picoseconds, - 'friction' : 1 * 1/unit.picoseconds, - 'temperature' : 100 * unit.kelvin, - 'nIter': 1, - 'nstepsMD': 10, - 'nstepsNC': 10,} - >>> simulations.generateSimulationSet(sim_cfg) - >>> hasattr(simulations, 'md') - True - >>> hasattr(simulations, 'ncmc') - True - - - After generating the Simulations, attach your own reporters by providing - the reporters in a list. Be sure to attach to either the MD or NCMC simulation. - - >>> from simtk.openmm.app import StateDataReporter - >>> md_reporters = [ StateDataReporter('test.log', 5) ] - >>> ncmc_reporters = [ StateDataReporter('test-ncmc.log', 5) ] - >>> simulations.md = simulations.attachReporters( simulations.md, md_reporters) - >>> simulations.ncmc = simulations.attachReporters( simulations.ncmc, ncmc_reporters) - - Alternatively, you can pass the configuration dict and list of reporters - upon class initialization. This will do all of the above for convenience. - - >>> simulations = SimulationFactory(systems, ligand_mover, sim_cfg, - md_reporters, ncmc_reporters) - >>> print(simulations) - >>> print(simulations.md) - >>> print(simulations.ncmc) - - - - >>> print(simulations.md.reporters) - >>> print(simulations.ncmc.reporters) - [] - [] - - """ - - def __init__(self, systems, move_engine, config=None, md_reporters=None, ncmc_reporters=None): - #Hide these properties since they exist on the SystemsFactory object - self._structure = systems.structure - self._system = systems.md - self._alch_system = systems.alch - #Atom indicies from move_engine - #TODO: change atom_indices selection for multiple regions - self._atom_indices = move_engine.moves[0].atom_indices - self._move_engine = move_engine - self.config = config - - #If parameters for generating the openmm.Simulation are given, make them. - if self.config: - try: - self.generateSimulationSet() - except Exception as e: - logger.exception(e) - raise e - - if md_reporters: - self._md_reporters = md_reporters - self.md = SimulationFactory.attachReporters(self.md, self._md_reporters) - if ncmc_reporters: - self._ncmc_reporters = ncmc_reporters - self.ncmc = SimulationFactory.attachReporters(self.ncmc, self._ncmc_reporters) - - @classmethod - def addBarostat(cls, system, temperature=300 * unit.kelvin, pressure=1 * unit.atmospheres, frequency=25, **kwargs): - """ - Adds a MonteCarloBarostat to the MD system. - - Parameters - ---------- - system : openmm.System - The OpenMM System object corresponding to the reference system. - temperature : float, default=300 - temperature (Kelvin) to be simulated at. - pressure : int, configional, default=None - Pressure (atm) for Barostat for NPT simulations. - frequency : int, default=25 - Frequency at which Monte Carlo pressure changes should be attempted (in time steps) - - Returns - ------- - system : openmm.System - The OpenMM System with the MonteCarloBarostat attached. - """ - logger.info('Adding MonteCarloBarostat with {}. MD simulation will be {} NPT.'.format(pressure, temperature)) - # Add Force Barostat to the system - system.addForce(openmm.MonteCarloBarostat(pressure, temperature, frequency)) - return system - - @classmethod - def generateIntegrator(cls, temperature=300 * unit.kelvin, dt=0.002 * unit.picoseconds, friction=1, **kwargs): - """ - Generates a LangevinIntegrator for the Simulations. - - Parameters - ---------- - temperature : float, default=300 - temperature (Kelvin) to be simulated at. - friction: float, default=1 - friction coefficient which couples to the heat bath, measured in 1/ps - dt: int, configional, default=0.002 - The timestep of the integrator to use (in ps). - - Returns - ------- - integrator : openmm.LangevinIntegrator - The LangevinIntegrator object intended for the System. - """ - integrator = openmm.LangevinIntegrator(temperature, friction, dt) - return integrator - - @classmethod - def generateNCMCIntegrator( - cls, - nstepsNC=None, - alchemical_functions={ - 'lambda_sterics': - 'min(1, (1/0.3)*abs(lambda-0.5))', - 'lambda_electrostatics': - 'step(0.2-lambda) - 1/0.2*lambda*step(0.2-lambda) + 1/0.2*(lambda-0.8)*step(lambda-0.8)' - }, - splitting="H V R O R V H", - temperature=300 * unit.kelvin, - dt=0.002 * unit.picoseconds, - nprop=1, - propLambda=0.3, - **kwargs): - """ - Generates the AlchemicalExternalLangevinIntegrator using openmmtools. - - Parameters - ----------- - nstepsNC : int - The number of NCMC relaxation steps to use. - alchemical_functions : dict - default = `{'lambda_sterics' : 'min(1, (1/0.3)*abs(lambda-0.5))', lambda_electrostatics' : 'step(0.2-lambda) - 1/0.2*lambda*step(0.2-lambda) + 1/0.2*(lambda-0.8)*step(lambda-0.8)'}` - key : value pairs such as "global_parameter" : function_of_lambda where function_of_lambda is a Lepton-compatible string that depends on the variable "lambda". - splitting : string, default: "H V R O R V H" - Sequence of R, V, O (and optionally V{i}), and { }substeps to be executed each timestep. There is also an H option, which increments the global parameter `lambda` by 1/nsteps_neq for each step. Forces are only used in V-step. Handle multiple force groups by appending the force group index to V-steps, e.g. "V0" will only use forces from force group 0. "V" will perform a step using all forces. ( will cause metropolization, and must be followed later by a ). - temperature : float, default=300 - temperature (Kelvin) to be simulated at. - dt: int, optional, default=0.002 - The timestep of the integrator to use (in ps). - nprop : int (Default: 1) - Controls the number of propagation steps to add in the lambda - region defined by `propLambda` - propLambda: float, optional, default=0.3 - The range which additional propogation steps are added, - defined by [0.5-propLambda, 0.5+propLambda]. - - Returns - ------- - ncmc_integrator : blues.integrator.AlchemicalExternalLangevinIntegrator - The NCMC integrator for the alchemical process in the NCMC simulation. - """ - #During NCMC simulation, lambda parameters are controlled by function dict below - # Keys correspond to parameter type (i.e 'lambda_sterics', 'lambda_electrostatics') - # 'lambda' = step/totalsteps where step corresponds to current NCMC step, - ncmc_integrator = AlchemicalExternalLangevinIntegrator( - alchemical_functions=alchemical_functions, - splitting=splitting, - temperature=temperature, - nsteps_neq=nstepsNC, - timestep=dt, - nprop=nprop, - prop_lambda=propLambda) - return ncmc_integrator - - @classmethod - def generateSimFromStruct(cls, structure, system, integrator, platform=None, properties={}, **kwargs): - """Generate the OpenMM Simulation objects from a given parmed.Structure() - - Parameters - ---------- - structure : parmed.Structure - ParmEd Structure object of the entire system to be simulated. - system : openmm.System - The OpenMM System object corresponding to the reference system. - integrator : openmm.Integrator - The OpenMM Integrator object for the simulation. - platform : str, default = None - Valid choices: 'Auto', 'OpenCL', 'CUDA' - If None is specified, the fastest available platform will be used. - - Returns - ------- - simulation : openmm.Simulation - The generated OpenMM Simulation from the parmed.Structure, openmm.System, - amd the integrator. - """ - #Specifying platform properties here used for local development. - if platform is None: - #Use the fastest available platform - simulation = app.Simulation(structure.topology, system, integrator) - else: - platform = openmm.Platform.getPlatformByName(platform) - #Make sure key/values are strings - properties = {str(k): str(v) for k, v in properties.items()} - simulation = app.Simulation(structure.topology, system, integrator, platform, properties) - - # Set initial positions/velocities - if structure.box_vectors: - simulation.context.setPeriodicBoxVectors(*structure.box_vectors) - simulation.context.setPositions(structure.positions) - simulation.context.setVelocitiesToTemperature(integrator.getTemperature()) - - return simulation - - @staticmethod - def attachReporters(simulation, reporter_list): - """Attach the list of reporters to the Simulation object - - Parameters - ---------- - simulation : openmm.Simulation - The Simulation object to attach reporters to. - reporter_list : list of openmm Reporeters - The list of reporters to attach to the OpenMM Simulation. - - Returns - ------- - simulation : openmm.Simulation - The Simulation object with the reporters attached. - - """ - for rep in reporter_list: - simulation.reporters.append(rep) - return simulation - - def generateSimulationSet(self, config=None): - """Generates the 3 OpenMM Simulation objects. - - Parameters - ---------- - config : dict - Dictionary of parameters for configuring the OpenMM Simulations - """ - if not config: config = self.config - - #Construct MD Integrator and Simulation - self.integrator = self.generateIntegrator(**config) - - #Check for pressure parameter to set simulation to NPT - if 'pressure' in config.keys(): - self._system = self.addBarostat(self._system, **config) - logger.warning( - 'NCMC simulation will NOT have pressure control. NCMC will use pressure from last MD state.') - else: - logger.info('MD simulation will be {} NVT.'.format(config['temperature'])) - self.md = self.generateSimFromStruct(self._structure, self._system, self.integrator, **config) - - #Alchemical Simulation is used for computing correction term from MD simulation. - alch_integrator = self.generateIntegrator(**config) - self.alch = self.generateSimFromStruct(self._structure, self._system, alch_integrator, **config) - - #If the moveStep hasn't been calculated, recheck the NCMC steps. - if 'moveStep' not in config.keys(): - logger.warning('Did not find `moveStep` in configuration. Checking NCMC paramters') - ncmc_parameters = utils.calculateNCMCSteps(**config) - for k, v in ncmc_parameters.items(): - config[k] = v - self.config = config - - #Construct NCMC Integrator and Simulation - self.ncmc_integrator = self.generateNCMCIntegrator(**config) - - #Initialize the Move Engine with the Alchemical System and NCMC Integrator - for move in self._move_engine.moves: - self._alch_system, self.ncmc_integrator = move.initializeSystem(self._alch_system, self.ncmc_integrator) - self.ncmc = self.generateSimFromStruct(self._structure, self._alch_system, self.ncmc_integrator, **config) - utils.print_host_info(self.ncmc) - - -class BLUESSimulation(object): - """BLUESSimulation class provides methods to execute the NCMC+MD - simulation. - - Parameters - ---------- - simulations : blues.simulation.SimulationFactory object - SimulationFactory Object which carries the 3 required - OpenMM Simulation objects (MD, NCMC, ALCH) required to run BLUES. - config : dict - Dictionary of parameters for configuring the OpenMM Simulations - If None, will search for configuration parameters on the `simulations` - object. - - Examples - -------- - Create our SimulationFactory object and run `BLUESSimulation` - - >>> sim_cfg = { 'platform': 'OpenCL', - 'properties' : { 'OpenCLPrecision': 'single', - 'OpenCLDeviceIndex' : 2}, - 'nprop' : 1, - 'propLambda' : 0.3, - 'dt' : 0.001 * unit.picoseconds, - 'friction' : 1 * 1/unit.picoseconds, - 'temperature' : 100 * unit.kelvin, - 'nIter': 1, - 'nstepsMD': 10, - 'nstepsNC': 10,} - >>> simulations = SimulationFactory(systems, ligand_mover, sim_cfg) - >>> blues = BLUESSimulation(simulations) - >>> blues.run() - - """ - - def __init__(self, simulations, config=None): - self._move_engine = simulations._move_engine - self._md_sim = simulations.md - self._alch_sim = simulations.alch - self._ncmc_sim = simulations.ncmc - - # Check if configuration has been specified in `SimulationFactory` object - if not config: - if hasattr(simulations, 'config'): - self._config = simulations.config - else: - #Otherwise take specified config - self._config = config - if self._config: - self._printSimulationTiming() - - self.accept = 0 - self.reject = 0 - self.acceptRatio = 0 - self.currentIter = 0 - - #Dict to keep track of each simulation state before/after each iteration - self.stateTable = {'md': {'state0': {}, 'state1': {}}, 'ncmc': {'state0': {}, 'state1': {}}} - - #specify nc integrator variables to report in verbose output - self._integrator_keys_ = ['lambda', 'shadow_work', 'protocol_work', 'Eold', 'Enew'] - - self._state_keys = { - 'getPositions': True, - 'getVelocities': True, - 'getForces': False, - 'getEnergy': True, - 'getParameters': True, - 'enforcePeriodicBox': True - } - - @classmethod - def getStateFromContext(cls, context, state_keys): - """Gets the State information from the given context and - list of state_keys to query it with. - - Returns the state data as a dict. - - Parameters - ---------- - context : openmm.Context - Context of the OpenMM Simulation to query. - state_keys : list - Default: [ positions, velocities, potential_energy, kinetic_energy ] - A list that defines what information to get from the context State. - - Returns - ------- - stateinfo : dict - Current positions, velocities, energies and box vectors of the context. - """ - - stateinfo = {} - state = context.getState(**state_keys) - stateinfo['positions'] = state.getPositions(asNumpy=True) - stateinfo['velocities'] = state.getVelocities(asNumpy=True) - stateinfo['potential_energy'] = state.getPotentialEnergy() - stateinfo['kinetic_energy'] = state.getKineticEnergy() - stateinfo['box_vectors'] = state.getPeriodicBoxVectors() - return stateinfo - - @classmethod - def getIntegratorInfo(cls, - ncmc_integrator, - integrator_keys=['lambda', 'shadow_work', 'protocol_work', 'Eold', 'Enew']): - """Returns a dict of alchemical/ncmc-swtiching data from querying the the NCMC - integrator. - - Parameters - ---------- - ncmc_integrator : openmm.Context.Integrator - The integrator from the NCMC Context - integrator_keys : list - list containing strings of the values to get from the integrator. - Default = ['lambda', 'shadow_work', 'protocol_work', 'Eold', 'Enew','Epert'] - - Returns - ------- - integrator_info : dict - Work values and energies from the NCMC integrator. - """ - integrator_info = {} - for key in integrator_keys: - integrator_info[key] = ncmc_integrator.getGlobalVariableByName(key) - return integrator_info - - @classmethod - def setContextFromState(cls, context, state, box=True, positions=True, velocities=True): - """Update a given Context from the given State. - - Parameters - ---------- - context : openmm.Context - The Context to be updated from the given State. - state : openmm.State - The current state (box_vectors, positions, velocities) of the - Simulation to update the given context. - - Returns - ------- - context : openmm.Context - The updated Context whose box_vectors, positions, and velocities - have been updated. - """ - # Replace ncmc data from the md context - if box: - context.setPeriodicBoxVectors(*state['box_vectors']) - if positions: - context.setPositions(state['positions']) - if velocities: - context.setVelocities(state['velocities']) - return context - - def _printSimulationTiming(self): - """Prints the simulation timing and related information.""" - - dt = self._config['dt'].value_in_unit(unit.picoseconds) - nIter = self._config['nIter'] - nprop = self._config['nprop'] - propLambda = self._config['propLambda'] - propSteps = self._config['propSteps'] - nstepsNC = self._config['nstepsNC'] - nstepsMD = self._config['nstepsMD'] - - force_eval = nIter * (propSteps + nstepsMD) - time_ncmc_iter = propSteps * dt - time_ncmc_total = time_ncmc_iter * nIter - time_md_iter = nstepsMD * dt - time_md_total = time_md_iter * nIter - time_iter = time_ncmc_iter + time_md_iter - time_total = time_iter * nIter - - msg = 'Total BLUES Simulation Time = %s ps (%s ps/Iter)\n' % (time_total, time_iter) - msg += 'Total Force Evaluations = %s \n' % force_eval - msg += 'Total NCMC time = %s ps (%s ps/iter)\n' % (time_ncmc_total, time_ncmc_iter) - - # Calculate number of lambda steps inside/outside region with extra propgation steps - steps_in_prop = int(nprop * (2 * math.floor(propLambda * nstepsNC))) - steps_out_prop = int((2 * math.ceil((0.5 - propLambda) * nstepsNC))) - - prop_lambda_window = self._ncmc_sim.context._integrator._prop_lambda - # prop_range = round(prop_lambda_window[1] - prop_lambda_window[0], 4) - if propSteps != nstepsNC: - msg += '\t%s lambda switching steps within %s total propagation steps.\n' % (nstepsNC, propSteps) - msg += '\tExtra propgation steps between lambda [%s, %s]\n' % (prop_lambda_window[0], - prop_lambda_window[1]) - msg += '\tLambda: 0.0 -> %s = %s propagation steps\n' % (prop_lambda_window[0], int(steps_out_prop / 2)) - msg += '\tLambda: %s -> %s = %s propagation steps\n' % (prop_lambda_window[0], prop_lambda_window[1], - steps_in_prop) - msg += '\tLambda: %s -> 1.0 = %s propagation steps\n' % (prop_lambda_window[1], int(steps_out_prop / 2)) - - msg += 'Total MD time = %s ps (%s ps/iter)\n' % (time_md_total, time_md_iter) - - #Get trajectory frame interval timing for BLUES simulation - if 'md_trajectory_interval' in self._config.keys(): - frame_iter = nstepsMD / self._config['md_trajectory_interval'] - timetraj_frame = (time_ncmc_iter + time_md_iter) / frame_iter - msg += 'Trajectory Interval = %s ps/frame (%s frames/iter)' % (timetraj_frame, frame_iter) - - logger.info(msg) - - def _setStateTable(self, simkey, stateidx, stateinfo): - """Updates `stateTable` (dict) containing: Positions, Velocities, Potential/Kinetic energies - of the state before and after a NCMC step or iteration. - - Parameters - ---------- - simkey : str (key: 'md', 'ncmc', 'alch') - Key corresponding to the simulation. - stateidx : str (key: 'state0' or 'state1') - Key corresponding to the state information being stored. - stateinfo : dict - Dictionary containing the State information. - """ - self.stateTable[simkey][stateidx] = stateinfo - - def _syncStatesMDtoNCMC(self): - """Retrieves data on the current State of the MD context to - replace the box vectors, positions, and velocties in the NCMC context. - """ - # Retrieve MD state from previous iteration - md_state0 = self.getStateFromContext(self._md_sim.context, self._state_keys) - self._setStateTable('md', 'state0', md_state0) - - # Sync MD state to the NCMC context - self._ncmc_sim.context = self.setContextFromState(self._ncmc_sim.context, md_state0) - - def _stepNCMC(self, nstepsNC, moveStep, move_engine=None): - """Advance the NCMC simulation. - - Parameters - ---------- - nstepsNC : int - The number of NCMC switching steps to advance by. - moveStep : int - The step number to perform the chosen move, which should be half - the number of nstepsNC. - move_engine : blues.moves.MoveEngine - The object that executes the chosen move. - - """ - - logger.info('Advancing %i NCMC switching steps...' % (nstepsNC)) - # Retrieve NCMC state before proposed move - ncmc_state0 = self.getStateFromContext(self._ncmc_sim.context, self._state_keys) - self._setStateTable('ncmc', 'state0', ncmc_state0) - - #choose a move to be performed according to move probabilities - #TODO: will have to change to work with multiple alch region - if not move_engine: move_engine = self._move_engine - self._ncmc_sim.currentIter = self.currentIter - move_engine.selectMove() - - lastStep = nstepsNC - 1 - for step in range(int(nstepsNC)): - try: - #Attempt anything related to the move before protocol is performed - if not step: - self._ncmc_sim.context = move_engine.selected_move.beforeMove(self._ncmc_sim.context) - - # Attempt selected MoveEngine Move at the halfway point - #to ensure protocol is symmetric - if step == moveStep: - if hasattr(logger, 'report'): - logger.info = logger.report - #Do move - logger.info('Performing %s...' % move_engine.move_name) - self._ncmc_sim.context = move_engine.runEngine(self._ncmc_sim.context) - - # Do 1 NCMC step with the integrator - self._ncmc_sim.step(1) - - #Attempt anything related to the move after protocol is performed - if step == lastStep: - self._ncmc_sim.context = move_engine.selected_move.afterMove(self._ncmc_sim.context) - - except Exception as e: - logger.error(e) - move_engine.selected_move._error(self._ncmc_sim.context) - break - - # ncmc_state1 stores the state AFTER a proposed move. - ncmc_state1 = self.getStateFromContext(self._ncmc_sim.context, self._state_keys) - self._setStateTable('ncmc', 'state1', ncmc_state1) - - def _computeAlchemicalCorrection(self): - """Computes the alchemical correction term from switching between the NCMC - and MD potentials.""" - # Retrieve the MD/NCMC state before the proposed move. - md_state0_PE = self.stateTable['md']['state0']['potential_energy'] - ncmc_state0_PE = self.stateTable['ncmc']['state0']['potential_energy'] - - # Retreive the NCMC state after the proposed move. - ncmc_state1 = self.stateTable['ncmc']['state1'] - ncmc_state1_PE = ncmc_state1['potential_energy'] - - # Set the box_vectors and positions in the alchemical simulation to after the proposed move. - self._alch_sim.context = self.setContextFromState(self._alch_sim.context, ncmc_state1, velocities=False) - - # Retrieve potential_energy for alch correction - alch_PE = self._alch_sim.context.getState(getEnergy=True).getPotentialEnergy() - correction_factor = (ncmc_state0_PE - md_state0_PE + alch_PE - ncmc_state1_PE) * ( - -1.0 / self._ncmc_sim.context._integrator.kT) - - return correction_factor - - def _acceptRejectMove(self, write_move=False): - """Choose to accept or reject the proposed move based - on the acceptance criterion. - - Parameters - ---------- - write_move : bool, default=False - If True, writes the proposed NCMC move to a PDB file. - """ - work_ncmc = self._ncmc_sim.context._integrator.getLogAcceptanceProbability(self._ncmc_sim.context) - randnum = math.log(np.random.random()) - - # Compute correction if work_ncmc is not NaN - if not np.isnan(work_ncmc): - correction_factor = self._computeAlchemicalCorrection() - logger.debug( - 'NCMCLogAcceptanceProbability = %.6f + Alchemical Correction = %.6f' % (work_ncmc, correction_factor)) - work_ncmc = work_ncmc + correction_factor - - if work_ncmc > randnum: - self.accept += 1 - logger.info('NCMC MOVE ACCEPTED: work_ncmc {} > randnum {}'.format(work_ncmc, randnum)) - - # If accept move, sync NCMC state to MD context - ncmc_state1 = self.stateTable['ncmc']['state1'] - self._md_sim.context = self.setContextFromState(self._md_sim.context, ncmc_state1, velocities=False) - - if write_move: - utils.saveSimulationFrame(self._md_sim, '{}acc-it{}.pdb'.format(self._config['outfname'], - self.currentIter)) - - else: - self.reject += 1 - logger.info('NCMC MOVE REJECTED: work_ncmc {} < {}'.format(work_ncmc, randnum)) - - # If reject move, do nothing, - # NCMC simulation be updated from MD Simulation next iteration. - - # Potential energy should be from last MD step in the previous iteration - md_state0 = self.stateTable['md']['state0'] - md_PE = self._md_sim.context.getState(getEnergy=True).getPotentialEnergy() - if not math.isclose(md_state0['potential_energy']._value, md_PE._value, rel_tol=float('1e-%s' % rtol)): - logger.error( - 'Last MD potential energy %s != Current MD potential energy %s. Potential energy should match the prior state.' - % (md_state0['potential_energy'], md_PE)) - sys.exit(1) - - def _resetSimulations(self, temperature=None): - """At the end of each iteration: - - 1. Reset the step number in the NCMC context/integrator - 2. Set the velocities to random values chosen from a Boltzmann distribution at a given `temperature`. - - Parameters - ---------- - temperature : float - The target temperature for the simulation. - - """ - if not temperature: - temperature = self._md_sim.context._integrator.getTemperature() - - self._ncmc_sim.currentStep = 0 - self._ncmc_sim.context._integrator.reset() - - #Reinitialize velocities, preserving detailed balance? - self._md_sim.context.setVelocitiesToTemperature(temperature) - - def _stepMD(self, nstepsMD): - """Advance the MD simulation. - - Parameters - ---------- - nstepsMD : int - The number of steps to advance the MD simulation. - """ - logger.info('Advancing %i MD steps...' % (nstepsMD)) - self._md_sim.currentIter = self.currentIter - # Retrieve MD state before proposed move - # Helps determine if previous iteration placed ligand poorly - md_state0 = self.stateTable['md']['state0'] - - for md_step in range(int(nstepsMD)): - try: - self._md_sim.step(1) - except Exception as e: - logger.error(e, exc_info=True) - logger.error('potential energy before NCMC: %s' % md_state0['potential_energy']) - logger.error('kinetic energy before NCMC: %s' % md_state0['kinetic_energy']) - #Write out broken frame - utils.saveSimulationFrame(self._md_sim, - 'MD-fail-it%s-md%i.pdb' % (self.currentIter, self._md_sim.currentStep)) - sys.exit(1) - - def run(self, nIter=0, nstepsNC=0, moveStep=0, nstepsMD=0, temperature=300, write_move=False, **config): - """Executes the BLUES engine to iterate over the actions: - Perform NCMC simulation, perform proposed move, accepts/rejects move, - then performs the MD simulation from the NCMC state, niter number of times. - **Note:** If the parameters are not given explicitly, will look for the parameters - in the provided configuration on the `SimulationFactory` object. - - Parameters - ---------- - nIter : int, default = None - Number of iterations of NCMC+MD to perform. - nstepsNC : int - The number of NCMC switching steps to advance by. - moveStep : int - The step number to perform the chosen move, which should be half - the number of nstepsNC. - nstepsMD : int - The number of steps to advance the MD simulation. - temperature : float - The target temperature for the simulation. - write_move : bool, default=False - If True, writes the proposed NCMC move to a PDB file. - - """ - if not nIter: nIter = self._config['nIter'] - if not nstepsNC: nstepsNC = self._config['nstepsNC'] - if not nstepsMD: nstepsMD = self._config['nstepsMD'] - if not moveStep: moveStep = self._config['moveStep'] - - logger.info('Running %i BLUES iterations...' % (nIter)) - for N in range(int(nIter)): - self.currentIter = N - logger.info('BLUES Iteration: %s' % N) - self._syncStatesMDtoNCMC() - self._stepNCMC(nstepsNC, moveStep) - self._acceptRejectMove(write_move) - self._resetSimulations(temperature) - self._stepMD(nstepsMD) - - # END OF NITER - self.acceptRatio = self.accept / float(nIter) - logger.info('Acceptance Ratio: %s' % self.acceptRatio) - logger.info('nIter: %s ' % nIter) - - -class MonteCarloSimulation(BLUESSimulation): - """Simulation class provides the functions that perform the MonteCarlo run. - - Parameters - ---------- - simulations : SimulationFactory - Contains 3 required OpenMM Simulationobjects - config : dict, default = None - Dict with configuration info. - """ - - def __init__(self, simulations, config=None): - super(MonteCarloSimulation, self).__init__(simulations, config) - - def _stepMC_(self): - """Function that performs the MC simulation. - """ - - #choose a move to be performed according to move probabilities - self._move_engine.selectMove() - #change coordinates according to Moves in MoveEngine - new_context = self._move_engine.runEngine(self._md_sim.context) - md_state1 = self.getStateFromContext(new_context, self._state_keys) - self._setStateTable('md', 'state1', md_state1) - - def _acceptRejectMove(self, temperature=None): - """Function that chooses to accept or reject the proposed move. - """ - md_state0 = self.stateTable['md']['state0'] - md_state1 = self.stateTable['md']['state1'] - work_mc = (md_state1['potential_energy'] - md_state0['potential_energy']) * ( - -1.0 / self._ncmc_sim.context._integrator.kT) - randnum = math.log(np.random.random()) - - if work_mc > randnum: - self.accept += 1 - logger.info('MC MOVE ACCEPTED: work_mc {} > randnum {}'.format(work_mc, randnum)) - self._md_sim.context.setPositions(md_state1['positions']) - else: - self.reject += 1 - logger.info('MC MOVE REJECTED: work_mc {} < {}'.format(work_mc, randnum)) - self._md_sim.context.setPositions(md_state0['positions']) - self._md_sim.context.setVelocitiesToTemperature(temperature) - - def run(self, nIter=0, mc_per_iter=0, nstepsMD=0, temperature=300, write_move=False): - """Function that runs the BLUES engine to iterate over the actions: - perform proposed move, accepts/rejects move, - then performs the MD simulation from the accepted or rejected state. - - Parameters - ---------- - nIter : None or int, optional default = None - The number of iterations to perform. If None, then - uses the nIter specified in the opt dictionary when - the Simulation class was created. - mc_per_iter : int, default = 1 - Number of Monte Carlo iterations. - nstepsMD : int, default = None - Number of steps the MD simulation will advance - write_move : bool, default = False - Writes the move if True - """ - if not nIter: nIter = self._config['nIter'] - if not nstepsMD: nstepsMD = self._config['nstepsMD'] - #controls how many mc moves are performed during each iteration - if not mc_per_iter: mc_per_iter = self._config['mc_per_iter'] - - self._syncStatesMDtoNCMC() - for N in range(nIter): - self.currentIter = N - logger.info('MonteCarlo Iteration: %s' % N) - for i in range(mc_per_iter): - self._syncStatesMDtoNCMC() - self._stepMC_() - self._acceptRejectMove(temperature) - self._stepMD(nstepsMD) diff --git a/blues/smartdart.py b/blues/smartdart.py deleted file mode 100644 index 3f4065d5..00000000 --- a/blues/smartdart.py +++ /dev/null @@ -1,693 +0,0 @@ -""" -smartdart.py: Provides the class for performing smart darting moves -during an NCMC simulation. - -Authors: Samuel C. Gill -Contributors: David L. Mobley -""" - -import mdtraj as md -import numpy as np -import simtk.unit as unit -from simtk.openmm import * -from simtk.openmm.app import * -from simtk.unit import * - -from blues.ncmc import SimNCMC - - -#TODO consider throwing a warning if particle choices -#are not dispersed enough (by some angle cutoff) -def changeBasis(a, b): - ''' - Changes positions of a particle (b) in the regular basis set to - another basis set (a). - - Arguments - --------- - a: 3x3 np.array - Defines vectors that will create the new basis. - b: 1x3 np.array - Defines position of particle to be transformed into - new basis set. - - Returns - ------- - changed_coord: 1x3 np.array - Coordinates of b in new basis. - - ''' - ainv = np.linalg.inv(a.T) - print('ainv', ainv) - print('b.T', b.T) - changed_coord = np.dot(ainv, b.T) * unit.nanometers - return changed_coord - - -def undoBasis(a, b): - ''' - Transforms positions in a transformed basis (b) to the regular - basis set. - - Arguments - --------- - a: 3x3 np.array - Defines vectors that defined the new basis. - b: 1x3 np.array - Defines position of particle to be transformed into - regular basis set. - - Returns - ------- - changed_coord: 1x3 np.array - Coordinates of b in new basis. - ''' - a = a.T - print('a', a.T) - print('b.T', b.T) - changed_coord = np.dot(a, b.T) * unit.nanometers - return changed_coord - - -def normalize(vector): - '''Normalize a given vector - - Arguemnts - --------- - vector: 1xn np.array - Vector to be normalized. - - Returns - ------- - unit_vec: 1xn np.array - Normalized vector. - ''' - magnitude = np.sqrt(np.sum(vector * vector)) - unit_vec = vector / magnitude - return unit_vec - - -def localcoord(particle1, particle2, particle3): - '''Defines a new coordinate system using 3 particles - returning the new basis set vectors - ''' - part1 = particle1 - particle1 - part2 = particle2 - particle1 - part3 = particle3 - particle1 - # vec1 = normalize(part2) - # vec2 = normalize(part3) - vec1 = part2 - vec2 = part3 - vec3 = np.cross(vec1, vec2) * unit.nanometers - print('vec3', vec3, normalize(vec3)) - print('vec1', vec1, 'vec2', vec2, 'vec3', vec3) - return vec1, vec2, vec3 - - -def findNewCoord(particle1, particle2, particle3, center): - '''Finds the coordinates of a given center in a new coordinate - system defined by particles1-3 - ''' - #calculate new basis set - vec1, vec2, vec3 = localcoord(particle1, particle2, particle3) - basis_set = np.zeros((3, 3)) * unit.nanometers - basis_set[0] = vec1 - basis_set[1] = vec2 - print('vec3', vec3) - basis_set[2] = vec3 - print('basis_set', basis_set) - #since the origin is centered at particle1 by convention - #subtract to account for this - recenter = center - particle1 - #find coordinate in new coordinate system - new_coord = changeBasis(basis_set, recenter) - print('new_coord', new_coord) - old_coord = undoBasis(basis_set, new_coord) - print('old_coord', old_coord) - print('old_recenter', recenter) - return new_coord - - -def findOldCoord(particle1, particle2, particle3, center): - '''Finds the coordinates of a given center (defined by a different basis - given by particles1-3) back in euclidian coordinates - system defined by particles1-3 - ''' - - print('particles', particle1, particle2, particle3) - vec1, vec2, vec3 = localcoord(particle1, particle2, particle3) - basis_set = np.zeros((3, 3)) * unit.nanometers - basis_set[0] = vec1 - basis_set[1] = vec2 - print('vec3', vec3) - basis_set[2] = vec3 - print('basis_set', basis_set) - #since the origin is centered at particle1 by convention - #subtract to account for this - old_coord = undoBasis(basis_set, center) - print('old coord before adjustment', old_coord) - print('particles', particle1, particle2, particle3) - adjusted_center = old_coord + particle1 - print('adjusted coord', adjusted_center) - return adjusted_center - - -class SmartDarting(SimNCMC): - """ - Class for performing smart darting moves during an NCMC simulation. - """ - - def __init__(self, *args, **kwds): - super(SmartDarting, self).__init__(**kwds) - self.dartboard = [] - self.n_dartboard = [] - self.particle_pairs = [] - self.particle_weights = [] - self.basis_particles = [] - self.dart_size = 0.2 * unit.nanometers - self.virtual_particles = [] - - def setDartUpdates(self, particle_pairs, particle_weights): - self.particle_pairs = particle_pairs - self.particle_weights = particle_weights - - def get_particle_masses(self, system, set_self=True, residueList=None): - if residueList == None: - residueList = self.residueList - mass_list = [] - total_mass = 0 * unit.dalton - for index in residueList: - mass = system.getParticleMass(index) - print('getting') - total_mass = total_mass + mass - print('mass', mass, 'total_mass', total_mass) - mass_list.append([mass]) - total_mass = np.sum(mass_list) - mass_list = np.asarray(mass_list) - mass_list.reshape((-1, 1)) - total_mass = np.array(total_mass) - total_mass = np.sum(mass_list) - temp_list = np.zeros((len(residueList), 1)) - for index in range(len(residueList)): - mass_list[index] = (np.sum(mass_list[index])).value_in_unit(unit.daltons) - mass_list = mass_list * unit.daltons - if set_self == True: - self.total_mass = total_mass - self.mass_list = mass_list - return total_mass, mass_list - - def dartsFromPDB(self, pdb_list, forcefield, residueList=None, basis_particles=None): - """ - Used to setup darts from a pdb, using the basis_particles to define - a new coordinate system. ResidueList corresponds to indicies of - the ligand residues. - - Arguments - --------- - pdb_list: list of str - List containing pdbs used to define smart darting - forcefield: str - Path of forcefield file - residueList: list of ints - List containing the ligand atom indices. - basis_particles: list of 3 ints - Specifies the 3 indices of particles whose coordinates will be used - as basis vectors. If None is specified, uses those found in basis particles. - - """ - if residueList == None: - residueList = self.residueList - if basis_particles == None: - basis_particles = self.basis_particles - n_dartboard = [] - dartboard = [] - for pdb_file in pdb_list: - pdb = PDBFile(pdb_file) - pdb.positions = pdb.positions.value_in_unit(unit.nanometers) - #print('pdbpositions,', pdb.positions) - #print('pdbtype', type(pdb.positions)) - #print('pdbtype', type(pdb.positions._value)) - - force = ForceField(forcefield) - system = force.createSystem(pdb.topology) - integrator = LangevinIntegrator(300 * kelvin, 1 / picosecond, 0.002 * picoseconds) - sim = Simulation(pdb.topology, system, integrator) - sim.context.setPositions(pdb.positions) - context_pos = sim.context.getState(getPositions=True).getPositions(asNumpy=True) - total_mass, mass_list = self.get_particle_masses(system, set_self=False, residueList=residueList) - com = self.calculate_com( - pos_state=context_pos, total_mass=total_mass, mass_list=mass_list, residueList=residueList) - #get particle positions - particle_pos = [] - for particle in basis_particles: - print('particle %i position' % (particle), context_pos[particle]) - particle_pos.append(context_pos[particle]) - new_coord = findNewCoord(particle_pos[0], particle_pos[1], particle_pos[2], com) - #keep this in for now to check code is correct - #old_coord should be equal to com - old_coord = findOldCoord(particle_pos[0], particle_pos[1], particle_pos[2], new_coord) - n_dartboard.append(new_coord) - dartboard.append(old_coord) - print('n_dartboard from pdb', n_dartboard) - print('dartboard from pdb', dartboard) - - self.n_dartboard = n_dartboard - self.dartboard = dartboard - - def dartsFromMDTraj(self, system, file_list, topology=None, residueList=None, basis_particles=None): - """ - Used to setup darts from a generic coordinate file, through MDtraj using the basis_particles to define - new basis vectors, which allows dart centers to remain consistant through a simulation. - This adds to the self.n_dartboard, which defines the centers used for smart darting. - - Arguments - --------- - system: simtk.openmm.system - Openmm System corresponding to the system to smart dart. - file_list: list of str - List containing coordinate files of the system for smart darting. - residueList: list of ints - List containing the ligand atom indices. If None uses self.residueList instead. - basis_particles: list of 3 ints - Specifies the 3 indices of particles whose coordinates will be used - as basis vectors. If None is specified, uses those found in basis particles. - If None uses self.basis_particles instead. - - Returns - ------- - n_dartboard: list of 1x3 np.arrays - Center of mass coordinates of residueList particles in new basis set. - - - """ - if residueList == None: - residueList = self.residueList - if basis_particles == None: - basis_particles = self.basis_particles - n_dartboard = [] - dartboard = [] - for md_file in file_list: - if topology == None: - temp_md = md.load(md_file) - else: - temp_md = md.load(md_file, top=topology) - context_pos = temp_md.openmm_positions(0) - print('context_pos', context_pos, 'context_pos') - print('context_pos type', type(context_pos._value)) - print('temp_md', temp_md) - context_pos = np.asarray(context_pos._value) * unit.nanometers - total_mass, mass_list = self.get_particle_masses(system, set_self=False, residueList=residueList) - com = self.calculate_com( - pos_state=context_pos, total_mass=total_mass, mass_list=mass_list, residueList=residueList) - #get particle positions - particle_pos = [] - for particle in basis_particles: - print('particle %i position' % (particle), context_pos[particle]) - particle_pos.append(context_pos[particle]) - new_coord = findNewCoord(particle_pos[0], particle_pos[1], particle_pos[2], com) - #keep this in for now to check code is correct - #old_coord should be equal to com - old_coord = findOldCoord(particle_pos[0], particle_pos[1], particle_pos[2], new_coord) - n_dartboard.append(new_coord) - dartboard.append(old_coord) - print('n_dartboard from pdb', n_dartboard) - print('dartboard from pdb', dartboard) - - self.n_dartboard = n_dartboard - self.dartboard = dartboard - - def dartsFromAmber(self, inpcrd_list, prmtop, residueList=None, basis_particles=None): - """ - Used to setup darts from an amber coordinate file, through MDtraj using the basis_particles to define - new basis vectors, which allows dart centers to remain consistant through a simulation. - This adds to the self.n_dartboard, which defines the centers used for smart darting. - - Parameters - ---------- - inpcrd_list: list of str - List of paths corresponding to inpcrd files from which to add smart darts - prmtop: str - Path of prmtop file to be sued with the inpcrds from inpcrd_list - residueList: list of ints - List containing the ligand atom indices. If None uses self.residueList instead. - basis_particles: list of 3 ints - Specifies the 3 indices of particles whose coordinates will be used - as basis vectors. If None is specified, uses those found in basis particles. - If None uses self.basis_particles instead. - - Returns - ------- - n_dartboard: list of 1x3 np.arrays - Center of mass coordinates of residueList particles in new basis set. - - """ - if residueList == None: - residueList = self.residueList - if basis_particles == None: - basis_particles = self.basis_particles - n_dartboard = [] - dartboard = [] - prmtop = AmberPrmtopFile(prmtop) - for inpcrd_file in inpcrd_list: - amber = AmberInpcrdFile(inpcrd_file) - system = prmtop.createSystem(nonbondedMethod=PME, nonbondedCutoff=1 * nanometer, constraints=HBonds) - integrator = LangevinIntegrator(300 * kelvin, 1 / picosecond, 0.002 * picoseconds) - sim = Simulation(prmtop.topology, system, integrator) - sim.context.setPositions(pdb.positions) - context_pos = sim.context.getState(getPositions=True).getPositions(asNumpy=True) - total_mass, mass_list = self.get_particle_masses(system, set_self=False, residueList=residueList) - com = self.calculate_com( - pos_state=context_pos, total_mass=total_mass, mass_list=mass_list, residueList=residueList) - #get particle positions - particle_pos = [] - for particle in basis_particles: - print('particle %i position' % (particle), context_pos[particle]) - particle_pos.append(context_pos[particle]) - new_coord = findNewCoord(particle_pos[0], particle_pos[1], particle_pos[2], com) - #keep this in for now to check code is correct - #old_coord should be equal to com - old_coord = findOldCoord(particle_pos[0], particle_pos[1], particle_pos[2], new_coord) - n_dartboard.append(new_coord) - dartboard.append(old_coord) - print('n_dartboard from pdb', n_dartboard) - print('dartboard from pdb', dartboard) - - self.n_dartboard = n_dartboard - self.dartboard = dartboard - - def add_dart(self, dart): - self.dartboard.append(dart) - - def addPart(self, particles): - self.basis_particles = particles[:] - - def findDart(self, particle_pairs=None, particle_weights=None): - """ - For dynamically updating dart positions based on positions - of other particles. - This takes the weighted average of the specified particles - and changes the dartboard of the object - - Arguments - --------- - particle_pairs: list of list of ints - each list defines the pairs to define darts - particle_weights: list of list of floats - Each list defines the weights assigned to each particle positions. - Returns - ------- - dart_list list of 1x3 np.arrays in units.nm - new dart positions calculated from the particle_pairs - and particle_weights - - """ - if particle_pairs == None: - particle_pairs = self.particle_pairs - if particle_weights == None: - particle_weights = self.particle_weights - #make sure there's an equal number of particle pair lists - #and particle weight lists - assert len(particle_pairs) == len(particle_weights) - - dart_list = [] - state_info = self.nc_context.getState(True, True, False, True, True, False) - temp_pos = state_info.getPositions(asNumpy=True) - #find particles positions and multiply by weights - for i, ppair in enumerate(particle_pairs): - temp_array = np.array([0, 0, 0]) * unit.nanometers - #weighted average - temp_wavg = 0 - for j, particle in enumerate(ppair): - print('temp_pos', particle, temp_pos[particle]) - temp_array += (temp_pos[particle] * float(particle_weights[i][j])) - temp_wavg += float(particle_weights[i][j]) - print(temp_array) - #divide by total number of particles in a list and append - #calculated postion to dart_list - dart_list.append(temp_array[:] / temp_wavg) - self.dartboard = dart_list[:] - return dart_list - - def n_findDart(self, basis_particles=None): - """ - Helper function to dynamically update dart positions based on positions - of other particles. - - - Arguments - --------- - basis_particles: list of 3 ints - Specifies the 3 indices of particles whose coordinates will be used - as basis vectors. If None is specified, uses those found in basis particles - - Returns - ------- - dart_list list of 1x3 np.arrays in units.nm - new dart positions calculated from the particle_pairs - and particle_weights - - """ - if basis_particles == None: - basis_particles = self.basis_particles - #make sure there's an equal number of particle pair lists - #and particle weight lists - dart_list = [] - state_info = self.nc_context.getState(True, True, False, True, True, False) - temp_pos = state_info.getPositions(asNumpy=True) - part1 = temp_pos[basis_particles[0]] - part2 = temp_pos[basis_particles[1]] - part3 = temp_pos[basis_particles[2]] - print('n_findDart before dartboard', self.dartboard) - for dart in self.n_dartboard: - print('particles', part1, part2, part3) - old_center = findOldCoord(part1, part2, part3, dart) - dart_list.append(old_center) - self.dartboard = dart_list[:] - print('n_findDart dartboard', self.dartboard) - return dart_list - - def virtualDart(self, virtual_particles=None): - """ - For dynamically updating dart positions based on positions - of other particles. - This takes the weighted average of the specified particles - and changes the dartboard of the object - - Arguments - --------- - virtual_particles: list of ints - Each int in the list specifies a particle - particle_weights: list of list of floats - each list defines the weights assigned to each particle positions - Returns - ------- - dart_list list of 1x3 np.arrays in units.nm - new dart positions calculated from the particle_pairs - and particle_weights - - """ - if virtual_particles == None: - virtual_particles = self.virtual_particles - - dart_list = [] - state_info = self.nc_context.getState(True, True, False, True, True, False) - temp_pos = state_info.getPositions(asNumpy=True) - #find virtual particles positions and add to dartboard - for particle in virtual_particles: - print('temp_pos', particle, temp_pos[particle]) - dart_list.append(temp_pos[particle]) - self.dartboard = dart_list[:] - return dart_list - - def calc_from_center(self, com): - - distList = [] - diffList = [] - indexList = [] - #Find the distances of the COM to each dart, appending - #the results to distList - for dart in self.dartboard: - diff = com - dart - dist = np.sqrt(np.sum((diff) * (diff))) * unit.nanometers - distList.append(dist) - diffList.append(diff) - selected = [] - #Find the dart(s) less than self.dart_size - for index, entry in enumerate(distList): - if entry <= self.dart_size: - selected.append(entry) - diff = diffList[index] - indexList.append(index) - #Dart error checking - #to ensure reversibility the COM should only be - #within self.dart_size of one dart - if len(selected) == 1: - return selected[0], diffList[indexList[0]] - elif len(selected) == 0: - return None, diff - elif len(selected) >= 2: - print(selected) - #COM should never be within two different darts - raise ValueError('sphere size overlap, check darts') - - def reDart(self, changevec): - """ - Helper function to choose a random dart and determine the vector - that would translate the COM to that dart center - """ - dartindex = np.random.randint(len(self.dartboard)) - dvector = self.dartboard[dartindex] - chboard = dvector + changevec - print('chboard', chboard) - return chboard - - def dartmove(self, context=None, residueList=None): - """ - Obsolete function kept for reference. - """ - if residueList == None: - residueList = self.residueList - if context == None: - self.nc_context - - stateinfo = self.context.getState(True, True, False, True, True, False) - oldDartPos = stateinfo.getPositions(asNumpy=True) - oldDartPE = stateinfo.getPotentialEnergy() - center = self.calculate_com(oldDartPos) - selectedboard, changevec = self.calc_from_center(com=center) - print('changevec', changevec) - if selectedboard != None: - #notes - #comMove is where the com ends up after accounting from where - #it was from the original dart center - #basically it's final displacement location - newDartPos = copy.deepcopy(oldDartPos) - comMove = self.reDart(changevec) - vecMove = comMove - center - for residue in residueList: - newDartPos[residue] = newDartPos[residue] + vecMove - context.setPositions(newDartPos) - newDartInfo = context.getState(True, True, False, True, True, False) - newDartPE = newDartInfo.getPotentialEnergy() - logaccept = -1.0 * (newDartPE - oldDartPE) * self.beta - randnum = math.log(np.random.random()) - print('logaccept', logaccept, randnum) - print('old/newPE', oldDartPE, newDartPE) - if logaccept >= randnum: - print('move accepted!') - self.acceptance = self.acceptance + 1 - else: - print('rejected') - context.setPositions(oldDartPos) - dartInfo = context.getState(True, False, False, False, False, False) - - return newDartInfo.getPositions(asNumpy=True) - - def justdartmove(self, context=None, residueList=None): - """ - Function for performing smart darting move with fixed coordinate darts - """ - if residueList == None: - residueList = self.residueList - if context == None: - context = self.nc_context - - stateinfo = context.getState(True, True, False, True, True, False) - oldDartPos = stateinfo.getPositions(asNumpy=True) - oldDartPE = stateinfo.getPotentialEnergy() - center = self.calculate_com(oldDartPos) - selectedboard, changevec = self.calc_from_center(com=center) - print('selectedboard', selectedboard) - print('changevec', changevec) - print('centermass', center) - if selectedboard != None: - newDartPos = copy.deepcopy(oldDartPos) - comMove = self.reDart(changevec) - print('comMove', comMove) - print('center', center) - vecMove = comMove - center - print('vecMove', vecMove) - for residue in residueList: - newDartPos[residue] = newDartPos[residue] + vecMove - print('worked') - print(newDartPos) - context.setPositions(newDartPos) - newDartInfo = context.getState(True, True, False, True, True, False) - # newDartPE = newDartInfo.getPotentialEnergy() - - return newDartInfo.getPositions(asNumpy=True) - - def updateDartMove(self, context=None, residueList=None): - """ - Function for performing smart darting move with darts that - depend on particle positions in the system - """ - - if residueList == None: - residueList = self.residueList - if context == None: - context = self.nc_context - - stateinfo = context.getState(True, True, False, True, True, False) - oldDartPos = stateinfo.getPositions(asNumpy=True) - oldDartPE = stateinfo.getPotentialEnergy() - self.n_findDart() - center = self.calculate_com(oldDartPos) - selectedboard, changevec = self.calc_from_center(com=center) - print('selectedboard', selectedboard) - print('changevec', changevec) - print('centermass', center) - if selectedboard != None: - newDartPos = copy.deepcopy(oldDartPos) - comMove = self.reDart(changevec) - print('comMove', comMove) - print('center', center) - vecMove = comMove - center - print('vecMove', vecMove) - for residue in residueList: - newDartPos[residue] = newDartPos[residue] + vecMove - print('worked') - print('old', oldDartPos) - print('new', newDartPos) - context.setPositions(newDartPos) - newDartInfo = context.getState(True, True, False, True, True, False) - # newDartPE = newDartInfo.getPotentialEnergy() - - return newDartInfo.getPositions(asNumpy=True) - - def virtualDartMove(self, context=None, residueList=None): - """ - Function for performing smart darting move with darts that - depend on particle positions in the system - """ - - if residueList == None: - residueList = self.residueList - if context == None: - context = self.nc_context - - stateinfo = context.getState(True, True, False, True, True, False) - oldDartPos = stateinfo.getPositions(asNumpy=True) - oldDartPE = stateinfo.getPotentialEnergy() - self.virtualDart() - center = self.calculate_com(oldDartPos) - selectedboard, changevec = self.calc_from_center(com=center) - print('selectedboard', selectedboard) - print('changevec', changevec) - print('centermass', center) - if selectedboard != None: - newDartPos = copy.deepcopy(oldDartPos) - comMove = self.reDart(changevec) - print('comMove', comMove) - print('center', center) - vecMove = comMove - center - print('vecMove', vecMove) - for residue in residueList: - newDartPos[residue] = newDartPos[residue] + vecMove - print('worked') - print(newDartPos) - context.setPositions(newDartPos) - newDartInfo = self.nc_context.getState(True, True, False, True, True, False) - # newDartPE = newDartInfo.getPotentialEnergy() - - return newDartInfo.getPositions(asNumpy=True) diff --git a/blues/reporters.py b/blues/storage.py similarity index 51% rename from blues/reporters.py rename to blues/storage.py index afa631cf..9781b083 100644 --- a/blues/reporters.py +++ b/blues/storage.py @@ -1,4 +1,6 @@ +import os import logging +import logging.config import sys import time @@ -10,11 +12,16 @@ from parmed import unit as u from parmed.geometry import box_vectors_to_lengths_and_angles from simtk.openmm import app +from simtk import openmm +import math import blues._version -import blues.reporters from blues.formats import * +from blues.utils import get_data_filename +VELUNIT = u.angstrom / u.picosecond +FRCUNIT = u.kilocalorie_per_mole / u.angstrom +logger = logging.getLogger(__name__) def _check_mode(m, modes): """ @@ -23,6 +30,31 @@ def _check_mode(m, modes): if m not in modes: raise ValueError('This operation is only available when a file ' 'is open in mode="%s".' % m) +def setup_logging(filename=None, + yml_path='logging.yml', + default_level=logging.INFO, + env_key='LOG_CFG'): + """Setup logging configuration + + """ + if not os.path.exists(yml_path): + yml_path = get_data_filename('blues', 'logging.yml') + path = yml_path + value = os.getenv(env_key, None) + if value: + path = value + if os.path.exists(path): + with open(path, 'rt') as f: + config = yaml.safe_load(f.read()) + if filename: + try: + config['handlers']['file_handler']['filename'] = str(filename) + except: + pass + logging.config.dictConfig(config) + else: + logging.basicConfig(level=default_level) + def addLoggingLevel(levelName, levelNum, methodName=None): """ @@ -63,11 +95,11 @@ def addLoggingLevel(levelName, levelNum, methodName=None): methodName = levelName.lower() if hasattr(logging, levelName): - logging.warn('{} already defined in logging module'.format(levelName)) + logging.warning('{} already defined in logging module'.format(levelName)) if hasattr(logging, methodName): - logging.warn('{} already defined in logging module'.format(methodName)) + logging.warning('{} already defined in logging module'.format(methodName)) if hasattr(logging.getLoggerClass(), methodName): - logging.warn('{} already defined in logger class'.format(methodName)) + logging.warning('{} already defined in logger class'.format(methodName)) # This method was inspired by the answers to Stack Overflow post # http://stackoverflow.com/q/2183233/2988730, especially @@ -93,7 +125,7 @@ def init_logger(logger, level=logging.INFO, stream=True, outfname=time.strftime( logger : logging.getLogger() The root logger object if it has been created already. level : logging. - Valid options for would be DEBUG, INFO, WARNING, ERROR, CRITICAL. + Valid options for would be DEBUG, INFO, warningING, ERROR, CRITICAL. stream : bool, default = True If True, the logger will also stream information to sys.stdout as well as the output file. @@ -125,231 +157,65 @@ def init_logger(logger, level=logging.INFO, stream=True, outfname=time.strftime( return logger - -class ReporterConfig: - """ - Generates a set of custom/recommended reporters for - BLUES simulations from YAML configuration. It can also be called - externally without a YAML configuration file. - - Parameters - ---------- - outfname : str, - Output filename prefix for files generated by the reporters. - reporter_config : dict - Dict of parameters for the md_reporters or ncmc_reporters. - Valid keys for reporters are: `state`, `traj_netcdf`, `restart`, - `progress`, and `stream`. All reporters except `stream` - are extensions of the parmed.openmm.reporters. More below: - - `state` : State data reporter for OpenMM simulations, but it is a little more generalized. Writes to a ``.ene`` file. For full list of parameters see `parmed.openmm.reporters.StateDataReporter`. - - `traj_netcdf` : Customized AMBER NetCDF (``.nc``) format reporter - - `restart` : Restart AMBER NetCDF (``.rst7``) format reporter - - `progress` : Write to a file (``.prog``), the progress report of how many steps has been done, how fast the simulation is running, and how much time is left (similar to the mdinfo file in Amber). File is overwritten at each reportInterval. For full list of parameters see `parmed.openmm.reporters.ProgressReporter` - - `stream` : Customized version of openmm.app.StateDataReporter.This - will instead stream/print the information to the terminal as opposed to - writing to a file. Takes the same parameters as the openmm.app.StateDataReporter - - logger : logging.Logger object - Provide the root logger for printing information. - - Examples - -------- - This class is intended to be called internally from `blues.config.set_Reporters`. - Below is an example to call this externally. - - >>> from blues.reporters import ReporterConfig - >>> import logging - >>> logger = logging.getLogger(__name__) - >>> md_reporters = { "restart": { "reportInterval": 1000 }, - "state" : { "reportInterval": 250 }, - "stream": { "progress": true, - "remainingTime": true, - "reportInterval": 250, - "speed": true, - "step": true, - "title": "md", - "totalSteps": 10000}, - "traj_netcdf": { "reportInterval": 250 } - } - >>> md_reporter_cfg = ReporterConfig(outfname='blues-test', md_reporters, logger) - >>> md_reporters_list = md_reporter_cfg.makeReporters() - - """ - - def __init__(self, outfname, reporter_config, logger=None): - - self._outfname = outfname - self._cfg = reporter_config - self._logger = logger - self.trajectory_interval = 0 - - def makeReporters(self): - """ - Returns a list of openmm Reporters based on the configuration at - initialization of the class. - """ - Reporters = [] - if 'state' in self._cfg.keys(): - - #Use outfname specified for reporter - if 'outfname' in self._cfg['state']: - outfname = self._cfg['state']['outfname'] - else: #Default to top level outfname - outfname = self._outfname - - state = parmed.openmm.reporters.StateDataReporter(outfname + '.ene', **self._cfg['state']) - Reporters.append(state) - - if 'traj_netcdf' in self._cfg.keys(): - - if 'outfname' in self._cfg['traj_netcdf']: - outfname = self._cfg['traj_netcdf']['outfname'] - else: - outfname = self._outfname - - #Store as an attribute for calculating time/frame - if 'reportInterval' in self._cfg['traj_netcdf'].keys(): - self.trajectory_interval = self._cfg['traj_netcdf']['reportInterval'] - - traj_netcdf = NetCDF4Reporter(outfname + '.nc', **self._cfg['traj_netcdf']) - Reporters.append(traj_netcdf) - - if 'restart' in self._cfg.keys(): - - if 'outfname' in self._cfg['restart']: - outfname = self._cfg['restart']['outfname'] - else: - outfname = self._outfname - - restart = parmed.openmm.reporters.RestartReporter(outfname + '.rst7', netcdf=True, **self._cfg['restart']) - Reporters.append(restart) - - if 'progress' in self._cfg.keys(): - - if 'outfname' in self._cfg['progress']: - outfname = self._cfg['progress']['outfname'] - else: - outfname = self._outfname - - progress = parmed.openmm.reporters.ProgressReporter(outfname + '.prog', **self._cfg['progress']) - Reporters.append(progress) - - if 'stream' in self._cfg.keys(): - if not self._logger: self._logger = logging.getLogger(__name__) - stream = blues.reporters.BLUESStateDataReporter(self._logger, **self._cfg['stream']) - Reporters.append(stream) - - return Reporters - - ###################### # REPORTERS # ###################### -class BLUESHDF5Reporter(HDF5Reporter): - """This is a subclass of the HDF5 class from mdtraj that handles - reporting of the trajectory. - - HDF5Reporter stores a molecular dynamics trajectory in the HDF5 format. - This object supports saving all kinds of information from the simulation -- - more than any other trajectory format. In addition to all of the options, - the topology of the system will also (of course) be stored in the file. All - of the information is compressed, so the size of the file is not much - different than DCD, despite the added flexibility. +class NetCDF4Storage(parmed.openmm.reporters.NetCDFReporter): + """ + Class to read or write NetCDF trajectory files + Inherited from `parmed.openmm.reporters.NetCDFReporter` Parameters ---------- - file : str, or HDF5TrajectoryFile - Either an open HDF5TrajecoryFile object to write to, or a string - specifying the filename of a new HDF5 file to save the trajectory to. - title : str, - String to specify the title of the HDF5 tables - frame_indices : list, frame numbers for writing the trajectory + file : str + Name of the file to write the trajectory to reportInterval : int - The interval (in time steps) at which to write frames. - coordinates : bool - Whether to write the coordinates to the file. - time : bool - Whether to write the current time to the file. - cell : bool - Whether to write the current unit cell dimensions to the file. - potentialEnergy : bool - Whether to write the potential energy to the file. - kineticEnergy : bool - Whether to write the kinetic energy to the file. - temperature : bool - Whether to write the instantaneous temperature to the file. - velocities : bool - Whether to write the velocities to the file. - atomSubset : array_like, default=None - Only write a subset of the atoms, with these (zero based) indices - to the file. If None, *all* of the atoms will be written to disk. + How frequently to write a frame to the trajectory + frame_indices : list, frame numbers for writing the trajectory + If this reporter is used for the NCMC simulation, + 0.5 will report at the moveStep and -1 will record at the last frame. + crds : bool=True + Should we write coordinates to this trajectory? (Default True) + vels : bool=False + Should we write velocities to this trajectory? (Default False) + frcs : bool=False + Should we write forces to this trajectory? (Default False) protocolWork : bool=False, Write the protocolWork for the alchemical process in the NCMC simulation alchemicalLambda : bool=False, Write the alchemicalLambda step for the alchemical process in the NCMC simulation. - parameters : dict - Dict of the simulation parameters. Useful for record keeping. - environment : bool - True will attempt to export your conda environment to JSON and - store the information in the HDF5 file. Useful for record keeping. - - Notes - ----- - If you use the ``atomSubset`` option to write only a subset of the atoms - to disk, the ``kineticEnergy``, ``potentialEnergy``, and ``temperature`` - fields will not change. They will still refer to the energy and temperature - of the *whole* system, and are not "subsetted" to only include the energy - of your subsystem. - """ - @property - def backend(self): - return BLUESHDF5TrajectoryFile - def __init__(self, file, reportInterval=1, - title='NCMC Trajectory', - coordinates=True, frame_indices=[], - time=False, - cell=True, - temperature=False, - potentialEnergy=False, - kineticEnergy=False, - velocities=False, - atomSubset=None, - protocolWork=True, - alchemicalLambda=True, - parameters=None, - environment=True): - - super(BLUESHDF5Reporter, self).__init__(file, reportInterval, coordinates, time, cell, potentialEnergy, - kineticEnergy, temperature, velocities, atomSubset) - self._protocolWork = bool(protocolWork) - self._alchemicalLambda = bool(alchemicalLambda) - - self._environment = bool(environment) - self._title = title - self._parameters = parameters - + crds=True, + vels=False, + frcs=False, + protocolWork=False, + alchemicalLambda=False): + """ + Create a NetCDFReporter instance. + """ + super(NetCDF4Storage, self).__init__(file, reportInterval, crds, vels, frcs) + self.crds, self.vels, self.frcs, self.protocolWork, self.alchemicalLambda = crds, vels, frcs, protocolWork, alchemicalLambda self.frame_indices = frame_indices if self.frame_indices: #If simulation.currentStep = 1, store the frame from the previous step. # i.e. frame_indices=[1,100] will store the first and frame 100 self.frame_indices = [x - 1 for x in frame_indices] - def describeNextReport(self, simulation): + def describeNextReport(self, context_state): """ Get information about the next report this object will generate. Parameters ---------- - simulation : :class:`app.Simulation` - The simulation to generate a report for + context_state : :class:`openmm.State` + The current state of the context Returns ------- @@ -357,83 +223,84 @@ def describeNextReport(self, simulation): nsteps is the number of steps until the next report pos, vel, frc, and ene are flags indicating whether positions, velocities, forces, and/or energies are needed from the Context - """ #Monkeypatch to report at certain frame indices if self.frame_indices: - if simulation.currentStep in self.frame_indices: + if context_state.currentStep in self.frame_indices: steps = 1 else: steps = -1 if not self.frame_indices: - steps_left = simulation.currentStep % self._reportInterval + steps_left = context_state.currentStep % self._reportInterval steps = self._reportInterval - steps_left - return (steps, self._coordinates, self._velocities, False, self._needEnergy) + return (steps, self.crds, self.vels, self.frcs, False) - def report(self, simulation, state): + def report(self, context_state, integrator): """Generate a report. Parameters ---------- - simulation : simtk.openmm.app.Simulation - The Simulation to generate a report for - state : simtk.openmm.State - The current state of the simulation + context_state : :class:`openmm.State` + The current state of the context + integrator : :class:`openmm.Integrator` + The integrator belonging to the given context """ - if not self._is_intialized: - self._initialize(simulation) - self._is_intialized = True - - self._checkForErrors(simulation, state) - - args = () - kwargs = {} - if self._coordinates: - coordinates = state.getPositions(asNumpy=True)[self._atomSlice] - coordinates = coordinates.value_in_unit(getattr(unit, self._traj_file.distance_unit)) - args = (coordinates, ) - if self._time: - kwargs['time'] = state.getTime() - if self._cell: - vectors = state.getPeriodicBoxVectors(asNumpy=True) - vectors = vectors.value_in_unit(getattr(unit, self._traj_file.distance_unit)) - a, b, c, alpha, beta, gamma = unitcell.box_vectors_to_lengths_and_angles(*vectors) - kwargs['cell_lengths'] = np.array([a, b, c]) - kwargs['cell_angles'] = np.array([alpha, beta, gamma]) - if self._potentialEnergy: - kwargs['potentialEnergy'] = state.getPotentialEnergy() - if self._kineticEnergy: - kwargs['kineticEnergy'] = state.getKineticEnergy() - if self._temperature: - kwargs['temperature'] = 2 * state.getKineticEnergy() / (self._dof * unit.MOLAR_GAS_CONSTANT_R) - if self._velocities: - kwargs['velocities'] = state.getVelocities(asNumpy=True)[self._atomSlice, :] + global VELUNIT, FRCUNIT - #add a portion like this to store things other than the protocol work - if self._protocolWork: - protocol_work = simulation.integrator.get_protocol_work(dimensionless=True) - kwargs['protocolWork'] = np.array([protocol_work]) - if self._alchemicalLambda: - kwargs['alchemicalLambda'] = np.array([simulation.integrator.getGlobalVariableByName('lambda')]) - if self._title: - kwargs['title'] = self._title - if self._parameters: - kwargs['parameters'] = self._parameters - if self._environment: - kwargs['environment'] = self._environment - - self._traj_file.write(*args, **kwargs) - # flush the file to disk. it might not be necessary to do this every - # report, but this is the most proactive solution. We don't want to - # accumulate a lot of data in memory only to find out, at the very - # end of the run, that there wasn't enough space on disk to hold the - # data. - if hasattr(self._traj_file, 'flush'): - self._traj_file.flush() - - -class BLUESStateDataReporter(app.StateDataReporter): + if self.crds: + crds = context_state.getPositions().value_in_unit(u.angstrom) + if self.vels: + vels = context_state.getVelocities().value_in_unit(VELUNIT) + if self.frcs: + frcs = context_state.getForces().value_in_unit(FRCUNIT) + if self.protocolWork: + protocolWork = integrator.get_protocol_work(dimensionless=True) + if self.alchemicalLambda: + alchemicalLambda = integrator.getGlobalVariableByName('lambda') + if self._out is None: + # This must be the first frame, so set up the trajectory now + if self.crds: + atom = len(crds) + elif self.vels: + atom = len(vels) + elif self.frcs: + atom = len(frcs) + self.uses_pbc = context_state.getPeriodicBoxVectors() is not None + self._out = NetCDF4Traj.open_new( + self.fname, + atom, + self.uses_pbc, + self.crds, + self.vels, + self.frcs, + title="ParmEd-created trajectory using OpenMM", + protocolWork=self.protocolWork, + alchemicalLambda=self.alchemicalLambda, + ) + + if self.uses_pbc: + vecs = context_state.getPeriodicBoxVectors() + lengths, angles = box_vectors_to_lengths_and_angles(*vecs) + self._out.add_cell_lengths_angles(lengths.value_in_unit(u.angstrom), angles.value_in_unit(u.degree)) + + # Add the coordinates, velocities, and/or forces as needed + if self.crds: + self._out.add_coordinates(crds) + if self.vels: + # The velocities get scaled right before writing + self._out.add_velocities(vels) + if self.frcs: + self._out.add_forces(frcs) + if self.protocolWork: + self._out.add_protocolWork(protocolWork) + if self.alchemicalLambda: + self._out.add_alchemicalLambda(alchemicalLambda) + # Now it's time to add the time. + self._out.add_time(context_state.getTime().value_in_unit(u.picosecond)) + + +class BLUESStateDataStorage(app.StateDataReporter): """StateDataReporter outputs information about a simulation, such as energy and temperature, to a file. To use it, create a StateDataReporter, then add it to the Simulation's list of reporters. The set of data to write is configurable using boolean flags passed to the constructor. By default the data is written in comma-separated-value (CSV) format, but you can specify a different separator to use. Inherited from `openmm.app.StateDataReporter` Parameters @@ -496,7 +363,7 @@ class BLUESStateDataReporter(app.StateDataReporter): """ def __init__(self, - file, + file=None, reportInterval=1, frame_indices=[], title='', @@ -518,10 +385,9 @@ def __init__(self, protocolWork=False, alchemicalLambda=False, currentIter=False): - super(BLUESStateDataReporter, self).__init__( - file, reportInterval, step, time, potentialEnergy, kineticEnergy, totalEnergy, temperature, volume, - density, progress, remainingTime, speed, elapsedTime, separator, systemMass, totalSteps) - self.log = self._out + super(BLUESStateDataStorage, self).__init__(file, reportInterval, step, time, potentialEnergy, kineticEnergy, + totalEnergy, temperature, volume, density, progress, remainingTime, + speed, elapsedTime, separator, systemMass, totalSteps) self.title = title self.frame_indices = frame_indices @@ -531,14 +397,14 @@ def __init__(self, # i.e. frame_indices=[1,100] will store the first and frame 100 self.frame_indices = [x - 1 for x in frame_indices] - def describeNextReport(self, simulation): + def describeNextReport(self, context_state): """ Get information about the next report this object will generate. Parameters ---------- - simulation : :class:`app.Simulation` - The simulation to generate a report for + context_state : :class:`openmm.State` + The current state of the context Returns ------- @@ -550,64 +416,114 @@ def describeNextReport(self, simulation): """ #Monkeypatch to report at certain frame indices if self.frame_indices: - if simulation.currentStep in self.frame_indices: + if context_state.currentStep in self.frame_indices: steps = 1 else: steps = -1 if not self.frame_indices: - steps_left = simulation.currentStep % self._reportInterval + steps_left = context_state.currentStep % self._reportInterval steps = self._reportInterval - steps_left return (steps, self._needsPositions, self._needsVelocities, self._needsForces, self._needEnergy) - def report(self, simulation, state): + def _initializeConstants(self, context_state): + """Initialize a set of constants required for the reports + + Parameters + ---------- + context_state : :class:`openmm.State` + The current state of the context + """ + system = context_state.system + if self._temperature: + # Compute the number of degrees of freedom. + dof = 0 + for i in range(system.getNumParticles()): + if system.getParticleMass(i) > 0 * unit.dalton: + dof += 3 + dof -= system.getNumConstraints() + if any(type(system.getForce(i)) == openmm.CMMotionRemover for i in range(system.getNumForces())): + dof -= 3 + self._dof = dof + if self._density: + if self._totalMass is None: + # Compute the total system mass. + self._totalMass = 0 * unit.dalton + for i in range(system.getNumParticles()): + self._totalMass += system.getParticleMass(i) + elif not unit.is_quantity(self._totalMass): + self._totalMass = self._totalMass * unit.dalton + + def report(self, context_state, integrator): """Generate a report. Parameters ---------- - simulation : Simulation - The Simulation to generate a report for - state : State - The current state of the simulation + context_state : :class:`openmm.State` + The current state of the context + integrator : :class:`openmm.Integrator` + The integrator belonging to the given context """ if not self._hasInitialized: - self._initializeConstants(simulation) + self._initializeConstants(context_state) headers = self._constructHeaders() - if hasattr(self.log, 'report'): - self.log.info = self.log.report - self.log.info('#"%s"' % ('"' + self._separator + '"').join(headers)) + headers_msg = '#"%s"' % ('"' + self._separator + '"').join(headers) + if isinstance(self._out, logging.Logger): + logger.info(headers_msg) + else: + print(headers_msg, file=self._out) try: self._out.flush() except AttributeError: pass self._initialClockTime = time.time() - self._initialSimulationTime = state.getTime() - self._initialSteps = simulation.currentStep + self._initialSimulationTime = context_state.getTime() + self._initialSteps = context_state.currentStep self._hasInitialized = True # Check for errors. - self._checkForErrors(simulation, state) + self._checkForErrors(context_state, integrator) # Query for the values - values = self._constructReportValues(simulation, state) + values = self._constructReportValues(context_state, integrator) # Write the values. - if hasattr(self.log, 'report'): - self.log.info = self.log.report - self.log.info('%s: %s' % (self.title, self._separator.join(str(v) for v in values))) + msg = '%s: %s' % (self.title, self._separator.join(str(v) for v in values)) + if isinstance(self._out, logging.Logger): + logger.info(msg) + else: + print(msg, file=self._out) try: self._out.flush() except AttributeError: pass - def _constructReportValues(self, simulation, state): - """Query the simulation for the current state of our observables of interest. + def _checkForErrors(self, context_state, integrator): + """Check for errors in the current state of the context + + Parameters + ---------- + context_state : :class:`openmm.State` + The current state of the context + integrator : :class:`openmm.Integrator` + The integrator belonging to the given context + """ + if self._needEnergy: + energy = (context_state.getKineticEnergy() + context_state.getPotentialEnergy()).value_in_unit( + unit.kilojoules_per_mole) + if math.isnan(energy): + raise ValueError('Energy is NaN') + if math.isinf(energy): + raise ValueError('Energy is infinite') + + def _constructReportValues(self, context_state, integrator): + """Query the contextfor the current state of our observables of interest. Parameters ---------- - simulation : Simulation - The Simulation to generate a report for - state : State - The current state of the simulation + context_state : :class:`openmm.State` + The current state of the context + integrator : :class:`openmm.Integrator` + The integrator belonging to the given context Returns ------- @@ -617,36 +533,37 @@ def _constructReportValues(self, simulation, state): of the columns in the resulting CSV file. """ values = [] - box = state.getPeriodicBoxVectors() + box = context_state.getPeriodicBoxVectors() volume = box[0][0] * box[1][1] * box[2][2] clockTime = time.time() - if self._currentIter: - if not hasattr(simulation, 'currentIter'): - simulation.currentIter = 0 - values.append(simulation.currentIter) + # if self._currentIter: + # if not hasattr(simulation, 'currentIter'): + # simulation.currentIter = 0 + # values.append(simulation.currentIter) if self._progress: - values.append('%.1f%%' % (100.0 * simulation.currentStep / self._totalSteps)) + values.append('%.1f%%' % (100.0 * context_state.currentStep / self._totalSteps)) if self._step: - values.append(simulation.currentStep) + values.append(context_state.currentStep) if self._time: - values.append(state.getTime().value_in_unit(unit.picosecond)) + values.append(context_state.getTime().value_in_unit(unit.picosecond)) #add a portion like this to store things other than the protocol work if self._alchemicalLambda: - alchemicalLambda = simulation.integrator.getGlobalVariableByName('lambda') + alchemicalLambda = integrator.getGlobalVariableByName('lambda') values.append(alchemicalLambda) if self._protocolWork: - protocolWork = simulation.integrator.get_protocol_work(dimensionless=True) + protocolWork = integrator.get_protocol_work(dimensionless=True) values.append(protocolWork) if self._potentialEnergy: - values.append(state.getPotentialEnergy().value_in_unit(unit.kilojoules_per_mole)) + values.append(context_state.getPotentialEnergy().value_in_unit(unit.kilojoules_per_mole)) if self._kineticEnergy: - values.append(state.getKineticEnergy().value_in_unit(unit.kilojoules_per_mole)) + values.append(context_state.getKineticEnergy().value_in_unit(unit.kilojoules_per_mole)) if self._totalEnergy: - values.append( - (state.getKineticEnergy() + state.getPotentialEnergy()).value_in_unit(unit.kilojoules_per_mole)) + values.append((context_state.getKineticEnergy() + context_state.getPotentialEnergy()).value_in_unit( + unit.kilojoules_per_mole)) if self._temperature: values.append( - (2 * state.getKineticEnergy() / (self._dof * unit.MOLAR_GAS_CONSTANT_R)).value_in_unit(unit.kelvin)) + (2 * context_state.getKineticEnergy() / (self._dof * unit.MOLAR_GAS_CONSTANT_R)).value_in_unit( + unit.kelvin)) if self._volume: values.append(volume.value_in_unit(unit.nanometer**3)) if self._density: @@ -654,7 +571,7 @@ def _constructReportValues(self, simulation, state): if self._speed: elapsedDays = (clockTime - self._initialClockTime) / 86400.0 - elapsedNs = (state.getTime() - self._initialSimulationTime).value_in_unit(unit.nanosecond) + elapsedNs = (context_state.getTime() - self._initialSimulationTime).value_in_unit(unit.nanosecond) if elapsedDays > 0.0: values.append('%.3g' % (elapsedNs / elapsedDays)) else: @@ -663,7 +580,7 @@ def _constructReportValues(self, simulation, state): values.append(time.time() - self._initialClockTime) if self._remainingTime: elapsedSeconds = clockTime - self._initialClockTime - elapsedSteps = simulation.currentStep - self._initialSteps + elapsedSteps = context_state.currentStep - self._initialSteps if elapsedSteps == 0: value = '--' else: @@ -695,8 +612,8 @@ def _constructHeaders(self): a list of strings giving the title of each observable being reported on. """ headers = [] - if self._currentIter: - headers.append('Iter') + # if self._currentIter: + # headers.append('Iter') if self._progress: headers.append('Progress (%)') if self._step: @@ -726,140 +643,3 @@ def _constructHeaders(self): if self._remainingTime: headers.append('Time Remaining') return headers - - -class NetCDF4Reporter(parmed.openmm.reporters.NetCDFReporter): - """ - Class to read or write NetCDF trajectory files - Inherited from `parmed.openmm.reporters.NetCDFReporter` - - Parameters - ---------- - file : str - Name of the file to write the trajectory to - reportInterval : int - How frequently to write a frame to the trajectory - frame_indices : list, frame numbers for writing the trajectory - If this reporter is used for the NCMC simulation, - 0.5 will report at the moveStep and -1 will record at the last frame. - crds : bool=True - Should we write coordinates to this trajectory? (Default True) - vels : bool=False - Should we write velocities to this trajectory? (Default False) - frcs : bool=False - Should we write forces to this trajectory? (Default False) - protocolWork : bool=False, - Write the protocolWork for the alchemical process in the NCMC simulation - alchemicalLambda : bool=False, - Write the alchemicalLambda step for the alchemical process in the NCMC simulation. - """ - - def __init__(self, - file, - reportInterval=1, - frame_indices=[], - crds=True, - vels=False, - frcs=False, - protocolWork=False, - alchemicalLambda=False): - """ - Create a NetCDFReporter instance. - """ - super(NetCDF4Reporter, self).__init__(file, reportInterval, crds, vels, frcs) - self.crds, self.vels, self.frcs, self.protocolWork, self.alchemicalLambda = crds, vels, frcs, protocolWork, alchemicalLambda - self.frame_indices = frame_indices - if self.frame_indices: - #If simulation.currentStep = 1, store the frame from the previous step. - # i.e. frame_indices=[1,100] will store the first and frame 100 - self.frame_indices = [x - 1 for x in frame_indices] - - def describeNextReport(self, simulation): - """ - Get information about the next report this object will generate. - - Parameters - ---------- - simulation : :class:`app.Simulation` - The simulation to generate a report for - - Returns - ------- - nsteps, pos, vel, frc, ene : int, bool, bool, bool, bool - nsteps is the number of steps until the next report - pos, vel, frc, and ene are flags indicating whether positions, - velocities, forces, and/or energies are needed from the Context - """ - #Monkeypatch to report at certain frame indices - if self.frame_indices: - if simulation.currentStep in self.frame_indices: - steps = 1 - else: - steps = -1 - if not self.frame_indices: - steps_left = simulation.currentStep % self._reportInterval - steps = self._reportInterval - steps_left - return (steps, self.crds, self.vels, self.frcs, False) - - def report(self, simulation, state): - """Generate a report. - - Parameters - ---------- - simulation : :class:`app.Simulation` - The Simulation to generate a report for - state : :class:`mm.State` - The current state of the simulation - - """ - global VELUNIT, FRCUNIT - if self.crds: - crds = state.getPositions().value_in_unit(u.angstrom) - if self.vels: - vels = state.getVelocities().value_in_unit(VELUNIT) - if self.frcs: - frcs = state.getForces().value_in_unit(FRCUNIT) - if self.protocolWork: - protocolWork = simulation.integrator.get_protocol_work(dimensionless=True) - if self.alchemicalLambda: - alchemicalLambda = simulation.integrator.getGlobalVariableByName('lambda') - if self._out is None: - # This must be the first frame, so set up the trajectory now - if self.crds: - atom = len(crds) - elif self.vels: - atom = len(vels) - elif self.frcs: - atom = len(frcs) - self.uses_pbc = simulation.topology.getUnitCellDimensions() is not None - self._out = NetCDF4Traj.open_new( - self.fname, - atom, - self.uses_pbc, - self.crds, - self.vels, - self.frcs, - title="ParmEd-created trajectory using OpenMM", - protocolWork=self.protocolWork, - alchemicalLambda=self.alchemicalLambda, - ) - - if self.uses_pbc: - vecs = state.getPeriodicBoxVectors() - lengths, angles = box_vectors_to_lengths_and_angles(*vecs) - self._out.add_cell_lengths_angles(lengths.value_in_unit(u.angstrom), angles.value_in_unit(u.degree)) - - # Add the coordinates, velocities, and/or forces as needed - if self.crds: - self._out.add_coordinates(crds) - if self.vels: - # The velocities get scaled right before writing - self._out.add_velocities(vels) - if self.frcs: - self._out.add_forces(frcs) - if self.protocolWork: - self._out.add_protocolWork(protocolWork) - if self.alchemicalLambda: - self._out.add_alchemicalLambda(alchemicalLambda) - # Now it's time to add the time. - self._out.add_time(state.getTime().value_in_unit(u.picosecond)) diff --git a/blues/switching.py b/blues/switching.py deleted file mode 100644 index b955e8ce..00000000 --- a/blues/switching.py +++ /dev/null @@ -1,1360 +0,0 @@ -""" -ncmc_switching.py: Provides an classes for controlling the primary NCMC engine, -and related alchemical functions. - -*Adapted from source code: -#https://github.com/choderalab/perses/blob/master/perses/annihilation/ncmc_switching.py - -Authors: Patrick B. Grinaway, Julie M. Behr, and John D. Chodera -Contributors: Samuel C. Gill -""" - -from __future__ import print_function - -import copy -import traceback - -import numpy as np -from simtk import openmm, unit - -default_functions = { - 'lambda_sterics': '2*lambda * step(0.5 - lambda) + (1.0 - step(0.5 - lambda))', - 'lambda_electrostatics': '2*(lambda - 0.5) * step(lambda - 0.5)', - 'lambda_bonds': '0.9*lambda + 0.1', # don't fully soften bonds - 'lambda_angles': '0.9*lambda + 0.1', # don't fully soften angles - 'lambda_torsions': 'lambda' -} - -functions_disable_all = { - 'lambda_sterics': 'lambda', - 'lambda_electrostatics': 'lambda', - 'lambda_bonds': 'lambda', - 'lambda_angles': 'lambda', - 'lambda_torsions': 'lambda' -} - -# make something hyperbolic or something to go from on to off to on -default_hybrid_functions = { - 'lambda_sterics': 'lambda', - 'lambda_electrostatics': 'lambda', - 'lambda_bonds': 'lambda', - 'lambda_angles': 'lambda', - 'lambda_torsions': 'lambda' -} - -default_temperature = 300.0 * unit.kelvin -default_nsteps = 1 -default_timestep = 1.0 * unit.femtoseconds -default_steps_per_propagation = 1 - - -class NaNException(Exception): - def __init__(self, *args, **kwargs): - super(NaNException, self).__init__(*args, **kwargs) - - -class NCMCEngine(object): - """ - NCMC switching engine - - Examples - -------- - - Create a transformation for an alanine dipeptide test system where the N-methyl group is eliminated. - - >>> from openmmtools import testsystems - >>> testsystem = testsystems.AlanineDipeptideVacuum() - >>> from perses.rjmc.topology_proposal import TopologyProposal - >>> new_to_old_atom_map = { index : index for index in range(testsystem.system.getNumParticles()) if (index > 3) } # all atoms but N-methyl - >>> topology_proposal = TopologyProposal(old_system=testsystem.system, old_topology=testsystem.topology, old_chemical_state_key='AA', new_chemical_state_key='AA', new_system=testsystem.system, new_topology=testsystem.topology, logp_proposal=0.0, new_to_old_atom_map=new_to_old_atom_map, metadata=dict()) - >>> ncmc_engine = NCMCEngine(temperature=300.0*unit.kelvin, functions=default_functions, nsteps=50, timestep=1.0*unit.femtoseconds) - >>> positions = testsystem.positions - >>> [positions, logP_delete, potential_delete] = ncmc_engine.integrate(topology_proposal, positions, direction='delete') - >>> [positions, logP_insert, potential_insert] = ncmc_engine.integrate(topology_proposal, positions, direction='insert') - - """ - - def __init__(self, - temperature=default_temperature, - functions=None, - nsteps=default_nsteps, - steps_per_propagation=default_steps_per_propagation, - timestep=default_timestep, - constraint_tolerance=None, - platform=None, - write_ncmc_interval=None, - integrator_type='GHMC', - storage=None, - verbose=False): - """ - This is the base class for NCMC switching between two different systems. - - Arguments - --------- - temperature : simtk.unit.Quantity with units compatible with kelvin - The temperature at which switching is to be run - functions : dict of str:str, optional, default=default_functions - functions[parameter] is the function (parameterized by 't' which switched from 0 to 1) that - controls how alchemical context parameter 'parameter' is switched - nsteps : int, optional, default=1 - The number of steps to use for switching. - steps_per_propagation : int, optional, default=1 - The number of intermediate propagation steps taken at each switching step - timestep : simtk.unit.Quantity with units compatible with femtoseconds, optional, default=1*femtosecond - The timestep to use for integration of switching velocity Verlet steps. - constraint_tolerance : float, optional, default=None - If not None, this relative constraint tolerance is used for position and velocity constraints. - platform : simtk.openmm.Platform, optional, default=None - If specified, the platform to use for OpenMM simulations. - write_ncmc_interval : int, optional, default=None - If a positive integer is specified, a snapshot frame will be written to storage with the specified interval on NCMC switching. - 'storage' must also be specified. - integrator_type : str, optional, default='GHMC' - NCMC internal integrator type ['GHMC', 'VV'] - storage : NetCDFStorageView, optional, default=None - If specified, write data using this class. - verbose : bool, optional, default=False - If True, print debug information. - """ - # Handle some defaults. - if functions == None: - functions = default_functions - if nsteps == None: - nsteps = default_nsteps - if timestep == None: - timestep = default_timestep - if temperature == None: - temperature = default_temperature - - self.temperature = temperature - self.functions = copy.deepcopy(functions) - self.nsteps = nsteps - self.timestep = timestep - self.constraint_tolerance = constraint_tolerance - self.platform = platform - self.integrator_type = integrator_type - self.steps_per_propagation = steps_per_propagation - self.verbose = verbose - self.disable_barostat = False - - self.nattempted = 0 - - self._storage = None - if storage is not None: - self._storage = NetCDFStorageView(storage, modname=self.__class__.__name__) - self.write_ncmc_interval = write_ncmc_interval - - @property - def beta(self): - kB = unit.BOLTZMANN_CONSTANT_kB * unit.AVOGADRO_CONSTANT_NA - kT = kB * self.temperature - beta = 1.0 / kT - return beta - - def _getAvailableParameters(self, system, prefix='lambda'): - """ - Return a list of available alchemical context parameters defined in the system - - Parameters - ---------- - system : simtk.openmm.System - The system for which available context parameters are to be determined - prefix : str, optional, default='lambda' - Prefix required for parameters to be returned. - - Returns - ------- - parameters : list of str - The list of available context parameters in the system - - """ - parameters = list() - for force_index in range(system.getNumForces()): - force = system.getForce(force_index) - if hasattr(force, 'getNumGlobalParameters'): - for parameter_index in range(force.getNumGlobalParameters()): - parameter_name = force.getGlobalParameterName(parameter_index) - if parameter_name[0:(len(prefix) + 1)] == (prefix + '_'): - parameters.append(parameter_name) - return parameters - - def _computeEnergyContribution(self, integrator): - """ - Compute NCMC energy contribution to log probability. - - See Eqs. 62 and 63 (two-stage) and Eq. 45 (hybrid) of reference document. - In both cases, the contribution is u(final_positions, final_lambda) - u(initial_positions, initial_lambda). - - Parameters - ---------- - itegrator : NCMCAlchemicalIntegrator subclasses - NCMC switching integrator to annihilate or introduce particles alchemically. - context : openmm.Context - Alchemical context - system : simtk.unit.System - Real fully-interacting system. - initial_positions : simtk.unit.Quantity of dimensions [nparticles,3] with units compatible with angstroms - The positions of the alchemical system at the start of the NCMC protocol - final_positions : simtk.unit.Quantity of dimensions [nparticles,3] with units compatible with angstroms - The positions of the alchemical system at the end of the NCMC protocol - direction : str, optional, default='insert' - Direction of topology proposal to use for identifying alchemical atoms (allowed values: ['insert', 'delete']) - - Returns - ------- - logP_energy : float - The NCMC energy contribution to log probability. - """ - logP = integrator.getGlobalVariableByName("final_reduced_potential") - integrator.getGlobalVariableByName( - "initial_reduced_potential") - - if np.isnan(logP): - msg = "A required potential of NCMC operation is NaN:\n" - msg += "initial_reduced_potential: %.3f kT\n" % integrator.getGlobalVariableByName( - "initial_reduced_potential") - msg += "final_reduced_potential: %.3f kT\n" % integrator.getGlobalVariableByName( - "final_reduced_potential") - raise NaNException(msg) - - return logP - - def _choose_system_from_direction(self, topology_proposal, direction): - """ - Based on the direction, return a topology, indices of alchemical - atoms, and system which relate to the chemical state being modified. - - Parameters - ---------- - topology_proposal : TopologyProposal - Contains old/new Topology and System objects and atom mappings. - direction : str, optional, default='insert' - Direction of topology proposal to use for identifying alchemical atoms (allowed values: ['insert', 'delete']) - - Returns - ------- - topology : openmm.app.Topology - Alchemical topology being modified - indices : list(int) - List of the indices of atoms that are turned on / off - unmodified_system : simtk.openmm.System - Unmodified real system corresponding to appropriate leg of transformation. - """ - # Select reference topology, indices, and system based on whether we are deleting or inserting. - if direction == 'delete': - return topology_proposal.old_topology, topology_proposal.unique_old_atoms, topology_proposal.old_system - elif direction == 'insert': - return topology_proposal.new_topology, topology_proposal.unique_new_atoms, topology_proposal.new_system - - def make_alchemical_system(self, unmodified_system, alchemical_atoms, direction='insert'): - """ - Generate an alchemically-modified system at the correct atoms - based on the topology proposal - - Arguments - --------- - unmodified_system : simtk.openmm.System - Unmodified real system corresponding to appropriate leg of transformation. - alchemical_atoms : list(int) - List of the indices of atoms that are turned on / off - direction : str, optional, default='insert' - Direction of topology proposal to use for identifying alchemical atoms (allowed values: ['insert', 'delete']) - - Returns - ------- - alchemical_system : simtk.openmm.System - The system with appropriate atoms alchemically modified - """ - # Create an alchemical factory. - from alchemy import AbsoluteAlchemicalFactory - alchemical_factory = AbsoluteAlchemicalFactory( - unmodified_system, - ligand_atoms=alchemical_atoms, - annihilate_electrostatics=True, - annihilate_sterics=True, - alchemical_torsions=True, - alchemical_bonds=True, - alchemical_angles=True, - softcore_beta=0.0) - - # Return the alchemically-modified system in fully-interacting form. - alchemical_system = alchemical_factory.createPerturbedSystem() - - if self.disable_barostat: - for force in alchemical_system.getForces(): - if hasattr(force, 'setFrequency'): - force.setFrequency(0) - - return alchemical_system - - def _integrate_switching(self, integrator, context, topology, indices, iteration, direction): - """ - Runs `self.nsteps` integrator steps - - For `delete`, lambda will go from 1 to 0 - For `insert`, lambda will go from 0 to 1 - - Parameters - ---------- - itegrator : NCMCAlchemicalIntegrator subclasses - NCMC switching integrator to annihilate or introduce particles alchemically. - context : openmm.Context - Alchemical context - topology : openmm.app.Topology - Alchemical topology being modified - indices : list(int) - List of the indices of atoms that are turned on / off - iteration : int or None - Iteration number, for storage purposes. - direction : str - Direction of alchemical switching: - 'insert' causes lambda to switch from 0 to 1 over nsteps steps of integration - 'delete' causes lambda to switch from 1 to 0 over nsteps steps of integration - - Returns - ------- - final_positions : simtk.unit.Quantity of dimensions [nparticles,3] with units compatible with angstroms - The final positions after `nsteps` steps of alchemical switching - logP_NCMC : float - The log acceptance probability of the NCMC moves - """ - # Integrate switching - try: - # Write atom indices that are changing. - if self._storage: - self._storage.write_object('atomindices', indices, iteration=iteration) - - nsteps = max(1, - self.nsteps) # we must take 1 step even if nsteps = 0 to run the integrator through one cycle - - # Allocate storage for work. - total_work = np.zeros([nsteps + 1], np.float64) # work[n] is the accumulated total work up to step n - shadow_work = np.zeros([nsteps + 1], np.float64) # work[n] is the accumulated shadow work up to step n - protocol_work = np.zeros([nsteps + 1], np.float64) # work[n] is the accumulated protocol work up to step n - - # Write trajectory frame. - if self._storage and self.write_ncmc_interval: - positions = context.getState(getPositions=True).getPositions(asNumpy=True) - self._storage.write_configuration( - 'positions', positions, topology, iteration=iteration, frame=0, nframes=(self.nsteps + 1)) - - # Perform NCMC integration. - for step in range(nsteps): - # Take a step. - try: - integrator.step(1) - except Exception as e: - print(e) - for index in range(integrator.getNumGlobalVariables()): - name = integrator.getGlobalVariableName(index) - val = integrator.getGlobalVariable(index) - print(name, val) - for index in range(integrator.getNumPerDofVariables()): - name = integrator.getPerDofVariableName(index) - val = integrator.getPerDofVariable(index) - print(name, val) - - # Store accumulated work - total_work[step + 1] = integrator.getTotalWork(context) - shadow_work[step + 1] = integrator.getShadowWork(context) - protocol_work[step + 1] = integrator.getProtocolWork(context) - - # Write trajectory frame. - if self._storage and self.write_ncmc_interval and (self.write_ncmc_interval % (step + 1) == 0): - positions = context.getState(getPositions=True).getPositions(asNumpy=True) - assert quantity_is_finite(positions) == True - self._storage.write_configuration( - 'positions', - positions, - topology, - iteration=iteration, - frame=(step + 1), - nframes=(self.nsteps + 1)) - - # Store work values. - if self._storage: - self._storage.write_array('total_work_%s' % direction, total_work, iteration=iteration) - self._storage.write_array('shadow_work_%s' % direction, shadow_work, iteration=iteration) - self._storage.write_array('protocol_work_%s' % direction, protocol_work, iteration=iteration) - - except Exception as e: - # Trap NaNs as a special exception (allowing us to reject later, if desired) - if str(e) == "Particle coordinate is nan": - msg = "Particle coordinate is nan during NCMC integration while using integrator_type '%s'" % self.integrator_type - if self.integrator_type == 'GHMC': - msg += '\n' - msg += 'This should NEVER HAPPEN with GHMC!' - raise NaNException(msg) - else: - traceback.print_exc() - raise e - - # Store final positions and log acceptance probability. - final_positions = context.getState(getPositions=True).getPositions(asNumpy=True) - assert quantity_is_finite(final_positions) == True - logP_NCMC = integrator.getLogAcceptanceProbability(context) - return final_positions, logP_NCMC - - def _choose_integrator(self, alchemical_system, functions, direction): - """ - Instantiate the appropriate type of NCMC integrator, setting - constraint tolerance if specified. - - Parameters - ---------- - alchemical_system : simtk.openmm.System - The system with appropriate atoms alchemically modified - functions : dict - functions[parameter] is the function (parameterized by 't' which switched from 0 to 1) that - controls how alchemical context parameter 'parameter' is switched - direction : str - Direction of alchemical switching: - 'insert' causes lambda to switch from 0 to 1 over nsteps steps of integration - 'delete' causes lambda to switch from 1 to 0 over nsteps steps of integration - - Returns - ------- - integrator : simtk.openmm.CustomIntegrator - NCMC switching integrator to annihilate or introduce particles alchemically. - """ - # Create an NCMC velocity Verlet integrator. - if self.integrator_type == 'VV': - integrator = NCMCVVAlchemicalIntegrator( - self.temperature, - alchemical_system, - functions, - nsteps=self.nsteps, - steps_per_propagation=self.steps_per_propagation, - timestep=self.timestep, - direction=direction) - elif self.integrator_type == 'GHMC': - integrator = NCMCGHMCAlchemicalIntegrator( - self.temperature, - alchemical_system, - functions, - nsteps=self.nsteps, - steps_per_propagation=self.steps_per_propagation, - timestep=self.timestep, - direction=direction) - else: - raise Exception("integrator_type '%s' unknown" % self.integrator_type) - - # Set the constraint tolerance if specified. - if self.constraint_tolerance is not None: - integrator.setConstraintTolerance(self.constraint_tolerance) - - return integrator - - def _create_context(self, system, integrator, positions): - """ - Instantiate context for alchemical system. - - Parameters - ---------- - system : simtk.openmm.System - The system with appropriate atoms alchemically modified - itegrator : NCMCAlchemicalIntegrator subclasses - NCMC switching integrator to annihilate or introduce particles alchemically. - positions : simtk.unit.Quantity with dimension [natoms, 3] with units of distance. - Positions of the atoms at the beginning of the NCMC switching. - - Returns - ------- - context : openmm.Context - Alchemical context - """ - - # Create a context on the specified platform. - if self.platform is not None: - context = openmm.Context(system, integrator, self.platform) - else: - context = openmm.Context(system, integrator) - #print('before setpositions:') - #print('positions', context.getState(getPositions=True).getPositions(asNumpy=True)) - #print('velocities', context.getState(getVelocities=True).getVelocities(asNumpy=True)) - context.setPositions(positions) - #print('after setpositions:') - #print('positions', context.getState(getPositions=True).getPositions(asNumpy=True)) - #print('velocities', context.getState(getVelocities=True).getVelocities(asNumpy=True)) - context.applyConstraints(integrator.getConstraintTolerance()) - #print('after applyConstraints:') - #print('positions', context.getState(getPositions=True).getPositions(asNumpy=True)) - #print('velocities', context.getState(getVelocities=True).getVelocities(asNumpy=True)) - # Set velocities to temperature and apply velocity constraints. - #print('after setVelocitiesToTemperature:') - context.setVelocitiesToTemperature(self.temperature) - #print('positions', context.getState(getPositions=True).getPositions(asNumpy=True)) - #print('velocities', context.getState(getVelocities=True).getVelocities(asNumpy=True)) - context.applyVelocityConstraints(integrator.getConstraintTolerance()) - #print('after applyVelocityConstraints:') - #print('positions', context.getState(getPositions=True).getPositions(asNumpy=True)) - #print('velocities', context.getState(getVelocities=True).getVelocities(asNumpy=True)) - - #state = context.getState(getPositions=True, getVelocities=True, getForces=True, getEnergy=True, getParameters=True) - #def write_file(filename, contents): - # outfile = open(filename, 'w') - # outfile.write(contents) - # outfile.close() - #write_file('system.xml', openmm.XmlSerializer.serialize(system)) - #write_file('integrator.xml', openmm.XmlSerializer.serialize(integrator)) - #write_file('state.xml', openmm.XmlSerializer.serialize(state)) - - return context - - def _get_functions(self, system): - """ - Select subset of switching functions based on which alchemical parameters are present in the system. - - Parameters - ---------- - system : simtk.openmm.System - The system with appropriate atoms alchemically modified - - Returns - ------- - functions : dict - functions[parameter] is the function (parameterized by 't' which switched from 0 to 1) that - controls how alchemical context parameter 'parameter' is switched - """ - available_parameters = self._getAvailableParameters(system) - functions = { - parameter_name: self.functions[parameter_name] - for parameter_name in self.functions if (parameter_name in available_parameters) - } - return functions - - def _clean_up_integration(self, alchemical_system, context, integrator): - """ - Delete the alchemical system, context and integrator, and increase - the counter of number of NCMC attempts. - - Parameters - ---------- - alchemical_system : simtk.openmm.System - The system with appropriate atoms alchemically modified - context : openmm.Context - Alchemical context - itegrator : NCMCAlchemicalIntegrator subclasses - NCMC switching integrator to annihilate or introduce particles alchemically. - - Returns - ------- - logP : float - The log contribution to the acceptance probability for this NCMC stage - """ - # Clean up alchemical system. - del alchemical_system, context, integrator - - # Keep track of statistics. - self.nattempted += 1 - - def integrate(self, topology_proposal, initial_positions, direction='insert', platform=None, iteration=None): - """ - Performs NCMC switching to either delete or insert atoms according to the provided `topology_proposal`. - - For `delete`, the system is first modified from fully interacting to alchemically modified, and then NCMC switching is used to eliminate atoms. - For `insert`, the system begins with eliminated atoms in an alchemically noninteracting form and NCMC switching is used to turn atoms on, followed by making system real. - - Parameters - ---------- - topology_proposal : TopologyProposal - Contains old/new Topology and System objects and atom mappings. - initial_positions : simtk.unit.Quantity with dimension [natoms, 3] with units of distance. - Positions of the atoms at the beginning of the NCMC switching. - direction : str, optional, default='insert' - Direction of alchemical switching: - 'insert' causes lambda to switch from 0 to 1 over nsteps steps of integration - 'delete' causes lambda to switch from 1 to 0 over nsteps steps of integration - platform : simtk.openmm.Platform, optional, default=None - If not None, this platform is used for integration. - iteration : int, optional, default=None - Iteration number, for storage purposes. - - Returns - ------- - final_positions : simtk.unit.Quantity of dimensions [nparticles,3] with units compatible with angstroms - The final positions after `nsteps` steps of alchemical switching - logP_work : float - The NCMC work contribution to the log acceptance probability (Eqs. 62 and 63) - logP_energy : float - The NCMC energy contribution to the log acceptance probability (Eqs. 62 and 63) - - """ - if direction not in ['insert', 'delete']: - raise Exception("'direction' must be one of ['insert', 'delete']; was '%s' instead" % direction) - - assert quantity_is_finite(initial_positions) == True - - topology, indices, system = self._choose_system_from_direction(topology_proposal, direction) - - # Create alchemical system. - alchemical_system = self.make_alchemical_system(system, indices, direction=direction) - - functions = self._get_functions(alchemical_system) - integrator = self._choose_integrator(alchemical_system, functions, direction) - context = self._create_context(alchemical_system, integrator, initial_positions) - - # Integrate switching - final_positions, logP_work = self._integrate_switching(integrator, context, topology, indices, iteration, - direction) - - # Compute contribution from switching between real and alchemical systems in correct order - logP_energy = self._computeEnergyContribution(integrator) - - self._clean_up_integration(alchemical_system, context, integrator) - - # Return - return [final_positions, logP_work, logP_energy] - - -class NCMCHybridEngine(NCMCEngine): - """ - NCMC switching engine which switches directly from old to new systems - via a hybrid alchemical topology - - Examples - -------- - ## EXAMPLE UNCHANGED FROM BASE CLASS ## - Create a transformation for an alanine dipeptide test system where the N-methyl group is eliminated. - >>> from openmmtools import testsystems - >>> testsystem = testsystems.AlanineDipeptideVacuum() - >>> from perses.rjmc.topology_proposal import TopologyProposal - >>> new_to_old_atom_map = { index : index for index in range(testsystem.system.getNumParticles()) if (index > 3) } # all atoms but N-methyl - >>> topology_proposal = TopologyProposal(old_system=testsystem.system, old_topology=testsystem.topology, old_chemical_state_key='AA', new_chemical_state_key='AA', new_system=testsystem.system, new_topology=testsystem.topology, logp_proposal=0.0, new_to_old_atom_map=new_to_old_atom_map, metadata=dict()) - >>> ncmc_engine = NCMCHybridEngine(temperature=300.0*unit.kelvin, functions=default_functions, nsteps=50, timestep=1.0*unit.femtoseconds) - - positions = testsystem.positions - (need a geometry proposal in here now) - [positions, new_old_positions, logP_insert, potential_insert] = ncmc_engine.integrate(topology_proposal, positions, proposed_positions) - """ - - def __init__(self, - temperature=default_temperature, - functions=None, - nsteps=default_nsteps, - timestep=default_timestep, - constraint_tolerance=None, - platform=None, - write_ncmc_interval=None, - integrator_type='GHMC', - storage=None): - """ - Subclass of NCMCEngine which switches directly between two different - systems using an alchemical hybrid topology. - - Arguments - --------- - temperature : simtk.unit.Quantity with units compatible with kelvin - The temperature at which switching is to be run - functions : dict of str:str, optional, default=default_functions - functions[parameter] is the function (parameterized by 't' which - switched from 0 to 1) that controls how alchemical context - parameter 'parameter' is switched - nsteps : int, optional, default=1 - The number of steps to use for switching. - timestep : simtk.unit.Quantity with units compatible with femtoseconds, - optional, default=1*femtosecond - The timestep to use for integration of switching velocity - Verlet steps. - constraint_tolerance : float, optional, default=None - If not None, this relative constraint tolerance is used for - position and velocity constraints. - platform : simtk.openmm.Platform, optional, default=None - If specified, the platform to use for OpenMM simulations. - write_ncmc_interval : int, optional, default=None - If a positive integer is specified, a PDB frame will be written - with the specified interval on NCMC switching, with a different - PDB file generated for each attempt. - integrator_type : str, optional, default='GHMC' - NCMC internal integrator type ['GHMC', 'VV'] - """ - if functions is None: - functions = default_hybrid_functions - super(NCMCHybridEngine, self).__init__( - temperature=temperature, - functions=functions, - nsteps=nsteps, - timestep=timestep, - constraint_tolerance=constraint_tolerance, - platform=platform, - write_ncmc_interval=write_ncmc_interval, - storage=storage, - integrator_type=integrator_type) - - def make_alchemical_system(self, topology_proposal, old_positions, new_positions): - """ - Generate an alchemically-modified system at the correct atoms - based on the topology proposal - Arguments - --------- - topology_proposal : TopologyProposal namedtuple - Contains old topology, proposed new topology, and atom mapping - old_positions : simtk.unit.Quantity with dimension [natoms, 3] with units of distance. - Positions of the atoms at the beginning of the NCMC switching. - new_positions : simtk.unit.Quantity with dimension [natoms, 3] with units of distance. - Positions of the atoms proposed by geometry engine. - - Returns - ------- - unmodified_old_system : simtk.openmm.System - Unmodified real system corresponding to old chemical state. - unmodified_new_system : simtk.openmm.System - Unmodified real system corresponding to new chemical state. - alchemical_system : simtk.openmm.System - The system with appropriate atoms alchemically modified - alchemical_topology : openmm.app.Topology - Topology which includes unique atoms of old and new states. - alchemical_positions : simtk.unit.Quantity of dimensions [nparticles,3] - with units compatible with angstroms - Positions for the alchemical hybrid topology - final_atom_map : dict(int : int) - Dictionary mapping the index of every atom in the new topology - to its index in the hybrid topology - initial_atom_map : dict(int : int) - Dictionary mapping the index of every atom in the old topology - to its index in the hybrid topology - """ - - atom_map = topology_proposal.old_to_new_atom_map - - #take the unique atoms as those not in the {new_atom : old_atom} atom map - unmodified_old_system = copy.deepcopy(topology_proposal.old_system) - unmodified_new_system = copy.deepcopy(topology_proposal.new_system) - old_topology = topology_proposal.old_topology - new_topology = topology_proposal.new_topology - - # Create an alchemical factory. - from perses.annihilation.relative import HybridTopologyFactory - alchemical_factory = HybridTopologyFactory(unmodified_old_system, unmodified_new_system, old_topology, - new_topology, old_positions, new_positions, atom_map) - - # Return the alchemically-modified system in fully-interacting form. - alchemical_system, alchemical_topology, alchemical_positions, final_atom_map, initial_atom_map = alchemical_factory.createPerturbedSystem( - ) - - # Disable barostat so that it isn't used during NCMC - if self.disable_barostat: - for force in alchemical_system.getForces(): - if hasattr(force, 'setFrequency'): - force.setFrequency(0) - - return [ - unmodified_old_system, unmodified_new_system, alchemical_system, alchemical_topology, alchemical_positions, - final_atom_map, initial_atom_map - ] - - def _convert_hybrid_positions_to_final(self, positions, atom_map): - final_positions = unit.Quantity(np.zeros([len(atom_map.keys()), 3]), unit=unit.nanometers) - for finalatom, hybridatom in atom_map.items(): - final_positions[finalatom] = positions[hybridatom] - return final_positions - - def integrate(self, topology_proposal, initial_positions, proposed_positions, platform=None, iteration=None): - """ - Performs NCMC switching to either delete or insert atoms according to the provided `topology_proposal`. - - Parameters - ---------- - topology_proposal : TopologyProposal - Contains old/new Topology and System objects and atom mappings. - initial_positions : simtk.unit.Quantity with dimension [natoms, 3] with units of distance. - Positions of the atoms at the beginning of the NCMC switching. - proposed_positions : simtk.unit.Quantity with dimension [natoms, 3] with units of distance. - Positions of the new system atoms proposed by geometry engine. - platform : simtk.openmm.Platform, optional, default=None - If not None, this platform is used for integration. - Returns - ------- - final_positions : simtk.unit.Quantity of dimensions [natoms, 3] with units of distance - The final positions after `nsteps` steps of alchemical switching - new_old_positions : simtk.unit.Quantity of dimensions [natoms, 3] with units of distance. - The final positions of the atoms of the old system after `nsteps` - steps of alchemical switching - logP_work : float - The NCMC work contribution to the log acceptance probability (Eq. 44) - logP_energy : float - The NCMC energy contribution to the log acceptance probability (Eq. 45) - """ - direction = 'insert' - - # Create alchemical system. - [ - unmodified_old_system, unmodified_new_system, alchemical_system, alchemical_topology, alchemical_positions, - final_to_hybrid_atom_map, initial_to_hybrid_atom_map - ] = self.make_alchemical_system(topology_proposal, initial_positions, proposed_positions) - - indices = [initial_to_hybrid_atom_map[idx] for idx in topology_proposal.unique_old_atoms - ] + [final_to_hybrid_atom_map[idx] for idx in topology_proposal.unique_new_atoms] - functions = self._get_functions(alchemical_system) - integrator = self._choose_integrator(alchemical_system, functions, direction) - context = self._create_context(alchemical_system, integrator, alchemical_positions) - - final_hybrid_positions, logP_work = self._integrate_switching(integrator, context, alchemical_topology, - indices, iteration, direction) - final_positions = self._convert_hybrid_positions_to_final(final_hybrid_positions, final_to_hybrid_atom_map) - new_old_positions = self._convert_hybrid_positions_to_final(final_hybrid_positions, initial_to_hybrid_atom_map) - - logP_energy = self._computeEnergyContribution(integrator) - - self._clean_up_integration(alchemical_system, context, integrator) - - # Return - return [final_positions, new_old_positions, logP_work, logP_energy] - - -class NCMCAlchemicalIntegrator(openmm.CustomIntegrator): - """ - Helper base class for NCMC alchemical integrators. - """ - - def __init__(self, temperature, system, functions, nsteps, steps_per_propagation, timestep, direction): - """ - Initialize base class for NCMC alchemical integrators. - - Parameters - ---------- - temperature : simtk.unit.Quantity with units compatible with kelvin - The temperature to use for computing the NCMC acceptance probability. - system : simtk.openmm.System - The system to be simulated. - functions : dict of str : str - functions[parameter] is the function (parameterized by 't' which switched from 0 to 1) that - controls how alchemical context parameter 'parameter' is switched - nsteps : int - The number of switching timesteps per call to integrator.step(1). - steps_per_propagation : int - The number of propagation steps taken at each value of lambda - timestep : simtk.unit.Quantity with units compatible with femtoseconds - The timestep to use for each NCMC step. - direction : str, optional, default='insert' - One of ['insert', 'delete']. - For `insert`, the parameter 'lambda' is switched from 0 to 1. - For `delete`, the parameter 'lambda' is switched from 1 to 0. - - """ - super(NCMCAlchemicalIntegrator, self).__init__(timestep) - - if direction not in ['insert', 'delete', 'flux']: - raise Exception("'direction' must be one of ['insert', 'delete', 'flux']; was '%s' instead" % direction) - self.direction = direction - - # Compute kT in natural openmm units. - kB = unit.BOLTZMANN_CONSTANT_kB * unit.AVOGADRO_CONSTANT_NA - kT = kB * temperature - self.kT = kT - - self.has_statistics = False # no GHMC statistics by default - - self.nsteps = nsteps - - # Make a list of parameters in the system - self.system_parameters = set() - self.alchemical_functions = functions - for force_index in range(system.getNumForces()): - force = system.getForce(force_index) - if hasattr(force, 'getNumGlobalParameters'): - for parameter_index in range(force.getNumGlobalParameters()): - self.system_parameters.add(force.getGlobalParameterName(parameter_index)) - - def addAlchemicalResetStep(self): - """ - Reset alchemical state to initial state, storing initial potential energy. - """ - # Set the master 'lambda' alchemical parameter to the initial state - if self.direction == 'insert': - self.addComputeGlobal('lambda', '0.0') - elif self.direction == 'delete': - self.addComputeGlobal('lambda', '1.0') - elif self.direction == 'flux': - self.addComputeGlobal('lambda', '1.0') - - # Update all slaved alchemical parameters - self.addUpdateAlchemicalParametersStep() - - # Store potential energy of initial alchemical state - self.addComputeGlobal('initial_reduced_potential', 'energy/kT') - - def addAlchemicalPerturbationStep(self): - """ - Add alchemical perturbation step, accumulating protocol work. - """ - # Store initial potential energy - self.addComputeGlobal("Eold", "Epert") - - # Set the master 'lambda' alchemical parameter to the current fractional state - if self.nsteps == 0: - # Toggle alchemical state - if self.direction == 'insert': - self.addComputeGlobal('lambda', '1.0') - elif self.direction == 'delete': - self.addComputeGlobal('lambda', '0.0') - elif self.direction == 'flux': - self.addComputeGlobal('lambda', '1.0') - - else: - # Use fractional state - if self.direction == 'insert': - self.addComputeGlobal('lambda', '(step+1)/nsteps') - elif self.direction == 'delete': - self.addComputeGlobal('lambda', '(nsteps - step - 1)/nsteps') - elif self.direction == 'flux': - self.addComputeGlobal('lambda', '(step+1)/nsteps') - - # Update all slaved alchemical parameters - self.addUpdateAlchemicalParametersStep() - - # Accumulate protocol work - self.addComputeGlobal("Enew", "energy") - self.addComputeGlobal("protocol_work", "protocol_work + (Enew-Eold)/kT") - - def addUpdateAlchemicalParametersStep(self): - """ - Update Context parameters according to provided functions. - """ - for context_parameter in self.alchemical_functions: - if context_parameter in self.system_parameters: - self.addComputeGlobal(context_parameter, self.alchemical_functions[context_parameter]) - - def addWorkResetStep(self): - """ - Reset work statistics. - """ - self.setGlobalVariableByName("total_work", 0.0) - self.setGlobalVariableByName("protocol_work", 0.0) - self.setGlobalVariableByName("shadow_work", 0.0) - - def addComputeTotalWorkStep(self): - """ - Compute total work, storing final potential energy. - """ - self.addComputeGlobal("total_work", "protocol_work + shadow_work") - - # Compute final potential - self.addComputeGlobal("final_reduced_potential", "energy/kT") - - def addVelocityVerletStep(self): - """ - Add velocity Verlet step, accumulating shadow work. - NOTE: Positions and velocities must have been constrained first. - - """ - # Allow context state to be updated - self.addUpdateContextState() - - # Store initial total energy - self.addComputeSum('kinetic', '0.5 * m * v^2') - self.addComputeGlobal("Eold", "energy + kinetic") - - # Symplectic velocity Verlet step - self.addComputePerDof("v", "v+0.5*dt*f/m") - self.addComputePerDof("x", "x+dt*v") - self.addComputePerDof("x1", "x") - self.addConstrainPositions() - self.addComputePerDof("v", "v+0.5*dt*f/m+(x-x1)/dt") - self.addConstrainVelocities() - - # Accumulate shadow work contribution - self.addComputeSum('kinetic', '0.5 * m * v^2') - self.addComputeGlobal("Enew", "energy + kinetic") - self.addComputeGlobal("shadow_work", "shadow_work + (Enew-Eold)/kT") - self.addComputeGlobal("Epert", "energy") - - def addGHMCStep(self): - """ - Add a GHMC step. - NOTE: Positions and velocities must have been constrained first. - - """ - self.hasStatistics = True - - # Only run on the first call - # TODO: This could be precomputed to save time - self.beginIfBlock('step = 0') - self.addComputePerDof("sigma", "sqrt(kT/m)") - self.endBlock() - - # Allow context state to be updated - self.addUpdateContextState() - - # - # Velocity perturbation - # - self.addComputePerDof("v", "sqrt(b)*v + sqrt(1-b)*sigma*gaussian") - self.addConstrainVelocities() - - # - # Metropolized symplectic step. - # - self.addComputeSum("kinetic", "0.5*m*v*v") - self.addComputeGlobal("Eold", "kinetic + Epert") - self.addComputePerDof("xold", "x") - self.addComputePerDof("vold", "v") - self.addComputePerDof("v", "v + 0.5*dt*f/m") - self.addComputePerDof("x", "x + v*dt") - self.addComputePerDof("x1", "x") - self.addConstrainPositions() - self.addComputePerDof("v", "v + 0.5*dt*f/m + (x-x1)/dt") - self.addConstrainVelocities() - self.addComputeSum("ke", "0.5*m*v*v") - self.addComputeGlobal("Enew", "kinetic + energy") - # Compute acceptance probability - self.addComputeGlobal("accept", "step(exp(-(Enew-Eold)/kT) - uniform)") - self.beginIfBlock("accept != 1") - # Reject sample, inverting velcoity - self.addComputePerDof("x", "xold") - self.addComputePerDof("v", "-vold") - self.endBlock() - - # - # Velocity perturbation - # - self.addComputePerDof("v", "sqrt(b)*v + sqrt(1-b)*sigma*gaussian") - self.addConstrainVelocities() - - # - # Accumulate statistics. - # - self.addComputeGlobal("naccept", "naccept + accept") - self.addComputeGlobal("ntrials", "ntrials + 1") - - def get_step(self): - return self.getGlobalVariableByName("step") - - def reset(self): - """ - Reset everything. - """ - self.setGlobalVariableByName("step", 0) - self.setGlobalVariableByName("lambda", 0.0) - self.setGlobalVariableByName("total_work", 0.0) - self.setGlobalVariableByName("protocol_work", 0.0) - self.setGlobalVariableByName("shadow_work", 0.0) - self.setGlobalVariableByName("initial_reduced_potential", 0.0) - self.setGlobalVariableByName("final_reduced_potential", 0.0) - if self.has_statistics: - self.setGlobalVariableByName("naccept", 0) - self.setGlobalVariableByName("ntrials", 0) - - def getStatistics(self, context): - if (self.has_statistics): - return (self.getGlobalVariableByName("naccept"), self.getGlobalVariableByName("ntrials")) - else: - return (0, 0) - - def getTotalWork(self, context): - """Retrieve accumulated total work (in units of kT) - """ - return self.getGlobalVariableByName("total_work") - - def getShadowWork(self, context): - """Retrieve accumulated shadow work (in units of kT) - """ - return self.getGlobalVariableByName("shadow_work") - - def getProtocolWork(self, context): - """Retrieve accumulated protocol work (in units of kT) - """ - return self.getGlobalVariableByName("protocol_work") - - def getLogAcceptanceProbability(self, context): - logp_accept = -1.0 * self.getGlobalVariableByName("total_work") - return logp_accept - - def addGlobalVariables(self, nsteps, steps_per_propagation): - self.addGlobalVariable( - 'lambda', - 0.0) # parameter switched from 0 <--> 1 during course of integrating internal 'nsteps' of dynamics - self.addGlobalVariable('total_work', 0.0) # cumulative total work in kT - self.addGlobalVariable('shadow_work', 0.0) # cumulative shadow work in kT - self.addGlobalVariable('protocol_work', 0.0) # cumulative protocol work in kT - self.addGlobalVariable("Eold", 0) # old energy - self.addGlobalVariable("Enew", 0) # new energy - self.addGlobalVariable("Epert", 0) # perturbation energy - self.addGlobalVariable('kinetic', 0.0) # kinetic energy - self.addGlobalVariable("initial_reduced_potential", 0) # potential energy at initial alchemical state - self.addGlobalVariable("final_reduced_potential", 0) # potential energy at final alchemical state - self.addGlobalVariable("kT", self.kT.value_in_unit_system(unit.md_unit_system)) # thermal energy - self.addGlobalVariable('nsteps', nsteps) # total number of NCMC steps to perform - self.addGlobalVariable('step', 0) # current NCMC step number - self.addPerDofVariable("x1", 0) # for velocity Verlet with constraints - self.addGlobalVariable('psteps', steps_per_propagation) - self.addGlobalVariable('pstep', 0) - - -class NCMCVVAlchemicalIntegrator(NCMCAlchemicalIntegrator): - """ - Use NCMC switching to annihilate or introduce particles alchemically. - - TODO: - ---- - * We may need to avoid unrolling integration steps. - - Examples - -------- - - Annihilate a Lennard-Jones particle - - >>> # Create an alchemically-perturbed test system - >>> from openmmtools import testsystems - >>> testsystem = testsystems.LennardJonesCluster() - >>> from alchemy import AbsoluteAlchemicalFactory - >>> alchemical_atoms = [0] - >>> factory = AbsoluteAlchemicalFactory(testsystem.system, ligand_atoms=alchemical_atoms) - >>> alchemical_system = factory.createPerturbedSystem() - >>> # Create an NCMC switching integrator. - >>> temperature = 300.0 * unit.kelvin - >>> nsteps = 5 - >>> functions = { 'lambda_sterics' : 'lambda' } - >>> ncmc_integrator = NCMCVVAlchemicalIntegrator(temperature, alchemical_system, functions, nsteps=nsteps, direction='delete') - >>> # Create a Context - >>> context = openmm.Context(alchemical_system, ncmc_integrator) - >>> context.setPositions(testsystem.positions) - >>> # Run the integrator - >>> ncmc_integrator.step(nsteps) - >>> # Retrieve the log acceptance probability - >>> log_ncmc = ncmc_integrator.getLogAcceptanceProbability(context) - - Turn on an atom and its associated angles and torsions in alanine dipeptide - - >>> # Create an alchemically-perturbed test system - >>> from openmmtools import testsystems - >>> testsystem = testsystems.AlanineDipeptideVacuum() - >>> from alchemy import AbsoluteAlchemicalFactory - >>> alchemical_atoms = [0,1,2,3] # terminal methyl group - >>> factory = AbsoluteAlchemicalFactory(testsystem.system, ligand_atoms=alchemical_atoms, alchemical_torsions=True, alchemical_angles=True, annihilate_sterics=True, annihilate_electrostatics=True) - >>> alchemical_system = factory.createPerturbedSystem() - >>> # Create an NCMC switching integrator. - >>> temperature = 300.0 * unit.kelvin - >>> nsteps = 10 - >>> functions = { 'lambda_sterics' : 'lambda', 'lambda_electrostatics' : 'lambda^0.5', 'lambda_torsions' : 'lambda', 'lambda_angles' : 'lambda^2' } - >>> ncmc_integrator = NCMCVVAlchemicalIntegrator(temperature, alchemical_system, functions, nsteps=nsteps, direction='delete') - >>> # Create a Context - >>> context = openmm.Context(alchemical_system, ncmc_integrator) - >>> context.setPositions(testsystem.positions) - >>> # Minimize - >>> openmm.LocalEnergyMinimizer.minimize(context) - >>> # Run the integrator - >>> ncmc_integrator.step(nsteps) - >>> # Retrieve the log acceptance probability - >>> log_ncmc = ncmc_integrator.getLogAcceptanceProbability(context) - - """ - - def __init__(self, - temperature, - system, - functions, - nsteps=0, - steps_per_propagation=1, - timestep=1.0 * unit.femtoseconds, - direction='insert'): - """ - Initialize an NCMC switching integrator to annihilate or introduce particles alchemically. - - Parameters - ---------- - temperature : simtk.unit.Quantity with units compatible with kelvin - The temperature to use for computing the NCMC acceptance probability. - system : simtk.openmm.System - The system to be simulated. - functions : dict of str : str - functions[parameter] is the function (parameterized by 't' which switched from 0 to 1) that - controls how alchemical context parameter 'parameter' is switched - nsteps : int, optional, default=10 - The number of switching timesteps per call to integrator.step(1). - steps_per_propagation : int, optional, default=1 - The number of propagation steps taken at each value of lambda - timestep : simtk.unit.Quantity with units compatible with femtoseconds - The timestep to use for each NCMC step. - direction : str, optional, default='insert' - One of ['insert', 'delete']. - For `insert`, the parameter 'lambda' is switched from 0 to 1. - For `delete`, the parameter 'lambda' is switched from 1 to 0. - - Note that each call to integrator.step(1) executes the entire integration program; this should not be called with more than one step. - - A symmetric protocol is used, in which the protocol begins and ends with a velocity Verlet step. - - TODO: - * Add a global variable that causes termination of future calls to step(1) after the first - - """ - super(NCMCVVAlchemicalIntegrator, self).__init__(temperature, system, functions, nsteps, steps_per_propagation, - timestep, direction) - - # - # Initialize global variables - # - - # NCMC variables - self.addGlobalVariables(nsteps, steps_per_propagation) - - if nsteps == 0: - self.beginIfBlock('step = 0') - # Constrain initial positions and velocities - self.addConstrainPositions() - self.addConstrainVelocities() - # Initialize alchemical state - self.addWorkResetStep() - self.addAlchemicalResetStep() - # Compute instantaneous work - self.addAlchemicalPerturbationStep() - # Update step - self.addComputeGlobal("step", "step+1") - # Compute total work - self.addComputeTotalWorkStep() - # End block - self.endBlock() - if nsteps > 0: - # Initial step only - self.beginIfBlock('step = 0') - # Constrain initial positions and velocities - self.addComputeGlobal("Epert", "energy") - self.addConstrainPositions() - self.addConstrainVelocities() - # Initialize alchemical state - self.addWorkResetStep() - self.addAlchemicalResetStep() - # Execute propagation steps. - self.addComputeGlobal('pstep', '0') - self.beginWhileBlock('pstep < psteps') - self.addVelocityVerletStep() - self.addComputeGlobal('pstep', 'pstep+1') - self.endBlock() - # End block - self.endBlock() - - # All steps, including initial step - self.beginIfBlock('step < nsteps') - # Accumulate protocol work - self.addAlchemicalPerturbationStep() - # Execute propagation steps. - self.addComputeGlobal('pstep', '0') - self.beginWhileBlock('pstep < psteps') - self.addVelocityVerletStep() - self.addComputeGlobal('pstep', 'pstep+1') - self.endBlock() - # Increment step - self.addComputeGlobal('step', 'step+1') - # Compute total work - self.addComputeTotalWorkStep() - # End block - self.endBlock() - - -class NCMCGHMCAlchemicalIntegrator(NCMCAlchemicalIntegrator): - """ - Use NCMC switching to annihilate or introduce particles alchemically. - """ - - def __init__(self, - temperature, - system, - functions, - nsteps=0, - steps_per_propagation=1, - collision_rate=9.1 / unit.picoseconds, - timestep=1.0 * unit.femtoseconds, - direction='insert'): - """ - Initialize an NCMC switching integrator to annihilate or introduce particles alchemically. - - Parameters - ---------- - temperature : simtk.unit.Quantity with units compatible with kelvin - The temperature to use for computing the NCMC acceptance probability. - system : simtk.openmm.System - The system to be simulated. - functions : dict of str : str - functions[parameter] is the function (parameterized by 't' which switched from 0 to 1) that - controls how alchemical context parameter 'parameter' is switched - nsteps : int, optional, default=0 - The number of switching timesteps per call to integrator.step(1). - steps_per_propagation : int, optional, default=1 - The number of propagation steps taken at each value of lambda - timestep : simtk.unit.Quantity with units compatible with femtoseconds - The timestep to use for each NCMC step. - direction : str, optional, default='insert' - One of ['insert', 'delete']. - For `insert`, the parameter 'lambda' is switched from 0 to 1. - For `delete`, the parameter 'lambda' is switched from 1 to 0. - - Note that each call to integrator.step(1) executes the entire integration program; this should not be called with more than one step. - - A symmetric protocol is used, in which the protocol begins and ends with a velocity Verlet step. - - TODO: - * Add a global variable that causes termination of future calls to step(1) after the first - - """ - super(NCMCGHMCAlchemicalIntegrator, self).__init__(temperature, system, functions, nsteps, - steps_per_propagation, timestep, direction) - - gamma = collision_rate - - # NCMC variables - self.addGlobalVariables(nsteps, steps_per_propagation) - - if (nsteps > 0): - # GHMC variables - self.addGlobalVariable("b", np.exp(-gamma * timestep)) # velocity mixing parameter - self.addPerDofVariable("sigma", 0) - self.addPerDofVariable("vold", 0) # old velocities - self.addPerDofVariable("xold", 0) # old positions - self.addGlobalVariable("accept", 0) # accept or reject - self.addGlobalVariable("naccept", 0) # number accepted - self.addGlobalVariable("ntrials", 0) # number of Metropolization trials - - if nsteps == 0: - # Only run on the first call - self.beginIfBlock('step = 0') - # Constrain initial positions and velocities - self.addConstrainPositions() - self.addConstrainVelocities() - # Initialize alchemical state - self.addWorkResetStep() - self.addAlchemicalResetStep() - # Accumulate protocol work - self.addAlchemicalPerturbationStep() - # Compute total work - self.addComputeTotalWorkStep() - # Update step counter - self.addComputeGlobal("step", "step+1") - # End block - self.endBlock() - - if nsteps > 0: - # Initial step only - self.beginIfBlock('step = 0') - self.addComputeGlobal("Epert", "energy") - # Constrain initial positions and velocities - self.addConstrainPositions() - self.addConstrainVelocities() - # Initialize alchemical state - self.addWorkResetStep() - self.addAlchemicalResetStep() - # Execute initial propagation steps for symmetry - #self.addComputeGlobal('pstep', '0') - #self.beginWhileBlock('pstep < psteps') - self.addGHMCStep() - #self.addComputeGlobal('pstep', 'pstep+1') - #self.endBlock() - # End block - self.endBlock() - - # All steps, including initial step - self.beginIfBlock('step < nsteps') - # Accumulate protocol work - self.addAlchemicalPerturbationStep() - # Execute propagation steps. - #self.addComputeGlobal('pstep', '0') - #self.beginWhileBlock('pstep < psteps') - self.addGHMCStep() - #self.addComputeGlobal('pstep', 'pstep+1') - #self.endBlock() - # Increment step - self.addComputeGlobal("Epert", "energy") - self.addComputeGlobal('step', 'step+1') - # Compute total work - self.addComputeTotalWorkStep() - # End block - self.endBlock() diff --git a/blues/systemfactory.py b/blues/systemfactory.py new file mode 100644 index 00000000..71fc4b67 --- /dev/null +++ b/blues/systemfactory.py @@ -0,0 +1,303 @@ +"""SystemFactory contains methods to generate/modify the OpenMM System object.""" + +import sys +import logging +from simtk import unit, openmm +from openmmtools import alchemy +from blues import utils +logger = logging.getLogger(__name__) + + +def generateAlchSystem(system, + atom_indices, + softcore_alpha=0.5, + softcore_a=1, + softcore_b=1, + softcore_c=6, + softcore_beta=0.0, + softcore_d=1, + softcore_e=1, + softcore_f=2, + annihilate_electrostatics=True, + annihilate_sterics=False, + disable_alchemical_dispersion_correction=True, + alchemical_pme_treatment='direct-space', + suppress_warnings=True, + **kwargs): + """Return the OpenMM System for alchemical perturbations. + + This function calls `openmmtools.alchemy.AbsoluteAlchemicalFactory` and + `openmmtools.alchemy.AlchemicalRegion` to generate the System for the + NCMC simulation. + + Parameters + ---------- + system : openmm.System + The OpenMM System object corresponding to the reference system. + atom_indices : list of int + Atom indicies of the move or designated for which the nonbonded forces + (both sterics and electrostatics components) have to be alchemically + modified. + annihilate_electrostatics : bool, optional + If True, electrostatics should be annihilated, rather than decoupled + (default is True). + annihilate_sterics : bool, optional + If True, sterics (Lennard-Jones or Halgren potential) will be annihilated, + rather than decoupled (default is False). + softcore_alpha : float, optional + Alchemical softcore parameter for Lennard-Jones (default is 0.5). + softcore_a, softcore_b, softcore_c : float, optional + Parameters modifying softcore Lennard-Jones form. Introduced in + Eq. 13 of Ref. [TTPham-JChemPhys135-2011]_ (default is 1). + softcore_beta : float, optional + Alchemical softcore parameter for electrostatics. Set this to zero + to recover standard electrostatic scaling (default is 0.0). + softcore_d, softcore_e, softcore_f : float, optional + Parameters modifying softcore electrostatics form (default is 1). + disable_alchemical_dispersion_correction : bool, optional, default=True + If True, the long-range dispersion correction will not be included for the alchemical + region to avoid the need to recompute the correction (a CPU operation that takes ~ 0.5 s) + every time 'lambda_sterics' is changed. If using nonequilibrium protocols, it is recommended + that this be set to True since this can lead to enormous (100x) slowdowns if the correction + must be recomputed every time step. + alchemical_pme_treatment : str, optional, default = 'direct-space' + Controls how alchemical region electrostatics are treated when PME is used. + Options are 'direct-space', 'coulomb', 'exact'. + - 'direct-space' only models the direct space contribution + - 'coulomb' includes switched Coulomb interaction + - 'exact' includes also the reciprocal space contribution, but it's + only possible to annihilate the charges and the softcore parameters + controlling the electrostatics are deactivated. Also, with this + method, modifying the global variable `lambda_electrostatics` is + not sufficient to control the charges. The recommended way to change + them is through the `AlchemicalState` class. + + Returns + ------- + alch_system : alchemical_system + System to be used for the NCMC simulation. + + References + ---------- + .. [TTPham-JChemPhys135-2011] T. T. Pham and M. R. Shirts, + J. Chem. Phys 135, 034114 (2011). http://dx.doi.org/10.1063/1.3607597 + """ + if suppress_warnings: + # Lower logger level to suppress excess warnings + logging.getLogger("openmmtools.alchemy").setLevel(logging.ERROR) + + # Disabled correction term due to increased computational cost + factory = alchemy.AbsoluteAlchemicalFactory( + disable_alchemical_dispersion_correction=disable_alchemical_dispersion_correction, + alchemical_pme_treatment=alchemical_pme_treatment) + alch_region = alchemy.AlchemicalRegion(alchemical_atoms=atom_indices, + softcore_alpha=softcore_alpha, + softcore_a=softcore_a, + softcore_b=softcore_b, + softcore_c=softcore_c, + softcore_beta=softcore_beta, + softcore_d=softcore_d, + softcore_e=softcore_e, + softcore_f=softcore_f, + annihilate_electrostatics=annihilate_electrostatics, + annihilate_sterics=annihilate_sterics) + + alch_system = factory.create_alchemical_system(system, alch_region) + return alch_system + +def zero_masses(system, atomList=None): + """ + Zeroes the masses of specified atoms to constrain certain degrees of freedom. + + Arguments + --------- + system : openmm.System + system to zero masses + atomList : list of ints + atom indicies to zero masses + + Returns + ------- + system : openmm.System + The modified system with massless atoms. + + """ + for index in (atomList): + system.setParticleMass(index, 0 * unit.daltons) + return system + +def restrain_positions(structure, system, selection="(@CA,C,N)", weight=5.0, **kwargs): + """Apply positional restraints to atoms in the openmm.System by the given parmed selection [amber-syntax]_. + + Parameters + ---------- + system : openmm.System + The OpenMM System object to be modified. + structure : parmed.Structure() + Structure of the system, used for atom selection. + selection : str, Default = "(@CA,C,N)" + AmberMask selection to apply positional restraints to + weight : float, Default = 5.0 + Restraint weight for xyz atom restraints in kcal/(mol A^2) + + Returns + ------- + system : openmm.System + Modified with positional restraints applied. + + """ + mask_idx = utils.amber_selection_to_atomidx(structure, selection) + + logger.info("{} positional restraints applied to selection: '{}' ({} atoms) on {}".format( + weight, selection, len(mask_idx), system)) + # define the custom force to restrain atoms to their starting positions + force = openmm.CustomExternalForce('k_restr*periodicdistance(x, y, z, x0, y0, z0)^2') + # Add the restraint weight as a global parameter in kcal/mol/A^2 + force.addGlobalParameter("k_restr", weight) + # force.addGlobalParameter("k_restr", weight*unit.kilocalories_per_mole/unit.angstroms**2) + # Define the target xyz coords for the restraint as per-atom (per-particle) parameters + force.addPerParticleParameter("x0") + force.addPerParticleParameter("y0") + force.addPerParticleParameter("z0") + + for i, atom_crd in enumerate(structure.positions): + if i in mask_idx: + logger.debug(i, structure.atoms[i]) + force.addParticle(i, atom_crd.value_in_unit(unit.nanometers)) + system.addForce(force) + + return system + + +def freeze_atoms(structure, system, freeze_selection=":LIG", **kwargs): + """Zero the masses of atoms from the given parmed selection [amber-syntax]_. + + Massless atoms will be ignored by the integrator and will not change + positions. + + Parameters + ---------- + system : openmm.System + The OpenMM System object to be modified. + structure : parmed.Structure() + Structure of the system, used for atom selection. + freeze_selection : str, Default = ":LIG" + AmberMask selection for the center in which to select atoms for + zeroing their masses. + Defaults to freezing protein backbone atoms. + + Returns + ------- + system : openmm.System + The modified system with the selected atoms + """ + mask_idx = utils.amber_selection_to_atomidx(structure, freeze_selection) + logger.info("Freezing selection '{}' ({} atoms) on {}".format(freeze_selection, len(mask_idx), system)) + + utils.atomidx_to_atomlist(structure, mask_idx) + system = zero_masses(system, mask_idx) + return system + + +def freeze_radius( + structure, + system, + freeze_distance=5.0 * unit.angstrom, + freeze_center=':LIG', + freeze_solvent=':HOH,NA,CL', + **kwargs): + """Zero the masses of atoms outside the given raidus of the `freeze_center` parmed selection [amber-syntax]_. + + Massless atoms will be ignored by the integrator and will not change + positions. This is intended to freeze the solvent and protein atoms around + the ligand binding site. + + Parameters + ---------- + system : openmm.System + The OpenMM System object to be modified. + structure : parmed.Structure() + Structure of the system, used for atom selection. + freeze_distance : float, Default = 5.0 + Distance (angstroms) to select atoms for retaining their masses. + Atoms outside the set distance will have their masses set to 0.0. + freeze_center : str, Default = ":LIG" + AmberMask selection for the center in which to select atoms for + zeroing their masses. Default: LIG + freeze_solvent : str, Default = ":HOH,NA,CL" + AmberMask selection in which to select solvent atoms for zeroing + their masses. + + Returns + ------- + system : openmm.System + Modified system with masses outside the `freeze center` zeroed. + + """ + N_atoms = system.getNumParticles() + # Select the LIG and atoms within 5 angstroms, except for WAT or IONS (i.e. selects the binding site) + if hasattr(freeze_distance, '_value'): + freeze_distance = freeze_distance._value + selection = "(%s<:%f)&!(%s)" % (freeze_center, freeze_distance, freeze_solvent) + logger.info('Inverting parmed selection for freezing: %s' % selection) + site_idx = utils.amber_selection_to_atomidx(structure, selection) + # Invert that selection to freeze everything but the binding site. + freeze_idx = set(range(N_atoms)) - set(site_idx) + center_idx = utils.amber_selection_to_atomidx(structure, freeze_center) + + freeze_threshold = 0.90 + freeze_warning = 0.75 + freeze_ratio = len(freeze_idx) / N_atoms + + # Ensure that the freeze selection is larger than the center selection of atoms + if len(site_idx) == len(center_idx): + err = "%i unfrozen atoms is equal to the number of atoms used as the selection center '%s' (%i atoms). Check your atom selection." % (len(site_idx), freeze_center, len(center_idx)) + logger.error(err) + + # Check if freeze selection has selected all atoms + elif len(freeze_idx) == N_atoms: + err = 'All %i atoms appear to be selected for freezing. Check your atom selection.' % len(freeze_idx) + logger.error(err) + + elif freeze_ratio >= freeze_threshold: + err = '%.0f%% of your system appears to be selected for freezing. Check your atom selection' % ( + 100 * freeze_threshold) + logger.error(err) + + elif freeze_warning <= freeze_ratio <= freeze_threshold: + warn = '%.0f%% of your system appears to be selected for freezing. This may cause unexpected behaviors.' % ( + 100 * freeze_ratio) + logger.warning(warn) + + + logger.info("Freezing {} atoms {} Angstroms from '{}' on {}".format(len(freeze_idx), freeze_distance, + freeze_center, system)) + + utils.atomidx_to_atomlist(structure, freeze_idx) + system = zero_masses(system, freeze_idx) + return system + + +def addBarostat(system, temperature=300 * unit.kelvin, pressure=1 * unit.atmospheres, frequency=25, **kwargs): + """Add a MonteCarloBarostat to the MD system. + + Parameters + ---------- + system : openmm.System + The OpenMM System object corresponding to the reference system. + temperature : float, default=300 + temperature (Kelvin) to be simulated at. + pressure : int, configional, default=None + Pressure (atm) for Barostat for NPT simulations. + frequency : int, default=25 + Frequency at which Monte Carlo pressure changes should be attempted (in time steps) + + Returns + ------- + system : openmm.System + The OpenMM System with the MonteCarloBarostat attached. + """ + logger.info('Adding MonteCarloBarostat with {}. MD simulation will be {} NPT.'.format(pressure, temperature)) + # Add Force Barostat to the system + system.addForce(openmm.MonteCarloBarostat(pressure, temperature, frequency)) + return system diff --git a/blues/tests/test_simulation.py b/blues/tests/simulation_test_old.py similarity index 53% rename from blues/tests/test_simulation.py rename to blues/tests/simulation_test_old.py index caf742b3..53411b5d 100644 --- a/blues/tests/test_simulation.py +++ b/blues/tests/simulation_test_old.py @@ -9,14 +9,6 @@ from simtk.openmm import app import numpy as np -#logger = logging.getLogger("blues.simulation") -#logger.setLevel(logging.INFO) - - -@pytest.fixture(scope='session') -def system_cfg(): - system_cfg = {'nonbondedMethod': app.PME, 'nonbondedCutoff': 8.0 * unit.angstroms, 'constraints': app.HBonds} - return system_cfg @pytest.fixture(scope='session') @@ -56,25 +48,6 @@ def state_keys(): return state_keys -@pytest.fixture(scope='session') -def structure(): - # Load the waterbox with toluene into a structure. - prmtop = utils.get_data_filename('blues', 'tests/data/TOL-parm.prmtop') - inpcrd = utils.get_data_filename('blues', 'tests/data/TOL-parm.inpcrd') - structure = parmed.load_file(prmtop, xyz=inpcrd) - return structure - - -@pytest.fixture(scope='session') -def tol_atom_indices(structure): - atom_indices = utils.atomIndexfromTop('LIG', structure.topology) - return atom_indices - - -@pytest.fixture(scope='session') -def system(structure, system_cfg): - system = structure.createSystem(**system_cfg) - return system class NoRandomLigandRotation(RandomLigandRotationMove): @@ -142,189 +115,7 @@ def blues_sim(simulations): return blues_sim -class TestSystemFactory(object): - def test_atom_selections(self, structure, tol_atom_indices): - atom_indices = SystemFactory.amber_selection_to_atomidx(structure, ':LIG') - - print('Testing AMBER selection parser') - assert isinstance(atom_indices, list) - assert len(atom_indices) == len(tol_atom_indices) - - def test_atomidx_to_atomlist(self, structure, tol_atom_indices): - print('Testing atoms from AMBER selection with parmed.Structure') - atom_list = SystemFactory.atomidx_to_atomlist(structure, tol_atom_indices) - atom_selection = [structure.atoms[i] for i in tol_atom_indices] - assert atom_selection == atom_list - - def test_generateSystem(self, structure, system, system_cfg): - # Create the OpenMM system - print('Creating OpenMM System') - md_system = SystemFactory.generateSystem(structure, **system_cfg) - - # Check that we get an openmm.System - assert isinstance(md_system, openmm.System) - # Check atoms in system is same in input parmed.Structure - assert md_system.getNumParticles() == len(structure.atoms) - assert md_system.getNumParticles() == system.getNumParticles() - - def test_generateAlchSystem(self, structure, system, tol_atom_indices): - # Create the OpenMM system - print('Creating OpenMM Alchemical System') - alch_system = SystemFactory.generateAlchSystem(system, tol_atom_indices) - - # Check that we get an openmm.System - assert isinstance(alch_system, openmm.System) - - # Check atoms in system is same in input parmed.Structure - assert alch_system.getNumParticles() == len(structure.atoms) - assert alch_system.getNumParticles() == system.getNumParticles() - - # Check customforces were added for the Alchemical system - alch_forces = alch_system.getForces() - alch_force_names = [force.__class__.__name__ for force in alch_forces] - assert len(system.getForces()) < len(alch_forces) - assert len(fnmatch.filter(alch_force_names, 'Custom*Force')) > 0 - - def test_restrain_postions(self, structure, system): - print('Testing positional restraints') - no_restr = system.getForces() - - md_system_restr = SystemFactory.restrain_positions(structure, system, ':LIG') - restr = md_system_restr.getForces() - - # Check that forces have been added to the system. - assert len(restr) != len(no_restr) - # Check that it has added the CustomExternalForce - assert isinstance(restr[-1], openmm.CustomExternalForce) - - def test_freeze_atoms(self, structure, system, tol_atom_indices): - print('Testing freeze_atoms') - masses = [system.getParticleMass(i)._value for i in tol_atom_indices] - frzn_lig = SystemFactory.freeze_atoms(structure, system, ':LIG') - massless = [frzn_lig.getParticleMass(i)._value for i in tol_atom_indices] - - # Check that masses have been zeroed - assert massless != masses - assert all(m == 0 for m in massless) - - def test_freeze_radius(self, system_cfg): - print('Testing freeze_radius') - freeze_cfg = {'freeze_center': ':LIG', 'freeze_solvent': ':Cl-', 'freeze_distance': 3.0 * unit.angstroms} - # Setup toluene-T4 lysozyme system - prmtop = utils.get_data_filename('blues', 'tests/data/TOL-parm.prmtop') - inpcrd = utils.get_data_filename('blues', 'tests/data/TOL-parm.inpcrd') - structure = parmed.load_file(prmtop, xyz=inpcrd) - atom_indices = utils.atomIndexfromTop('LIG', structure.topology) - system = SystemFactory.generateSystem(structure, **system_cfg) - - # Freeze everything around the binding site - frzn_sys = SystemFactory.freeze_radius(structure, system, **freeze_cfg) - - # Check that the ligand has NOT been frozen - lig_masses = [system.getParticleMass(i)._value for i in atom_indices] - assert all(m != 0 for m in lig_masses) - - # Check that the binding site has NOT been frozen - selection = "({freeze_center}<:{freeze_distance._value})&!({freeze_solvent})".format(**freeze_cfg) - site_idx = SystemFactory.amber_selection_to_atomidx(structure, selection) - masses = [frzn_sys.getParticleMass(i)._value for i in site_idx] - assert all(m != 0 for m in masses) - - # Check that the selection has been frozen - # Invert that selection to freeze everything but the binding site. - freeze_idx = set(range(system.getNumParticles())) - set(site_idx) - massless = [frzn_sys.getParticleMass(i)._value for i in freeze_idx] - assert all(m == 0 for m in massless) - - -class TestSimulationFactory(object): - def test_addBarostat(self, system): - print('Testing MonteCarloBarostat') - forces = system.getForces() - npt_system = SimulationFactory.addBarostat(system) - npt_forces = npt_system.getForces() - - #Check that forces have been added to the system. - assert len(forces) != len(npt_forces) - #Check that it has added the MonteCarloBarostat - assert isinstance(npt_forces[-1], openmm.MonteCarloBarostat) - - def test_generateIntegrator(self): - print('Testing LangevinIntegrator') - cfg = {'temperature': 500 * unit.kelvin, 'dt': 0.004 * unit.picoseconds} - integrator = SimulationFactory.generateIntegrator(**cfg) - #Check we made the right integrator - assert isinstance(integrator, openmm.LangevinIntegrator) - #Check that the integrator has taken our Parameters - assert integrator.getTemperature() == cfg['temperature'] - assert integrator.getStepSize() == cfg['dt'] - - def test_generateNCMCIntegrator(self): - print('Testing AlchemicalExternalLangevinIntegrator') - cfg = { - 'nstepsNC': 100, - 'temperature': 100 * unit.kelvin, - 'dt': 0.001 * unit.picoseconds, - 'nprop': 2, - 'propLambda': 0.1, - 'splitting': 'V H R O R H V', - 'alchemical_functions': { - 'lambda_sterics': '1', - 'lambda_electrostatics': '1' - } - } - ncmc_integrator = SimulationFactory.generateNCMCIntegrator(**cfg) - #Check we made the right integrator - assert isinstance(ncmc_integrator, AlchemicalExternalLangevinIntegrator) - #Check that the integrator has taken our Parameters - assert round(abs(ncmc_integrator.getTemperature()._value - cfg['temperature']._value), 7) == 0 - assert ncmc_integrator.getStepSize() == cfg['dt'] - assert ncmc_integrator._n_steps_neq == cfg['nstepsNC'] - assert ncmc_integrator._n_lambda_steps == \ - cfg['nstepsNC'] * cfg['nprop'] - assert ncmc_integrator._alchemical_functions == \ - cfg['alchemical_functions'] - assert ncmc_integrator._splitting == cfg['splitting'] - prop_range = (0.5 - cfg['propLambda'], 0.5 + cfg['propLambda']) - assert ncmc_integrator._prop_lambda == prop_range - - def test_generateSimFromStruct(self, structure, system, tmpdir): - print('Generating Simulation from parmed.Structure') - integrator = openmm.LangevinIntegrator(100 * unit.kelvin, 1, 0.002 * unit.picoseconds) - simulation = SimulationFactory.generateSimFromStruct(structure, system, integrator) - - #Check that we've made a Simulation object - assert isinstance(simulation, app.Simulation) - state = simulation.context.getState(getPositions=True) - positions = state.getPositions(asNumpy=True) / unit.nanometers - box_vectors = state.getPeriodicBoxVectors(asNumpy=True) / unit.nanometers - struct_box = np.array(structure.box_vectors.value_in_unit(unit.nanometers)) - struct_pos = np.array(structure.positions.value_in_unit(unit.nanometers)) - - #Check that the box_vectors/positions in the Simulation - # have been set from the parmed.Structure - np.testing.assert_array_almost_equal(positions, struct_pos) - np.testing.assert_array_equal(box_vectors, struct_box) - - print('Attaching Reporter') - reporters = [app.StateDataReporter(tmpdir.join('test.log'), 5)] - assert len(simulation.reporters) == 0 - simulation = SimulationFactory.attachReporters(simulation, reporters) - assert len(simulation.reporters) == 1 - - def test_generateSimulationSet(self, structure, systems, engine, sim_cfg): - print('Testing generateSimulationSet') - simulations = SimulationFactory(systems, engine) - simulations.generateSimulationSet(sim_cfg) - #Check that we've made the MD/ALCH/NCMC simulation set - assert hasattr(simulations, 'md') - assert hasattr(simulations, 'alch') - assert hasattr(simulations, 'ncmc') - #Check that the physical parameters are equivalent - assert simulations.ncmc_integrator.getStepSize() == sim_cfg['dt'] - assert simulations.integrator.getStepSize() == sim_cfg['dt'] - assert round(abs(simulations.ncmc_integrator.getTemperature()._value - sim_cfg['temperature']._value), 7) == 0 - assert round(abs(simulations.integrator.getTemperature()._value - sim_cfg['temperature']._value), 7) == 0 + class TestBLUESSimulation(object): @@ -360,7 +151,7 @@ def test_setContextFromState(self, md_sim, state_keys): assert np.not_equal(pos0, pos).any() def test_printSimulationTiming(self, blues_sim, caplog): - caplog.set_level(logging.INFO, logger="blues.simulation") + caplog.set_level(logging.INFO) blues_sim._printSimulationTiming() assert 'Total BLUES Simulation Time' in caplog.text #assert 'Total Force Evaluations' in caplog.text @@ -523,11 +314,8 @@ def test_blues_simulationRunPython(self, systems, simulations, engine, tmpdir, s md_reporters = ReporterConfig(tmpdir.join('tol-test'), md_rep_cfg).makeReporters() ncmc_reporters = ReporterConfig(tmpdir.join('tol-test-ncmc'), ncmc_rep_cfg).makeReporters() - simulations = SimulationFactory(systems, - engine, - sim_cfg, - md_reporters=md_reporters, - ncmc_reporters=ncmc_reporters) + simulations = SimulationFactory( + systems, engine, sim_cfg, md_reporters=md_reporters, ncmc_reporters=ncmc_reporters) blues = BLUESSimulation(simulations) blues._md_sim.minimizeEnergy() diff --git a/blues/tests/test_ethylene.py b/blues/tests/test_ethylene.py index b8086987..48936356 100644 --- a/blues/tests/test_ethylene.py +++ b/blues/tests/test_ethylene.py @@ -1,107 +1,75 @@ -import pytest -import parmed -import fnmatch import logging -import os -from blues import utils -from blues.simulation import SystemFactory, SimulationFactory, BLUESSimulation -from blues.integrators import AlchemicalExternalLangevinIntegrator -from blues.moves import RandomLigandRotationMove, MoveEngine -from blues.reporters import (BLUESStateDataReporter, NetCDF4Reporter, ReporterConfig, init_logger) -from blues.settings import Settings -from simtk import openmm, unit -from simtk.openmm import app -import numpy as np -import mdtraj as md from collections import Counter -logger = logging.getLogger("blues.simulation") -logger = init_logger(logger, level=logging.ERROR, stream=True) +import mdtraj as md +import numpy as np +import parmed +from openmmtools import cache +from openmmtools.states import SamplerState, ThermodynamicState +from simtk import openmm, unit + +from blues import utils +from blues.storage import BLUESStateDataStorage, NetCDF4Storage +from blues.ncmc import RandomLigandRotationMove, ReportLangevinDynamicsMove, BLUESSampler + +logger = logging.getLogger(__name__) -def runEthyleneTest(N): - filename = 'ethylene-test_%s' % N +def runEthyleneTest(dir, N): + filename = dir.join('ethylene-test_%s' % N) print('Running %s...' % filename) - seed = np.random.randint(low=1, high=5000) - #print('Seed', seed) - # filename = 'ethylene-test_%s' % N - # print(filename) # Set Simulation parameters - sim_cfg = { - 'platform': 'CPU', - 'nprop': 1, - 'propLambda': 0.3, - 'dt': 1 * unit.femtoseconds, - 'friction': 1 / unit.picoseconds, - 'temperature': 200 * unit.kelvin, - 'nIter': 100, - 'nstepsMD': 20, - 'nstepsNC': 20, - 'propSteps': 20, - 'moveStep': 10 - } - - totalSteps = int(sim_cfg['nIter'] * sim_cfg['nstepsMD']) + temperature = 200 * unit.kelvin + collision_rate = 1 / unit.picoseconds + timestep = 1.0 * unit.femtoseconds + n_steps = 20 + nIter = 100 reportInterval = 5 alchemical_atoms = [2, 3, 4, 5, 6, 7] - alchemical_functions = { - 'lambda_sterics': 'min(1, (1/0.3)*abs(lambda-0.5))', - 'lambda_electrostatics': - 'step(0.2-lambda) - 1/0.2*lambda*step(0.2-lambda) + 1/0.2*(lambda-0.8)*step(lambda-0.8)' - } - - md_reporters = {'traj_netcdf': {'reportInterval': reportInterval}} + platform = openmm.Platform.getPlatformByName('CPU') + context_cache = cache.ContextCache(platform) # Load a Parmed Structure for the Topology and create our openmm.Simulation structure_pdb = utils.get_data_filename('blues', 'tests/data/ethylene_structure.pdb') structure = parmed.load_file(structure_pdb) - # Initialize our move proposal class - rot_move = RandomLigandRotationMove(structure, 'LIG') - mover = MoveEngine(rot_move) + nc_reporter = NetCDF4Storage(filename + '_MD.nc', reportInterval) + + + + # Iniitialize our Move set + rot_move = RandomLigandRotationMove( + timestep=timestep, + n_steps=n_steps, + atom_subset=alchemical_atoms, + context_cache=context_cache, + reporters=[nc_reporter]) + langevin_move = ReportLangevinDynamicsMove( + timestep=timestep, + collision_rate=collision_rate, + n_steps=n_steps, + reassign_velocities=True, + context_cache=context_cache) # Load our OpenMM System and create Integrator system_xml = utils.get_data_filename('blues', 'tests/data/ethylene_system.xml') with open(system_xml, 'r') as infile: xml = infile.read() system = openmm.XmlSerializer.deserialize(xml) - integrator = openmm.LangevinIntegrator(sim_cfg['temperature'], sim_cfg['friction'], sim_cfg['dt']) - integrator.setRandomNumberSeed(seed) - - alch_integrator = openmm.LangevinIntegrator(sim_cfg['temperature'], sim_cfg['friction'], sim_cfg['dt']) - alch_integrator.setRandomNumberSeed(seed) - - alch_system = SystemFactory.generateAlchSystem(system, alchemical_atoms) - ncmc_integrator = AlchemicalExternalLangevinIntegrator( - nsteps_neq=sim_cfg['nstepsNC'], - alchemical_functions=alchemical_functions, - splitting="H V R O R V H", - temperature=sim_cfg['temperature'], - timestep=sim_cfg['dt']) - # ncmc_integrator.setRandomNumberSeed(seed) - # Pack our systems into a single object - systems = SystemFactory(structure, alchemical_atoms) - systems.md = system - systems.alch = alch_system + thermodynamic_state = ThermodynamicState(system=system, temperature=temperature) + sampler_state = SamplerState(positions=structure.positions.in_units_of(unit.nanometers)) - # Make our reporters - md_reporter_cfg = ReporterConfig(filename, md_reporters) - md_reporters_list = md_reporter_cfg.makeReporters() + sampler = BLUESSampler( + thermodynamic_state=thermodynamic_state, + sampler_state=sampler_state, + ncmc_move=rot_move, + dynamics_move=langevin_move, + topology=structure.topology) + sampler.run(nIter) - # Pack our simulations into a single object - simulations = SimulationFactory(systems, mover) - - simulations.md = SimulationFactory.generateSimFromStruct(structure, system, integrator, 'CPU') - simulations.md = SimulationFactory.attachReporters(simulations.md, md_reporters_list) - - simulations.alch = SimulationFactory.generateSimFromStruct(structure, system, alch_integrator, 'CPU') - - simulations.ncmc = SimulationFactory.generateSimFromStruct(structure, alch_system, ncmc_integrator, 'CPU') - - ethylene_sim = BLUESSimulation(simulations, sim_cfg) - ethylene_sim.run() + return filename def getPopulations(traj): @@ -116,7 +84,6 @@ def getPopulations(traj): def graphConvergence(dist, n_points=10): - bins = len(dist) / n_points bin_count = [] bin_points = [] for N in range(1, len(dist) + 1, n_points): @@ -137,14 +104,12 @@ def graphConvergence(dist, n_points=10): return bin_err_arr[-1, :] -def test_runEthyleneRepeats(): - [runEthyleneTest(i) for i in range(5)] - +def test_runEthyleneRepeats(tmpdir): + dir = tmpdir.mkdir("tmp") + outfnames = [runEthyleneTest(dir, N=i) for i in range(5)] -def test_runAnalysis(): - outfnames = ['ethylene-test_%s.nc' % i for i in range(5)] structure_pdb = utils.get_data_filename('blues', 'tests/data/ethylene_structure.pdb') - trajs = [md.load(traj, top=structure_pdb) for traj in outfnames] + trajs = [md.load('%s_MD.nc' % traj, top=structure_pdb) for traj in outfnames] dists = [] freqs = [] errs = [] @@ -160,4 +125,4 @@ def test_runAnalysis(): avg_err = np.mean(errs, axis=0) print(avg_freq, avg_err, np.absolute(avg_freq - populations)) check = np.allclose(avg_freq, populations, atol=avg_err) - assert check == True + assert check is True diff --git a/blues/tests/test_randomrotation.py b/blues/tests/test_randomrotation.py deleted file mode 100644 index 07f5361a..00000000 --- a/blues/tests/test_randomrotation.py +++ /dev/null @@ -1,65 +0,0 @@ -import unittest, parmed -from blues import utils -from blues.simulation import SystemFactory, SimulationFactory, BLUESSimulation -from blues.moves import RandomLigandRotationMove -from blues.moves import MoveEngine -from simtk.openmm import app -from simtk import unit -import numpy as np - - -class RandomRotationTester(unittest.TestCase): - """ - Test the RandomLigandRotationMove class. - """ - - def setUp(self): - # Obtain topologies/positions - prmtop = utils.get_data_filename('blues', 'tests/data/TOL-parm.prmtop') - inpcrd = utils.get_data_filename('blues', 'tests/data/TOL-parm.inpcrd') - structure = parmed.load_file(prmtop, xyz=inpcrd) - - self.atom_indices = utils.atomIndexfromTop('LIG', structure.topology) - - #Initialize the Move object - self.move = RandomLigandRotationMove(structure, 'LIG', 3134) - self.engine = MoveEngine(self.move) - self.engine.selectMove() - - self.system_cfg = {'nonbondedMethod': app.NoCutoff, 'constraints': app.HBonds} - systems = SystemFactory(structure, self.move.atom_indices, self.system_cfg) - - #Initialize the SimulationFactory object - self.cfg = { - 'dt': 0.002 * unit.picoseconds, - 'friction': 1 * 1 / unit.picoseconds, - 'temperature': 300 * unit.kelvin, - 'nprop': 1, - 'nIter': 1, - 'nstepsMD': 1, - 'nstepsNC': 10, - 'alchemical_functions': { - 'lambda_sterics': - 'step(0.199999-lambda) + step(lambda-0.2)*step(0.8-lambda)*abs(lambda-0.5)*1/0.3 + step(lambda-0.800001)', - 'lambda_electrostatics': - 'step(0.2-lambda)- 1/0.2*lambda*step(0.2-lambda) + 1/0.2*(lambda-0.8)*step(lambda-0.8)' - } - } - self.simulations = SimulationFactory(systems, self.engine, self.cfg) - self.ncmc_sim = self.simulations.ncmc - self.initial_positions = self.ncmc_sim.context.getState(getPositions=True).getPositions(asNumpy=True) - - def test_random_rotation(self): - before_move = self.simulations.ncmc.context.getState(getPositions=True).getPositions( - asNumpy=True)[self.atom_indices, :] - self.simulations.ncmc.context = self.engine.runEngine(self.simulations.ncmc.context) - after_move = self.simulations.ncmc.context.getState(getPositions=True).getPositions( - asNumpy=True)[self.atom_indices, :] - - #Check that the ligand has been rotated - pos_compare = np.not_equal(before_move, after_move).all() - assert pos_compare - - -if __name__ == "__main__": - unittest.main() diff --git a/blues/tests/test_sidechain.py b/blues/tests/test_sidechain.py deleted file mode 100644 index 75ea7146..00000000 --- a/blues/tests/test_sidechain.py +++ /dev/null @@ -1,84 +0,0 @@ -import unittest, parmed -from blues import utils -from blues.simulation import SystemFactory, SimulationFactory, BLUESSimulation -from simtk.openmm import app -from blues.moves import SideChainMove -from blues.moves import MoveEngine -from openmmtools import testsystems -import simtk.unit as unit -import numpy as np -from unittest import skipUnless - -try: - import openeye.oechem as oechem - if not oechem.OEChemIsLicensed(): - raise ImportError("Need License for OEChem! SideChainMove class will be unavailable.") - try: - import oeommtools.utils as oeommtools - except ImportError: - raise ImportError('Could not import oeommtools. SideChainMove class will be unavailable.') - has_openeye = True -except ImportError: - has_openeye = False - print('Could not import openeye-toolkits. SideChainMove class will be unavailable.') - - -@skipUnless(has_openeye, 'Cannot test SideChainMove without openeye-toolkits and oeommtools.') -class SideChainTester(unittest.TestCase): - """ - Test the SmartDartMove.move() function. - """ - - def setUp(self): - # Obtain topologies/positions - prmtop = utils.get_data_filename('blues', 'tests/data/vacDivaline.prmtop') - inpcrd = utils.get_data_filename('blues', 'tests/data/vacDivaline.inpcrd') - self.struct = parmed.load_file(prmtop, xyz=inpcrd) - - self.sidechain = SideChainMove(self.struct, [1]) - self.engine = MoveEngine(self.sidechain) - self.engine.selectMove() - - self.system_cfg = {'nonbondedMethod': app.NoCutoff, 'constraints': app.HBonds} - self.systems = SystemFactory(self.struct, self.sidechain.atom_indices, self.system_cfg) - - self.cfg = { - 'dt': 0.002 * unit.picoseconds, - 'friction': 1 * 1 / unit.picoseconds, - 'temperature': 300 * unit.kelvin, - 'nIter': 1, - 'nstepsMD': 1, - 'nstepsNC': 4, - 'alchemical_functions': { - 'lambda_sterics': - 'step(0.199999-lambda) + step(lambda-0.2)*step(0.8-lambda)*abs(lambda-0.5)*1/0.3 + step(lambda-0.800001)', - 'lambda_electrostatics': - 'step(0.2-lambda)- 1/0.2*lambda*step(0.2-lambda) + 1/0.2*(lambda-0.8)*step(lambda-0.8)' - } - } - - self.simulations = SimulationFactory(self.systems, self.engine, self.cfg) - - def test_getRotBondAtoms(self): - vals = [v for v in self.sidechain.rot_atoms[1].values()][0] - assert len(vals) == 11 - #Ensure it selects 1 rotatable bond in Valine - assert len(self.sidechain.rot_bonds) == 1 - - def test_sidechain_move(self): - atom_indices = [v for v in self.sidechain.rot_atoms[1].values()][0] - before_move = self.simulations.ncmc.context.getState(getPositions=True).getPositions( - asNumpy=True)[atom_indices, :] - self.simulations.ncmc.context = self.engine.runEngine(self.simulations.ncmc.context) - after_move = self.simulations.ncmc.context.getState(getPositions=True).getPositions( - asNumpy=True)[atom_indices, :] - - #Check that our system has run dynamics - # Integrator must step for context to update positions - # Remove the first two atoms in check as these are the anchor atoms and are not rotated. - pos_compare = np.not_equal(before_move, after_move)[2:, :].all() - assert pos_compare - - -if __name__ == "__main__": - unittest.main() diff --git a/blues/tests/test_storage.py b/blues/tests/test_storage.py new file mode 100644 index 00000000..ee78cf9e --- /dev/null +++ b/blues/tests/test_storage.py @@ -0,0 +1,176 @@ +import pytest +import parmed +import fnmatch +import numpy +import logging +from netCDF4 import Dataset +from openmmtools.cache import ContextCache +from openmmtools.states import SamplerState, ThermodynamicState, CompoundThermodynamicState +from blues.systemfactory import * +from simtk.openmm import app +from openmmtools import cache +from simtk import unit +from blues.storage import * +from blues.integrators import AlchemicalExternalLangevinIntegrator +from blues.ncmc import RandomLigandRotationMove, ReportLangevinDynamicsMove, BLUESSampler + + +def get_states(): + # Set Simulation parameters + temperature = 200 * unit.kelvin + collision_rate = 1 / unit.picoseconds + timestep = 1.0 * unit.femtoseconds + n_steps = 20 + nIter = 100 + alchemical_atoms = [2, 3, 4, 5, 6, 7] + platform = openmm.Platform.getPlatformByName('CPU') + context_cache = cache.ContextCache(platform) + + # Load a Parmed Structure for the Topology and create our openmm.Simulation + structure_pdb = utils.get_data_filename('blues', 'tests/data/ethylene_structure.pdb') + structure = parmed.load_file(structure_pdb) + + + # Load our OpenMM System and create Integrator + system_xml = utils.get_data_filename('blues', 'tests/data/ethylene_system.xml') + with open(system_xml, 'r') as infile: + xml = infile.read() + system = openmm.XmlSerializer.deserialize(xml) + + thermodynamic_state = ThermodynamicState(system=system, temperature=temperature) + sampler_state = SamplerState(positions=structure.positions.in_units_of(unit.nanometers)) + + alch_system = generateAlchSystem(thermodynamic_state.get_system(), + alchemical_atoms) + alch_state = alchemy.AlchemicalState.from_system(alch_system) + alch_thermodynamic_state = ThermodynamicState( + alch_system, thermodynamic_state.temperature) + alch_thermodynamic_state = CompoundThermodynamicState( + alch_thermodynamic_state, composable_states=[alch_state]) + + return structure, thermodynamic_state, alch_thermodynamic_state + + +def test_add_logging_level(): + print("Testing adding logger level") + addLoggingLevel('TRACE', logging.DEBUG - 5) + assert True == hasattr(logging, 'TRACE') + + +def test_init_logger(tmpdir): + print('Testing logger initialization') + dir = tmpdir.mkdir("tmp") + outfname = dir.join('testlog') + logger = logging.getLogger(__name__) + level = logger.getEffectiveLevel() + logger = init_logger(logger, level=logging.INFO, outfname=outfname, stream=False) + new_level = logger.getEffectiveLevel() + assert level != new_level + + +def test_netcdf4storage(tmpdir): + dir = tmpdir.mkdir("tmp") + outfname = dir.join('testlog.nc') + context_cache = ContextCache() + ncmc_storage = NetCDF4Storage(outfname, 5, crds=True, vels=True, frcs=True, + protocolWork=True, alchemicalLambda=True) + + + structure, thermodynamic_state, alch_thermodynamic_state = get_states() + ncmc_integrator = AlchemicalExternalLangevinIntegrator( + alchemical_functions={ + 'lambda_sterics': + 'min(1, (1/0.3)*abs(lambda-0.5))', + 'lambda_electrostatics': + 'step(0.2-lambda) - 1/0.2*lambda*step(0.2-lambda) + 1/0.2*(lambda-0.8)*step(lambda-0.8)' + }, + splitting="H V R O R V H", + temperature=alch_thermodynamic_state.temperature, + nsteps_neq=10, + timestep=1.0*unit.femtoseconds, + nprop=1, + prop_lambda=0.3) + context, integrator = context_cache.get_context(alch_thermodynamic_state, ncmc_integrator) + context.setPositions(structure.positions) + context.setVelocitiesToTemperature(200 * unit.kelvin) + integrator.step(5) + + context_state = context.getState( + getPositions=True, + getVelocities=True, + getEnergy=True, + getForces=True, + enforcePeriodicBox=thermodynamic_state.is_periodic) + context_state.currentStep = 5 + context_state.system = alch_thermodynamic_state.get_system() + + # Check for preparation of next report + report = ncmc_storage.describeNextReport(context_state) + assert len(report) == 5 + + # Check that data has been stored in NC file + ncmc_storage.report(context_state, integrator) + dataset = Dataset(outfname) + nc_keys = set(dataset.variables.keys()) + keys = set(['coordinates', 'velocities', 'forces', 'protocolWork', 'alchemicalLambda']) + assert set() == keys - nc_keys + + +def test_statedatastorage(tmpdir): + context_cache = ContextCache() + dir = tmpdir.mkdir("tmp") + outfname = dir.join('blues.log') + state_storage = BLUESStateDataStorage(outfname, + reportInterval=5, + step=True, time=True, + potentialEnergy=True, + kineticEnergy=True, + totalEnergy=True, + temperature=True, + volume=True, + density=True, + progress=True, + remainingTime=True, + speed=True, + elapsedTime=True, + systemMass=True, + totalSteps=20, + protocolWork=True, + alchemicalLambda=True ) + structure, thermodynamic_state, alch_thermodynamic_state = get_states() + ncmc_integrator = AlchemicalExternalLangevinIntegrator( + alchemical_functions={ + 'lambda_sterics': + 'min(1, (1/0.3)*abs(lambda-0.5))', + 'lambda_electrostatics': + 'step(0.2-lambda) - 1/0.2*lambda*step(0.2-lambda) + 1/0.2*(lambda-0.8)*step(lambda-0.8)' + }, + splitting="H V R O R V H", + temperature=alch_thermodynamic_state.temperature, + nsteps_neq=10, + timestep=1.0*unit.femtoseconds, + nprop=1, + prop_lambda=0.3) + context, integrator = context_cache.get_context(alch_thermodynamic_state, ncmc_integrator) + context.setPositions(structure.positions) + context.setVelocitiesToTemperature(200 * unit.kelvin) + integrator.step(5) + + context_state = context.getState( + getPositions=True, + getVelocities=True, + getEnergy=True, + getForces=True, + enforcePeriodicBox=thermodynamic_state.is_periodic) + context_state.currentStep = 5 + context_state.system = alch_thermodynamic_state.get_system() + + # Check for preparation of next report + report = state_storage.describeNextReport(context_state) + assert len(report) == 5 + + #Check fields have been reported + state_storage.report(context_state, integrator) + with open(outfname, 'r') as input: + headers = input.read().splitlines()[0].split('\t') + assert len(headers) >= 1 diff --git a/blues/tests/test_utilities.py b/blues/tests/test_utilities.py new file mode 100644 index 00000000..819f082c --- /dev/null +++ b/blues/tests/test_utilities.py @@ -0,0 +1,211 @@ +import pytest +import parmed +import fnmatch +import numpy +import os +from openmmtools.cache import ContextCache +from openmmtools.states import ThermodynamicState +from blues.systemfactory import * +from simtk.openmm import app +from simtk import unit + + +@pytest.fixture(scope='session') +def system_cfg(): + system_cfg = {'nonbondedMethod': app.PME, 'nonbondedCutoff': 8.0 * unit.angstroms, 'constraints': app.HBonds} + return system_cfg + +@pytest.fixture(scope='session') +def structure(): + # Load the waterbox with toluene into a structure. + prmtop = utils.get_data_filename('blues', 'tests/data/TOL-parm.prmtop') + inpcrd = utils.get_data_filename('blues', 'tests/data/TOL-parm.inpcrd') + structure = parmed.load_file(prmtop, xyz=inpcrd) + return structure + +@pytest.fixture(scope='session') +def tol_atom_indices(structure): + atom_indices = utils.atomIndexfromTop('LIG', structure.topology) + return atom_indices + +@pytest.fixture(scope='function') +def system(structure, system_cfg): + system = structure.createSystem(**system_cfg) + return system + +@pytest.fixture(scope='function') +def context(system, structure): + context_cache = ContextCache() + thermodynamic_state = ThermodynamicState(system, 300*unit.kelvin) + context, integrator = context_cache.get_context(thermodynamic_state) + context.setPositions(structure.positions) + return context + +### Utils ### +def test_amber_atom_selections(structure, tol_atom_indices): + atom_indices = utils.amber_selection_to_atomidx(structure, ':LIG') + + print('Testing AMBER selection parser') + assert isinstance(atom_indices, list) + assert len(atom_indices) == len(tol_atom_indices) + +def test_amber_selection_check(structure, caplog): + print('Testing AMBER selection check') + assert True == utils.check_amber_selection(structure, ':LIG') + assert True == utils.check_amber_selection(structure, '@1') + assert False == utils.check_amber_selection(structure, ':XYZ') + assert False == utils.check_amber_selection(structure, '@999') + +def test_atomidx_to_atomlist(structure, tol_atom_indices): + print('Testing atoms from AMBER selection with parmed.Structure') + atom_list = utils.atomidx_to_atomlist(structure, tol_atom_indices) + atom_selection = [structure.atoms[i] for i in tol_atom_indices] + assert atom_selection == atom_list + +def test_get_masses(structure, tol_atom_indices): + print('Testing get masses from a Topology') + masses, totalmass = utils.getMasses(tol_atom_indices, structure.topology) + total = numpy.sum(numpy.vstack(masses)) + assert total == totalmass._value + +def test_get_center_of_mass(structure, tol_atom_indices): + print('Testing get center of mass') + masses, totalmass = utils.getMasses(tol_atom_indices, structure.topology) + coordinates = numpy.array(structure.positions._value, numpy.float32)[tol_atom_indices] + com = utils.getCenterOfMass(coordinates, masses) + assert len(com) == 3 + +def test_print_host_info(context, caplog): + print('Testing Host Printout') + with caplog.at_level(logging.INFO): + utils.print_host_info(context) + assert 'version' in caplog.text + +def test_saveContextFrame(context, structure, caplog, tmpdir): + print('Testing Save Context Frame') + filename = 'testContext.pdb' + with caplog.at_level(logging.INFO): + utils.saveContextFrame(context, structure.topology, filename) + assert 'Saving Frame to' in caplog.text + exists = os.path.isfile('testContext.pdb') + assert exists == True + if exists: + os.remove(filename) +### SystemFactories ### + +def test_generateAlchSystem(structure, system, tol_atom_indices): + # Create the OpenMM system + print('Creating OpenMM Alchemical System') + alch_system = generateAlchSystem(system, tol_atom_indices) + + # Check that we get an openmm.System + assert isinstance(alch_system, openmm.System) + + # Check atoms in system is same in input parmed.Structure + assert alch_system.getNumParticles() == len(structure.atoms) + assert alch_system.getNumParticles() == system.getNumParticles() + + # Check customforces were added for the Alchemical system + alch_forces = alch_system.getForces() + alch_force_names = [force.__class__.__name__ for force in alch_forces] + assert len(system.getForces()) < len(alch_forces) + assert len(fnmatch.filter(alch_force_names, 'Custom*Force')) > 0 + +def test_restrain_postions(structure, system): + print('Testing positional restraints') + no_restr = system.getForces() + + md_system_restr = restrain_positions(structure, system, ':LIG') + restr = md_system_restr.getForces() + + # Check that forces have been added to the system. + assert len(restr) != len(no_restr) + # Check that it has added the CustomExternalForce + assert isinstance(restr[-1], openmm.CustomExternalForce) + +def test_zero_masses(system, tol_atom_indices): + print('Testing zero masses') + masses = [system.getParticleMass(i)._value for i in tol_atom_indices] + massless_system = zero_masses(system, tol_atom_indices) + massless = [massless_system.getParticleMass(i)._value for i in tol_atom_indices] + + # Check that masses have been zeroed + assert massless != masses + assert all(m == 0 for m in massless) + +def test_freeze_atoms(structure, system, tol_atom_indices): + print('Testing freeze_atoms') + masses = [system.getParticleMass(i)._value for i in tol_atom_indices] + frzn_lig = freeze_atoms(structure, system, ':LIG') + massless = [frzn_lig.getParticleMass(i)._value for i in tol_atom_indices] + + # Check that masses have been zeroed + assert massless != masses + assert all(m == 0 for m in massless) + + +def test_freeze_radius(structure, system_cfg, caplog): + print('Testing freeze_radius') + freeze_cfg = {'freeze_center': ':LIG', 'freeze_solvent': ':Cl-', 'freeze_distance': 100.0 * unit.angstroms} + # Setup toluene-T4 lysozyme system + prmtop = utils.get_data_filename('blues', 'tests/data/eqToluene.prmtop') + inpcrd = utils.get_data_filename('blues', 'tests/data/eqToluene.inpcrd') + structure = parmed.load_file(prmtop, xyz=inpcrd) + atom_indices = utils.atomIndexfromTop('LIG', structure.topology) + system = structure.createSystem(**system_cfg) + + # Freeze everything around the binding site + frzn_sys = freeze_radius(structure, system, **freeze_cfg) + + # Check that the ligand has NOT been frozen + lig_masses = [system.getParticleMass(i)._value for i in atom_indices] + assert all(m != 0 for m in lig_masses) + + # Check that the binding site has NOT been frozen + selection = "({freeze_center}<:{freeze_distance._value})&!({freeze_solvent})".format(**freeze_cfg) + site_idx = utils.amber_selection_to_atomidx(structure, selection) + masses = [frzn_sys.getParticleMass(i)._value for i in site_idx] + assert all(m != 0 for m in masses) + + # Check that the selection has been frozen + # Invert that selection to freeze everything but the binding site. + freeze_idx = set(range(system.getNumParticles())) - set(site_idx) + massless = [frzn_sys.getParticleMass(i)._value for i in freeze_idx] + assert all(m == 0 for m in massless) + + # Check number of frozen atoms is equal to center + system = structure.createSystem(**system_cfg) + with caplog.at_level(logging.ERROR): + frzn_all = freeze_radius(structure, system, freeze_solvent=':WAT', freeze_distance=1*unit.angstrom) + assert 'ERROR' in caplog.text + + # Check all frozen error + system = structure.createSystem(**system_cfg) + with caplog.at_level(logging.ERROR): + frzn_all = freeze_radius(structure, system, freeze_solvent=':WAT', freeze_distance=0*unit.angstrom) + assert 'ERROR' in caplog.text + + # Check freeze threshold error + system = structure.createSystem(**system_cfg) + with caplog.at_level(logging.ERROR): + frzn_all = freeze_radius(structure, system, freeze_solvent=':WAT', freeze_distance=2*unit.angstrom) + assert 'ERROR' in caplog.text + + # Check freeze threshold error + system = structure.createSystem(**system_cfg) + with caplog.at_level(logging.WARNING): + frzn_sys = freeze_radius(structure, system, freeze_solvent=':Cl-', freeze_distance=20*unit.angstrom) + assert 'WARNING' in caplog.text + + + +def test_addBarostat(system): + print('Testing MonteCarloBarostat') + forces = system.getForces() + npt_system = addBarostat(system) + npt_forces = npt_system.getForces() + + #Check that forces have been added to the system. + assert len(forces) != len(npt_forces) + #Check that it has added the MonteCarloBarostat + assert isinstance(npt_forces[-1], openmm.MonteCarloBarostat) diff --git a/blues/utils.py b/blues/utils.py index 1b394581..dfe803cd 100644 --- a/blues/utils.py +++ b/blues/utils.py @@ -8,6 +8,7 @@ import logging import os import sys +import numpy as np from math import ceil, floor from platform import uname @@ -17,133 +18,30 @@ logger = logging.getLogger(__name__) -def saveSimulationFrame(simulation, outfname): - """Extracts a ParmEd structure and writes the frame given - an OpenMM Simulation object. - - Parameters - ---------- - simulation : openmm.Simulation - The OpenMM Simulation to write a frame from. - outfname : str - The output file name to save the simulation frame from. Supported - extensions: - - - PDB (.pdb, pdb) - - PDBx/mmCIF (.cif, cif) - - PQR (.pqr, pqr) - - Amber topology file (.prmtop/.parm7, amber) - - CHARMM PSF file (.psf, psf) - - CHARMM coordinate file (.crd, charmmcrd) - - Gromacs topology file (.top, gromacs) - - Gromacs GRO file (.gro, gro) - - Mol2 file (.mol2, mol2) - - Mol3 file (.mol3, mol3) - - Amber ASCII restart (.rst7/.inpcrd/.restrt, rst7) - - Amber NetCDF restart (.ncrst, ncrst) - +def amber_selection_to_atomidx(structure, selection): """ - topology = simulation.topology - system = simulation.context.getSystem() - state = simulation.context.getState( - getPositions=True, - getVelocities=True, - getParameters=True, - getForces=True, - getParameterDerivatives=True, - getEnergy=True, - enforcePeriodicBox=True) - - # Generate the ParmEd Structure - structure = parmed.openmm.load_topology(topology, system, xyz=state.getPositions()) - - structure.save(outfname, overwrite=True) - logger.info('\tSaving Frame to: %s' % outfname) - - -def print_host_info(simulation): - """Prints hardware related information for the openmm.Simulation + Converts AmberMask selection [amber-syntax]_ to list of atom indices. Parameters ---------- - simulation : openmm.Simulation - The OpenMM Simulation to write a frame from. - - """ - # OpenMM platform information - mmver = openmm.version.version - mmplat = simulation.context.getPlatform() - msg = 'OpenMM({}) simulation generated for {} platform\n'.format(mmver, mmplat.getName()) - - # Host information - for k, v in uname()._asdict().items(): - msg += '{} = {} \n'.format(k, v) - - # Platform properties - for prop in mmplat.getPropertyNames(): - val = mmplat.getPropertyValue(simulation.context, prop) - msg += '{} = {} \n'.format(prop, val) - logger.info(msg) - + structure : parmed.Structure() + Structure of the system, used for atom selection. + selection : str + AmberMask selection that gets converted to a list of atom indices. -def calculateNCMCSteps(nstepsNC=0, nprop=1, propLambda=0.3, **kwargs): - """ - Calculates the number of NCMC switching steps. + Returns + ------- + mask_idx : list of int + List of atom indices. - Parameters + References ---------- - nstepsNC : int - The number of NCMC switching steps - nprop : int, default=1 - The number of propagation steps per NCMC switching steps - propLambda : float, default=0.3 - The lambda values in which additional propagation steps will be added - or 0.5 +/- propLambda. If 0.3, this will add propgation steps at lambda - values 0.2 to 0.8. + .. [amber-syntax] J. Swails, ParmEd Documentation (2015). http://parmed.github.io/ParmEd/html/amber.html#amber-mask-syntax """ - ncmc_parameters = {} - # Make sure provided NCMC steps is even. - if (nstepsNC % 2) != 0: - rounded_val = nstepsNC & ~1 - msg = 'nstepsNC=%i must be even for symmetric protocol.' % (nstepsNC) - if rounded_val: - logger.warning(msg + ' Setting to nstepsNC=%i' % rounded_val) - nstepsNC = rounded_val - else: - logger.error(msg) - sys.exit(1) - # Calculate the total number of lambda switching steps - lambdaSteps = nstepsNC / (2 * (nprop * propLambda + 0.5 - propLambda)) - if int(lambdaSteps) % 2 == 0: - lambdaSteps = int(lambdaSteps) - else: - lambdaSteps = int(lambdaSteps) + 1 - - # Calculate number of lambda steps inside/outside region with extra propgation steps - in_portion = (propLambda) * lambdaSteps - out_portion = (0.5 - propLambda) * lambdaSteps - in_prop = int(nprop * (2 * floor(in_portion))) - out_prop = int((2 * ceil(out_portion))) - propSteps = int(in_prop + out_prop) - - if propSteps != nstepsNC: - logger.warn("nstepsNC=%s is incompatible with prop_lambda=%s and nprop=%s." % (nstepsNC, propLambda, nprop)) - logger.warn("Changing NCMC protocol to %s lambda switching within %s total propagation steps." % (lambdaSteps, - propSteps)) - nstepsNC = lambdaSteps - - moveStep = int(nstepsNC / 2) - ncmc_parameters = { - 'nstepsNC': nstepsNC, - 'propSteps': propSteps, - 'moveStep': moveStep, - 'nprop': nprop, - 'propLambda': propLambda - } - - return ncmc_parameters - + mask = parmed.amber.AmberMask(structure, str(selection)) + mask_idx = [i for i in mask.Selected()] + return mask_idx def check_amber_selection(structure, selection): """ @@ -163,9 +61,11 @@ def check_amber_selection(structure, selection): """ - mask_idx = [] - mask = parmed.amber.AmberMask(structure, str(selection)) - mask_idx = [i for i in mask.Selected()] + try: + mask = parmed.amber.AmberMask(structure, str(selection)) + mask_idx = [i for i in mask.Selected()] + except: + mask_idx = [] if not mask_idx: if ':' in selection: res_set = set(residue.name for residue in structure.residues) @@ -174,7 +74,33 @@ def check_amber_selection(structure, selection): elif '@' in selection: atom_set = set(atom.name for atom in structure.atoms) logger.error("'{}' was not a valid Amber selection. Valid atoms: {}".format(selection, atom_set)) - sys.exit(1) + return False + else: + return True + +def atomidx_to_atomlist(structure, mask_idx): + """ + Goes through the structure and matches the previously selected atom + indices to the atom type. + + Parameters + ---------- + structure : parmed.Structure() + Structure of the system, used for atom selection. + mask_idx : list of int + List of atom indices. + + Returns + ------- + atom_list : list of atoms + The atoms that were previously selected in mask_idx. + """ + atom_list = [] + for i, at in enumerate(structure.atoms): + if i in mask_idx: + atom_list.append(structure.atoms[i]) + logger.debug('\nFreezing {}'.format(atom_list)) + return atom_list def parse_unit_quantity(unit_quantity_str): @@ -199,28 +125,6 @@ def parse_unit_quantity(unit_quantity_str): return unit.Quantity(float(value), eval('unit.%s' % u)) -def zero_masses(system, atomList=None): - """ - Zeroes the masses of specified atoms to constrain certain degrees of freedom. - - Arguments - --------- - system : penmm.System - system to zero masses - atomList : list of ints - atom indicies to zero masses - - Returns - ------- - system : openmm.System - The modified system with massless atoms. - - """ - for index in (atomList): - system.setParticleMass(index, 0 * unit.daltons) - return system - - def atomIndexfromTop(resname, topology): """ Get atom indices of a ligand from OpenMM Topology. @@ -244,6 +148,122 @@ def atomIndexfromTop(resname, topology): lig_atoms.append(atom.index) return lig_atoms +def getMasses(atom_subset, topology): + """ + Returns a list of masses of the specified ligand atoms. + + Parameters + ---------- + topology: parmed.Topology + ParmEd topology object containing atoms of the system. + Returns + ------- + masses: 1xn numpy.array * simtk.unit.dalton + array of masses of len(self.atom_indices), denoting + the masses of the atoms in self.atom_indices + totalmass: float * simtk.unit.dalton + The sum of the mass found in masses + """ + if isinstance(atom_subset, slice): + atoms = list(topology.atoms())[atom_subset] + else: + atoms = [ list(topology.atoms())[i] for i in atom_subset] + masses = unit.Quantity(np.zeros([int(len(atoms)), 1], np.float32), unit.dalton) + for idx, atom in enumerate(atoms): + masses[idx] = atom.element._mass + totalmass = masses.sum() + return masses, totalmass + +def getCenterOfMass(positions, masses): + """ + Returns the calculated center of mass of the ligand as a numpy.array + + Parameters + ---------- + positions: nx3 numpy array * simtk.unit compatible with simtk.unit.nanometers + ParmEd positions of the atoms to be moved. + masses : numpy.array + numpy.array of particle masses + Returns + ------- + center_of_mass: numpy array * simtk.unit compatible with simtk.unit.nanometers + 1x3 numpy.array of the center of mass of the given positions + """ + if isinstance(positions, unit.Quantity): + coordinates = np.asarray(positions._value, np.float32) + pos_unit = positions.unit + else: + coordinates = np.asarray(positions, np.float32) + pos_unit = unit.angstroms + center_of_mass = parmed.geometry.center_of_mass(coordinates, masses) * pos_unit + return center_of_mass + +def saveContextFrame(context, topology, outfname): + """Extracts a ParmEd structure and writes the frame given + an OpenMM Simulation object. + + Parameters + ---------- + simulation : openmm.Simulation + The OpenMM Simulation to write a frame from. + outfname : str + The output file name to save the simulation frame from. Supported + extensions: + + - PDB (.pdb, pdb) + - PDBx/mmCIF (.cif, cif) + - PQR (.pqr, pqr) + - Amber topology file (.prmtop/.parm7, amber) + - CHARMM PSF file (.psf, psf) + - CHARMM coordinate file (.crd, charmmcrd) + - Gromacs topology file (.top, gromacs) + - Gromacs GRO file (.gro, gro) + - Mol2 file (.mol2, mol2) + - Mol3 file (.mol3, mol3) + - Amber ASCII restart (.rst7/.inpcrd/.restrt, rst7) + - Amber NetCDF restart (.ncrst, ncrst) + + """ + system = context.getSystem() + state = context.getState( + getPositions=True, + getVelocities=True, + getParameters=True, + getForces=True, + getParameterDerivatives=True, + getEnergy=True, + enforcePeriodicBox=True) + + # Generate the ParmEd Structure + structure = parmed.openmm.load_topology(topology, system, xyz=state.getPositions()) + + structure.save(outfname, overwrite=True) + logger.info('\tSaving Frame to: %s' % outfname) + +def print_host_info(context): + """Prints hardware related information for the openmm.Simulation + + Parameters + ---------- + simulation : openmm.Simulation + The OpenMM Simulation to write a frame from. + + """ + # OpenMM platform information + mmver = openmm.version.version + mmplat = context.getPlatform() + msg = 'OpenMM({}) Context generated for {} platform\n'.format(mmver, mmplat.getName()) + + # Host information + for k, v in uname()._asdict().items(): + msg += '{} = {} \n'.format(k, v) + + # Platform properties + for prop in mmplat.getPropertyNames(): + val = mmplat.getPropertyValue(context, prop) + msg += '{} = {} \n'.format(prop, val) + logger.info(msg) + def get_data_filename(package_root, relative_path): """Get the full path to one of the reference files in testsystems. @@ -271,99 +291,3 @@ def get_data_filename(package_root, relative_path): if not os.path.exists(fn): raise ValueError("Sorry! %s does not exist. If you just added it, you'll have to re-install" % fn) return fn - - -def spreadLambdaProtocol(switching_values, steps, switching_types='auto', kind='cubic', return_tab_function=True): - """ - Takes a list of lambda values (either for sterics or electrostatics) and transforms that list - to be spread out over a given `steps` range to be easily compatible with the OpenMM Discrete1DFunction - tabulated function. - - Parameters - ---------- - switching_values : list - A list of lambda values decreasing from 1 to 0. - steps : int - The number of steps wanted for the tabulated function. - switching_types : str, optional, default='auto' - The type of lambda switching the `switching_values` corresponds to, either 'auto', 'electrostatics', - or 'sterics'. If 'electrostatics' this assumes the inital value immediately decreases from 1. - If 'sterics' this assumes the inital values stay at 1 for some period. - If 'auto' this function tries to guess the switching_types based on this, based on typical - lambda protocols turning off the electrostatics completely, before turning off sterics. - kind : str, optional, default='cubic' - The kind of interpolation that should be performed (using scipy.interpolate.interp1d) to - define the lines between the points of switching_values. - - Returns - ------- - tab_steps : list or simtk.openmm.openmm.Discrete1DFunction - List of length `steps` that corresponds to the tabulated-friendly version of the input switching_values. - If return-tab_function=True - - Examples - -------- - >>> from simtk.openmm.openmm import Continuous1DFunction, Discrete1DFunction - >>> sterics = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.95, 0.8848447462380346, - 0.8428373352131427, 0.7928373352131427, 0.7490146003095886, 0.6934088361682191, - 0.6515123083157823, 0.6088924298371354, 0.5588924298371354, 0.5088924298371353, - 0.4649556683144045, 0.4298606804827029, 0.3798606804827029, 0.35019373288005945, - 0.31648339779024653, 0.2780498882483276, 0.2521302239477468, 0.23139484523965026, - 0.18729812232625365, 0.15427643961733822, 0.12153116162972155, - 0.09632462702545555, 0.06463743549588846, 0.01463743549588846, - 0.0] - - >>> statics = [1.0, 0.8519493439593149, 0.7142750443470669, - 0.5385929179832776, 0.3891972949356391, 0.18820309596839535, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - >>> statics_tab = spreadLambdaProtocol(statics, opt['nstepsNC'], switching_types='auto') - >>> sterics_tab = spreadLambdaProtocol(sterics, opt['nstepsNC'], switching_types='sterics') - - >>> # Assuming some Context already exists: - >>> context._integrator.addTabulatedFunction( 'sterics_tab', sterics_tab) - >>> context._integrator.addTabulatedFunction( 'electrostatics_tab', statics_tab) - - - """ - #In order to symmetrize the interpolation of turning lambda on/off use the 1.0/0.0 values as a guide - one_counts = switching_values.count(1.0) - counts = switching_values.index(0.0) - #symmetrize the lambda values so that the off state is at the middle - switching_values = switching_values + (switching_values)[::-1][1:] - #find the original scaling of lambda, from 0 to 1 - x = [float(j) / float(len(switching_values) - 1) for j in range(len(switching_values))] - #find the new scaling of lambda, accounting for the number of steps - xsteps = np.arange(0, 1. + 1. / float(steps), 1. / float(steps)) - #interpolate to find the intermediate values of lambda - interpolate = interp1d(x, switching_values, kind=kind) - - #next we check if we're doing a electrostatic or steric protocol - #interpolation doesn't guarantee - if switching_types == 'auto': - if switching_values[1] == 1.0: - switching_types = 'sterics' - else: - switching_types = 'electrostatics' - if switching_types == 'sterics': - tab_steps = [ - 1.0 if (xsteps[i] < x[(one_counts - 1)] or xsteps[i] > x[-(one_counts)]) else j - for i, j in enumerate(interpolate(xsteps)) - ] - elif switching_types == 'electrostatics': - tab_steps = [ - 0.0 if (xsteps[i] > x[(counts)] and xsteps[i] < x[-(counts + 1)]) else j - for i, j in enumerate(interpolate(xsteps)) - ] - else: - raise ValueError('`switching_types` should be either sterics or electrostatics, currently ' + switching_types) - tab_steps = [j if i <= floor(len(tab_steps) / 2.) else tab_steps[(-i) - 1] for i, j in enumerate(tab_steps)] - for i, j in enumerate(tab_steps): - if j < 0.0 or j > 1.0: - raise ValueError( - 'This function is not working properly.', - 'value %f at index %i is not bounded by 0.0 and 1.0 Please check if your switching_type is correct' % - (j, i)) - if return_tab_function: - tab_steps = Discrete1DFunction(tab_steps) - return tab_steps diff --git a/devdocs/README.md b/devdocs/README.md deleted file mode 100644 index cd9676c8..00000000 --- a/devdocs/README.md +++ /dev/null @@ -1,134 +0,0 @@ -# Diagram of classes in BLUES -![Class diagram](class-diagram.png) - -## Legend -- ![#97D017](https://placehold.it/15/97D077/000000?text=+) `User Input` -- ![#B3B3B3](https://placehold.it/15/B3B3B3/000000?text=+) `Core BLUES Objects` - - Class attributes are represented with : `+ ` - - Class functions are represented with : **+ function()** -- ![#6C8EBF](https://placehold.it/15/6C8EBF/000000?text=+) `Simulation() functions` - -### ![#97D017](https://placehold.it/15/97D077/000000?text=+) `User Input` and ![#B3B3B3](https://placehold.it/15/B3B3B3/000000?text=+) `Core BLUES Objects` -Before running the BLUES simulation, the user is expected to have a forcefield parameterized `parmed.Structure` of the solvated protein:ligand system. -For example, in the `blues/example.py` script we have: - -```python -#Generate the ParmEd Structure -prmtop = utils.get_data_filename('blues', 'tests/data/eqToluene.prmtop') -inpcrd = utils.get_data_filename('blues', 'tests/data/eqToluene.inpcrd') -struct = parmed.load_file(prmtop, xyz=inpcrd) -``` - -3 other inputs are required to generate the 3 core BLUES objects (described in more detail below): -- `Move()` -- `MoveEngine()` -- `SimulationFactory()` - -The `Move()` class arguments can vary dramatically between subclasses, but the inputs generally allow the selection of particular atoms to be moved during the NCMC simluation. One method that all `Move()` classes have in common is `move()`, which takes in a context and changes the positions of the specified atoms, in a way that depends on the particular move. -In the example, we are rotating the toluene ligand in T4 lysozyme using the `RandomLigandRotationMove` which specifes the ligand atoms to be moved by specifying the resname:`'LIG'`. - - -```python -from blues.move import RandomLigandRotationMove -#Define the 'Move' object we are perturbing here. -ligand = RandomLigandRotationMove(struct, 'LIG') -ligand.calculateProperties() -``` - -`MoveEngine()`, defines what types of moves will be performed during the NCMC protocol with what probability. -`MoveEngine` takes in either a `Move` or list of `Move` objects. If a list is passed in, then one of those `Move.move()` methods will be selected during an NCMC iteration depending on a probability defined by the `probabilities` argument. If no probability is specified, each move is given equal weight. -In the example we have just a single move so we just pass that into the `MoveEngine` directly. - -```python -# Initialize object that proposes moves. -from blues.engine import MoveEngine -ligand_mover = MoveEngine(ligand) -``` - -Now that we have selected the ligand, defined the NCMC move, and created their corresponding Python objects, we generate the OpenMM Simulations in the `SimulationFactory()`. -This class takes in 3 inputs: - 1. `parmed.Structure` of the solvated protein:ligand system. - 2. `MoveEngine` to obtain the atom indices to generate the alchemical system. - 3. `opt` is a dictionary of various simulation parameters (see below). - -Snippet from the example script below: -```python -opt = { 'temperature' : 300.0, 'friction' : 1, 'dt' : 0.002, - 'nIter' : 10, 'nstepsNC' : 10, 'nstepsMD' : 5000, - 'nonbondedMethod' : 'PME', 'nonbondedCutoff': 10, 'constraints': 'HBonds', - 'trajectory_interval' : 1000, 'reporter_interval' : 1000, - 'platform' : 'OpenCL', - 'verbose' : True } -# Generate the MD, NCMC, ALCHEMICAL Simulation objects -simulations = ncmc.SimulationFactory(struct, ligand, **opt) -simulations.createSimulationSet() -``` - -### ![#6C8EBF](https://placehold.it/15/6C8EBF/000000?text=+) `Simulation() functions` -In order to run the BLUES simulation, we provide the `Simulation()` class the 3 core objects described above. -From the example: - -```python -blues = Simulation(simulations, ligand, ligand_mover, **opt) -blues.run() -``` - -In each NCMC iteration, there are 4 general stages: - 1. `setStateConditions()`: store the current state of our Simulations to a dict. - 2. `simulateNCMC()` : Alchemically scale our ligand interactions and perform the rotational move. - 3. `acceptReject()` : Accept/reject move based on Metropolis criterion. - 4. `simulateMD()` : Allow the system to advance in time. - -Described below are the simulation stages described in more detail: -#### `simulateNCMC`: Performing the rotational move. -At this stage we operate on all 3 of the BLUES core objects. -First, we advance the NCMC simulation by referencing `simulations.nc` generated from the `SimulationFactory()` class. -As we take steps in the NCMC simulation, the ligand interactions are alchemically scaled off/on. - -Before we take any steps, a `Move` object is randomly chosen from `ligand_mover.moves` to determine which atoms need to be alchemically treated. -We perform the rotation (and moves in general) half-way through the number of NCMC steps to ensure the protocol is symmetric, to help maintain detailed balance. - -In the diagram, attributes are marked with a `*` to denote that these are dynamic variables that **need** to be updated with each step. - -#### `acceptReject`: Metropolis-Hastings Acceptance or Rejection. -After performing the rotational move in the NCMC simulation, we accept or reject the move in accordance to the Metropolis criterion. -Then, we obtain the `LogAcceptanceProbability` from the NCMC integrator and add in the alchemical correction factor. - -The alchemical correction factor is obtained by setting the positions `simulations.alch` (from the `SimulationFactory()` class) to that of the current state of the NCMC simulation. -Then, we compute the difference in potential energy from the previous state. - -If the corrected log acceptance probability is greater than a randomly generated number we accept the move, otherwise the move is rejected. -On move acceptance, we set the positions of `simulations.md` to the current (i.e rotated) positions. -On rejection, we reset the positions of `simulations.nc` back to their positions before the NCMC simulation. -In either case, after the positions have been set, we randomly set the velocities of `simulations.md` by the temperature. - -#### `simulateMD` : Relaxing the system. -After the move has been accepted or rejected, we simply allow the system to relax by advancing the MD simulation, referenced from `simulations.md`. -After the MD simulation has completed the specified number of steps, we set the positions and velocities of `simulations.nc` to that of the final state of the MD simulation. - -In regards to velocities, it may be important to note: -- *Before the MD simulation*, velocities are randomly initialized by the selected temperature. -- *After the MD simulation*, the NCMC simulation uses velocities from the final state of the MD simulation. - - -### Implementing Custom Moves -Users can implement their own MC moves into NCMC by inheriting from an appropriate `blues.moves.Move` class and constructing a custom `move()` method that only takes in an Openmm context object as a parameter. The `move()` method will then access the positions of that context, change those positions, then update the positions of that context. For example if you would like to add a move that randomly translates a set of coordinates the code would look similar to this pseudocode: - -```python -from blues.moves import Move -class TranslationMove(Move): - def __init__(self, atom_indices): - self.atom_indices = atom_indices - def move(context): - positions = context.context.getState(getPositions=True).getPositions(asNumpy=True) - #get positions from context - #use some function that translates atom_indices - newPositions = RandomTranslation(positions[self.atom_indices]) - context.setPositions(newPositions) - return context -``` - -###Combining Moves together -If you're interested in combining moves together sequentially–say you'd like to perform a rotation and translation move together–instead of coding up a new `Move` class that performs that, you can instead leverage the functionality of existing `Move`s using the `CombinationMove` class. `CombinationMove` takes in a list of instantiated `Move` objects. The `CombinationMove`'s `move()` method perfroms the moves in either listed or reverse order. Replicating a rotation and translation move on t, then, can effectively be done by passing in an instantiated TranslationMove (from the pseudocode example above) and RandomLigandRotation. -One important non-obvious thing to note about the CombinationMove class is that to ensure detailed balance is maintained, moves are done half the time in listed order and half the time in the reverse order. -``` diff --git a/devdocs/class-diagram.html b/devdocs/class-diagram.html deleted file mode 100644 index 433cdc16..00000000 --- a/devdocs/class-diagram.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - -class-diagram - - -
- - - \ No newline at end of file diff --git a/devdocs/class-diagram.png b/devdocs/class-diagram.png deleted file mode 100644 index 5d66c6754da6e6920c3ecb631c3a27f6f5cdc42e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116965 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.15.0 + - openmmtools <=0.16.0 - mdtraj - openmm >=7.2.2 + - scipy <=1.1.0 - parmed - pymbar - netcdf4 @@ -42,6 +44,6 @@ test: - blues about: - home: https://github.com/mobleylab/blues + home: https://github.com/nathanmlim/blues license: MIT license_file: LICENSE diff --git a/docs/BLUES_tutorial.ipynb b/docs/BLUES_tutorial.ipynb deleted file mode 100644 index b75c0bbd..00000000 --- a/docs/BLUES_tutorial.ipynb +++ /dev/null @@ -1,1012 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\"nbsphinx-toctree\": {\n", - " \"maxdepth\": 2\n", - "}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Introduction to BLUES\n", - "\n", - "In this Jupyter Notebook, we will cover the following topics:\n", - "\n", - "- YAML configuration\n", - "- Setting up a system for BLUES\n", - "- Advanced options (HMR, restraints, freezing)\n", - "- Configuring reporters\n", - "- Running a BLUES simulation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Background\n", - "\n", - "## Coupling MD simulations with random NCMC moves for enhanced sampling of ligand binding modes via BLUES\n", - " [**BLUES**](https://pubs.acs.org/doi/abs/10.1021/acs.jpcb.7b11820) is an approach that combines molecular dynamics ([MD](http://www.ch.embnet.org/MD_tutorial/pages/MD.Part1.html)) simulations and the Non-equilibrium Candidate Monte Carlo ([NCMC](http://www.pnas.org/content/108/45/E1009)) framework to enhance ligand binding mode sampling [(Github)](https://github.com/MobleyLab/blues) \n", - " \n", - "During a MD simulation, BLUES will perform a random rotation of the bound ligand and then allow the system to relax through [_alchemically_](http://www.alchemistry.org/wiki/Free_Energy_Fundamentals#Why_the_name_.22Alchemical.22.3F) scaling off/on the ligand-receptor interactions. BLUES enables us to sample alternative ligand binding modes, that would normally take _very_ long simulations to capture in a traditional MD simulation because of the large gap in [timescales](https://www.ncbi.nlm.nih.gov/pubmed/20934381) between atomistic motions and biological motions.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# YAML Configuration\n", - "\n", - "BLUES can be configured in either pure python through dictionaries of the appropriate parameters or using a YAML file (which is converted to a dict under the hood). Below we will walk through the keywords for configuring BLUES. An example YAML configuration file can be found in `rotmove_cuda.yaml`. \n", - "\n", - "Note: Code blocks in this notebook denoted with ------ indicate a section from a YAML configuration file." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Input/Output\n", - "\n", - "```\n", - "------\n", - "output_dir: .\n", - "outfname: t4-toluene\n", - "logger_level: info #critical, error, warning, info, debug\n", - "-----\n", - "```\n", - "\n", - "### Output files\n", - "Specify the directory you want all the simulation output files to be saved to with **`output_dir`**. By default, BLUES will save them in the current directory that you're running BLUES in. The parameter **`outfname`** will be used for the filename prefix for all output files (e.g. `t4-toluene.nc`, `t4-toleune.log`). The level of verbosity can be controlled by **`logger_level`**. The default **`logger_level`** is set to `info` and valid choices are `critical`, `error`, `warning`, `info` or `debug`.\n", - "\n", - "### Input files to generate the structure of your system\n", - "\n", - "- Input a Parameter/topology file and a Coordinate file, which will be used to generate the ParmEd Structure. \n", - "\n", - "- The ParmEd Structure is a chemical structure composed of atoms, bonds, angles, torsions, and other topological features. \n", - "\n", - "To see a full list of supported file formats: https://parmed.github.io/ParmEd/html/readwrite.html\n", - "\n", - "```\n", - "----------------\n", - "structure:\n", - " filename: tests/data/eqToluene.prmtop\n", - " xyz: tests/data/eqToluene.inpcrd\n", - " restart: t4-toluene_2.rst7\n", - "----------------\n", - "```\n", - "\n", - "BLUES simulations all begin from a `parmed.Structure`. Keywords nested under **`structure`** are for generating the `parmed.Structure` by calling [parmed.load_file()](https://parmed.github.io/ParmEd/html/readwrite.html#reading-files-with-load-file) under the hood. The **`filename`** keyword should point to the file containing the parameters for your system. For example, if coming from AMBER you would specify the `.prmtop` file. The **`xyz`** keyword is intended for specifying the coordinates of your system (e.g. `.inpcrd` or `.pdb`).\n", - "\n", - "### Restart files\n", - "BLUES supports \"soft\" restarts of simulations from AMBER restart files **`.rst7`** via [parmed.amber.Rst7](https://parmed.github.io/ParmEd/html/amberobj/parmed.amber.Rst7.html?highlight=rst7#parmed-amber-rst7). \"Soft\" restart implies the simulation will begin from the saved positions, velocities, and box vectors but does not store any internal data such as the states of random number generators. It should be noted that velocities are re-initialized at every BLUES iteration, so storing these is not so important." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## System configuration \n", - "\n", - "From the YAML file, there is a section dedicated for generating an `openmm.System` from a `parmed.Structure`. For definitions of **`system`** keywords and valid options see [parmed.Structure.createSystem()]( https://parmed.github.io/ParmEd/html/structobj/parmed.structure.Structure.html#parmed.structure.Structure.createSystem)\n", - "\n", - "Below we provide an example for generating a system in a cubic box with explicit solvent.\n", - "\n", - "```\n", - "--------\n", - "system:\n", - " nonbondedMethod: PME\n", - " nonbondedCutoff: 10 * angstroms\n", - " constraints: HBonds\n", - " rigidWater: True\n", - " removeCMMotion: True\n", - " ewaldErrorTolerance: 0.005\n", - " flexibleConstraints: True\n", - " splitDihedrals: False\n", - "---------\n", - "```\n", - "\n", - "### (Optional) Hydrogen mass repartitioning\n", - "BLUES has the option to use the hydrogen mass repartitioning scheme [HMR](https://pubs.acs.org/doi/abs/10.1021/ct5010406) to allow use of longer time steps in the simulation. Simply provide the keyword **`hydrogenMass`** in the YAML file like below:\n", - "\n", - "```\n", - "--------\n", - "system:\n", - " nonbondedMethod: PME\n", - " nonbondedCutoff: 10 * angstroms\n", - " constraints: HBonds\n", - " rigidWater: True\n", - " removeCMMotion: True\n", - " ewaldErrorTolerance: 0.005\n", - " flexibleConstraints: True\n", - " splitDihedrals: False\n", - " hydrogenMass: 3.024 * daltons\n", - "---------\n", - "```\n", - "\n", - "If using HMR, you can set the timestep (**`dt`**) for the simulation to 4fs and **`constraints`** should be set to either **`HBonds`** or **`AllBonds`**. \n", - "\n", - "\n", - "### (Optional) Alchemical system configuration \n", - "\n", - "Nested under the system parameters, you can modify parameters for the alchemical system. Below are the default settings and are not required to be specified in the YAML configuration. **Modifications to these parameters are for advanced users.** \n", - "\n", - "```\n", - "-------\n", - "system:\n", - " nonbondedMethod: PME\n", - " nonbondedCutoff: 10 * angstroms\n", - " constraints: HBonds\n", - " rigidWater: True\n", - " removeCMMotion: True\n", - " ewaldErrorTolerance: 0.005\n", - " flexibleConstraints: True\n", - " splitDihedrals: False\n", - " alchemical:\n", - " # Sterics\n", - " softcore_alpha: 0.5\n", - " softcore_a : 1\n", - " softcore_b : 1\n", - " softcore_c : 6\n", - " \n", - " # Electrostatics\n", - " softcore_beta : 0.0\n", - " softcore_d : 1\n", - " softcore_e : 1\n", - " softcore_f : 2\n", - " \n", - " annihilate_electrostatics : True \n", - " annihilate_sterics : False\n", - "-------\n", - "```\n", - "\n", - "For further details on alchemical parameters see: http://getyank.org/0.16.2/yamlpages/options.html" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Simulation Configuration\n", - "The keywords for configuring the Simulations for BLUES are explained below:\n", - "\n", - "- **`dt`**: timestep\n", - "- **`nIter`**: number of iterations or proposed moves\n", - "- **`nstepsMD`**: number of MD steps\n", - "- **`nstepsNC`**: number NCMC steps\n", - "\n", - "The configuration below will run BLUES in NVT with 2fs timesteps for 10 iterations. The MD and NCMC simulation will run 10,000 steps per iteration. \n", - "```\n", - "----------\n", - "simulation:\n", - " platform: CUDA \n", - " dt: 0.002 * picoseconds\n", - " friction: 1 * 1/picoseconds\n", - " temperature: 300 * kelvin\n", - " nIter: 10\n", - " nstepsMD: 10000\n", - " nstepsNC: 10000\n", - "----------\n", - "```\n", - "\n", - "### NPT Simulation\n", - "\n", - "To run BLUES in NPT, simply specify a **`pressure`**:\n", - "```\n", - "----------\n", - "simulation:\n", - " platform: CUDA \n", - " dt: 0.002 * picoseconds\n", - " friction: 1 * 1/picoseconds\n", - " temperature: 300 * kelvin\n", - " nIter: 10\n", - " nstepsMD: 10000\n", - " nstepsNC: 10000\n", - " pressure: 1 * atmospheres\n", - "----------\n", - "```\n", - "\n", - "### (Optional) Additional relaxation steps in the NCMC simulation\n", - "Keywords **`nprop`** and **`propLambda`** allow you to add additional relxation steps between a set range in the lambda schedule for the alchemical process in the NCMC simulation. \n", - "Setting **`propLambda`** to 0.3 will select a lambda range of -/+ 0.3 from the midpoint (0.5), giving [0.2, 0.8]. During the alchemical process, when lambda is between 0.2 to 0.8, **`nprop`** controls the number of additional relaxation steps to add at each lambda step (change in lambda). Additional relaxation steps has been show to increase acceptance proposed NCMC moves.\n", - "```\n", - "----------\n", - "simulation:\n", - " platform: CUDA \n", - " dt: 0.002 * picoseconds\n", - " friction: 1 * 1/picoseconds\n", - " temperature: 300 * kelvin\n", - " nIter: 10\n", - " nstepsMD: 10000\n", - " nstepsNC: 10000\n", - " pressure: 1 * atmospheres\n", - " nprop: 3\n", - " propLambda: 0.3\n", - "----------\n", - "```\n", - "\n", - "\n", - "### (Optional) Platform Properties\n", - "If you need to modify platform properties for the simulation, you can set the keyword **`properties`** like below:\n", - "\n", - "#### Example: OpenCL in single precision on GPU device 2 \n", - "*Note: works for running on the GPU on MacBook Pro 2017*\n", - "\n", - "```\n", - "----------\n", - "simulation:\n", - " platform: OpenCL\n", - " properties:\n", - " OpenCLPrecision: single\n", - " OpenCLDeviceIndex: 2\n", - " dt: 0.002 * picoseconds\n", - " friction: 1 * 1/picoseconds\n", - " temperature: 300 * kelvin\n", - " nIter: 10\n", - " nstepsMD: 10000\n", - " nstepsNC: 10000\n", - " pressure: 1 * atmospheres\n", - "----------\n", - "```\n", - "\n", - "#### Example: CUDA in double precision on GPU device 0\n", - "\n", - "```\n", - "----------\n", - "simulation:\n", - " platform: CUDA\n", - " properties:\n", - " CudaPrecision: double\n", - " CudaDeviceIndex: 0\n", - " dt: 0.002 * picoseconds\n", - " friction: 1 * 1/picoseconds\n", - " temperature: 300 * kelvin\n", - " nIter: 10\n", - " nstepsMD: 10000\n", - " nstepsNC: 10000\n", - " pressure: 1 * atmospheres\n", - "----------\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Reporter Configuration\n", - "\n", - "We provide functionality to configure a recommended set of reporters from the YAML file. These are used to record information for either the MD or NCMC simulation. Below are the keywords for each reporter. Each reporter will require the **`reportInterval`** keyword to specify the frequency to store the simulation data:\n", - "- **`state`** : State data reporter for OpenMM simulations, but it is a little more generalized. Writes to a `.ene` file.\n", - " - For full list of parameters see [parmed.openmm.reporters.StateDataReporter](https://parmed.github.io/ParmEd/html/api/parmed/parmed.openmm.reporters.html#parmed.openmm.reporters.StateDataReporter)\n", - "- **`traj_netcdf`** : Customized AMBER NetCDF (`.nc`) format reporter \n", - "- **`restart`** : Restart AMBER NetCDF (`.rst7`) format reporter\n", - "- **`progress`** : Write to a file (`.prog`), the progress report of how many steps has been done, how fast the simulation is running, and how much time is left (similar to the mdinfo file in Amber). File is overwritten at each reportInterval.\n", - " - For full list of parameters see [parmed.openmm.reporters.ProgressReporter](https://parmed.github.io/ParmEd/html/api/parmed/parmed.openmm.reporters.html#parmed.openmm.reporters.ProgressReporter)\n", - "- **`stream`** : Customized version of openmm.app.StateDataReporter. This will instead stream/print the information to the terminal as opposed to writing to a file. \n", - " - takes the same parameters as the [openmm.app.StateDataReporter](http://docs.openmm.org/development/api-python/generated/simtk.openmm.app.statedatareporter.StateDataReporter.html#simtk.openmm.app.statedatareporter.StateDataReporter)\n", - "\n", - "To attach them to the MD simulation. You nest the reporter keywords under the keyword **`md_reporters`** like below. To attach the reporters to NCMC simulation, use the **`ncmc_reporters`** keyword instead.\n", - "\n", - "```\n", - "------\n", - "md_reporters:\n", - " state:\n", - " reportInterval: 250\n", - " traj_netcdf:\n", - " reportInterval: 250\n", - " restart:\n", - " reportInterval: 1000\n", - " progress:\n", - " totalSteps: 10000\n", - " reportInterval: 10\n", - " stream:\n", - " title: md\n", - " reportInterval: 250\n", - " totalSteps: 10000\n", - " step: True\n", - " speed: True\n", - " progress: True\n", - " remainingTime: True\n", - "----\n", - "```\n", - "\n", - "In the above example, we are using the **`stream`** reporter to print the speeds on the intergrator at regular intervals. This may be a bit redudant with the **`progress`** reporter if are running the job remotely and don't need the information streamed to terminal.\n", - "\n", - "### (Optional) Advanced options to the **`traj_netcdf`** reporter\n", - "The **`traj_netcdf`** reporter can store additional information that may be useful for the NCMC simulation or record at specific frames. In the example below, we will store the first, midpoint (when the move is applied), and last frame of each NCMC iteration, along with the **`alchemicalLambda`** step and the **`protocolWork`**. \n", - "\n", - "```\n", - "----\n", - "ncmc_reporters:\n", - " traj_netcdf:\n", - " frame_indices: [1, 0.5, -1]\n", - " alchemicalLambda: True\n", - " protocolWork: True\n", - "-----\n", - "```\n", - "\n", - "To access the numerical data stored in the NetCDF file:\n", - "```\n", - "from netCDF4 import Dataset\n", - "\n", - "f = Dataset(\"t4-toluene-ncmc.nc\")\n", - "print(f.variables['alchemicalLambda'][:])\n", - "print(f.variables['protocolWork'][:])\n", - "\n", - ">>> [ 0.001 0.5 1. ]\n", - ">>> [ 0.03706791 30.72696877 25.708498 ]\n", - "\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Running a BLUES simulation\n", - "\n", - "Below we will provide an example for running an NPT BLUES simulation which applies random rotational moves to the toluene ligand in T4-lysozyme from a YAML configuration." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "yaml_cfg = \"\"\"\n", - "output_dir: .\n", - "outfname: t4-toluene\n", - "logger_level: info \n", - "\n", - "structure:\n", - " filename: ../blues/tests/data/eqToluene.prmtop\n", - " xyz: ../blues/tests/data/eqToluene.inpcrd\n", - "\n", - "system:\n", - " nonbondedMethod: PME\n", - " nonbondedCutoff: 10 * angstroms\n", - " constraints: HBonds\n", - " rigidWater: True\n", - " removeCMMotion: True\n", - " hydrogenMass: 3.024 * daltons\n", - " ewaldErrorTolerance: 0.005\n", - " flexibleConstraints: True\n", - " splitDihedrals: False\n", - "\n", - "freeze:\n", - " freeze_center: ':LIG'\n", - " freeze_solvent: ':WAT,Cl-'\n", - " freeze_distance: 5 * angstroms\n", - "\n", - "simulation:\n", - " platform: CUDA\n", - " properties:\n", - " CudaPrecision: single\n", - " CudaDeviceIndex: 0\n", - " dt: 0.004 * picoseconds\n", - " friction: 1 * 1/picoseconds\n", - " pressure: 1 * atmospheres\n", - " temperature: 300 * kelvin\n", - " nIter: 5\n", - " nstepsMD: 1000\n", - " nstepsNC: 1000\n", - "\n", - "md_reporters:\n", - " state:\n", - " reportInterval: 250\n", - " traj_netcdf:\n", - " reportInterval: 250\n", - " restart:\n", - " reportInterval: 1000\n", - " stream:\n", - " title: md\n", - " reportInterval: 250\n", - " totalSteps: 5000 # nIter * nstepsMD\n", - " step: True\n", - " speed: True\n", - " progress: True\n", - " remainingTime: True\n", - " \n", - "ncmc_reporters:\n", - " stream:\n", - " title: ncmc\n", - " reportInterval: 250\n", - " totalSteps: 1000 # Use nstepsNC\n", - " step: True\n", - " speed: True\n", - " progress: True\n", - " remainingTime: True\n", - "\"\"\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Import the following BLUES modules required for the following steps.\n", - "\n", - "- Make sure to specify the type of move that you want to import from __blues.moves__\n", - "- Available moves can be veiwed in __moves.py__, supported/tested moves include:\n", - " - RandomLigandRotationMove\n", - " - SideChainMove" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from blues.moves import RandomLigandRotationMove\n", - "from blues.engine import MoveEngine\n", - "from blues.simulation import *\n", - "from blues.settings import *" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "./t4-toluene\n" - ] - } - ], - "source": [ - "#Read in the YAML file\n", - "cfg = Settings(yaml_cfg).asDict()\n", - "#Shortcut to access `parmed.Structure` from dict\n", - "structure = cfg['Structure']" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Below is what the resulting configuration dictionary looks like (formatted into JSON for readability)." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\n", - " \"Logger\": \"\",\n", - " \"Structure\": \"../blues/tests/data/eqToluene.prmtop\",\n", - " \"freeze\": {\n", - " \"freeze_center\": \":LIG\",\n", - " \"freeze_distance\": \"5.0 A\",\n", - " \"freeze_solvent\": \":WAT,Cl-\"\n", - " },\n", - " \"logger_level\": \"info\",\n", - " \"md_reporters\": [\n", - " \"\",\n", - " \"\",\n", - " \"\",\n", - " \"\"\n", - " ],\n", - " \"ncmc_reporters\": [\n", - " \"\"\n", - " ],\n", - " \"outfname\": \"./t4-toluene\",\n", - " \"output_dir\": \".\",\n", - " \"simulation\": {\n", - " \"dt\": \"0.004 ps\",\n", - " \"friction\": \"1.0 /ps\",\n", - " \"md_trajectory_interval\": 250,\n", - " \"moveStep\": 500,\n", - " \"nIter\": 5,\n", - " \"nprop\": 1,\n", - " \"nstepsMD\": 1000,\n", - " \"nstepsNC\": 1000,\n", - " \"outfname\": \"./t4-toluene\",\n", - " \"platform\": \"CUDA\",\n", - " \"pressure\": \"1.0 atm\",\n", - " \"propLambda\": 0.3,\n", - " \"propSteps\": 1000,\n", - " \"properties\": {\n", - " \"CudaDeviceIndex\": 0,\n", - " \"CudaPrecision\": \"single\"\n", - " },\n", - " \"temperature\": \"300.0 K\",\n", - " \"verbose\": false\n", - " },\n", - " \"structure\": {\n", - " \"filename\": \"../blues/tests/data/eqToluene.prmtop\",\n", - " \"xyz\": \"../blues/tests/data/eqToluene.inpcrd\"\n", - " },\n", - " \"system\": {\n", - " \"constraints\": \"HBonds\",\n", - " \"ewaldErrorTolerance\": 0.005,\n", - " \"flexibleConstraints\": true,\n", - " \"hydrogenMass\": \"3.024 Da\",\n", - " \"nonbondedCutoff\": \"10.0 A\",\n", - " \"nonbondedMethod\": \"PME\",\n", - " \"removeCMMotion\": true,\n", - " \"rigidWater\": true,\n", - " \"splitDihedrals\": false,\n", - " \"verbose\": false\n", - " },\n", - " \"verbose\": false\n", - "}\n" - ] - } - ], - "source": [ - "import json\n", - "print(json.dumps(cfg, sort_keys=True, indent=2, skipkeys=True, default=str))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Selecting a move and initialize the move engine\n", - "\n", - "Here, we initialize the **`RandomLigandRotationMove`** from the **`blues.moves`** module which proposes random rotations on the toluene ligand. We select the toluene ligand by providing the residue name `LIG` and the `parmed.Structure` to select the atoms from. If we begin BLUES from our YAML configuration, the `parmed.Structure` for our system is generated from the call to `startup()`. We can access it at the top level with `cfg['Structure']`\n", - "\n", - "After initialization of the selected move, we pass the move object to the **`MoveEngine`** from the **`blues.engine`** module. The **`MoveEngine`** controls what types of moves will be performed during the NCMC protocol and with a given probability. This will be more useful when we use multiple move types." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "#Initialize the move class and pass it to the engine\n", - "ligand = RandomLigandRotationMove(structure, 'LIG')\n", - "ligand_mover = MoveEngine(ligand)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Generating the Systems for openMM: **`SystemFactory`**\n", - "Next, we must generate the `openmm.System` from the `parmed.Structure` by calling the **`SystemFactory`** class from the `blues.simulation` module. The class must be initialized by providing 3 required arguments:\n", - "```\n", - "structure : parmed.Structure\n", - " A chemical structure composed of atoms, bonds, angles, torsions, and\n", - " other topological features.\n", - "atom_indices : list of int\n", - " Atom indicies of the move or designated for which the nonbonded forces\n", - " (both sterics and electrostatics components) have to be alchemically\n", - " modified.\n", - "config : dict, parameters for generating the `openmm.System for the MD\n", - " and NCMC simulation. For complete parameters, see docs for `generateSystem`\n", - " and `generateAlchSystem`\n", - "```\n", - "\n", - "Upon initialization, this class will create the system for the MD simulation and the NCMC simulation. They can be accessed through the attributes **`systems.md`** or **`systems.alch`**. Any modifications to either of these Systems should be done within the context of this object. Once the systems are passed into an `openmm.Simulation`, you will not be able to modify the system easily." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: Adding bonds...\n", - "INFO: Adding angles...\n", - "INFO: Adding dihedrals...\n", - "INFO: Adding Ryckaert-Bellemans torsions...\n", - "INFO: Adding Urey-Bradleys...\n", - "INFO: Adding improper torsions...\n", - "INFO: Adding CMAP torsions...\n", - "INFO: Adding trigonal angle terms...\n", - "INFO: Adding out-of-plane bends...\n", - "INFO: Adding pi-torsions...\n", - "INFO: Adding stretch-bends...\n", - "INFO: Adding torsion-torsions...\n", - "INFO: Adding Nonbonded force...\n" - ] - } - ], - "source": [ - "systems = SystemFactory(structure, ligand.atom_indices, cfg['system'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### (Optional) Applying restraints or freezing atoms\n", - "The **`SystemFactory`** class also provides functionality for restraining or freezing the atoms. **Use extreme caution when freezing/restraining atoms. You should consider if freezing/restraining should be applied to BOTH the MD and alchemical system.**\n", - "\n", - "\n", - "Selections for either restraining or freezing atoms in your system use the [Amber mask syntax](http://parmed.github.io/ParmEd/html/amber.html#amber-mask-syntax).\n", - "\n", - "#### Positional restraints: **`SystemFactory.restrain_positions()`**\n", - "To apply positional restraints, you can call **`SystemFactory.restrain_positions()`**.\n", - "You can specify the parameters/selection for applying positional restraints in the YAML file.\n", - "\n", - "```\n", - "------\n", - "restraints:\n", - " selection: '@CA,C,N' \n", - " weight: 5 \n", - "------\n", - "```\n", - "\n", - "restrain keywords:\n", - "- **`selection`**: Specify what to apply positional restraints to using Amber mask syntax. Default = '@CA,C,N'\n", - "- **`weight`**: Restraint weight for xyz atom restraints in kcal/(mol A^2). Default = 5\n", - "\n", - "\n", - "From the YAML example above, we would be applying positional restraints to the backbone atoms of the protein. If applying restraints, you most likely will want to apply it to BOTH the MD and alchemical systems like below:\n", - "\n", - "```\n", - "systems.md = SystemFactory.restrain_positions(structure, systems.md, **cfg['restraints'])\n", - "systems.alch = SystemFactory.restrain_positions(structure, systems.alch, **cfg['restraints'])\n", - "```\n", - "\n", - "#### Freezing selected atoms: **`SystemFactory.freeze_atoms()`**\n", - "To freeze a selection, call **`SystemFactory.freeze_atoms()`**. Atoms that have a mass of zero will be ignored by the integrator and will not change positions during the simulation, effectively they are frozen.\n", - "\n", - "To freeze atoms using a given selection string. Use the keyword **`freeze_selection`**.\n", - "\n", - "```\n", - "-----\n", - "freeze:\n", - " freeze_selection: ':LIG'\n", - "------\n", - "```\n", - "\n", - "From the YAML example above, we would be freezing only the atoms belonging to the residue `LIG`. Although freezing the ligand in this example wouldn't be very useful. It would be applied like\n", - "\n", - "```\n", - "systems.md = SystemFactory.freeze_atoms(structure, systems.md, **cfg['freeze'])\n", - "```\n", - "\n", - "\n", - "#### Freezing atoms around a selection: **`SystemFactory.freeze_radius()`**\n", - "Alternatively, you can choose to freeze atoms around a given selection. To do so, call **`SystemFactory.freeze_radius()`**. For example, you may want to freeze atoms that are 5 angstroms away from the ligand and include the solvent.\n", - "\n", - "```\n", - "-----\n", - "freeze:\n", - " freeze_center: ':LIG' \n", - " freeze_solvent: ':WAT,Cl-' \n", - " freeze_distance: 5 * angstroms\n", - "------\n", - "```\n", - "\n", - "- **`freeze_center`**: Specifies the center of the object for freezing, masses will be zeroed. Default = ':LIG'\n", - "- **`freeze_solvent`**: select which solvent atoms should have their masses zeroed. Default = ':HOH,NA,CL' \n", - "- **`freeze_distance`**: Distance ( in angstroms) to select atoms for retaining their masses. Atoms outside the set distance will have their masses set to 0.0. Default = 5.0\n", - "\n", - "We often utilize this type of freezing to speed up the alchemical process during the NCMC simulation while leaving them completely free in the MD simulation for proper relaxation. \n", - "```\n", - "systems.alch = SystemFactory.freeze_radius(structure, systems.alch, **cfg['freeze'])\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this notebook example, our YAML config indicates we will be freezing around the ligand (keyword: **`freeze_center`**). So we will call the **`freeze_radius`** function." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: Freezing 22065 atoms 5.0 Angstroms from ':LIG' on >\n" - ] - } - ], - "source": [ - "systems.alch = SystemFactory.freeze_radius(structure, systems.alch, **cfg['freeze'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Generating the OpenMM Simulations: **`SimulationFactory`**\n", - "\n", - "Now that we have generated our `openmm.System` for the MDand frozen the solvent around the ligand. We are now ready to create the set of simulations for running BLUES. We do this by calling the **`SimulationFactory`** class . The expected parameters are:\n", - "\n", - "```\n", - "systems : blues.simulation.SystemFactory object\n", - " The object containing the MD and alchemical openmm.Systems\n", - "move_engine : blues.engine.MoveEngine object\n", - " MoveProposal object which contains the dict of moves performed\n", - " in the NCMC simulation.\n", - "config : dict of parameters for the simulation (i.e timestep, temperature, etc.)\n", - "md_reporters : list of Reporter objects for the MD openmm.Simulation\n", - "ncmc_reporters : list of Reporter objects for the NCMC openmm.Simulation\n", - "```\n", - "\n", - "If you wish to use your own openmm reporters, simply pass them into the arguments as a *list* of Reporter objects. Since we have configured our reporters from the YAML file, we can pass them into the arguments **`md_reporters`** and **`ncmc_reporters`**. " - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ]" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# List of MD reporters\n", - "cfg['md_reporters']" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# List of NCMC reporters\n", - "cfg['ncmc_reporters']" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: Adding MonteCarloBarostat with 1.0 atm. MD simulation will be 300.0 K NPT.\n", - "WARNING: NCMC simulation will NOT have pressure control. NCMC will use pressure from last MD state.\n", - "INFO: OpenMM(7.1.1.dev-c1a64aa) simulation generated for CUDA platform\n", - "system = Linux \n", - "node = titanpascal \n", - "release = 4.13.0-41-generic \n", - "version = #46~16.04.1-Ubuntu SMP Thu May 3 10:06:43 UTC 2018 \n", - "machine = x86_64 \n", - "processor = x86_64 \n", - "DeviceIndex = 0 \n", - "DeviceName = TITAN Xp \n", - "UseBlockingSync = true \n", - "Precision = single \n", - "UseCpuPme = false \n", - "CudaCompiler = /usr/local/cuda-8.0/bin/nvcc \n", - "TempDirectory = /tmp \n", - "CudaHostCompiler = \n", - "DisablePmeStream = false \n", - "DeterministicForces = false \n", - "\n" - ] - } - ], - "source": [ - "simulations = SimulationFactory(systems, ligand_mover, cfg['simulation'], \n", - " cfg['md_reporters'], cfg['ncmc_reporters'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Accessing the MD or NCMC simulation\n", - "If you would like to access the MD or NCMC simulation. You can access them as attributes to the **`SimulationFactory`** class with **`simulations.md`** or **`simulations.ncmc`**. This will allow you to do things like \n", - "energy minimize the system or run a few steps of regular dynamics before running the hybrid (MD+NCMC) BLUES approach. The NCMC simulation will automatically be synced to the state of the MD simulation when running the BLUES simulation." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Pre-Minimized energy = -69057.34671058532 kcal/mol\n", - "Minimized energy = -87007.38938198877 kcal/mol\n" - ] - } - ], - "source": [ - "# Energy minimization\n", - "state = simulations.md.context.getState(getPositions=True, getEnergy=True)\n", - "print('Pre-Minimized energy = {}'.format(state.getPotentialEnergy().in_units_of(unit.kilocalorie_per_mole)))\n", - "\n", - "simulations.md.minimizeEnergy(maxIterations=0)\n", - "state = simulations.md.context.getState(getPositions=True, getEnergy=True)\n", - "print('Minimized energy = {}'.format(state.getPotentialEnergy().in_units_of(unit.kilocalorie_per_mole)))" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "#\"Progress (%)\"\t\"Step\"\t\"Speed (ns/day)\"\t\"Time Remaining\"\n", - "md: 5.0%\t250\t0\t--\n", - "md: 10.0%\t500\t271\t0:05\n" - ] - } - ], - "source": [ - "# Running only the MD simulation\n", - "simulations.md.step(500)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Run the BLUES Simulation\n", - "To run the full BLUES simulation, where we apply NCMC moves and follow-up with the MD simulation, we simply pass the **`SimulationFactory`** object to the **`Simulation`** class and call the **`run()`** function which takes **`nIter`, `nstepsNC`, `nstepsMD` ** as arguments (or we can pass it the simulation configuration from the YAML on initialization of the class)." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: Total BLUES Simulation Time = 40.0 ps (8.0 ps/Iter)\n", - "Total Force Evaluations = 10000 \n", - "Total NCMC time = 20.0 ps (4.0 ps/iter)\n", - "Total MD time = 20.0 ps (4.0 ps/iter)\n", - "Trajectory Interval = 2.0 ps/frame (4.0 frames/iter)\n", - "INFO: Running 5 BLUES iterations...\n", - "INFO: BLUES Iteration: 0\n", - "INFO: Advancing 1000 NCMC switching steps...\n", - "#\"Progress (%)\"\t\"Step\"\t\"Speed (ns/day)\"\t\"Time Remaining\"\n", - "ncmc: 25.0%\t250\t0\t--\n", - "ncmc: 50.0%\t500\t110\t0:01\n", - "Performing RandomLigandRotationMove...\n", - "ncmc: 75.0%\t750\t93.3\t0:00\n", - "ncmc: 100.0%\t1000\t93\t0:00\n", - "NCMC MOVE REJECTED: work_ncmc -17.61007128922719 < -3.476065340494845\n", - "Advancing 1000 MD steps...\n", - "md: 15.0%\t750\t31.9\t0:46\n", - "md: 20.0%\t1000\t43.1\t0:32\n", - "md: 25.0%\t1250\t54.8\t0:23\n", - "md: 30.0%\t1500\t65.5\t0:18\n", - "BLUES Iteration: 1\n", - "Advancing 1000 NCMC switching steps...\n", - "ncmc: 25.0%\t250\t57.2\t--\n", - "ncmc: 50.0%\t500\t62.6\t0:13\n", - "Performing RandomLigandRotationMove...\n", - "ncmc: 75.0%\t750\t65.4\t0:03\n", - "ncmc: 100.0%\t1000\t69.1\t0:00\n", - "NCMC MOVE REJECTED: work_ncmc -28.930984747001787 < -2.602259467723195\n", - "Advancing 1000 MD steps...\n", - "md: 35.0%\t1750\t45.5\t0:24\n", - "md: 40.0%\t2000\t50.5\t0:20\n", - "md: 45.0%\t2250\t56.3\t0:16\n", - "md: 50.0%\t2500\t61.9\t0:13\n", - "BLUES Iteration: 2\n", - "Advancing 1000 NCMC switching steps...\n", - "ncmc: 25.0%\t250\t57.9\t--\n", - "ncmc: 50.0%\t500\t60.1\t0:25\n", - "Performing RandomLigandRotationMove...\n", - "ncmc: 75.0%\t750\t62.4\t0:06\n", - "ncmc: 100.0%\t1000\t64.6\t0:00\n", - "NCMC MOVE REJECTED: work_ncmc -9.178847171172448 < -0.3081492900307722\n", - "Advancing 1000 MD steps...\n", - "md: 55.0%\t2750\t49.7\t0:15\n", - "md: 60.0%\t3000\t53.1\t0:13\n", - "md: 65.0%\t3250\t56.7\t0:10\n", - "md: 70.0%\t3500\t60.4\t0:08\n", - "BLUES Iteration: 3\n", - "Advancing 1000 NCMC switching steps...\n", - "ncmc: 25.0%\t250\t58.4\t--\n", - "ncmc: 50.0%\t500\t60.4\t0:37\n", - "Performing RandomLigandRotationMove...\n", - "ncmc: 75.0%\t750\t61.6\t0:09\n", - "ncmc: 100.0%\t1000\t63.6\t0:00\n", - "NCMC MOVE REJECTED: work_ncmc -6.022089748510081 < -0.28447339331708726\n", - "Advancing 1000 MD steps...\n", - "md: 75.0%\t3750\t52.6\t0:08\n", - "md: 80.0%\t4000\t55\t0:06\n", - "md: 85.0%\t4250\t58\t0:04\n", - "md: 90.0%\t4500\t60.8\t0:02\n", - "BLUES Iteration: 4\n", - "Advancing 1000 NCMC switching steps...\n", - "ncmc: 25.0%\t250\t58.6\t--\n", - "ncmc: 50.0%\t500\t59.8\t0:49\n", - "Performing RandomLigandRotationMove...\n", - "ncmc: 75.0%\t750\t60.7\t0:12\n", - "ncmc: 100.0%\t1000\t61.9\t0:00\n", - "NCMC MOVE REJECTED: work_ncmc -63.43688730563919 < -0.5417610940298393\n", - "Advancing 1000 MD steps...\n", - "md: 95.0%\t4750\t53.3\t0:01\n", - "md: 100.0%\t5000\t55.2\t0:00\n", - "md: 105.0%\t5250\t57.5\t23:59:59\n", - "md: 110.0%\t5500\t59.8\t23:59:58\n", - "Acceptance Ratio: 0.0\n", - "nIter: 5 \n" - ] - } - ], - "source": [ - "blues = BLUESSimulation(simulations, cfg['simulation'])\n", - "blues.run()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [default]", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.5" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/conf.py b/docs/conf.py index 3cac6309..a3692308 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -131,9 +131,9 @@ def setup(app): # built documents. # # The short X.Y version. -version = '0.2.2' +version = '0.2.5' # The full version, including alpha/beta/rc tags. -release = '0.2.2' +release = '0.2.5' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/devdoc.rst b/docs/devdoc.rst new file mode 100644 index 00000000..1c2a7f4f --- /dev/null +++ b/docs/devdoc.rst @@ -0,0 +1,286 @@ +Developer Guide +======================= +UML Diagram +----------- +.. image:: ../images/uml.png + +OpenMMTools Objects +------------------- +Highlighted in red are 3 objects that we use from the ``openmmtools`` library. They are the **ThermodynamicState**, **CompoundThermodynamicState**, and **SamplerState** objects. For more details of each class, please see the official `openmmtools documentation `_. + +Briefly, the **ThermodynamicState** class represents the portion of the state of an ``openmm.Context`` that does not change with integration (i.e. particles, temperature, or pressure). The **CompoundThermodynamicState** class is essentially the same as the **ThermodynamicState** class except in this package, it is used for the handling the ``openmmtools.alchemy.AlchemicalState`` object. Thus, in order to create the **CompoundThermodynamicState**, one needs to first create the plain **ThermodynamicState** object first. If a **CompoundThermodynamicState** object is not provided to the ``blues.ncmc.BLUESSampler`` class, one is created using the default parameters from the given **ThermodynamicState**. Lastly, the **SamplerState** class represents the state of an ``openmm.Context`` which does change with integration (i.e positions, velocities, and box_vectors). Within the context of this package, the **SamplerState** is used to sync information between the MD and NCMC simulations. + +Integrators and Moves +--------------------- +**Integrators** +Integrators are the lowest level openmm objects this package interacts with, where each intergrator is tied to an ``openmm.Context`` that it advances. Each integrator is generated by using the embedded function ``_get_integrator()`` function within each move class. The integrators will control whether we are carrying out the Non-equilibirum Candidate Monte Carlo (NCMC) or Molecular Dynamics (MD) simulation. + +Every move class has 3 hidden methods: ``_get_integrator()`` for generating the integrator of each move class, ``_before_integration()`` for performing any necessary setup before integration, and ``_after_integration()`` for performing any cleanup or data collection after integration. Every move class also contains the ``apply()`` method which carries out calls to the 3 hidden methods and stepping with the integrator. + +In this package, we provide the move class ``blues.ncmc.ReportLangevinDynamicsMove`` to execute the MD simulation. As the name suggests, this will carry forward the MD simulation using Langevin dynamics, by generating an ``openmm.LangevinIntegrator``. This class is essentially the same as the ``openmmtools.LangevinDynamicsMove`` but with modifications to the ``apply()`` method which allows storing simulation data for the MD simulation. + +For running the NCMC simulation, we provide a custom integrator +``blues.integrator.AlchemicalExternalLangevinIntegrator``. This integrator is generated in every move which inherits from the base class ``blues.ncmc.NCMCMove``. Every class which inherits from the base move class must override the ``_propose_positions()`` method. If necessary, one can override the ``_before_integration()`` and ``_after_integration()`` methods for any necessary setup and cleanup. Again, these hidden methods will be called when a call is made to the ``apply()`` method from the move class. + +**Moves** +In order to implement custom NCMC moves, inherit from the base class and override the ``_propose_positions()`` method. This method is expected to take in a positions array of the atoms to be modified and returns the proposed positions. In pseudo-code, it would look something like: + +.. code-block:: python + + from blues.ncmc import NCMCMove + class CustomNCMCMove(NCMCMove): + def _propose_positions(positions): + """Add 1 nanometer displacement vector.""" + positions_unit = positions.unit + unitless_displacement = 1.0 / positions_unit + displacement_vector = unit.Quantity(np.random.randn(3) * unitless_displacement_sigma, positions_unit) + proposed_positions = positions + displacement_vector + return proposed_positions + +In this package, we provide the ``blues.ncmc.RandomLigandRotationMove`` in order to propose a random ligand rotation about the center of mass. This class overrides the ``_before_integration()`` method for obtaining the masses of the ligand and overrides the ``_propose_positions()`` function for generating the rotated coordinates. Updating the context with the rotated coordinates is handled when the ``apply()`` method is called in the move class. Code snippet of the class is shown below: + +.. code-block:: python + + from blues.ncmc import RandomLigandRotationMove + class RandomLigandRotationMove(NCMCMove): + def _before_integration(self, context, thermodynamic_state): + """Obtain the masses of the ligand before integration.""" + super(RandomLigandRotationMove, self)._before_integration(context, thermodynamic_state) + masses, totalmass = utils.getMasses(self.atom_subset, thermodynamic_state.topology) + self.masses = masses + def _propose_positions(self, positions): + # Calculate the center of mass + center_of_mass = utils.getCenterOfMass(positions, self.masses) + reduced_pos = positions - center_of_mass + # Define random rotational move on the ligand + rand_quat = mdtraj.utils.uniform_quaternion(size=None) + rand_rotation_matrix = mdtraj.utils.rotation_matrix_from_quaternion(rand_quat) + # multiply lig coordinates by rot matrix and add back COM translation from origin + proposed_positions = numpy.dot(reduced_pos, rand_rotation_matrix) * positions.unit + center_of_mass + + return proposed_positions + +Since BLUES (v0.2.5) the API has been re-written to be more compatible with the ``openmmtools`` API. This means one can turn a regular `Markov Chain Monte Carlo (MCMC) `_ move from the ``openmmtools`` library into an NCMC move to be used in this package. In this case, one simply needs to make use of dual inheritance, using the ``blues.ncmc.NCMCMove`` that we provide and override the ``_get_integrator()`` method to generate the NCMC integrator we provide, i.e. ``blues.integrator.AlchemicalExternalLangevinIntegrator``. When using dual inheritance, it is important that you first inherit the desired MCMC move and then the ``blues.ncmc.NCMCMove`` class. For example, if we wanted to take the ``openmmtools.mcmc.MCDisplacementMove`` class and turn it into an NCMC move, it would look like: + +.. code-block:: python + + from blues.ncmc import NCMCMove + from openmmtools.mcmc import MCDisplacementMove + class NCMCDisplacementMove(MCDisplacementMove, NCMCMove): + def _get_integrator(self, thermodynamic_state): + return NCMCMove._get_integrator(self,thermodynamic_state) + +BLUESSampler +------------ +The ``blues.ncmc.BLUESSampler`` object ties together all the previously mentioned state objects and the two move classes for running the NCMC+MD simulation. Details of the parameters for this class are listed in the :doc:`module_doc` documentation. For a more detailed example of it's usage see the :doc:`usage` documentation. + +To be explicit, the input parameters refer to the objects below: + +- **thermodynamic_state** : ``openmmtools.states.ThermodynamicState`` +- **alch_thermodynamic_state** : ``openmmtools.states.CompoundThermodynamicState`` +- **sampler_state** : ``openmmtools.states.SamplerState`` +- **dynamics_move** : ``blues.ncmc.ReportLangevinDynamicsMove`` +- **ncmc_move** : ``blues.ncmc.RandomLigandRotationMove`` +- **topology** : ``openmm.Topology`` + +When the ``run()`` method in the ``blues.ncmc.BLUESSampler`` is called the following takes place: + +- Initialization: + - ``_print_host_info()`` : Information print out of host + - ``_printSimulationTiming()`` : Calculation of total number of steps + - ``equil()`` : Equilibration +- BLUES iterations: + - ``ncmc_move.apply()`` : NCMC simulation + - ``_acceptRejectMove()`` : Metropolization + - ``dynamics_move.apply()`` : MD Simulation + +A code snippet of the ``run()`` method is shown below: + +.. code-block:: python + + def run(self, n_iterations=1): + context, integrator = cache.global_context_cache.get_context(self.thermodynamic_state) + utils.print_host_info(context) + self._printSimulationTiming(n_iterations) + if self.iteration == 0: + self.equil(1) + + self.iteration = 0 + for iteration in range(n_iterations): + self.ncmc_move.apply(self.alch_thermodynamic_state, self.sampler_state) + + self._acceptRejectMove() + + self.dynamics_move.apply(self.thermodynamic_state, self.sampler_state) + + self.iteration += 1 + + +Initialization +`````````````` +The first thing that occurs when ``run()`` is called is the initialization stage. During this stage, a call is made to ``utils.print_host_info()`` and the ``_printSimulationTiming()`` method which will print out some information about the host machine and the total number of force evaluations and simulation time. The output will look something like below: + +.. code-block:: python + OpenMM(7.3.1.dev-4a269c0) Context generated for CUDA platform + system = Linux + node = titanpascal + release = 4.15.0-50-generic + version = #54~16.04.1-Ubuntu SMP Wed May 8 15:55:19 UTC 2019 + machine = x86_64 + processor = x86_64 + DeviceIndex = 0 + DeviceName = TITAN Xp + UseBlockingSync = true + Precision = single + UseCpuPme = false + CudaCompiler = /usr/local/cuda-9.2/bin/nvcc + TempDirectory = /tmp + CudaHostCompiler = + DisablePmeStream = false + DeterministicForces = false + + Total BLUES Simulation Time = 4.0 ps (0.04 ps/Iter) + Total Force Evaluations = 4000 + Total NCMC time = 2.0 ps (0.02 ps/iter) + Total MD time = 2.0 ps (0.02 ps/iter) + +In the ``blues.ncmc.BLUESSampler`` class, there is an ``equil()`` method which lets you run iterations of just the MD simulation in order to equilibrate your system before running the NCMC+MD hybrid simulation. An equilibration iteration, in this case is controlled by the given attribute *n_steps* from the *dynamics_move* class. For example, if I create a ``blues.ncmc.ReportLangevinDynamicsMove`` class with *n_steps=20* and call the ``blues.ncmc.BLUESSampler.equil(n_iterations=100)``, this will run *(n_steps x n_iterations)* or 2000 steps of MD or 2 picoseconds of MD simulation time. When the ``run()`` method is called without a prior call to the ``equil()`` method, the class will always run 1 iteration of equilibration in order to set the initial conditions in the MD simulation. This is required prior to running the NCMC simulation. + +BLUES Iterations +```````````````` +**NCMC Simulation** + +After at least 1 iteration of equilibration, the ``blues.ncmc.BLUESSampler`` class will then proceed forward with running iterations of the NCMC+MD hybrid simulation. It will first run the NCMC simulation by calling the ``apply()`` method on the **ncmc_move** class or, for sake of this example, the ``blues.ncmc.RandomLigandRotationMove`` class. The ``apply()`` method for the **ncmc_move** will take in the **alch_thermodynamic_state** parameter or specifically the ``openmmtools.states.CompoundThermodynamicState`` object. + +A code snippet of the ``ncmc_move.apply()`` method is shown below: + +.. code-block:: python + + def apply(self, thermodynamic_state, sampler_state): + if self.context_cache is None: + context_cache = cache.global_context_cache + else: + context_cache = self.context_cache + integrator = self._get_integrator(thermodynamic_state) + context, integrator = context_cache.get_context(thermodynamic_state, integrator) + sampler_state.apply_to_context(context, ignore_velocities=False) + self._before_integration(context, thermodynamic_state) + try: + endStep = self.currentStep + self.n_steps + while self.currentStep < endStep: + alch_lambda = integrator.getGlobalVariableByName('lambda') + if alch_lambda == 0.5: + sampler_state.update_from_context(context) + proposed_positions = self._propose_positions(sampler_state.positions[self.atom_subset]) + sampler_state.positions[self.atom_subset] = proposed_positions + sampler_state.apply_to_context(context, ignore_velocities=True) + + nextSteps = endStep - self.currentStep + stepsToGo = nextSteps + while stepsToGo > 10: + integrator.step(10) + stepsToGo -= 10 + integrator.step(stepsToGo) + self.currentStep += nextSteps + except Exception as e: + print(e) + else: + context_state = context.getState( + getPositions=True, + getVelocities=True, + getEnergy=True, + enforcePeriodicBox=thermodynamic_state.is_periodic) + + self._after_integration(context, thermodynamic_state) + sampler_state.update_from_context( + context_state, ignore_positions=False, ignore_velocities=False, ignore_collective_variables=True) + sampler_state.update_from_context( + context, ignore_positions=True, ignore_velocities=True, ignore_collective_variables=False) + +When the ``apply()`` method on **ncmc_move** is called, it will first generate the ``blues.integrators.AlchemicalExternalLangevinIntegrator`` by calling the ``_get_integrator()`` method inherent to the move class. Then, it will create (or fetch from the **context_cache**) a corresponding ``openmm.Context`` given the **alch_thermodynamic_state**. Next, the **sampler_state** which contains the last state of the MD simulation is synced to the newly created context from the corresponding **alch_thermodynamic_state**. Particularly, the context will be updated with the *box_vectors*, *positions*, and *velocities* from the last state of the MD simulation. + +Just prior to integration, a call is made to the ``_before_integration()`` method in order to store the initial *energies*, *positions*, *box_vectors* and the *masses* of the ligand to be rotated. Then, we actually step with the integrator where we perform the ligand rotation when *lambda* has reached the half-way point or *lambda=0.5*, continuing integration until we have completed the *n_steps*. After the integration steps have been completed, a call is made to the ``_after_integration()`` method to store the final *energies*, *positions*, and *box_vectors*. Lastly, the **sampler_state** is updated from the final state of the context. + + +**Metropolization** + +After advancing the NCMC simulation, a call is made to the ``_acceptRejectMove()`` method embedded in the ``blues.ncmc.BLUESSampler`` class for metropolization of the proposed move. + +A code snippet of the ``_acceptRejectMove()`` is shown below: + +.. code-block:: python + + def _acceptRejectMove(self): + integrator = self.dynamics_move._get_integrator(self.thermodynamic_state) + context, integrator = cache.global_context_cache.get_context(self.thermodynamic_state, integrator) + self.sampler_state.apply_to_context(context, ignore_velocities=True) + alch_energy = self.thermodynamic_state.reduced_potential(context) + + correction_factor = (self.ncmc_move.initial_energy - self.dynamics_move.final_energy + alch_energy - self.ncmc_move.final_energy) + logp_accept = self.ncmc_move.logp_accept + randnum = numpy.log(numpy.random.random()) + + logp_accept = logp_accept + correction_factor + if (not numpy.isnan(logp_accept) and logp_accept > randnum): + self.n_accepted += 1 + else: + self.accept = False + self.sampler_state.positions = self.ncmc_move.initial_positions + self.sampler_state.box_vectors = self.ncmc_move.initial_box_vectors + +Here, is we compute a correction term for switching between the MD and NCMC integrators and factor this in with natural log of the acceptance probability (**logp_accept**). Then, a random number is generated in which: the move is accepted if the random number is less than the **logp_accept** or rejected if greater. When the move is rejected, we set the *positions* and *box_vectors* on the **sampler_state** to the initial positions and box_vectors from the NCMC simulation. If the move is accepted, nothing on the **sampler_state** is changed so that the following MD simulation will contain the final state of the NCMC simulation. + +**MD Simulation** + +After metropolization of the previously proposed move, a call is made to the ``apply()`` method on the given **dynamics_move** object. In this example, this would refer to the ``blues.ncmc.ReportLangevinDynamicsMove`` class to run the MD simulation. + +A code snippet of the ``dynamics_move.apply()`` method is shown below: + +.. code-block:: python + + def apply(self, thermodynamic_state, sampler_state): + if self.context_cache is None: + context_cache = cache.global_context_cache + else: + context_cache = self.context_cache + + integrator = self._get_integrator(thermodynamic_state) + context, integrator = context_cache.get_context(thermodynamic_state, integrator) + thermodynamic_state.apply_to_context(context) + + sampler_state.apply_to_context(context, ignore_velocities=self.reassign_velocities) + if self.reassign_velocities: + context.setVelocitiesToTemperature(thermodynamic_state.temperature) + + self._before_integration(context, thermodynamic_state) + try: + endStep = self.currentStep + self.n_steps + while self.currentStep < endStep: + nextSteps = endStep - self.currentStep + stepsToGo = nextSteps + while stepsToGo > 10: + integrator.step(10) + stepsToGo -= 10 + integrator.step(stepsToGo) + self.currentStep += nextSteps + + except Exception as e: + print(e) + + else: + context_state = context.getState( + getPositions=True, + getVelocities=True, + getEnergy=True, + enforcePeriodicBox=thermodynamic_state.is_periodic) + self._after_integration(context, thermodynamic_state) + sampler_state.update_from_context( + context_state, ignore_positions=False, ignore_velocities=False, ignore_collective_variables=True) + sampler_state.update_from_context( + context, ignore_positions=True, ignore_velocities=True, ignore_collective_variables=False) + +When the ``apply()`` method is called, a very similar procedure to the NCMC simulation occurs. The first thing that happens is to generate the integrator through a call to ``_get_integrator()``, where in this given class, it will generate an ``openmm.LangevinIntegrator`` given the **thermodynamic_state** parameter. Then, it will create (or fetch from the **context_cache**) a corresponding ``openmm.Context`` given the **thermodynamic_state**. Next, the **sampler_state**, which contains the last state of the NCMC simulation if the previous move was accepted or the initial state of the NCMC simulation if the move was rejected, is used to update *box_vectors* and *positions* in the newly created ``openmm.Context``. In this case, we reassign the *velocities* in the MD simulation in order to preserve detailed balance. + +Following, a call is made to ``_before_integration()`` to store the intial *positions*, *box_vectors* and *energies* and then we carry forward with the integration for *n_steps*. After the integration steps have been completed, a call is made to the ``_after_integration()`` method to store the final *energies* and *positions*. Lastly, the **sampler_state** object is updated from the final state of the MD simulation context. + +This completes 1 iteration of the BLUES cycle. Here, the **sampler_state** is then used to sync the final state of the MD simulation (i.e. *box_vectors*, *positions*, and *velocities*) from the previous iteration to the NCMC simulation of the next iteration. Then, we repeat the cycle of **NCMC -> Metropolization -> MD** for the given number of iterations. diff --git a/docs/environment.yml b/docs/environment.yml index e1ada82f..032287ce 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -8,18 +8,17 @@ channels: - conda-forge dependencies: - - python >=3.5 + - python >=3.6 - pytest - sphinx >=1.4 - nbconvert - ipykernel - setuptools - - openmmtools >=0.15.0 + - openmmtools=0.15.0 - mdtraj - openmm >=7.2.2 - parmed - netcdf4 - - hdf5 - pyyaml - numpydoc - ipython diff --git a/docs/index.rst b/docs/index.rst index ea64bf73..cb78f495 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,8 +12,10 @@ Welcome to the BLUES documentation! intro installation + usage module_doc - tutorial + devdoc + Indices and tables ================== diff --git a/docs/installation.rst b/docs/installation.rst index cabcbd41..9e9c5c48 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,7 +1,7 @@ Installation ================== -BLUES is compatible with MacOSX/Linux with Python>=3.5 (blues<1.1 still works with Python 2.7) +BLUES is compatible with MacOSX/Linux with Python>=3.6 (blues<1.1 still works with Python 2.7) This is a python tool kit with a few dependencies. We recommend installing `miniconda `_. Then you can create an @@ -9,7 +9,7 @@ environment with the following commands: .. code-block:: bash - conda create -n blues python=3.5 + conda create -n blues python=3.6 source activate blues Stable Releases @@ -48,7 +48,7 @@ source code. .. code-block:: bash git clone https://github.com/MobleyLab/blues.git - conda install -c omnia -c conda-forge openmmtools=0.15.0 openmm=7.2.2 numpy cython + conda install -c omnia -c conda-forge openmmtools openmm numpy cython pip install -e . To validate your BLUES installation run the tests. diff --git a/docs/intro.rst b/docs/intro.rst index 0a297cc7..0ddd83b8 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -27,7 +27,7 @@ Github .. image:: https://codecov.io/gh/MobleyLab/blues/branch/master/graph/badge.svg :target: https://codecov.io/gh/MobleyLab/blues - + .. image:: https://anaconda.org/mobleylab/blues/badges/version.svg :target: https://anaconda.org/mobleylab/blues @@ -40,11 +40,11 @@ Publication .. image:: ../images/jp-2017-11820n_0015.gif :target: https://pubs.acs.org/doi/abs/10.1021/acs.jpcb.7b11820 -.. rubric:: Binding Modes of Ligands Using Enhanced Sampling (BLUES): Rapid Decorrelation of Ligand Binding Modes via Nonequilibrium Candidate Monte Carl +.. rubric:: Binding Modes of Ligands Using Enhanced Sampling (BLUES): Rapid Decorrelation of Ligand Binding Modes via Nonequilibrium Candidate Monte Carlo Samuel C. Gill, Nathan M. Lim, Patrick B. Grinaway, Ariën S. Rustenburg, Josh Fass, Gregory A. Ross, John D. Chodera , and David L. Mobley -*The Journal of Physical Chemistry B* **2018** *122* (21), 5579-5598 +*Journal of Physical Chemistry B* **2018** *122* (21), 5579-5598 **DOI:** `10.1021/acs.jpcb.7b11820 `_ @@ -55,6 +55,26 @@ Publication Date (Web): February 27, 2018 .. epigraph:: Accurately predicting protein–ligand binding affinities and binding modes is a major goal in computational chemistry, but even the prediction of ligand binding modes in proteins poses major challenges. Here, we focus on solving the binding mode prediction problem for rigid fragments. That is, we focus on computing the dominant placement, conformation, and orientations of a relatively rigid, fragment-like ligand in a receptor, and the populations of the multiple binding modes which may be relevant. This problem is important in its own right, but is even more timely given the recent success of alchemical free energy calculations. Alchemical calculations are increasingly used to predict binding free energies of ligands to receptors. However, the accuracy of these calculations is dependent on proper sampling of the relevant ligand binding modes. Unfortunately, ligand binding modes may often be uncertain, hard to predict, and/or slow to interconvert on simulation time scales, so proper sampling with current techniques can require prohibitively long simulations. We need new methods which dramatically improve sampling of ligand binding modes. Here, we develop and apply a nonequilibrium candidate Monte Carlo (NCMC) method to improve sampling of ligand binding modes. In this technique, the ligand is rotated and subsequently allowed to relax in its new position through alchemical perturbation before accepting or rejecting the rotation and relaxation as a nonequilibrium Monte Carlo move. When applied to a T4 lysozyme model binding system, this NCMC method shows over 2 orders of magnitude improvement in binding mode sampling efficiency compared to a brute force molecular dynamics simulation. This is a first step toward applying this methodology to pharmaceutically relevant binding of fragments and, eventually, drug-like molecules. We are making this approach available via our new Binding modes of ligands using enhanced sampling (BLUES) package which is freely available on GitHub. +Citations +--------- +.. image:: ../images/ct-2018-01018f_0014.jpeg + :target: https://pubs.acs.org/doi/abs/10.1021/acs.jctc.8b01018 + +.. rubric:: Enhancing Side Chain Rotamer Sampling Using Nonequilibrium Candidate Monte Carlo + +Kalistyn H. Burley, Samuel C. Gill, Nathan M. Lim, and David L. Mobley + +*Journal of Chemical Theory and Computation.* **2019** *15* (3), 1848-1862 + +**DOI:** `10.1021/acs.jctc.8b01018` + +Publication Date (Web): January 24, 2019 + +.. rubric:: Abstract + +.. epigraph:: + Molecular simulations are a valuable tool for studying biomolecular motions and thermodynamics. However, such motions can be slow compared to simulation time scales, yet critical. Specifically, adequate sampling of side chain motions in protein binding pockets is crucial for obtaining accurate estimates of ligand binding free energies from molecular simulations. The time scale of side chain rotamer flips can range from a few ps to several hundred ns or longer, particularly in crowded environments like the interior of proteins. Here, we apply a mixed nonequilibrium candidate Monte Carlo (NCMC)/molecular dynamics (MD) method to enhance sampling of side chain rotamers. The NCMC portion of our method applies a switching protocol wherein the steric and electrostatic interactions between target side chain atoms and the surrounding environment are cycled off and then back on during the course of a move proposal. Between NCMC move proposals, simulation of the system continues via traditional molecular dynamics. Here, we first validate this approach on a simple, solvated valine-alanine dipeptide system and then apply it to a well-studied model ligand binding site in T4 lysozyme L99A. We compute the rate of rotamer transitions for a valine side chain using our approach and compare it to that of traditional molecular dynamics simulations. Here, we show that our NCMC/MD method substantially enhances side chain sampling, especially in systems where the torsional barrier to rotation is high (≥10 kcal/mol). These barriers can be intrinsic torsional barriers or steric barriers imposed by the environment. Overall, this may provide a promising strategy to selectively improve side chain sampling in molecular simulations. + Theory ------ Suggested readings: diff --git a/docs/module_doc.rst b/docs/module_doc.rst index 45b6b2d7..7eb7cd25 100644 --- a/docs/module_doc.rst +++ b/docs/module_doc.rst @@ -1,13 +1,18 @@ Modules ==================== -Moves --------------------------------------------------------- -.. automodule:: blues.moves +Nonequilibrium Candidate Monte Carlo (NCMC) +------------------------------------------- +.. automodule:: blues.ncmc -Move -~~~~ -.. autoclass:: Move +ReportLangevinDynamicsMove +~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: ReportLangevinDynamicsMove + :members: + +NCMCMove +~~~~~~~~ +.. autoclass:: NCMCMove :members: RandomLigandRotationMove @@ -15,54 +20,18 @@ RandomLigandRotationMove .. autoclass:: RandomLigandRotationMove :members: - -MoveEngine -~~~~~~~~~~ -.. autoclass:: MoveEngine +BLUESSampler +~~~~~~~~~~~~ +.. autoclass:: BLUESSampler :members: -Under Development -~~~~~~~~~~~~~~~~~~~~~~~~~ -**WARNING:** The following move classes have not been tested. Use at your own risk. - -.. autoclass:: SideChainMove - :members: -.. autoclass:: SmartDartMove - :members: -.. autoclass:: CombinationMove - :members: - -Simulation ---------------------------------------------------------- -.. automodule:: blues.simulation - SystemFactory -~~~~~~~~~~~~~ -.. rubric:: Methods -.. autoautosummary:: blues.simulation.SystemFactory - :methods: - -.. autoclass:: SystemFactory - :members: - -SimulationFactory -~~~~~~~~~~~~~~~~~ -.. rubric:: Methods -.. autoautosummary:: blues.simulation.SimulationFactory - :methods: - -.. autoclass:: SimulationFactory +--------------- +.. automodule:: blues.systemfactory :members: -BLUESSimulation -~~~~~~~~~~~~~~~ -.. autoclass:: BLUESSimulation - :members: - :private-members: - - Integrators ------------------------------------------------------------ +----------- .. automodule:: blues.integrators :members: @@ -72,9 +41,9 @@ Utilities :members: :undoc-members: -Reporters ---------- -.. automodule:: blues.reporters +Storage +-------- +.. automodule:: blues.storage :members: Formats diff --git a/docs/tutorial.rst b/docs/tutorial.rst deleted file mode 100644 index 8e680463..00000000 --- a/docs/tutorial.rst +++ /dev/null @@ -1,18 +0,0 @@ -Tutorial -======== - -This page provides examples on how to use BLUES. -A Jupyter notebook is available in the -`examples folder `_ -on github so you can try them out yourself or you can view it on `nbviewer `_ - -.. toctree:: - :maxdepth: 2 - - BLUES_tutorial.ipynb - - -.. role:: raw-html(raw) - :format: html - -Let us know if you have any problems or suggestions through our issue tracker: :raw-html:`Issue` diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 00000000..3fa64e12 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,55 @@ +Usage +===== + +This package takes advantage of non-equilibrium candidate Monte Carlo moves (NCMC) to help sample between different ligand binding modes using the OpenMM simulation package. One goal for this package is to allow for easy additions of other moves of interest, which will be covered below. + +The integrator from **BLUES** contains the framework necessary for NCMC. Specifically, the integrator class calculates the work done during a NCMC move. It also controls the lambda scaling of parameters. The integrator that BLUES uses inherits from ``openmmtools.integrators.AlchemicalExternalLangevinIntegrator`` to keep track of the work done outside integration steps, allowing Monte Carlo (MC) moves to be incorporated together with the NCMC thermodynamic perturbation protocol. Currently, the ``openmmtools.alchemy`` package is used to generate the lambda parameters for the ligand, allowing alchemical modification of the sterics and electrostatics of the system. + +The **BLUESSampler** class in ``ncmc.py`` serves as a wrapper for running NCMC+MD simulations. To run the hybrid simulation, the **BLUESSampler** class requires defining two moves for running the (1) MD simulation and (2) the NCMC protcol. These moves are defined in the ``ncmc.py`` module. A simple example is provided below. + +Example +------- +Using the BLUES framework requires the use of a **ThermodynamicState** and **SamplerState** from ``openmmtools`` which we import from ``openmmtools.states``: + +.. code-block:: python + + from openmmtools.states import ThermodynamicState, SamplerState + from openmmtools.testsystems import TolueneVacuum + from blues.ncmc import * + from simtk import unit + + +Create the states for a toluene molecule in vacuum. + +.. code-block:: python + + tol = TolueneVacuum() + thermodynamic_state = ThermodynamicState(tol.system, temperature=300*unit.kelvin) + sampler_state = SamplerState(positions=tol.positions) + + +Define our langevin dynamics move for the MD simulation portion and then our NCMC move which performs a random rotation. Here, we use a customized ``openmmtools.mcmc.LangevinDynamicsMove`` which allows us to store information from the MD simulation portion. + +.. code-block:: python + + dynamics_move = ReportLangevinDynamicsMove(n_steps=10) + ncmc_move = RandomLigandRotationMove(n_steps=10, atom_subset=list(range(15))) + + +Provide the **BLUESSampler** class with an ``openmm.Topology`` and these objects to run the NCMC+MD simulation. + +.. code-block:: python + + sampler = BLUESSampler(thermodynamic_state=thermodynamic_state, + sampler_state=sampler_state, + dynamics_move=dynamics_move, + ncmc_move=ncmc_move, + topology=tol.topology) + sampler.run(n_iterations=1) + + + +.. role:: raw-html(raw) + :format: html + +Let us know if you have any problems or suggestions through our :raw-html:`Issue` tracker. diff --git a/examples/example_rotmove.py b/examples/example_rotmove.py deleted file mode 100644 index cd1d0fce..00000000 --- a/examples/example_rotmove.py +++ /dev/null @@ -1,33 +0,0 @@ -from blues.moves import RandomLigandRotationMove, MoveEngine -from blues.simulation import * -import json -from blues.settings import * - - -def rotmove_cuda(yaml_file): - # Parse a YAML configuration, return as Dict - cfg = Settings('rotmove_cuda.yaml').asDict() - structure = cfg['Structure'] - - #Select move type - ligand = RandomLigandRotationMove(structure, 'LIG') - #Iniitialize object that selects movestep - ligand_mover = MoveEngine(ligand) - - #Generate the openmm.Systems outside SimulationFactory to allow modifications - systems = SystemFactory(structure, ligand.atom_indices, cfg['system']) - - #Freeze atoms in the alchemical system to speed up alchemical calculation - systems.alch = systems.freeze_radius(structure, systems.alch, **cfg['freeze']) - - #Generate the OpenMM Simulations - simulations = SimulationFactory(systems, ligand_mover, cfg['simulation'], cfg['md_reporters'], - cfg['ncmc_reporters']) - - # Run BLUES Simulation - blues = BLUESSimulation(simulations, cfg['simulation']) - blues.run() - - -if __name__ == "__main__": - rotmove_cuda('rotmove_cuda.yaml') diff --git a/examples/example_sidechain.py b/examples/example_sidechain.py deleted file mode 100644 index a7f980b2..00000000 --- a/examples/example_sidechain.py +++ /dev/null @@ -1,36 +0,0 @@ -from blues.moves import SideChainMove -from blues.engine import MoveEngine -from blues.simulation import * -import json -from blues.settings import * - -# Parse a YAML configuration, return as Dict -cfg = Settings('sidechain_cuda.yaml').asDict() -structure = cfg['Structure'] - -#Select move type -sidechain = SideChainMove(structure, [1]) -#Iniitialize object that selects movestep -sidechain_mover = MoveEngine(sidechain) - -#Generate the openmm.Systems outside SimulationFactory to allow modifications -systems = SystemFactory(structure, sidechain.atom_indices, cfg['system']) - -#Generate the OpenMM Simulations -simulations = SimulationFactory(systems, sidechain_mover, cfg['simulation'], cfg['md_reporters'], - cfg['ncmc_reporters']) - -# Run BLUES Simulation -blues = BLUESSimulation(simulations, cfg['simulation']) -blues.run() - -#Analysis -import mdtraj as md -import numpy as np - -traj = md.load_netcdf('vacDivaline-test/vacDivaline.nc', top='tests/data/vacDivaline.prmtop') -indicies = np.array([[0, 4, 6, 8]]) -dihedraldata = md.compute_dihedrals(traj, indicies) -with open("vacDivaline-test/dihedrals.txt", 'w') as output: - for value in dihedraldata: - output.write("%s\n" % str(value)[1:-1]) diff --git a/examples/rotmove_cuda.yml b/examples/rotmove_cuda.yml deleted file mode 100644 index d15f62ec..00000000 --- a/examples/rotmove_cuda.yml +++ /dev/null @@ -1,102 +0,0 @@ -# YAML configuration for simulating toluene bound to T4 lysozyme -# Atoms 5 angstroms away from the ligand are frozen -# Simulation on a CUDA device with index 0 -# NVT simulation with Hydrogen Mass Repartioning + 4fs timesteps -# 5 BLUES iterations (random rotational move proposals), 1000 steps of NCMC and MD per iteration -# MD Reporters: State (energies), NetCDF, Restart, and speed for benchmarking -# NCMC Reporters: NetCDF (stores protocol work at 1st, mid, and last frame) and speed for benchmarking - -output_dir: . -outfname: t4-toluene -logger: - level: info - stream: True - -structure: - filename: tests/data/eqToluene.prmtop - xyz: tests/data/eqToluene.inpcrd - -system: - nonbondedMethod: PME - nonbondedCutoff: 10 * angstroms - constraints: HBonds - rigidWater: True - removeCMMotion: True - hydrogenMass: 3.024 * daltons - ewaldErrorTolerance: 0.005 - flexibleConstraints: True - splitDihedrals: False - - alchemical: - softcore_alpha: 0.5 - softcore_a : 1 - softcore_b : 1 - softcore_c : 6 - softcore_beta : 0.0 - softcore_d : 1 - softcore_e : 1 - softcore_f : 2 - annihilate_electrostatics : True - annihilate_sterics : False - -freeze: - freeze_center: ':LIG' - freeze_solvent: ':WAT, NA, Cl-' - freeze_distance: 5 * angstroms - -simulation: - platform: CUDA - properties: - CudaPrecision: single - CudaDeviceIndex: 0 - dt: 0.004 * picoseconds - friction: 1 * 1/picoseconds - temperature: 300 * kelvin - nIter: 5000 - nstepsMD: 10000 - nstepsNC: 10000 - #Defauilt - nprop: 1 - propLambda: 0.3 - ###Advanced: Add additional relaxation steps in NCMC simulation### - # Suggested options for nprop and nstepsNC - # nprop: 2 nstepsNC: Any value in increments of 1000 - # nprop: 3 nstepsNC: 3000, 6000, 11000, 14000, 17000, 22000, 25000 - # nprop: 4 nstepsNC: 1000, 7000, 8000, 14000, 15000, 21000, 22000 - # nprop: 5 nstepsNC: 9000, 13000, 17000 - ################################################################## - - -md_reporters: - state: - reportInterval: 2500 - traj_netcdf: - reportInterval: 2500 - restart: - reportInterval: 10000 #nstepsMD - stream: - title: md - reportInterval: 2500 - totalSteps: 5000000 #nIter * nstepsMD - step: True - speed: True - progress: True - remainingTime: True - currentIter : True - -ncmc_reporters: - traj_netcdf: - frame_indices: [1, 0.5, -1] - alchemicalLambda: True - protocolWork: True - stream: - title: ncmc - reportInterval: 2000 - totalSteps: 5000000 #nIter * nstepsNC - step: True - speed: True - progress: True - remainingTime : True - protocolWork : True - alchemicalLambda : True - currentIter : True diff --git a/examples/sidechain_cuda.yml b/examples/sidechain_cuda.yml deleted file mode 100644 index eac7cf58..00000000 --- a/examples/sidechain_cuda.yml +++ /dev/null @@ -1,95 +0,0 @@ -# YAML configuration for simulating divaline in vacuum -# Simulation on a CUDA device with index 0 -# NVT simulation with Hydrogen Mass Repartioning + 4fs timesteps -# 5 BLUES iterations (sidechain move proposals), 1000 steps of NCMC and MD per iteration -# MD Reporters: State (energies), NetCDF, Restart, and speed for benchmarking -# NCMC Reporters: NetCDF (stores protocol work at 1st, mid, and last frame) and speed for benchmarking - -output_dir: vacDivaline-test -outfname: vacDivaline -logger: - level: info - stream: True - -structure: - filename: tests/data/vacDivaline.prmtop - xyz: tests/data/vacDivaline.inpcrd - -system: - nonbondedMethod: PME - nonbondedCutoff: 10 * angstroms - constraints: HBonds - rigidWater: True - removeCMMotion: True - hydrogenMass: 3.024 * daltons - ewaldErrorTolerance: 0.005 - flexibleConstraints: True - splitDihedrals: False - - alchemical: - softcore_alpha: 0.5 - softcore_a : 1 - softcore_b : 1 - softcore_c : 6 - softcore_beta : 0.0 - softcore_d : 1 - softcore_e : 1 - softcore_f : 2 - annihilate_electrostatics : True - annihilate_sterics : False - -simulation: - platform: CUDA - properties: - CudaPrecision: single - CudaDeviceIndex: 0 - dt: 0.004 * picoseconds - friction: 1 * 1/picoseconds - temperature: 300 * kelvin - nIter: 5 - nstepsMD: 1000 - nstepsNC: 1000 - #Defauilt - nprop: 1 - propLambda: 0.3 - ###Advanced: Add additional relaxation steps in NCMC simulation### - # Suggested options for nprop and nstepsNC - # nprop: 2 nstepsNC: Any value in increments of 1000 - # nprop: 3 nstepsNC: 3000, 6000, 11000, 14000, 17000, 22000, 25000 - # nprop: 4 nstepsNC: 1000, 7000, 8000, 14000, 15000, 21000, 22000 - # nprop: 5 nstepsNC: 9000, 13000, 17000 - ################################################################## - -md_reporters: - state: - reportInterval: 250 - traj_netcdf: - reportInterval: 250 - restart: - reportInterval: 1000 - stream: - title: md - reportInterval: 250 - totalSteps: 5000 - step: True - speed: True - progress: True - remainingTime: True - currentIter : True - -ncmc_reporters: - traj_netcdf: - frame_indices: [1, 0.5, -1] - alchemicalLambda: True - protocolWork: True - stream: - title: ncmc - reportInterval: 100 - totalSteps: 5000 - step: True - speed: True - progress: True - remainingTime : True - protocolWork : True - alchemicalLambda : True - currentIter : True diff --git a/images/ct-2018-01018f_0014.jpeg b/images/ct-2018-01018f_0014.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..cfb5208d7f257fb8a3447000e2e896f7d92da033 GIT binary patch literal 173663 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%0A_84x_4X*ofI3j6H!e@z>B|{N8*u6e-D6 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 \ No newline at end of file From 68fff6b17b392ff83642de7af808f6c506a22e88 Mon Sep 17 00:00:00 2001 From: Nathan Lim Date: Tue, 18 Jun 2019 09:52:22 -0700 Subject: [PATCH 2/9] Fixes in README and usernames --- .travis.yml | 2 +- README.md | 14 +++----------- devtools/conda-recipe/meta.yaml | 2 +- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 862d94a6..be59b015 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ branches: env: global: - ORGNAME="omnia" - - USERNAME="nathanmlim" + - USERNAME="mobleylab" - PKG_NAME="blues" - RELEASE=false #- RELEASE=false diff --git a/README.md b/README.md index 62233a46..6a5b77d2 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,13 @@ This package takes advantage of non-equilibrium candidate Monte Carlo moves (NCMC) to help sample between different ligand binding modes. - -Latest release: -[![Build Status](https://travis-ci.com/nathanmlim/blues.svg?branch=master)](https://travis-ci.com/nathanmlim/blues) -[![Documentation Status](https://readthedocs.org/projects/blues-fork/badge/?version=master)](https://blues-fork.readthedocs.io/en/master/?badge=master) -[![codecov](https://codecov.io/gh/nathanmlim/blues/branch/master/graph/badge.svg)](https://codecov.io/gh/nathanmlim/blues) -[![Anaconda-Server Badge](https://anaconda.org/nathanmlim/blues/badges/version.svg)](https://anaconda.org/nathanmlim/blues) - [![DOI](https://zenodo.org/badge/62096511.svg)](https://zenodo.org/badge/latestdoi/62096511) ## Citations #### Publications @@ -146,8 +138,8 @@ class NCMCDisplacementMove(MCDisplacementMove, NCMCMove): - [Version 0.2.0](https://doi.org/10.5281/zenodo.1284568): YAML support, API changes, custom reporters. - [Version 0.2.1](https://doi.org/10.5281/zenodo.1288925): Bug fix in alchemical correction term - [Version 0.2.2](https://doi.org/10.5281/zenodo.1324415): Bug fixes for OpenEye tests and restarting from the YAML; enhancements to the Logger and package installation. -- [Version 0.2.3](https://zenodo.org/badge/latestdoi/62096511): Improvements to Travis CI, fix in velocity synicng, and add tests for checking freezing selection. -- [Version 0.2.4](): Addition of a simple test that can run on CPU. +- [Version 0.2.3](https://doi.org/10.5281/zenodo.1409272): Improvements to Travis CI, fix in velocity synicng, and add tests for checking freezing selection. +- [Version 0.2.4](https://doi.org/10.5281/zenodo.2672932): Addition of a simple test that can run on CPU. - [Version 0.2.5](): API redesign for compatibility with `openmmtools` ## Acknowledgements diff --git a/devtools/conda-recipe/meta.yaml b/devtools/conda-recipe/meta.yaml index 802aec7c..89576b10 100755 --- a/devtools/conda-recipe/meta.yaml +++ b/devtools/conda-recipe/meta.yaml @@ -44,6 +44,6 @@ test: - blues about: - home: https://github.com/nathanmlim/blues + home: https://github.com/mobleylab/blues license: MIT license_file: LICENSE From 7470034900d7c02b9897fd8db9c4c239217830cb Mon Sep 17 00:00:00 2001 From: Nathan Lim Date: Tue, 18 Jun 2019 09:53:10 -0700 Subject: [PATCH 3/9] Switch to release flag --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index be59b015..e346b865 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ env: - ORGNAME="omnia" - USERNAME="mobleylab" - PKG_NAME="blues" - - RELEASE=false + - RELEASE=true #- RELEASE=false - CONDA_ENV="${PKG_NAME}-${TRAVIS_OS_NAME}" - OE_LICENSE="$HOME/oe_license.txt" From c425fa8dc93981b18a60dc452653a3b50ca09085 Mon Sep 17 00:00:00 2001 From: "Nathan M. Lim" Date: Thu, 27 Jun 2019 11:30:46 -0700 Subject: [PATCH 4/9] Remove unused code and fix some documentation. --- blues/ncmc.py | 21 +++++++++++++-------- blues/storage.py | 47 +++-------------------------------------------- 2 files changed, 16 insertions(+), 52 deletions(-) diff --git a/blues/ncmc.py b/blues/ncmc.py index 81cee5a6..4d20d627 100644 --- a/blues/ncmc.py +++ b/blues/ncmc.py @@ -18,6 +18,7 @@ logger = logging.getLogger(__name__) + class ReportLangevinDynamicsMove(object): """Langevin dynamics segment as a (pseudo) Monte Carlo move. @@ -159,9 +160,7 @@ def _get_integrator(self, thermodynamic_state): integrator : openmm.LangevinIntegrator The LangevinIntegrator object intended for the System. """ - integrator = openmm.LangevinIntegrator(thermodynamic_state.temperature, - self.collision_rate, - self.timestep) + integrator = openmm.LangevinIntegrator(thermodynamic_state.temperature, self.collision_rate, self.timestep) return integrator def _before_integration(self, context, thermodynamic_state): @@ -268,6 +267,7 @@ def apply(self, thermodynamic_state, sampler_state): sampler_state.update_from_context( context_state, ignore_positions=False, ignore_velocities=False, ignore_collective_variables=True) + class NCMCMove(MCMCMove): """A general NCMC move that applies an alchemical integrator. @@ -764,7 +764,8 @@ def _computeAlchemicalCorrection(self): self.thermodynamic_state.apply_to_context(context) self.sampler_state.apply_to_context(context, ignore_velocities=True) alch_energy = self.thermodynamic_state.reduced_potential(context) - correction_factor = (self.ncmc_move.initial_energy - self.dynamics_move.final_energy + alch_energy - self.ncmc_move.final_energy) + correction_factor = (self.ncmc_move.initial_energy - self.dynamics_move.final_energy + alch_energy - + self.ncmc_move.final_energy) return correction_factor def _acceptRejectMove(self): @@ -785,7 +786,13 @@ def _acceptRejectMove(self): self.sampler_state.box_vectors = self.ncmc_move.initial_box_vectors def equil(self, n_iterations=1): - """Equilibrate the system for N iterations.""" + """Equilibrate the system for N iterations. + + Parameters + ---------- + n_iterations : int, optional, default=1 + Number of iterations to run the sampler for. + """ # Set initial conditions by running 1 iteration of MD first for iteration in range(n_iterations): self.dynamics_move.apply(self.thermodynamic_state, self.sampler_state) @@ -795,11 +802,9 @@ def equil(self, n_iterations=1): def run(self, n_iterations=1): """Run the sampler for the specified number of iterations. - descriptive summary here - Parameters ---------- - niterations : int, optional, default=1 + n_iterations : int, optional, default=1 Number of iterations to run the sampler for. """ context, integrator = cache.global_context_cache.get_context(self.thermodynamic_state) diff --git a/blues/storage.py b/blues/storage.py index 9781b083..da0307e9 100644 --- a/blues/storage.py +++ b/blues/storage.py @@ -23,6 +23,7 @@ logger = logging.getLogger(__name__) + def _check_mode(m, modes): """ Check if the file has a read or write mode, otherwise throw an error. @@ -30,10 +31,8 @@ def _check_mode(m, modes): if m not in modes: raise ValueError('This operation is only available when a file ' 'is open in mode="%s".' % m) -def setup_logging(filename=None, - yml_path='logging.yml', - default_level=logging.INFO, - env_key='LOG_CFG'): + +def setup_logging(filename=None, yml_path='logging.yml', default_level=logging.INFO, env_key='LOG_CFG'): """Setup logging configuration """ @@ -117,46 +116,6 @@ def logToRoot(message, *args, **kwargs): setattr(logging, methodName, logToRoot) -def init_logger(logger, level=logging.INFO, stream=True, outfname=time.strftime("blues-%Y%m%d-%H%M%S")): - """Initialize the Logger module with the given logger_level and outfname. - - Parameters - ---------- - logger : logging.getLogger() - The root logger object if it has been created already. - level : logging. - Valid options for would be DEBUG, INFO, warningING, ERROR, CRITICAL. - stream : bool, default = True - If True, the logger will also stream information to sys.stdout as well - as the output file. - outfname : str, default = time.strftime("blues-%Y%m%d-%H%M%S") - The output file path prefix to store the logged data. This will always - write to a file with the extension `.log`. - - Returns - ------- - logger : logging.getLogger() - The logging object with additional Handlers added. - """ - fmt = LoggerFormatter() - - if stream: - # Stream to terminal - stdout_handler = logging.StreamHandler(stream=sys.stdout) - stdout_handler.setFormatter(fmt) - logger.addHandler(stdout_handler) - - # Write to File - if outfname: - fh = logging.FileHandler(outfname + '.log') - fh.setFormatter(fmt) - logger.addHandler(fh) - - logger.addHandler(logging.NullHandler()) - logger.setLevel(level) - - return logger - ###################### # REPORTERS # ###################### From 7988873bdfe95806c7391ced75fb32852838acd7 Mon Sep 17 00:00:00 2001 From: "Nathan M. Lim" Date: Fri, 28 Jun 2019 17:47:33 -0700 Subject: [PATCH 5/9] Remove init_logger test --- blues/tests/test_storage.py | 66 +++++++++++++++---------------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/blues/tests/test_storage.py b/blues/tests/test_storage.py index ee78cf9e..40cdad77 100644 --- a/blues/tests/test_storage.py +++ b/blues/tests/test_storage.py @@ -30,7 +30,6 @@ def get_states(): structure_pdb = utils.get_data_filename('blues', 'tests/data/ethylene_structure.pdb') structure = parmed.load_file(structure_pdb) - # Load our OpenMM System and create Integrator system_xml = utils.get_data_filename('blues', 'tests/data/ethylene_system.xml') with open(system_xml, 'r') as infile: @@ -40,13 +39,10 @@ def get_states(): thermodynamic_state = ThermodynamicState(system=system, temperature=temperature) sampler_state = SamplerState(positions=structure.positions.in_units_of(unit.nanometers)) - alch_system = generateAlchSystem(thermodynamic_state.get_system(), - alchemical_atoms) + alch_system = generateAlchSystem(thermodynamic_state.get_system(), alchemical_atoms) alch_state = alchemy.AlchemicalState.from_system(alch_system) - alch_thermodynamic_state = ThermodynamicState( - alch_system, thermodynamic_state.temperature) - alch_thermodynamic_state = CompoundThermodynamicState( - alch_thermodynamic_state, composable_states=[alch_state]) + alch_thermodynamic_state = ThermodynamicState(alch_system, thermodynamic_state.temperature) + alch_thermodynamic_state = CompoundThermodynamicState(alch_thermodynamic_state, composable_states=[alch_state]) return structure, thermodynamic_state, alch_thermodynamic_state @@ -57,24 +53,12 @@ def test_add_logging_level(): assert True == hasattr(logging, 'TRACE') -def test_init_logger(tmpdir): - print('Testing logger initialization') - dir = tmpdir.mkdir("tmp") - outfname = dir.join('testlog') - logger = logging.getLogger(__name__) - level = logger.getEffectiveLevel() - logger = init_logger(logger, level=logging.INFO, outfname=outfname, stream=False) - new_level = logger.getEffectiveLevel() - assert level != new_level - - def test_netcdf4storage(tmpdir): dir = tmpdir.mkdir("tmp") outfname = dir.join('testlog.nc') context_cache = ContextCache() - ncmc_storage = NetCDF4Storage(outfname, 5, crds=True, vels=True, frcs=True, - protocolWork=True, alchemicalLambda=True) - + ncmc_storage = NetCDF4Storage( + outfname, 5, crds=True, vels=True, frcs=True, protocolWork=True, alchemicalLambda=True) structure, thermodynamic_state, alch_thermodynamic_state = get_states() ncmc_integrator = AlchemicalExternalLangevinIntegrator( @@ -87,7 +71,7 @@ def test_netcdf4storage(tmpdir): splitting="H V R O R V H", temperature=alch_thermodynamic_state.temperature, nsteps_neq=10, - timestep=1.0*unit.femtoseconds, + timestep=1.0 * unit.femtoseconds, nprop=1, prop_lambda=0.3) context, integrator = context_cache.get_context(alch_thermodynamic_state, ncmc_integrator) @@ -120,23 +104,25 @@ def test_statedatastorage(tmpdir): context_cache = ContextCache() dir = tmpdir.mkdir("tmp") outfname = dir.join('blues.log') - state_storage = BLUESStateDataStorage(outfname, - reportInterval=5, - step=True, time=True, - potentialEnergy=True, - kineticEnergy=True, - totalEnergy=True, - temperature=True, - volume=True, - density=True, - progress=True, - remainingTime=True, - speed=True, - elapsedTime=True, - systemMass=True, - totalSteps=20, - protocolWork=True, - alchemicalLambda=True ) + state_storage = BLUESStateDataStorage( + outfname, + reportInterval=5, + step=True, + time=True, + potentialEnergy=True, + kineticEnergy=True, + totalEnergy=True, + temperature=True, + volume=True, + density=True, + progress=True, + remainingTime=True, + speed=True, + elapsedTime=True, + systemMass=True, + totalSteps=20, + protocolWork=True, + alchemicalLambda=True) structure, thermodynamic_state, alch_thermodynamic_state = get_states() ncmc_integrator = AlchemicalExternalLangevinIntegrator( alchemical_functions={ @@ -148,7 +134,7 @@ def test_statedatastorage(tmpdir): splitting="H V R O R V H", temperature=alch_thermodynamic_state.temperature, nsteps_neq=10, - timestep=1.0*unit.femtoseconds, + timestep=1.0 * unit.femtoseconds, nprop=1, prop_lambda=0.3) context, integrator = context_cache.get_context(alch_thermodynamic_state, ncmc_integrator) From f2e0828924912b60fdffcabb308a86a8f1760e66 Mon Sep 17 00:00:00 2001 From: "Nathan M. Lim" Date: Thu, 11 Jul 2019 12:30:58 -0700 Subject: [PATCH 6/9] Remove openeye toolkits installation --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e346b865..bb49c71a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -80,8 +80,8 @@ install: # Install OpenEye dependencies # Use beta version for partial bond orders - - conda install -c openeye/label/beta openeye-toolkits && python -c "import openeye; print(openeye.__version__)" - - conda install -q -c openeye/label/Orion -c omnia oeommtools packmol + #- conda install -c openeye/label/beta openeye-toolkits && python -c "import openeye; print(openeye.__version__)" + #- conda install -q -c openeye/label/Orion -c omnia oeommtools packmol # Build and install package - conda build -q --python=${PYTHON_VER} devtools/conda-recipe From bf0292174f7c5834c90f2c071da8a802caffc5e0 Mon Sep 17 00:00:00 2001 From: "Nathan M. Lim" Date: Thu, 11 Jul 2019 12:38:42 -0700 Subject: [PATCH 7/9] Increase iterations in ethylene test --- blues/tests/test_ethylene.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/blues/tests/test_ethylene.py b/blues/tests/test_ethylene.py index 48936356..f3a8db2c 100644 --- a/blues/tests/test_ethylene.py +++ b/blues/tests/test_ethylene.py @@ -24,7 +24,7 @@ def runEthyleneTest(dir, N): collision_rate = 1 / unit.picoseconds timestep = 1.0 * unit.femtoseconds n_steps = 20 - nIter = 100 + nIter = 1000 reportInterval = 5 alchemical_atoms = [2, 3, 4, 5, 6, 7] platform = openmm.Platform.getPlatformByName('CPU') @@ -36,8 +36,6 @@ def runEthyleneTest(dir, N): nc_reporter = NetCDF4Storage(filename + '_MD.nc', reportInterval) - - # Iniitialize our Move set rot_move = RandomLigandRotationMove( timestep=timestep, From cd213b2813f65f5d3f76b6f99db9564212d95d21 Mon Sep 17 00:00:00 2001 From: "Nathan M. Lim" Date: Thu, 11 Jul 2019 12:47:20 -0700 Subject: [PATCH 8/9] Increase steps/iterations in ethylene test --- blues/tests/test_ethylene.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blues/tests/test_ethylene.py b/blues/tests/test_ethylene.py index f3a8db2c..08de74b5 100644 --- a/blues/tests/test_ethylene.py +++ b/blues/tests/test_ethylene.py @@ -23,8 +23,8 @@ def runEthyleneTest(dir, N): temperature = 200 * unit.kelvin collision_rate = 1 / unit.picoseconds timestep = 1.0 * unit.femtoseconds - n_steps = 20 - nIter = 1000 + n_steps = 40 + nIter = 2000 reportInterval = 5 alchemical_atoms = [2, 3, 4, 5, 6, 7] platform = openmm.Platform.getPlatformByName('CPU') From da2c29f790e9d015855b513de71e8da3316fbfed Mon Sep 17 00:00:00 2001 From: "Nathan M. Lim" Date: Thu, 11 Jul 2019 13:02:15 -0700 Subject: [PATCH 9/9] Add more ethylene test repeats --- blues/tests/test_ethylene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blues/tests/test_ethylene.py b/blues/tests/test_ethylene.py index 08de74b5..440fc80b 100644 --- a/blues/tests/test_ethylene.py +++ b/blues/tests/test_ethylene.py @@ -104,7 +104,7 @@ def graphConvergence(dist, n_points=10): def test_runEthyleneRepeats(tmpdir): dir = tmpdir.mkdir("tmp") - outfnames = [runEthyleneTest(dir, N=i) for i in range(5)] + outfnames = [runEthyleneTest(dir, N=i) for i in range(9)] structure_pdb = utils.get_data_filename('blues', 'tests/data/ethylene_structure.pdb') trajs = [md.load('%s_MD.nc' % traj, top=structure_pdb) for traj in outfnames]