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..bb49c71a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 + #- 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 - - conda info blues - conda install --use-local -q ${PKG_NAME} - conda list diff --git a/README.md b/README.md index 8552f539..6a5b77d2 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,16 @@ This package takes advantage of non-equilibrium candidate Monte Carlo moves (NCM Latest release: [![Build Status](https://travis-ci.org/MobleyLab/blues.svg?branch=master)](https://travis-ci.org/MobleyLab/blues) -[![Documentation Status](https://readthedocs.org/projects/mobleylab-blues/badge/?version=stable)](https://mobleylab-blues.readthedocs.io/en/stable/?badge=stable) +[![Documentation Status](https://readthedocs.org/projects/mobleylab-blues/badge/?version=master)](https://mobleylab-blues.readthedocs.io/en/stable/?badge=master) [![codecov](https://codecov.io/gh/MobleyLab/blues/branch/master/graph/badge.svg)](https://codecov.io/gh/MobleyLab/blues) [![Anaconda-Server Badge](https://anaconda.org/mobleylab/blues/badges/version.svg)](https://anaconda.org/mobleylab/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 +22,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 +58,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 +70,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. + +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 `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`: -### 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` +```python +from openmmtools.states import ThermodynamicState, SamplerState +from openmmtools.testsystems import TolueneVacuum +from blues.ncmc import * +from simtk import unit +``` -### 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. +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) +``` -### 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: +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 -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 +dynamics_move = ReportLangevinDynamicsMove(n_steps=10) +ncmc_move = RandomLigandRotationMove(n_steps=10, atom_subset=list(range(15))) ``` -### 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. +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 @@ -116,7 +138,9 @@ One important non-obvious thing to note about the CombinationMove class is that - [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.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 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..4d20d627 --- /dev/null +++ b/blues/ncmc.py @@ -0,0 +1,829 @@ +"""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. + + 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) + self.dynamics_move.currentStep = 0 + self.iteration += 1 + + def run(self, n_iterations=1): + """Run the sampler for the specified number of iterations. + + Parameters + ---------- + 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) + 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/reporters.py b/blues/reporters.py deleted file mode 100644 index afa631cf..00000000 --- a/blues/reporters.py +++ /dev/null @@ -1,865 +0,0 @@ -import logging -import sys -import time - -import numpy as np -import parmed -import simtk.unit as unit -from mdtraj.reporters import HDF5Reporter -from mdtraj.utils import unitcell -from parmed import unit as u -from parmed.geometry import box_vectors_to_lengths_and_angles -from simtk.openmm import app - -import blues._version -import blues.reporters -from blues.formats import * - - -def _check_mode(m, modes): - """ - Check if the file has a read or write mode, otherwise throw an error. - """ - if m not in modes: - raise ValueError('This operation is only available when a file ' 'is open in mode="%s".' % m) - - -def addLoggingLevel(levelName, levelNum, methodName=None): - """ - Comprehensively adds a new logging level to the `logging` module and the - currently configured logging class. - - `levelName` becomes an attribute of the `logging` module with the value - `levelNum`. `methodName` becomes a convenience method for both `logging` - itself and the class returned by `logging.getLoggerClass()` (usually just - `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is - used. - - To avoid accidental clobberings of existing attributes, this method will - raise an `AttributeError` if the level name is already an attribute of the - `logging` module or if the method name is already present - - Parameters - ---------- - levelName : str - The new level name to be added to the `logging` module. - levelNum : int - The level number indicated for the logging module. - methodName : str, default=None - The method to call on the logging module for the new level name. - For example if provided 'trace', you would call `logging.trace()`. - - Example - ------- - >>> addLoggingLevel('TRACE', logging.DEBUG - 5) - >>> logging.getLogger(__name__).setLevel("TRACE") - >>> logging.getLogger(__name__).trace('that worked') - >>> logging.trace('so did this') - >>> logging.TRACE - 5 - - """ - if not methodName: - methodName = levelName.lower() - - if hasattr(logging, levelName): - logging.warn('{} already defined in logging module'.format(levelName)) - if hasattr(logging, methodName): - logging.warn('{} already defined in logging module'.format(methodName)) - if hasattr(logging.getLoggerClass(), methodName): - logging.warn('{} 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 - # http://stackoverflow.com/a/13638084/2988730 - def logForLevel(self, message, *args, **kwargs): - if self.isEnabledFor(levelNum): - self._log(levelNum, message, args, **kwargs) - - def logToRoot(message, *args, **kwargs): - logging.log(levelNum, message, *args, **kwargs) - - logging.addLevelName(levelNum, levelName) - setattr(logging, levelName, levelNum) - setattr(logging.getLoggerClass(), methodName, logForLevel) - 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, WARNING, 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 - - -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. - - 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 - 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. - 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 - - 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._coordinates, self._velocities, False, self._needEnergy) - - def report(self, simulation, state): - """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 - - """ - 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, :] - - #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): - """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 - ---------- - file : string or file - The file to write to, specified as a file name or file-like object (Logger) - reportInterval : int - The interval (in time steps) at which to write frames - frame_indices : list, frame numbers for writing the trajectory - title : str, - Text prefix for each line of the report. Used to distinguish - between the NCMC and MD simulation reports. - step : bool=False - Whether to write the current step index to the file - time : bool=False - Whether to write the current time to the file - potentialEnergy : bool=False - Whether to write the potential energy to the file - kineticEnergy : bool=False - Whether to write the kinetic energy to the file - totalEnergy : bool=False - Whether to write the total energy to the file - temperature : bool=False - Whether to write the instantaneous temperature to the file - volume : bool=False - Whether to write the periodic box volume to the file - density : bool=False - Whether to write the system density to the file - progress : bool=False - Whether to write current progress (percent completion) to the file. - If this is True, you must also specify totalSteps. - remainingTime : bool=False - Whether to write an estimate of the remaining clock time until - completion to the file. If this is True, you must also specify - totalSteps. - speed : bool=False - Whether to write an estimate of the simulation speed in ns/day to - the file - elapsedTime : bool=False - Whether to write the elapsed time of the simulation in seconds to - the file. - separator : string=',' - The separator to use between columns in the file - systemMass : mass=None - The total mass to use for the system when reporting density. If - this is None (the default), the system mass is computed by summing - the masses of all particles. This parameter is useful when the - particle masses do not reflect their actual physical mass, such as - when some particles have had their masses set to 0 to immobilize - them. - totalSteps : int=None - The total number of steps that will be included in the simulation. - This is required if either progress or remainingTime is set to True, - and defines how many steps will indicate 100% completion. - 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=[], - title='', - step=False, - time=False, - potentialEnergy=False, - kineticEnergy=False, - totalEnergy=False, - temperature=False, - volume=False, - density=False, - progress=False, - remainingTime=False, - speed=False, - elapsedTime=False, - separator='\t', - systemMass=None, - totalSteps=None, - 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 - self.title = title - - self.frame_indices = frame_indices - self._protocolWork, self._alchemicalLambda, self._currentIter = protocolWork, alchemicalLambda, currentIter - 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._needsPositions, self._needsVelocities, self._needsForces, self._needEnergy) - - def report(self, simulation, state): - """Generate a report. - - Parameters - ---------- - simulation : Simulation - The Simulation to generate a report for - state : State - The current state of the simulation - """ - if not self._hasInitialized: - self._initializeConstants(simulation) - headers = self._constructHeaders() - if hasattr(self.log, 'report'): - self.log.info = self.log.report - self.log.info('#"%s"' % ('"' + self._separator + '"').join(headers)) - try: - self._out.flush() - except AttributeError: - pass - self._initialClockTime = time.time() - self._initialSimulationTime = state.getTime() - self._initialSteps = simulation.currentStep - self._hasInitialized = True - - # Check for errors. - self._checkForErrors(simulation, state) - # Query for the values - values = self._constructReportValues(simulation, state) - - # 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))) - try: - self._out.flush() - except AttributeError: - pass - - def _constructReportValues(self, simulation, state): - """Query the simulation for 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 - - Returns - ------- - values : list - A list of values summarizing the current state of the simulation, - to be printed or saved. Each element in the list corresponds to one - of the columns in the resulting CSV file. - """ - values = [] - box = 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._progress: - values.append('%.1f%%' % (100.0 * simulation.currentStep / self._totalSteps)) - if self._step: - values.append(simulation.currentStep) - if self._time: - values.append(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') - values.append(alchemicalLambda) - if self._protocolWork: - protocolWork = simulation.integrator.get_protocol_work(dimensionless=True) - values.append(protocolWork) - if self._potentialEnergy: - values.append(state.getPotentialEnergy().value_in_unit(unit.kilojoules_per_mole)) - if self._kineticEnergy: - values.append(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)) - if self._temperature: - values.append( - (2 * 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: - values.append((self._totalMass / volume).value_in_unit(unit.gram / unit.item / unit.milliliter)) - - if self._speed: - elapsedDays = (clockTime - self._initialClockTime) / 86400.0 - elapsedNs = (state.getTime() - self._initialSimulationTime).value_in_unit(unit.nanosecond) - if elapsedDays > 0.0: - values.append('%.3g' % (elapsedNs / elapsedDays)) - else: - values.append('--') - if self._elapsedTime: - values.append(time.time() - self._initialClockTime) - if self._remainingTime: - elapsedSeconds = clockTime - self._initialClockTime - elapsedSteps = simulation.currentStep - self._initialSteps - if elapsedSteps == 0: - value = '--' - else: - estimatedTotalSeconds = (self._totalSteps - self._initialSteps) * elapsedSeconds / elapsedSteps - remainingSeconds = int(estimatedTotalSeconds - elapsedSeconds) - remainingDays = remainingSeconds // 86400 - remainingSeconds -= remainingDays * 86400 - remainingHours = remainingSeconds // 3600 - remainingSeconds -= remainingHours * 3600 - remainingMinutes = remainingSeconds // 60 - remainingSeconds -= remainingMinutes * 60 - if remainingDays > 0: - value = "%d:%d:%02d:%02d" % (remainingDays, remainingHours, remainingMinutes, remainingSeconds) - elif remainingHours > 0: - value = "%d:%02d:%02d" % (remainingHours, remainingMinutes, remainingSeconds) - elif remainingMinutes > 0: - value = "%d:%02d" % (remainingMinutes, remainingSeconds) - else: - value = "0:%02d" % remainingSeconds - values.append(value) - return values - - def _constructHeaders(self): - """Construct the headers for the CSV output - - Returns - ------- - headers : list - a list of strings giving the title of each observable being reported on. - """ - headers = [] - if self._currentIter: - headers.append('Iter') - if self._progress: - headers.append('Progress (%)') - if self._step: - headers.append('Step') - if self._time: - headers.append('Time (ps)') - if self._alchemicalLambda: - headers.append('alchemicalLambda') - if self._protocolWork: - headers.append('protocolWork') - if self._potentialEnergy: - headers.append('Potential Energy (kJ/mole)') - if self._kineticEnergy: - headers.append('Kinetic Energy (kJ/mole)') - if self._totalEnergy: - headers.append('Total Energy (kJ/mole)') - if self._temperature: - headers.append('Temperature (K)') - if self._volume: - headers.append('Box Volume (nm^3)') - if self._density: - headers.append('Density (g/mL)') - if self._speed: - headers.append('Speed (ns/day)') - if self._elapsedTime: - headers.append('Elapsed Time (s)') - 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/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/storage.py b/blues/storage.py new file mode 100644 index 00000000..da0307e9 --- /dev/null +++ b/blues/storage.py @@ -0,0 +1,604 @@ +import os +import logging +import logging.config +import sys +import time + +import numpy as np +import parmed +import simtk.unit as unit +from mdtraj.reporters import HDF5Reporter +from mdtraj.utils import unitcell +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 +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): + """ + Check if the file has a read or write mode, otherwise throw an error. + """ + 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): + """ + Comprehensively adds a new logging level to the `logging` module and the + currently configured logging class. + + `levelName` becomes an attribute of the `logging` module with the value + `levelNum`. `methodName` becomes a convenience method for both `logging` + itself and the class returned by `logging.getLoggerClass()` (usually just + `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is + used. + + To avoid accidental clobberings of existing attributes, this method will + raise an `AttributeError` if the level name is already an attribute of the + `logging` module or if the method name is already present + + Parameters + ---------- + levelName : str + The new level name to be added to the `logging` module. + levelNum : int + The level number indicated for the logging module. + methodName : str, default=None + The method to call on the logging module for the new level name. + For example if provided 'trace', you would call `logging.trace()`. + + Example + ------- + >>> addLoggingLevel('TRACE', logging.DEBUG - 5) + >>> logging.getLogger(__name__).setLevel("TRACE") + >>> logging.getLogger(__name__).trace('that worked') + >>> logging.trace('so did this') + >>> logging.TRACE + 5 + + """ + if not methodName: + methodName = levelName.lower() + + if hasattr(logging, levelName): + logging.warning('{} already defined in logging module'.format(levelName)) + if hasattr(logging, methodName): + logging.warning('{} already defined in logging module'.format(methodName)) + if hasattr(logging.getLoggerClass(), 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 + # http://stackoverflow.com/a/13638084/2988730 + def logForLevel(self, message, *args, **kwargs): + if self.isEnabledFor(levelNum): + self._log(levelNum, message, args, **kwargs) + + def logToRoot(message, *args, **kwargs): + logging.log(levelNum, message, *args, **kwargs) + + logging.addLevelName(levelNum, levelName) + setattr(logging, levelName, levelNum) + setattr(logging.getLoggerClass(), methodName, logForLevel) + setattr(logging, methodName, logToRoot) + + +###################### +# REPORTERS # +###################### + + +class NetCDF4Storage(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(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, context_state): + """ + Get information about the next report this object will generate. + + Parameters + ---------- + context_state : :class:`openmm.State` + The current state of the context + + 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 context_state.currentStep in self.frame_indices: + steps = 1 + else: + steps = -1 + if not self.frame_indices: + steps_left = context_state.currentStep % self._reportInterval + steps = self._reportInterval - steps_left + return (steps, self.crds, self.vels, self.frcs, False) + + def report(self, context_state, integrator): + """Generate a report. + + Parameters + ---------- + context_state : :class:`openmm.State` + The current state of the context + integrator : :class:`openmm.Integrator` + The integrator belonging to the given context + + """ + global VELUNIT, FRCUNIT + + 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 + ---------- + file : string or file + The file to write to, specified as a file name or file-like object (Logger) + reportInterval : int + The interval (in time steps) at which to write frames + frame_indices : list, frame numbers for writing the trajectory + title : str, + Text prefix for each line of the report. Used to distinguish + between the NCMC and MD simulation reports. + step : bool=False + Whether to write the current step index to the file + time : bool=False + Whether to write the current time to the file + potentialEnergy : bool=False + Whether to write the potential energy to the file + kineticEnergy : bool=False + Whether to write the kinetic energy to the file + totalEnergy : bool=False + Whether to write the total energy to the file + temperature : bool=False + Whether to write the instantaneous temperature to the file + volume : bool=False + Whether to write the periodic box volume to the file + density : bool=False + Whether to write the system density to the file + progress : bool=False + Whether to write current progress (percent completion) to the file. + If this is True, you must also specify totalSteps. + remainingTime : bool=False + Whether to write an estimate of the remaining clock time until + completion to the file. If this is True, you must also specify + totalSteps. + speed : bool=False + Whether to write an estimate of the simulation speed in ns/day to + the file + elapsedTime : bool=False + Whether to write the elapsed time of the simulation in seconds to + the file. + separator : string=',' + The separator to use between columns in the file + systemMass : mass=None + The total mass to use for the system when reporting density. If + this is None (the default), the system mass is computed by summing + the masses of all particles. This parameter is useful when the + particle masses do not reflect their actual physical mass, such as + when some particles have had their masses set to 0 to immobilize + them. + totalSteps : int=None + The total number of steps that will be included in the simulation. + This is required if either progress or remainingTime is set to True, + and defines how many steps will indicate 100% completion. + 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=None, + reportInterval=1, + frame_indices=[], + title='', + step=False, + time=False, + potentialEnergy=False, + kineticEnergy=False, + totalEnergy=False, + temperature=False, + volume=False, + density=False, + progress=False, + remainingTime=False, + speed=False, + elapsedTime=False, + separator='\t', + systemMass=None, + totalSteps=None, + protocolWork=False, + alchemicalLambda=False, + currentIter=False): + 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 + self._protocolWork, self._alchemicalLambda, self._currentIter = protocolWork, alchemicalLambda, currentIter + 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, context_state): + """ + Get information about the next report this object will generate. + + Parameters + ---------- + context_state : :class:`openmm.State` + The current state of the context + + 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 context_state.currentStep in self.frame_indices: + steps = 1 + else: + steps = -1 + if not self.frame_indices: + steps_left = context_state.currentStep % self._reportInterval + steps = self._reportInterval - steps_left + + return (steps, self._needsPositions, self._needsVelocities, self._needsForces, self._needEnergy) + + 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 + ---------- + 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(context_state) + headers = self._constructHeaders() + 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 = context_state.getTime() + self._initialSteps = context_state.currentStep + self._hasInitialized = True + + # Check for errors. + self._checkForErrors(context_state, integrator) + # Query for the values + values = self._constructReportValues(context_state, integrator) + + # Write the 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 _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 + ---------- + context_state : :class:`openmm.State` + The current state of the context + integrator : :class:`openmm.Integrator` + The integrator belonging to the given context + + Returns + ------- + values : list + A list of values summarizing the current state of the simulation, + to be printed or saved. Each element in the list corresponds to one + of the columns in the resulting CSV file. + """ + values = [] + 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._progress: + values.append('%.1f%%' % (100.0 * context_state.currentStep / self._totalSteps)) + if self._step: + values.append(context_state.currentStep) + if self._time: + 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 = integrator.getGlobalVariableByName('lambda') + values.append(alchemicalLambda) + if self._protocolWork: + protocolWork = integrator.get_protocol_work(dimensionless=True) + values.append(protocolWork) + if self._potentialEnergy: + values.append(context_state.getPotentialEnergy().value_in_unit(unit.kilojoules_per_mole)) + if self._kineticEnergy: + values.append(context_state.getKineticEnergy().value_in_unit(unit.kilojoules_per_mole)) + if self._totalEnergy: + values.append((context_state.getKineticEnergy() + context_state.getPotentialEnergy()).value_in_unit( + unit.kilojoules_per_mole)) + if self._temperature: + values.append( + (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: + values.append((self._totalMass / volume).value_in_unit(unit.gram / unit.item / unit.milliliter)) + + if self._speed: + elapsedDays = (clockTime - self._initialClockTime) / 86400.0 + elapsedNs = (context_state.getTime() - self._initialSimulationTime).value_in_unit(unit.nanosecond) + if elapsedDays > 0.0: + values.append('%.3g' % (elapsedNs / elapsedDays)) + else: + values.append('--') + if self._elapsedTime: + values.append(time.time() - self._initialClockTime) + if self._remainingTime: + elapsedSeconds = clockTime - self._initialClockTime + elapsedSteps = context_state.currentStep - self._initialSteps + if elapsedSteps == 0: + value = '--' + else: + estimatedTotalSeconds = (self._totalSteps - self._initialSteps) * elapsedSeconds / elapsedSteps + remainingSeconds = int(estimatedTotalSeconds - elapsedSeconds) + remainingDays = remainingSeconds // 86400 + remainingSeconds -= remainingDays * 86400 + remainingHours = remainingSeconds // 3600 + remainingSeconds -= remainingHours * 3600 + remainingMinutes = remainingSeconds // 60 + remainingSeconds -= remainingMinutes * 60 + if remainingDays > 0: + value = "%d:%d:%02d:%02d" % (remainingDays, remainingHours, remainingMinutes, remainingSeconds) + elif remainingHours > 0: + value = "%d:%02d:%02d" % (remainingHours, remainingMinutes, remainingSeconds) + elif remainingMinutes > 0: + value = "%d:%02d" % (remainingMinutes, remainingSeconds) + else: + value = "0:%02d" % remainingSeconds + values.append(value) + return values + + def _constructHeaders(self): + """Construct the headers for the CSV output + + Returns + ------- + headers : list + a list of strings giving the title of each observable being reported on. + """ + headers = [] + # if self._currentIter: + # headers.append('Iter') + if self._progress: + headers.append('Progress (%)') + if self._step: + headers.append('Step') + if self._time: + headers.append('Time (ps)') + if self._alchemicalLambda: + headers.append('alchemicalLambda') + if self._protocolWork: + headers.append('protocolWork') + if self._potentialEnergy: + headers.append('Potential Energy (kJ/mole)') + if self._kineticEnergy: + headers.append('Kinetic Energy (kJ/mole)') + if self._totalEnergy: + headers.append('Total Energy (kJ/mole)') + if self._temperature: + headers.append('Temperature (K)') + if self._volume: + headers.append('Box Volume (nm^3)') + if self._density: + headers.append('Density (g/mL)') + if self._speed: + headers.append('Speed (ns/day)') + if self._elapsedTime: + headers.append('Elapsed Time (s)') + if self._remainingTime: + headers.append('Time Remaining') + return headers 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..440fc80b 100644 --- a/blues/tests/test_ethylene.py +++ b/blues/tests/test_ethylene.py @@ -1,107 +1,73 @@ -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 = 40 + nIter = 2000 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 +82,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 +102,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(9)] -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 +123,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..40cdad77 --- /dev/null +++ b/blues/tests/test_storage.py @@ -0,0 +1,162 @@ +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_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 5d66c675..00000000 Binary files a/devdocs/class-diagram.png and /dev/null differ diff --git a/devtools/conda-recipe/meta.yaml b/devtools/conda-recipe/meta.yaml index 7fdba0f7..89576b10 100755 --- a/devtools/conda-recipe/meta.yaml +++ b/devtools/conda-recipe/meta.yaml @@ -1,6 +1,7 @@ package: name: blues - version: {{ GIT_DESCRIBE_TAG }} + version: 0.2.5 + #version: {{ GIT_DESCRIBE_TAG }} source: path : ../.. @@ -21,9 +22,10 @@ requirements: - python - pytest - setuptools - - openmmtools >=0.15.0 + - openmmtools <=0.16.0 - mdtraj - openmm >=7.2.2 + - scipy <=1.1.0 - parmed - pymbar - netcdf4 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 00000000..cfb5208d Binary files /dev/null and b/images/ct-2018-01018f_0014.jpeg differ diff --git a/images/uml.png b/images/uml.png new file mode 100644 index 00000000..b677cc8a Binary files /dev/null and b/images/uml.png differ diff --git a/images/uml.xml b/images/uml.xml new file mode 100644 index 00000000..2c804729 --- /dev/null +++ b/images/uml.xml @@ -0,0 +1 @@ +7Z1vc9u4EYc/jWfaF/GIoqg/LxMlvnaadHJx2l77hgNRsMQzSehASLbv0xegQIrSrhw5FgHlbm9uJiIEySLwEFjs/oC9Cqf540+SrZafxJxnV/3e/PEqfH/V7wdhf6L/MSVPtmTYH2xLFjKdb8t6u4Lb9HduK9al63TOS1u2LVJCZCpd7Rcmoih4ovbKmJTiYb/ancjmewUrtuCg4DZhGSz9TzpXS1sa9Hq7N/7G08XS/ulxZN+YseR+IcW6sH/vqh/eVf9t385Z/V22frlkc/HQKgo/XIVTKYTavsofpzwzjbvfbDdH3m1+t+SFOuUDo/FkGATR5K435JMxH78JRtuv2LBsbRvjn9NP009iw+0vVk91K5UPaZ6xQl+9W6o804WBfnknCnVrK5lrlqWLQr9O9E/iUhdsuFSpbuu39g0lVro0WabZ/CN7Emvzw0ulG7K+ercUMv1dfy2r/4Z+WyqLTX+4V+PWfFIX93Sp5KWu87lujaAp+shKZeskIsvYqkxnzQ/OmVykxTuhlMhtpfpOb9Ism4pMyKoB6q7V32p6nM/r2nWnbr8/TxP7OmMznr1rEKm/qRBVG5ZKinve+vpe9V/zTk1i3zRy64fYj5t2v2F5mpmH7t9czlnB6u7YtlTQt9fY34DkWJhMd/HHVpEl6Scucq7kk65i3w2H9iP2sX8TDexj/7B7iMKeLVu2np/BaGgfXvvgLpov38GrX1h+T2S5uYMdy0VcKq4HEf1YhG/NZ/XNHmKt71btI73fM3V7wy6oUc/4nToKerliSVosPlZ13g92JV9se5iih2Wq+K0uN7/pQY+yukw/gvIuq8BapvM5LyrwFFNsy67pxZXQd1S1YfRO/68f/mnvOrqK9H1N9XWwu9b/m+pSw1Do+2Np1e1cPxgP3DwcCBDHB4tvI2KR6A9PA6J/Bh7ulw+zzT8+59Hnm9//9+HnX0P52xQb35K1NPd4q8EgLk7lAnT5Sagc5SLqu+MCjhMhYEKlOS/bQJRpru6v10Wqrn9es0Kl6okAcThwjMY+AYngoKHNhrRMRRFLpjhhciGYBL0TLY5uOBkgnBSmv+OEJcsdJmLFizw3S5ryunrnerqtN91WI2LcERP2fJskQ0CN5KYduNwZqnqwIYvEpUUSRL5N1RCaJVf9YWbueJ5u9MuFeZmJxSpmScJXqn5X/7VWBeQzddFMHpYcfpSAcwfc2KENjAMHpy8EnlQbNynLYl5wuXgi5n5k5vo9h2Y1zhyc/PStpwawlSg1abo5CAmXSIQOLWgciQlAYiWFpoHPiQk/TAx928ghdNvdpUVrFiIa3NEw9m4Zj4/QQMODDyDCwKf3NoDTBej9LD2MUu6H1ILv6vpcd1jGd3391aDw/k0AeAghDyHSz1V48LNFWJfJbd2D/vfiJQnDEw3FcQc9XAfLWz0cz/idkDzWd88XkpkG+8tf6aF3ScTA4SwAkYD2QMzuFJdEhD8iRt4dGH1IxYKrhgkhCQm3lsHEu38BakBiu5rcWYtEhVMqBn3fLoYALiDYapU9EQhuQYh8+xWCHhZkOUCAzxe8VjTqJlqKhdCLzQ+70gP9367OR1EtCQw1v3KlnqwQj62V2GeKF/O3RreqL2eZMAJGU2TkhvY74XpFv9/W9elOkE+/mNq6R+3lf+2Hq4v3j3tXT/Zqe7fmFo92qy0qxVom/EhP11FUxaSecY9UGoQ4DpJn2mDb7P8CrK/tRz+LSp9V6w6HI6vPqLGqZbr1V2x/lP3Ujhjd4uypVc0+Lkf/TlRHaGpHiGXl5vvq6xfbX7DDt2mT77OKQ7hQQiIxX1gxF/nHdKH//WJGDD0FbvW9XYdyLkQwHF2RYPjqEgTDg1rvX4dA+9cRmBD6vSGcEIJas3fWVSViLOasLDm5E79/HdmMSadrhE9koBPHAuJaBp1P3sRX9nAUnNbDXTgTkegiORN9P/LRxOMjP0Dcy+Qi8AvE2OccMDhJePfu478+3N6yfJVx+WPZv3/oDXM3N2A+Di/V/g2Hkz37NwygP2TQR7Bvgi/n5f40/Z+Gspojr/Y2S5GW76xDZjMGvWZrHcpON0Mm3CFTWFWyfsppV50fJJBddQ6RgIa2WnKZi/lToUfdJNZzksK3xHxt17vdViNsnGGD7LVzh00ErXGWJcv4VHamIl8ZW4EY8soQthGvM4iOhHYASOXWWn+GHmvPEy9nDwUei/08sw3PMS/IaRFJnsR5FRqxrMz0e+W1Kb8+eiwKYdIhJsiuPMeY9AEmdpIpj6LypdrO+ZEVC54W721tYsc1O8gGO4d2DRxeQO9TYOG1a2BkPxvaxV1EFiI4MlTqQ2O+8tz0yNbwIFeyWyYC71NGdJJjLV7J1Lge83VW+de+prnuIkML+dY8Tx0aGJ9TB4xHWN/aF/4rT5SxJGhQcexsdegjwQcVOKbw39ZpRiC4HRkm3v0c0PUu1yRgcItB2PfuvqjFaxcmXD6QKWM6Zitcbi4q2fJOxdytcHliH59nhctRD+/9lwmXX6o3ntRjS7NvYnSAyZn1wxM4lDzrECU9Q7d6hpubXk+vaH8YPcN4vK/nHQyglRTUZXt6hkkXC/IJPFsGWXa1zgygddZZzepmOHmNhgHlpZNl1gTuKUZw2XA9zWlgOPHinxdE4OCQFygVn4nHeKNX5ELSlgGXHCCKhc44QK3v4UmS0ZVQ+q6/fXgekXO+hdsWphfpFNwNIUEPxgwQbu7TguuOI2ouZbzB1AqOBxzo/lO6EelANB9jCCJKcEwDXLhvRLbOSWDglANEYOCYA7jiLWJ9S7qnMtrF6hQGTIrg0rDAUhFkmV6bpBseb5hMTVsSEk69G8jRui6ROOGoE1IgvdqDdaJleAaHJz4FQC9WdbRRrERsk5FQbNDtTIBoBRybBch5uVLkxIMfHhDJgFseRlCLul7NmeIxYeENC0xC4BgLuGMmLWsa9L/5iikTJiUw3IKBnLTreP6AWzmXrIwLRjIjxyggR+y6RaHxfP5ZZEb8MVW/tF63PqWvdh8yF6+TJuFjsu3w589VHODUdCxPqiFrsrxP2niB+uNJ8Kr6YRgd4Ps6+dMRNSUWkXEP/JyVy+rzB/SLjdFAteBHjgK9OI1dUPvC2hSjzT85ywmhLyZ5tH8Oy5vQHi/YmdKuaZEWZifsWCfBnQPBXQ88QhcsuNtXiDabmNv2wWgE7YNuBHdBDxqK5VOpeE5W4vcnYmiGilfJ6DAKuvEzIwcvaAJW5tSotaSIpFMSMIGcQxKgL3qlp4OSMHCMAaaPc4cBkr+J1AnuIUClbl1R8OxiluQJDlJvbAF5mYTN4dSA7c2gYPS5H3lMmYZ1clfR6BHUHFA02vODP/Q+DSDaJMlN+JGQ8IPExOFcgCMBBazm6Jyt84BwcIsDdmKOYxygdWBw2K4aCAfHOCCH3zjGAfoRSJhwCWRgyja3ZMAwiiajOmaNuPDHBaZwc8sFDERIPl8nfB43mzCJCrdUoAI3p1SMoSMSUBEztR0/KOWPaz4wnZtbPqAutqRViC8cMK2bw6DF88dp8WwmHtqKnxYE3xIdMKn2MvvulBXmrJ+W2CfJWFkaLcNxvY+RYpyoQ6h+sr6oYbs6UBnBPjtJ22N75FmBWu1rPlASvVbnA4Q8/Tp7RJNKcnI9HvfDSFskQTAaWgVZ843bOwOJgc8mJgvDZyHqTCyp6lTQ/Rflgn69yhLt5u9TPgaTE8A6T0bpl+rFBqN95WPfJpzuVpkITdrWufKbbxwsT8qxP1zq5W9MuM8rx+ph+7hiqFnE7enGahv+7LYXIhIotCHOV/o57FH6sHOYYM8MKy+RkqFgdGaT96FNnqylualbDQex4ZENRFzmmI0+YEOlOS/bYJRpru6v10Wqrn9eM73EV3Q4jmtQEPmZY1CgDMHYEmmZiiKW7WRghIt/XDChmmNeoEJBcrMWXxRx+wBQC81MiIwocU0JImBzTAkULTQxSZYs8QyD1TvX02296bYakeOWHEQV55gcqG+Q1Vqey92ooqcnsmmds4Gc4eaYDewY6gMK/nzK2C6WtsgJbWhXdyWO7UPxQjzjd0LyWLcCX8gqDRjFndyTgQjgHA8C0Nkbszs9PRAZfslAtHBuyRhAP2mVZbLmQkjCwoOX1LeHI4RYVBstCAb3MCDqN8djBPSZv22S0H541NOICSfbsOHfm4EDkEJBQwoa7h03cdWOGb6JBgjo4xCCHtVpDs5POowAFFXQMC74bxQc6nIUHLz4DAqUje4GQcTnT4HDi2ADCxw6ZWMIJ8hiJQVR4ZMKLErolgpoQ2NJnDQnccby2Zw1uOjmZ4ry8lwERmj00ClHESJZaVohViLjkhUJP6SHOHHLCRY/dMsJHG/KVZYqpXuhgUM3CqHhGg0sQOjWeIX+X9as4OO7dZHYlLWWknma0AjiHBMsVugWEyQLHAngLo8UNNTolhRMq0IKuEvlBckU5ZgXGJtWPOer+hBPguVyYEFSTjk2ZKEXNtd3rkGJt67y+EHI+yuSS/qkZOzbpo2gQ7amZMmZVMSHTz7CwLcxO4SjSHyY+ZoWxf4ACX3bsBFc7QAKSBl5jq5GjohEu7orZWQEVyuVyKnleydhi3sskBM5HI8AUDcfs/m8JX7bbhkmONzDMfE+PUDhfAVHy3G60ve9lrNKPVuRQqA4B2XQ9+3biKBvQ08uH8XibZLwlTKxuc9SzNgszVJFCkoPiETePRrY+YElp5OpPcAw8u24COrdGN6OcRr9uMc41TGyCzzGaVx72WsTpk0KqD0cT47X7ubIp7rtWqPQVOQrAxYlC/Su3vaQLPBV6u1RcJAsMJogI2u9v2hPv93ZQn8MfcIzJoU5gJUm2nNPtNvh5EWKbYyG7o5fhf69GVeMQHAMAibPdgsCdP/N+R1bZyqeicd4wxMlJOULc80FJtB2ywV0/6WlceqkYq5nceLBLQ+o0totENDld/+VOHDNAaakdssBdNdQYkmPQGD6aadATKC0njIP+6IBk0m7pQHJ60Bp6b2sMTEhtFsWoE+NstN7BAJTOrsFArqgKDu5JweUdzsSRoYBBaQwO0dXY2pkrKu7cjxPYE9TYmr/IwB2dJLb2QCuJCk3tWcqsMSBbqkIenA9SfmpfSKB5Ap0jQRcVFKOap9IYOpkx0jAtSXlqb4QOjB5slvLAka3KVX1JaCBCpLdogHj3ZSt+gLAwGTIbsHA8kRQwuoLQgQTJ7tFBIbAKWe1T/FUbQT6M0ORjfYAAwf69b3k0i1NuthUuaePprTuXJJen4XTlqTjh6oP8a4+WZZ+pB/1pRRCtcXi+iFYfhJzbmr8Hw== \ No newline at end of file