"vscode:/vscode.git/clone" did not exist on "47ab0a622ff443696c38fe8c21cc63ba0d538bf2"
Commit dca54ec7 authored by Saurabh Belsare's avatar Saurabh Belsare
Browse files

Merged fork with latest original master

parents cace5edf 01f9e415
......@@ -6,7 +6,7 @@
* Biological Structures at Stanford, funded under the NIH Roadmap for *
* Medical Research, grant U54 GM072970. See https://simtk.org. *
* *
* Portions copyright (c) 2008-2015 Stanford University and the Authors. *
* Portions copyright (c) 2008-2016 Stanford University and the Authors. *
* Authors: Peter Eastman, Lee-Ping Wang *
* Contributors: *
* *
......@@ -75,7 +75,7 @@ void testIdealGas() {
// Test it for three different temperatures.
for (int i = 0; i < 3; i++) {
barostat->setTemperature(temp[i]);
barostat->setDefaultTemperature(temp[i]);
LangevinIntegrator integrator(temp[i], 0.1, 0.01);
Context context(system, integrator, platform);
context.setPositions(positions);
......@@ -135,7 +135,7 @@ void testIdealGasAxis(int axis) {
// Test it for three different temperatures.
for (int i = 0; i < 3; i++) {
barostat->setTemperature(temp[i]);
barostat->setDefaultTemperature(temp[i]);
LangevinIntegrator integrator(temp[i], 0.1, 0.01);
Context context(system, integrator, platform);
context.setPositions(positions);
......@@ -371,7 +371,7 @@ void testEinsteinCrystal() {
// Create the barostat.
MonteCarloAnisotropicBarostat* barostat = new MonteCarloAnisotropicBarostat(Vec3(pres3[p], pres3[p], pres3[p]), temp, (a==0||a==3), (a==1||a==3), (a==2||a==3), frequency);
system.addForce(barostat);
barostat->setTemperature(temp);
barostat->setDefaultTemperature(temp);
LangevinIntegrator integrator(temp, 0.1, 0.01);
Context context(system, integrator, platform);
context.setPositions(positions);
......@@ -417,7 +417,7 @@ void testEinsteinCrystal() {
// Create the barostat.
MonteCarloBarostat* barostat = new MonteCarloBarostat(pres3[p], temp, frequency);
system.addForce(barostat);
barostat->setTemperature(temp);
barostat->setDefaultTemperature(temp);
LangevinIntegrator integrator(temp, 0.1, 0.001);
Context context(system, integrator, platform);
context.setPositions(positions);
......
......@@ -6,7 +6,7 @@
* Biological Structures at Stanford, funded under the NIH Roadmap for *
* Medical Research, grant U54 GM072970. See https://simtk.org. *
* *
* Portions copyright (c) 2008-2015 Stanford University and the Authors. *
* Portions copyright (c) 2008-2016 Stanford University and the Authors. *
* Authors: Peter Eastman *
* Contributors: *
* *
......@@ -111,7 +111,7 @@ void testIdealGas() {
// Test it for three different temperatures.
for (int i = 0; i < 3; i++) {
barostat->setTemperature(temp[i]);
barostat->setDefaultTemperature(temp[i]);
LangevinIntegrator integrator(temp[i], 0.1, 0.01);
Context context(system, integrator, platform);
context.setPositions(positions);
......
......@@ -6,7 +6,7 @@
* Biological Structures at Stanford, funded under the NIH Roadmap for *
* Medical Research, grant U54 GM072970. See https://simtk.org. *
* *
* Portions copyright (c) 2008-2015 Stanford University and the Authors. *
* Portions copyright (c) 2008-2016 Stanford University and the Authors. *
* Authors: Peter Eastman *
* Contributors: *
* *
......@@ -88,12 +88,43 @@ void testPeriodicTorsions() {
}
}
void testPeriodic() {
// Create a force that uses periodic boundary conditions.
System system;
system.addParticle(1.0);
system.addParticle(1.0);
system.addParticle(1.0);
system.addParticle(1.0);
system.setDefaultPeriodicBoxVectors(Vec3(3, 0, 0), Vec3(0, 3, 0), Vec3(0, 0, 3));
VerletIntegrator integrator(0.01);
PeriodicTorsionForce* torsions = new PeriodicTorsionForce();
torsions->addTorsion(0, 1, 2, 3, 2, PI_M/3, 1.1);
torsions->setUsesPeriodicBoundaryConditions(true);
system.addForce(torsions);
Context context(system, integrator, platform);
vector<Vec3> positions(4);
positions[0] = Vec3(0, 1, 0);
positions[1] = Vec3(0, 0, 0);
positions[2] = Vec3(1, 0, 0);
positions[3] = Vec3(1, 0, 2);
context.setPositions(positions);
State state = context.getState(State::Forces | State::Energy);
const vector<Vec3>& forces = state.getForces();
double torque = -2*1.1*std::sin(2*PI_M/3);
ASSERT_EQUAL_VEC(Vec3(0, 0, torque), forces[0], TOL);
ASSERT_EQUAL_VEC(Vec3(0, -torque, 0), forces[3], TOL);
ASSERT_EQUAL_VEC(Vec3(forces[0][0]+forces[1][0]+forces[2][0]+forces[3][0], forces[0][1]+forces[1][1]+forces[2][1]+forces[3][1], forces[0][2]+forces[1][2]+forces[2][2]+forces[3][2]), Vec3(0, 0, 0), TOL);
ASSERT_EQUAL_TOL(1.1*(1+std::cos(2*PI_M/3)), state.getPotentialEnergy(), TOL);
}
void runPlatformTests();
int main(int argc, char* argv[]) {
try {
initializeTests(argc, argv);
testPeriodicTorsions();
testPeriodic();
runPlatformTests();
}
catch(const exception& e) {
......
......@@ -6,7 +6,7 @@
* Biological Structures at Stanford, funded under the NIH Roadmap for *
* Medical Research, grant U54 GM072970. See https://simtk.org. *
* *
* Portions copyright (c) 2008-2015 Stanford University and the Authors. *
* Portions copyright (c) 2008-2016 Stanford University and the Authors. *
* Authors: Peter Eastman *
* Contributors: *
* *
......@@ -107,12 +107,53 @@ void testRBTorsions() {
}
}
void testPeriodic() {
// Create a force that uses periodic boundary conditions.
System system;
system.addParticle(1.0);
system.addParticle(1.0);
system.addParticle(1.0);
system.addParticle(1.0);
system.setDefaultPeriodicBoxVectors(Vec3(3, 0, 0), Vec3(0, 3, 0), Vec3(0, 0, 3));
VerletIntegrator integrator(0.01);
RBTorsionForce* torsions = new RBTorsionForce();
torsions->addTorsion(0, 1, 2, 3, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6);
torsions->setUsesPeriodicBoundaryConditions(true);
system.addForce(torsions);
Context context(system, integrator, platform);
vector<Vec3> positions(4);
positions[0] = Vec3(0, 1, 0);
positions[1] = Vec3(0, 0, 0);
positions[2] = Vec3(1, 0, 0);
positions[3] = Vec3(0, 0, 2);
context.setPositions(positions);
State state = context.getState(State::Forces | State::Energy);
const vector<Vec3>& forces = state.getForces();
double psi = 0.5*PI_M;
double torque = 0.0;
for (int i = 1; i < 6; ++i) {
double c = 0.1*(i+1);
torque += -c*i*std::pow(std::cos(psi), i-1)*std::sin(psi);
}
ASSERT_EQUAL_VEC(Vec3(0, 0, torque), forces[0], TOL);
ASSERT_EQUAL_VEC(Vec3(0, -torque, 0), forces[3], TOL);
ASSERT_EQUAL_VEC(Vec3(forces[0][0]+forces[1][0]+forces[2][0]+forces[3][0], forces[0][1]+forces[1][1]+forces[2][1]+forces[3][1], forces[0][2]+forces[1][2]+forces[2][2]+forces[3][2]), Vec3(0, 0, 0), TOL);
double energy = 0.0;
for (int i = 0; i < 6; ++i) {
double c = 0.1*(i+1);
energy += c*std::pow(std::cos(psi), i);
}
ASSERT_EQUAL_TOL(energy, state.getPotentialEnergy(), TOL);
}
void runPlatformTests();
int main(int argc, char* argv[]) {
try {
initializeTests(argc, argv);
testRBTorsions();
testPeriodic();
runPlatformTests();
}
catch(const exception& e) {
......
......@@ -397,6 +397,7 @@ extern OPENMM_EXPORT void %(name)s_insert(%(name)s* set, %(type)s value);""" % v
/* These methods need to be handled specially, since their C++ APIs cannot be directly translated to C.
Unlike the C++ versions, the return value is allocated on the heap, and you must delete it yourself. */
extern OPENMM_EXPORT OpenMM_State* OpenMM_Context_getState(const OpenMM_Context* target, int types, int enforcePeriodicBox);
extern OPENMM_EXPORT OpenMM_State* OpenMM_Context_getState_2(const OpenMM_Context* target, int types, int enforcePeriodicBox, int groups);
extern OPENMM_EXPORT OpenMM_StringArray* OpenMM_Platform_loadPluginsFromDirectory(const char* directory);
extern OPENMM_EXPORT OpenMM_StringArray* OpenMM_Platform_getPluginLoadFailures();
extern OPENMM_EXPORT char* OpenMM_XmlSerializer_serializeSystem(const OpenMM_System* system);
......@@ -801,6 +802,10 @@ OPENMM_EXPORT OpenMM_State* OpenMM_Context_getState(const OpenMM_Context* target
State result = reinterpret_cast<const Context*>(target)->getState(types, enforcePeriodicBox);
return reinterpret_cast<OpenMM_State*>(new State(result));
}
OPENMM_EXPORT OpenMM_State* OpenMM_Context_getState_2(const OpenMM_Context* target, int types, int enforcePeriodicBox, int groups) {
State result = reinterpret_cast<const Context*>(target)->getState(types, enforcePeriodicBox, groups);
return reinterpret_cast<OpenMM_State*>(new State(result));
}
OPENMM_EXPORT OpenMM_StringArray* OpenMM_Platform_loadPluginsFromDirectory(const char* directory) {
vector<string> result = Platform::loadPluginsFromDirectory(string(directory));
return reinterpret_cast<OpenMM_StringArray*>(new vector<string>(result));
......@@ -1314,6 +1319,14 @@ MODULE OpenMM
integer*4 enforcePeriodicBox
type(OpenMM_State) result
end subroutine
subroutine OpenMM_Context_getState_2(target, types, enforcePeriodicBox, groups, result)
use OpenMM_Types; implicit none
type (OpenMM_Context) target
integer*4 types
integer*4 enforcePeriodicBox
integer*4 groups
type(OpenMM_State) result
end subroutine
subroutine OpenMM_Platform_loadPluginsFromDirectory(directory, result)
use OpenMM_Types; implicit none
character(*) directory
......@@ -1991,6 +2004,12 @@ OPENMM_EXPORT void openmm_context_getstate_(const OpenMM_Context*& target, int c
OPENMM_EXPORT void OPENMM_CONTEXT_GETSTATE(const OpenMM_Context*& target, int const& types, int const& enforcePeriodicBox, OpenMM_State*& result) {
result = OpenMM_Context_getState(target, types, enforcePeriodicBox);
}
OPENMM_EXPORT void openmm_context_getstate_2_(const OpenMM_Context*& target, int const& types, int const& enforcePeriodicBox, int const& groups, OpenMM_State*& result) {
result = OpenMM_Context_getState_2(target, types, enforcePeriodicBox, groups);
}
OPENMM_EXPORT void OPENMM_CONTEXT_GETSTATE_2(const OpenMM_Context*& target, int const& types, int const& enforcePeriodicBox, int const& groups, OpenMM_State*& result) {
result = OpenMM_Context_getState_2(target, types, enforcePeriodicBox, groups);
}
OPENMM_EXPORT void openmm_platform_loadpluginsfromdirectory_(const char* directory, OpenMM_StringArray*& result, int length) {
result = OpenMM_Platform_loadPluginsFromDirectory(makeString(directory, length).c_str());
}
......
......@@ -20,7 +20,6 @@ set(STAGING_OUTPUT_FILES "") # Will contain all required package files
file(GLOB STAGING_INPUT_FILES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/MANIFEST.in"
"${CMAKE_CURRENT_SOURCE_DIR}/README.txt"
"${CMAKE_CURRENT_SOURCE_DIR}/filterPythonFiles.py"
)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/setup.py ${OPENMM_PYTHON_STAGING_DIR}/setup.py)
......@@ -51,6 +50,7 @@ foreach(SUBDIR ${SUBDIRS})
"${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}/*README.txt"
"${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}/*.py"
"${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}/*.i"
"${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}/*.ini"
"${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}/*.sh"
"${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}/*.xml"
"${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}/*.pdb"
......@@ -108,17 +108,9 @@ else(SWIG_EXECUTABLE)
set(SWIG_VERSION "0.0.0" CACHE STRING "Swig version" FORCE)
endif(SWIG_EXECUTABLE)
# Enforce swig version
if (PYTHON_VERSION_MAJOR EQUAL 3)
string(COMPARE LESS "${SWIG_VERSION}" "2.0.4" SWIG_VERSION_ERROR)
if(SWIG_VERSION_ERROR)
message("Swig version must be 2.0.4 or greater for Python 3! (You have ${SWIG_VERSION})")
endif(SWIG_VERSION_ERROR)
else(PYTHON_VERSION_MAJOR EQUAL 3)
string(COMPARE LESS "${SWIG_VERSION}" "1.3.39" SWIG_VERSION_ERROR)
if(SWIG_VERSION_ERROR)
message("Swig version must be 1.3.39 or greater for Python 2! (You have ${SWIG_VERSION})")
endif(SWIG_VERSION_ERROR)
endif(PYTHON_VERSION_MAJOR EQUAL 3)
if(SWIG_VERSION VERSION_LESS "3.0.5")
message(SEND_ERROR "Swig version must be 3.0.5 or greater! (You have ${SWIG_VERSION})")
endif(SWIG_VERSION VERSION_LESS "3.0.5")
find_package(Doxygen REQUIRED)
mark_as_advanced(CLEAR DOXYGEN_EXECUTABLE)
......@@ -185,7 +177,7 @@ add_custom_command(
COMMENT "Creating OpenMM Python swig input files..."
)
INSTALL_FILES(/include FILES "${SWIG_OPENMM_DIR}/OpenMMSwigHeaders.i")
INSTALL_FILES(/include/swig FILES "${SWIG_OPENMM_DIR}/OpenMMSwigHeaders.i" "${SWIG_OPENMM_DIR}/swig_lib/python/typemaps.i")
#~ swig -python -c++ \
#~ -outdir $PYTHON_PACKAGE_DIR \
......
from __future__ import print_function
import re
import sys
import textwrap
from inspect import cleandoc
from numpydoc.docscrape import NumpyDocString
# Doxygen does a bad job of generating documentation based on docstrings.
# This script is run as a filter
# on each file, and converts the docstrings into Doxygen style comments
# so we get better documentation.
input = open(sys.argv[1])
while True:
line = input.readline()
if len(line) == 0:
break
stripped = line.lstrip()
if stripped.startswith('def') or stripped.startswith('class'):
prefix = line[:len(line)-len(stripped)]
split = stripped.split()
if split[0] == 'class' and split[1][0].islower():
# Classes that start with a lowercase letter were defined by SWIG. We want to hide them.
print("%s## @private" % prefix, end='')
if split[1][0] == '_' and split[1][1] != '_':
# Names starting with a single _ are assumed to be private.
print("%s## @private" % prefix, end='')
# We're at the start of a class or function definition. Find all lines that contain the declaration.
declaration = line
while len(line) > 0 and line.find(':') == -1:
line = input.readline()
declaration += line
# Now look for a docstring.
docstringlines = []
line = input.readline()
l = line.strip()
if l.startswith('"""') or l.startswith("'''"):
if l.count('"""') == 2 or l.count("'''") == 2:
docstringlines.append(l[3:-3])
else:
docstringlines.append(l[3:])
line = input.readline()
l = line.strip()
while l.find('"""') == -1 and l.find("'''") == -1:
docstringlines.append(line.rstrip())
line = input.readline()
l = line.strip()
if line.rstrip().endswith('"""') or line.rstrip().endswith("'''"):
# last line
docstringlines.append(line.rstrip()[:-3])
docstring = NumpyDocString(cleandoc('\n'.join(docstringlines)))
# print(docstringlines)
# Print out the docstring in Doxygen syntax, followed by the declaration.
for line in docstring['Summary']:
print('{prefix}## {line}'.format(prefix=prefix, line=line))
if len(docstring['Extended Summary']) > 0:
print('{prefix}##'.format(prefix=prefix))
for line in docstring['Extended Summary']:
print('{prefix}## {line}'.format(prefix=prefix, line=line.strip()))
print('{prefix}##'.format(prefix=prefix))
for name, type, descr in docstring['Parameters']:
print('{prefix}## @param {name} ({type}) {descr}'.format(prefix=prefix, type=type, name=name, descr=' '.join(descr)))
for name, type, descr in docstring['Returns']:
if type == '':
type = name
print('{prefix}## @return ({type}) {descr}'.format(prefix=prefix, type=type, name=name, descr=' '.join(descr)))
print(declaration, end='')
if len(docstringlines) == 0:
print(line, end='')
else:
print(line, end='')
......@@ -8,11 +8,10 @@ Structures at Stanford, funded under the NIH Roadmap for Medical Research,
grant U54 GM072970. See https://simtk.org. This code was originally part of
the ParmEd program and was ported for use with OpenMM.
Copyright (c) 2014-2015 the Authors
Copyright (c) 2014-2016 the Authors
Author: Jason M. Swails
Contributors:
Date: August 19, 2014
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
......@@ -32,9 +31,7 @@ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
from __future__ import division
from __future__ import absolute_import
from __future__ import print_function
from __future__ import division, absolute_import, print_function
from functools import wraps
from math import pi, cos, sin, sqrt
......@@ -47,7 +44,7 @@ import simtk.unit as u
from simtk.openmm.app import (forcefield as ff, Topology, element)
from simtk.openmm.app.amberprmtopfile import HCT, OBC1, OBC2, GBn, GBn2
from simtk.openmm.app.internal.customgbforces import (GBSAHCTForce,
GBSAOBC1Force, GBSAOBC2Force, GBSAGBnForce, GBSAGBn2Force, convertParameters)
GBSAOBC1Force, GBSAOBC2Force, GBSAGBnForce, GBSAGBn2Force)
from simtk.openmm.app.internal.unitcell import computePeriodicBoxVectors
# CHARMM imports
from simtk.openmm.app.internal.charmm.topologyobjects import (
......@@ -113,7 +110,7 @@ def _strip_optunit(thing, unit):
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
_resre = re.compile(r'(\d+)([a-zA-Z]*)')
_resre = re.compile(r'(-?\d+)([a-zA-Z]*)')
class CharmmPsfFile(object):
"""A chemical structure instantiated from CHARMM files.
......@@ -666,88 +663,6 @@ class CharmmPsfFile(object):
return topology
def _get_gb_params(self, gb_model=HCT):
""" Gets the GB parameters. Need this method to special-case GB neck """
screen = [0 for atom in self.atom_list]
if gb_model is GBn:
radii = _bondi_radii(self.atom_list)
screen = [0.5 for atom in self.atom_list]
for i, atom in enumerate(self.atom_list):
if atom.type.atomic_number == 6:
screen[i] = 0.48435382330
elif atom.type.atomic_number == 1:
screen[i] = 1.09085413633
elif atom.type.atomic_number == 7:
screen[i] = 0.700147318409
elif atom.type.atomic_number == 8:
screen[i] = 1.06557401132
elif atom.type.atomic_number == 16:
screen[i] = 0.602256336067
elif gb_model is GBn2:
radii = _mbondi3_radii(self.atom_list)
# Add non-optimized values as defaults
alpha = [1.0 for i in self.atom_list]
beta = [0.8 for i in self.atom_list]
gamma = [4.85 for i in self.atom_list]
screen = [0.5 for i in self.atom_list]
for i, atom in enumerate(self.atom_list):
if atom.type.atomic_number == 6:
screen[i] = 1.058554
alpha[i] = 0.733756
beta[i] = 0.506378
gamma[i] = 0.205844
elif atom.type.atomic_number == 1:
screen[i] = 1.425952
alpha[i] = 0.788440
beta[i] = 0.798699
gamma[i] = 0.437334
elif atom.type.atomic_number == 7:
screen[i] = 0.733599
alpha[i] = 0.503364
beta[i] = 0.316828
gamma[i] = 0.192915
elif atom.type.atomic_number == 8:
screen[i] = 1.061039
alpha[i] = 0.867814
beta[i] = 0.876635
gamma[i] = 0.387882
elif atom.type.atomic_number == 16:
screen[i] = -0.703469
alpha[i] = 0.867814
beta[i] = 0.876635
gamma[i] = 0.387882
else:
# Set the default screening parameters
for i, atom in enumerate(self.atom_list):
if atom.type.atomic_number == 1:
screen[i] = 0.85
elif atom.type.atomic_number == 6:
screen[i] = 0.72
elif atom.type.atomic_number == 7:
screen[i] = 0.79
elif atom.type.atomic_number == 8:
screen[i] = 0.85
elif atom.type.atomic_number == 9:
screen[i] = 0.88
elif atom.type.atomic_number == 15:
screen[i] = 0.86
elif atom.type.atomic_number == 16:
screen[i] = 0.96
else:
screen[i] = 0.8
# Determine which radii set we need
if gb_model is OBC1 or gb_model is OBC2:
radii = _mbondi2_radii(self.atom_list)
elif gb_model is HCT:
radii = _mbondi_radii(self.atom_list)
length_conv = u.angstrom.conversion_factor_to(u.nanometer)
radii = [x * length_conv for x in radii]
if gb_model is GBn2:
return zip(radii, screen, alpha, beta, gamma)
return zip(radii, screen)
def createSystem(self, params, nonbondedMethod=ff.NoCutoff,
nonbondedCutoff=1.0*u.nanometer,
switchDistance=0.0*u.nanometer,
......@@ -1249,9 +1164,6 @@ class CharmmPsfFile(object):
# Add GB model if we're doing one
if implicitSolvent is not None:
if verbose: print('Adding GB parameters...')
gb_parms = self._get_gb_params(implicitSolvent)
gb_parms = convertParameters(gb_parms, str(implicitSolvent))
# If implicitSolventKappa is None, compute it from salt
# concentration
if implicitSolventKappa is None:
......@@ -1290,6 +1202,7 @@ class CharmmPsfFile(object):
elif implicitSolvent is GBn2:
gb = GBSAGBn2Force(solventDielectric, soluteDielectric, None,
cutoff, kappa=implicitSolventKappa)
gb_parms = gb.getStandardParameters(self.topology)
for atom, gb_parm in zip(self.atom_list, gb_parms):
gb.addParticle([atom.charge] + list(gb_parm))
# Set cutoff method
......
......@@ -220,6 +220,7 @@
<Variant name="HID"/>
<Variant name="HIE"/>
<Variant name="HIP"/>
<Variant name="HIN"/>
<H name="H" parent="N"/>
<H name="H2" parent="N" terminal="N"/>
<H name="H3" parent="N" terminal="N"/>
......
......@@ -88,8 +88,7 @@ class DCDReporter(object):
if self._dcd is None:
self._dcd = DCDFile(self._out, simulation.topology, simulation.integrator.getStepSize(), 0, self._reportInterval)
a,b,c = state.getPeriodicBoxVectors()
self._dcd.writeModel(state.getPositions(), mm.Vec3(a[0].value_in_unit(nanometer), b[1].value_in_unit(nanometer), c[2].value_in_unit(nanometer))*nanometer)
self._dcd.writeModel(state.getPositions(), periodicBoxVectors=state.getPeriodicBoxVectors())
def __del__(self):
self._out.close()
......@@ -6,7 +6,7 @@ Simbios, the NIH National Center for Physics-Based Simulation of
Biological Structures at Stanford, funded under the NIH Roadmap for
Medical Research, grant U54 GM072970. See https://simtk.org.
Portions copyright (c) 2012-2015 Stanford University and the Authors.
Portions copyright (c) 2012-2016 Stanford University and the Authors.
Authors: Peter Eastman, Mark Friedrichs
Contributors:
......@@ -28,8 +28,8 @@ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
from __future__ import absolute_import
from __future__ import print_function
from __future__ import absolute_import, print_function
__author__ = "Peter Eastman"
__version__ = "1.0"
......@@ -38,6 +38,8 @@ import itertools
import xml.etree.ElementTree as etree
import math
from math import sqrt, cos
from copy import deepcopy
from heapq import heappush, heappop
import simtk.openmm as mm
import simtk.unit as unit
from . import element as elem
......@@ -115,81 +117,181 @@ class ForceField(object):
"""
self._atomTypes = {}
self._templates = {}
self._patches = {}
self._templatePatches = {}
self._templateSignatures = {None:[]}
self._atomClasses = {'':set()}
self._forces = []
self._scripts = []
for file in files:
self.loadFile(file)
self._templateGenerators = []
self.loadFile(files)
def loadFile(self, file):
"""Load an XML file and add the definitions from it to this FieldField.
def loadFile(self, files):
"""Load an XML file and add the definitions from it to this ForceField.
Parameters
----------
file : string or file
An XML file containing force field definitions. It may be either an
absolute file path, a path relative to the current working
files : string or file or tuple
An XML file or tuple of XML files containing force field definitions.
Each entry may be either an absolute file path, a path relative to the current working
directory, a path relative to this module's data subdirectory (for
built in force fields), or an open file-like object with a read()
method from which the forcefield XML data can be loaded.
"""
try:
# this handles either filenames or open file-like objects
tree = etree.parse(file)
except IOError:
tree = etree.parse(os.path.join(os.path.dirname(__file__), 'data', file))
root = tree.getroot()
if not isinstance(files, tuple):
files = (files,)
trees = []
for file in files:
try:
# this handles either filenames or open file-like objects
tree = etree.parse(file)
except IOError:
tree = etree.parse(os.path.join(os.path.dirname(__file__), 'data', file))
except Exception as e:
# Fail with an error message about which file could not be read.
# TODO: Also handle case where fallback to 'data' directory encounters problems,
# but this is much less worrisome because we control those files.
msg = str(e) + '\n'
if hasattr(file, 'name'):
filename = file.name
else:
filename = str(file)
msg += "ForceField.loadFile() encountered an error reading file '%s'\n" % filename
raise Exception(msg)
trees.append(tree)
# Load the atom types.
if tree.getroot().find('AtomTypes') is not None:
for type in tree.getroot().find('AtomTypes').findall('Type'):
self.registerAtomType(type.attrib)
for tree in trees:
if tree.getroot().find('AtomTypes') is not None:
for type in tree.getroot().find('AtomTypes').findall('Type'):
self.registerAtomType(type.attrib)
# Load the residue templates.
if tree.getroot().find('Residues') is not None:
for residue in root.find('Residues').findall('Residue'):
resName = residue.attrib['name']
template = ForceField._TemplateData(resName)
atomIndices = {}
for atom in residue.findall('Atom'):
params = {}
for key in atom.attrib:
if key not in ('name', 'type'):
params[key] = _convertParameterToNumber(atom.attrib[key])
atomName = atom.attrib['name']
if atomName in atomIndices:
raise ValueError('Residue '+resName+' contains multiple atoms named '+atomName)
atomIndices[atomName] = len(template.atoms)
template.atoms.append(ForceField._TemplateAtomData(atomName, atom.attrib['type'], self._atomTypes[atom.attrib['type']][2], params))
for site in residue.findall('VirtualSite'):
template.virtualSites.append(ForceField._VirtualSiteData(site, atomIndices))
for bond in residue.findall('Bond'):
if 'atomName1' in bond.attrib:
template.addBond(atomIndices[bond.attrib['atomName1']], atomIndices[bond.attrib['atomName2']])
else:
template.addBond(int(bond.attrib['from']), int(bond.attrib['to']))
for bond in residue.findall('ExternalBond'):
if 'atomName' in bond.attrib:
b = atomIndices[bond.attrib['atomName']]
for tree in trees:
if tree.getroot().find('Residues') is not None:
for residue in tree.getroot().find('Residues').findall('Residue'):
resName = residue.attrib['name']
template = ForceField._TemplateData(resName)
if 'override' in residue.attrib:
template.overrideLevel = int(residue.attrib['override'])
atomIndices = {}
for atom in residue.findall('Atom'):
params = {}
for key in atom.attrib:
if key not in ('name', 'type'):
params[key] = _convertParameterToNumber(atom.attrib[key])
atomName = atom.attrib['name']
if atomName in atomIndices:
raise ValueError('Residue '+resName+' contains multiple atoms named '+atomName)
atomIndices[atomName] = len(template.atoms)
typeName = atom.attrib['type']
template.atoms.append(ForceField._TemplateAtomData(atomName, typeName, self._atomTypes[typeName].element, params))
for site in residue.findall('VirtualSite'):
template.virtualSites.append(ForceField._VirtualSiteData(site, atomIndices))
for bond in residue.findall('Bond'):
if 'atomName1' in bond.attrib:
template.addBondByName(bond.attrib['atomName1'], bond.attrib['atomName2'])
else:
template.addBond(int(bond.attrib['from']), int(bond.attrib['to']))
for bond in residue.findall('ExternalBond'):
if 'atomName' in bond.attrib:
template.addExternalBondByName(bond.attrib['atomName'])
else:
template.addExternalBond(int(bond.attrib['from']))
for patch in residue.findall('AllowPatch'):
patchName = patch.attrib['name']
if ':' in patchName:
colonIndex = patchName.find(':')
self.registerTemplatePatch(resName, patchName[:colonIndex], int(patchName[colonIndex+1:])-1)
else:
self.registerTemplatePatch(resName, patchName, 0)
self.registerResidueTemplate(template)
# Load the patch defintions.
for tree in trees:
if tree.getroot().find('Patches') is not None:
for patch in tree.getroot().find('Patches').findall('Patch'):
patchName = patch.attrib['name']
if 'residues' in patch.attrib:
numResidues = int(patch.attrib['residues'])
else:
b = int(bond.attrib['from'])
template.externalBonds.append(b)
template.atoms[b].externalBonds += 1
self.registerResidueTemplate(template)
numResidues = 1
patchData = ForceField._PatchData(patchName, numResidues)
allAtomNames = set()
for atom in patch.findall('AddAtom'):
params = {}
for key in atom.attrib:
if key not in ('name', 'type'):
params[key] = _convertParameterToNumber(atom.attrib[key])
atomName = atom.attrib['name']
if atomName in allAtomNames:
raise ValueError('Patch '+patchName+' contains multiple atoms named '+atomName)
allAtomNames.add(atomName)
atomDescription = ForceField._PatchAtomData(atomName)
typeName = atom.attrib['type']
patchData.addedAtoms[atomDescription.residue].append(ForceField._TemplateAtomData(atomDescription.name, typeName, self._atomTypes[typeName].element, params))
for atom in patch.findall('ChangeAtom'):
params = {}
for key in atom.attrib:
if key not in ('name', 'type'):
params[key] = _convertParameterToNumber(atom.attrib[key])
atomName = atom.attrib['name']
if atomName in allAtomNames:
raise ValueError('Patch '+patchName+' contains multiple atoms named '+atomName)
allAtomNames.add(atomName)
atomDescription = ForceField._PatchAtomData(atomName)
typeName = atom.attrib['type']
patchData.changedAtoms[atomDescription.residue].append(ForceField._TemplateAtomData(atomDescription.name, typeName, self._atomTypes[typeName].element, params))
for atom in patch.findall('RemoveAtom'):
atomName = atom.attrib['name']
if atomName in allAtomNames:
raise ValueError('Patch '+patchName+' contains multiple atoms named '+atomName)
allAtomNames.add(atomName)
atomDescription = ForceField._PatchAtomData(atomName)
patchData.deletedAtoms.append(atomDescription)
for bond in patch.findall('AddBond'):
atom1 = ForceField._PatchAtomData(bond.attrib['atomName1'])
atom2 = ForceField._PatchAtomData(bond.attrib['atomName2'])
patchData.addedBonds.append((atom1, atom2))
for bond in patch.findall('RemoveBond'):
atom1 = ForceField._PatchAtomData(bond.attrib['atomName1'])
atom2 = ForceField._PatchAtomData(bond.attrib['atomName2'])
patchData.deletedBonds.append((atom1, atom2))
for bond in patch.findall('AddExternalBond'):
atom = ForceField._PatchAtomData(bond.attrib['atomName'])
patchData.addedExternalBonds.append(atom)
for bond in patch.findall('RemoveExternalBond'):
atom = ForceField._PatchAtomData(bond.attrib['atomName'])
patchData.deletedExternalBonds.append(atom)
for residue in patch.findall('ApplyToResidue'):
name = residue.attrib['name']
if ':' in name:
colonIndex = name.find(':')
self.registerTemplatePatch(name[colonIndex+1:], patchName, int(name[:colonIndex])-1)
else:
self.registerTemplatePatch(name, patchName, 0)
self.registerPatch(patchData)
# Load force definitions
for child in root:
if child.tag in parsers:
parsers[child.tag](child, self)
for tree in trees:
for child in tree.getroot():
if child.tag in parsers:
parsers[child.tag](child, self)
# Load scripts
for node in tree.getroot().findall('Script'):
self.registerScript(node.text)
for tree in trees:
for node in tree.getroot().findall('Script'):
self.registerScript(node.text)
def getGenerators(self):
"""Get the list of all registered generators."""
......@@ -211,7 +313,7 @@ class ForceField(object):
element = parameters['element']
if not isinstance(element, elem.Element):
element = elem.get_by_symbol(element)
self._atomTypes[name] = (atomClass, mass, element)
self._atomTypes[name] = ForceField._AtomType(name, atomClass, mass, element)
if atomClass in self._atomClasses:
typeSet = self._atomClasses[atomClass]
else:
......@@ -222,6 +324,23 @@ class ForceField(object):
def registerResidueTemplate(self, template):
"""Register a new residue template."""
if template.name in self._templates:
# There is already a template with this name, so check the override levels.
existingTemplate = self._templates[template.name]
if template.overrideLevel < existingTemplate.overrideLevel:
# The existing one takes precedence, so just return.
return
if template.overrideLevel > existingTemplate.overrideLevel:
# We need to delete the existing template.
del self._templates[template.name]
existingSignature = _createResidueSignature([atom.element for atom in existingTemplate.atoms])
self._templateSignatures[existingSignature].remove(existingTemplate)
else:
raise ValueError('Residue template %s with the same override level %d already exists.' % (template.name, template.overrideLevel))
# Register the template.
self._templates[template.name] = template
signature = _createResidueSignature([atom.element for atom in template.atoms])
if signature in self._templateSignatures:
......@@ -229,12 +348,80 @@ class ForceField(object):
else:
self._templateSignatures[signature] = [template]
def registerPatch(self, patch):
"""Register a new patch that can be applied to templates."""
self._patches[patch.name] = patch
def registerTemplatePatch(self, residue, patch, patchResidueIndex):
"""Register that a particular patch can be used with a particular residue."""
if residue not in self._templatePatches:
self._templatePatches[residue] = []
self._templatePatches[residue].append((patch, patchResidueIndex))
def registerScript(self, script):
"""Register a new script to be executed after building the System."""
self._scripts.append(script)
def registerTemplateGenerator(self, generator):
"""Register a residue template generator that can be used to parameterize residues that do not match existing forcefield templates.
This functionality can be used to add handlers to parameterize small molecules or unnatural/modified residues.
.. CAUTION:: This method is experimental, and its API is subject to change.
Parameters
----------
generator : function
A function that will be called when a residue is encountered that does not match an existing forcefield template.
When a residue without a template is encountered, the `generator` function is called with:
::
success = generator(forcefield, residue)
```
where `forcefield` is the calling `ForceField` object and `residue` is a simtk.openmm.app.topology.Residue object.
`generator` must conform to the following API:
::
Parameters
----------
forcefield : simtk.openmm.app.ForceField
The ForceField object to which residue templates and/or parameters are to be added.
residue : simtk.openmm.app.Topology.Residue
The residue topology for which a template is to be generated.
Returns
-------
success : bool
If the generator is able to successfully parameterize the residue, `True` is returned.
If the generator cannot parameterize the residue, it should return `False` and not modify `forcefield`.
The generator should either register a residue template directly with `forcefield.registerResidueTemplate(template)`
or it should call `forcefield.loadFile(file)` to load residue definitions from an ffxml file.
It can also use the `ForceField` programmatic API to add additional atom types (via `forcefield.registerAtomType(parameters)`)
or additional parameters.
"""
self._templateGenerators.append(generator)
def _findAtomTypes(self, attrib, num):
"""Parse the attributes on an XML tag to find the set of atom types for each atom it involves."""
"""Parse the attributes on an XML tag to find the set of atom types for each atom it involves.
Parameters
----------
attrib : dict of attributes
The dictionary of attributes for an XML parameter tag.
num : int
The number of atom specifiers (e.g. 'class1' through 'class4') to extract.
Returns
-------
types : list
A list of atom types that match.
"""
types = []
for i in range(num):
if num == 1:
......@@ -250,11 +437,15 @@ class ForceField(object):
types.append(None) # Unknown atom class
else:
types.append(self._atomClasses[attrib[classAttrib]])
else:
if typeAttrib not in attrib or attrib[typeAttrib] not in self._atomTypes:
elif typeAttrib in attrib:
if attrib[typeAttrib] == '':
types.append(self._atomClasses[''])
elif attrib[typeAttrib] not in self._atomTypes:
types.append(None) # Unknown atom type
else:
types.append([attrib[typeAttrib]])
else:
types.append(None) # Unknown atom type
return types
def _parseTorsion(self, attrib):
......@@ -271,7 +462,7 @@ class ForceField(object):
index += 1
return torsion
class _SystemData:
class _SystemData(object):
"""Inner class used to encapsulate data about the system being created."""
def __init__(self):
self.atomType = {}
......@@ -296,8 +487,18 @@ class ForceField(object):
else:
self.constraints[key] = distance
system.addConstraint(atom1, atom2, distance)
class _TemplateData:
def recordMatchedAtomParameters(self, residue, template, matches):
"""Record parameters for atoms based on having matched a residue to a template."""
matchAtoms = dict(zip(matches, residue.atoms()))
for atom, match in zip(residue.atoms(), matches):
self.atomType[atom] = template.atoms[match].type
self.atomParameters[atom] = template.atoms[match].parameters
for site in template.virtualSites:
if match == site.index:
self.virtualSites[atom] = (site, [matchAtoms[i].index for i in site.atoms], matchAtoms[site.excludeWith].index)
class _TemplateData(object):
"""Inner class used to encapsulate data about a residue template definition."""
def __init__(self, name):
self.name = name
......@@ -305,13 +506,42 @@ class ForceField(object):
self.virtualSites = []
self.bonds = []
self.externalBonds = []
self.overrideLevel = 0
def getAtomIndexByName(self, atom_name):
"""Look up an atom index by atom name, providing a helpful error message if not found."""
for (index, atom) in enumerate(self.atoms):
if atom.name == atom_name:
return index
# Provide a helpful error message if atom name not found.
msg = "Atom name '%s' not found in residue template '%s'.\n" % (atom_name, self.name)
msg += "Possible atom names are: %s" % str(atomIndices.keys())
raise ValueError(msg)
def addBond(self, atom1, atom2):
"""Add a bond between two atoms in a template given their indices in the template."""
self.bonds.append((atom1, atom2))
self.atoms[atom1].bondedTo.append(atom2)
self.atoms[atom2].bondedTo.append(atom1)
class _TemplateAtomData:
def addBondByName(self, atom1_name, atom2_name):
"""Add a bond between two atoms in a template given their atom names."""
atom1 = self.getAtomIndexByName(atom1_name)
atom2 = self.getAtomIndexByName(atom2_name)
self.addBond(atom1, atom2)
def addExternalBond(self, atom_index):
"""Designate that an atom in a residue template has an external bond, using atom index within template."""
self.externalBonds.append(atom_index)
self.atoms[atom_index].externalBonds += 1
def addExternalBondByName(self, atom_name):
"""Designate that an atom in a residue template has an external bond, using atom name within template."""
atom = self.getAtomIndexByName(atom_name)
self.addExternalBond(atom)
class _TemplateAtomData(object):
"""Inner class used to encapsulate data about an atom in a residue template definition."""
def __init__(self, name, type, element, parameters={}):
self.name = name
......@@ -321,7 +551,7 @@ class ForceField(object):
self.bondedTo = []
self.externalBonds = 0
class _BondData:
class _BondData(object):
"""Inner class used to encapsulate data about a bond."""
def __init__(self, atom1, atom2):
self.atom1 = atom1
......@@ -329,7 +559,7 @@ class ForceField(object):
self.isConstrained = False
self.length = 0.0
class _VirtualSiteData:
class _VirtualSiteData(object):
"""Inner class used to encapsulate data about a virtual site."""
def __init__(self, node, atomIndices):
attrib = node.attrib
......@@ -362,7 +592,101 @@ class ForceField(object):
else:
self.excludeWith = self.atoms[0]
class _AtomTypeParameters:
class _PatchData(object):
"""Inner class used to encapsulate data about a patch definition."""
def __init__(self, name, numResidues):
self.name = name
self.numResidues = numResidues
self.addedAtoms = [[] for i in range(numResidues)]
self.changedAtoms = [[] for i in range(numResidues)]
self.deletedAtoms = []
self.addedBonds = []
self.deletedBonds = []
self.addedExternalBonds = []
self.deletedExternalBonds = []
def createPatchedTemplates(self, templates):
"""Apply this patch to a set of templates, creating new modified ones."""
if len(templates) != self.numResidues:
raise ValueError("Patch '%s' expected %d templates, received %d", (self.name, self.numResidues, len(templates)))
# Construct a new version of each template.
newTemplates = []
for index, template in enumerate(templates):
newTemplate = ForceField._TemplateData("%s-%s" % (template.name, self.name))
newTemplates.append(newTemplate)
# Build the list of atoms in it.
for atom in template.atoms:
if not any(deleted.name == atom.name and deleted.residue == index for deleted in self.deletedAtoms):
newTemplate.atoms.append(ForceField._TemplateAtomData(atom.name, atom.type, atom.element, atom.parameters))
for atom in self.addedAtoms[index]:
if any(a.name == atom.name for a in newTemplate.atoms):
raise ValueError("Patch '%s' adds an atom with the same name as an existing atom: %s" % (self.name, atom.name))
newTemplate.atoms.append(ForceField._TemplateAtomData(atom.name, atom.type, atom.element, atom.parameters))
oldAtomIndex = dict([(atom.name, i) for i, atom in enumerate(template.atoms)])
newAtomIndex = dict([(atom.name, i) for i, atom in enumerate(newTemplate.atoms)])
for atom in self.changedAtoms[index]:
if atom.name not in newAtomIndex:
raise ValueError("Patch '%s' modifies nonexistent atom '%s' in template '%s'" % (self.name, atom.name, template.name))
newTemplate.atoms[newAtomIndex[atom.name]] = ForceField._TemplateAtomData(atom.name, atom.type, atom.element, atom.parameters)
# Copy over the virtual sites, translating the atom indices.
indexMap = dict([(oldAtomIndex[name], newAtomIndex[name]) for name in newAtomIndex if name in oldAtomIndex])
for site in template.virtualSites:
if site.index in indexMap and all(i in indexMap for i in site.atoms):
newSite = deepcopy(site)
newSite.index = indexMap[site.index]
newSite.atoms = [indexMap[i] for i in site.atoms]
newTemplate.virtualSites.append(newSite)
# Build the lists of bonds and external bonds.
atomMap = dict([(template.atoms[i], indexMap[i]) for i in indexMap])
deletedBonds = [(atom1.name, atom2.name) for atom1, atom2 in self.deletedBonds if atom1.residue == index and atom2.residue == index]
for atom1, atom2 in template.bonds:
a1 = template.atoms[atom1]
a2 = template.atoms[atom2]
if a1 in atomMap and a2 in atomMap and (a1.name, a2.name) not in deletedBonds and (a2.name, a1.name) not in deletedBonds:
newTemplate.addBond(atomMap[a1], atomMap[a2])
deletedExternalBonds = [atom.name for atom in self.deletedExternalBonds if atom.residue == index]
for atom in template.externalBonds:
if template.atoms[atom].name not in deletedExternalBonds:
newTemplate.addExternalBond(indexMap[atom])
for atom1, atom2 in self.addedBonds:
if atom1.residue == index and atom2.residue == index:
newTemplate.addBondByName(atom1.name, atom2.name)
elif atom1.residue == index:
newTemplate.addExternalBondByName(atom1.name)
elif atom2.residue == index:
newTemplate.addExternalBondByName(atom2.name)
for atom in self.addedExternalBonds:
newTemplate.addExternalBondByName(atom.name)
return newTemplates
class _PatchAtomData(object):
"""Inner class used to encapsulate data about an atom in a patch definition."""
def __init__(self, description):
if ':' in description:
colonIndex = description.find(':')
self.residue = int(description[:colonIndex])-1
self.name = description[colonIndex+1:]
else:
self.residue = 0
self.name = description
class _AtomType(object):
"""Inner class used to record atom types and associated properties."""
def __init__(self, name, atomClass, mass, element):
self.name = name
self.atomClass = atomClass
self.mass = mass
self.element = element
class _AtomTypeParameters(object):
"""Inner class used to record parameter values for atom types."""
def __init__(self, forcefield, forceName, atomTag, paramNames):
self.ff = forcefield
......@@ -433,8 +757,174 @@ class ForceField(object):
raise ValueError('%s: No parameters defined for atom type %s' % (self.forceName, t))
def _getResidueTemplateMatches(self, res, bondedToAtom, templateSignatures=None):
"""Return the residue template matches, or None if none are found.
Parameters
----------
res : Topology.Residue
The residue for which template matches are to be retrieved.
bondedToAtom : list of set of int
bondedToAtom[i] is the set of atoms bonded to atom index i
Returns
-------
template : _ForceFieldTemplate
The matching forcefield residue template, or None if no matches are found.
matches : list
a list specifying which atom of the template each atom of the residue
corresponds to, or None if it does not match the template
"""
template = None
matches = None
if templateSignatures is None:
templateSignatures = self._templateSignatures
signature = _createResidueSignature([atom.element for atom in res.atoms()])
if signature in templateSignatures:
allMatches = []
for t in templateSignatures[signature]:
match = _matchResidue(res, t, bondedToAtom)
if match is not None:
allMatches.append((t, match))
if len(allMatches) == 1:
template = allMatches[0][0]
matches = allMatches[0][1]
elif len(allMatches) > 1:
raise Exception('Multiple matching templates found for residue %d (%s).' % (res.index+1, res.name))
return [template, matches]
def _buildBondedToAtomList(self, topology):
"""Build a list of which atom indices are bonded to each atom.
Parameters
----------
topology : Topology
The Topology whose bonds are to be indexed.
Returns
-------
bondedToAtom : list of set of int
bondedToAtom[index] is the set of atom indices bonded to atom `index`
"""
bondedToAtom = []
for atom in topology.atoms():
bondedToAtom.append(set())
for (atom1, atom2) in topology.bonds():
bondedToAtom[atom1.index].add(atom2.index)
bondedToAtom[atom2.index].add(atom1.index)
return bondedToAtom
def getUnmatchedResidues(self, topology):
"""Return a list of Residue objects from specified topology for which no forcefield templates are available.
.. CAUTION:: This method is experimental, and its API is subject to change.
Parameters
----------
topology : Topology
The Topology whose residues are to be checked against the forcefield residue templates.
Returns
-------
unmatched_residues : list of Residue
List of Residue objects from `topology` for which no forcefield residue templates are available.
Note that multiple instances of the same residue appearing at different points in the topology may be returned.
This method may be of use in generating missing residue templates or diagnosing parameterization failures.
"""
# Find the template matching each residue, compiling a list of residues for which no templates are available.
bondedToAtom = self._buildBondedToAtomList(topology)
unmatched_residues = list() # list of unmatched residues
for res in topology.residues():
# Attempt to match one of the existing templates.
[template, matches] = self._getResidueTemplateMatches(res, bondedToAtom)
if matches is None:
# No existing templates match.
unmatched_residues.append(res)
return unmatched_residues
def getMatchingTemplates(self, topology):
"""Return a list of forcefield residue templates matching residues in the specified topology.
.. CAUTION:: This method is experimental, and its API is subject to change.
Parameters
----------
topology : Topology
The Topology whose residues are to be checked against the forcefield residue templates.
Returns
-------
templates : list of _TemplateData
List of forcefield residue templates corresponding to residues in the topology.
templates[index] is template corresponding to residue `index` in topology.residues()
This method may be of use in debugging issues related to parameter assignment.
"""
# Find the template matching each residue, compiling a list of residues for which no templates are available.
bondedToAtom = self._buildBondedToAtomList(topology)
templates = list() # list of templates matching the corresponding residues
for res in topology.residues():
# Attempt to match one of the existing templates.
[template, matches] = self._getResidueTemplateMatches(res, bondedToAtom)
# Raise an exception if we have found no templates that match.
if matches is None:
raise ValueError('No template found for residue %d (%s). %s' % (res.index+1, res.name, _findMatchErrors(self, res)))
else:
templates.append(template)
return templates
def generateTemplatesForUnmatchedResidues(self, topology):
"""Generate forcefield residue templates for residues in specified topology for which no forcefield templates are available.
.. CAUTION:: This method is experimental, and its API is subject to change.
Parameters
----------
topology : Topology
The Topology whose residues are to be checked against the forcefield residue templates.
Returns
-------
templates : list of _TemplateData
List of forcefield residue templates corresponding to residues in `topology` for which no forcefield templates are currently available.
Atom types will be set to `None`, but template name, atom names, elements, and connectivity will be taken from corresponding Residue objects.
residues : list of Residue
List of Residue objects that were used to generate the templates.
`residues[index]` is the Residue that was used to generate the template `templates[index]`
"""
# Get a non-unique list of unmatched residues.
unmatched_residues = self.getUnmatchedResidues(topology)
# Generate a unique list of unmatched residues by comparing fingerprints.
bondedToAtom = self._buildBondedToAtomList(topology)
unique_unmatched_residues = list() # list of unique unmatched Residue objects from topology
templates = list() # corresponding _TemplateData templates
signatures = set()
for residue in unmatched_residues:
signature = _createResidueSignature([ atom.element for atom in residue.atoms() ])
template = _createResidueTemplate(residue)
is_unique = True
if signature in signatures:
# Signature is the same as an existing residue; check connectivity.
for check_residue in unique_unmatched_residues:
matches = _matchResidue(check_residue, template, bondedToAtom)
if matches is not None:
is_unique = False
if is_unique:
# Residue is unique.
unique_unmatched_residues.append(residue)
signatures.add(signature)
templates.append(template)
return [templates, unique_unmatched_residues]
def createSystem(self, topology, nonbondedMethod=NoCutoff, nonbondedCutoff=1.0*unit.nanometer,
constraints=None, rigidWater=True, removeCMMotion=True, hydrogenMass=None, **args):
constraints=None, rigidWater=True, removeCMMotion=True, hydrogenMass=None, residueTemplates=dict(), **args):
"""Construct an OpenMM System representing a Topology with this force field.
Parameters
......@@ -458,6 +948,13 @@ class ForceField(object):
The mass to use for hydrogen atoms bound to heavy atoms. Any mass
added to a hydrogen is subtracted from the heavy atom to keep
their total mass the same.
residueTemplates : dict=dict()
Key: Topology Residue object
Value: string, name of _TemplateData residue template object to use for
(Key) residue
This allows user to specify which template to apply to particular Residues
in the event that multiple matching templates are available (e.g Fe2+ and Fe3+
templates in the ForceField for a monoatomic iron ion in the topology).
args
Arbitrary additional keyword arguments may also be specified.
This allows extra parameters to be specified that are specific to
......@@ -493,36 +990,75 @@ class ForceField(object):
# Find the template matching each residue and assign atom types.
unmatchedResidues = []
for chain in topology.chains():
for res in chain.residues():
template = None
matches = None
signature = _createResidueSignature([atom.element for atom in res.atoms()])
if signature in self._templateSignatures:
for t in self._templateSignatures[signature]:
matches = _matchResidue(res, t, bondedToAtom)
if matches is not None:
template = t
break
if res in residueTemplates:
tname = residueTemplates[res]
template = self._templates[tname]
matches = _matchResidue(res, template, bondedToAtom)
if matches is None:
raise Exception('User-supplied template %s does not match the residue %d (%s)' % (tname, res.index+1, res.name))
else:
# Attempt to match one of the existing templates.
[template, matches] = self._getResidueTemplateMatches(res, bondedToAtom)
if matches is None:
raise ValueError('No template found for residue %d (%s). %s' % (res.index+1, res.name, _findMatchErrors(self, res)))
matchAtoms = dict(zip(matches, res.atoms()))
for atom, match in zip(res.atoms(), matches):
data.atomType[atom] = template.atoms[match].type
data.atomParameters[atom] = template.atoms[match].parameters
for site in template.virtualSites:
if match == site.index:
data.virtualSites[atom] = (site, [matchAtoms[i].index for i in site.atoms], matchAtoms[site.excludeWith].index)
unmatchedResidues.append(res)
else:
data.recordMatchedAtomParameters(res, template, matches)
# Try to apply patches to find matches for any unmatched residues.
if len(unmatchedResidues) > 0:
unmatchedResidues = _applyPatchesToMatchResidues(self, data, unmatchedResidues, bondedToAtom)
# If we still haven't found a match for a residue, attempt to use residue template generators to create
# new templates (and potentially atom types/parameters).
for res in unmatchedResidues:
# A template might have been generated on an earlier iteration of this loop.
[template, matches] = self._getResidueTemplateMatches(res, bondedToAtom)
if matches is None:
# Try all generators.
for generator in self._templateGenerators:
if generator(self, res):
# This generator has registered a new residue template that should match.
[template, matches] = self._getResidueTemplateMatches(res, bondedToAtom)
if matches is None:
# Something went wrong because the generated template does not match the residue signature.
raise Exception('The residue handler %s indicated it had correctly parameterized residue %s, but the generated template did not match the residue signature.' % (generator.__class__.__name__, str(res)))
else:
# We successfully generated a residue template. Break out of the for loop.
break
if matches is None:
raise ValueError('No template found for residue %d (%s). %s' % (res.index+1, res.name, _findMatchErrors(self, res)))
else:
data.recordMatchedAtomParameters(res, template, matches)
# Create the System and add atoms
sys = mm.System()
for atom in topology.atoms():
sys.addParticle(self._atomTypes[data.atomType[atom]][1])
# Look up the atom type name, returning a helpful error message if it cannot be found.
if atom not in data.atomType:
raise Exception("Could not identify atom type for atom '%s'." % str(atom))
typename = data.atomType[atom]
# Look up the type name in the list of registered atom types, returning a helpful error message if it cannot be found.
if typename not in self._atomTypes:
msg = "Could not find typename '%s' for atom '%s' in list of known atom types.\n" % (typename, str(atom))
msg += "Known atom types are: %s" % str(self._atomTypes.keys())
raise Exception(msg)
# Adjust masses.
# Add the particle to the OpenMM system.
mass = self._atomTypes[typename].mass
sys.addParticle(mass)
# Adjust hydrogen masses if requested.
if hydrogenMass is not None:
if not unit.is_quantity(hydrogenMass):
hydrogenMass *= unit.dalton
for atom1, atom2 in topology.bonds():
if atom1.element == elem.hydrogen:
(atom1, atom2) = (atom2, atom1)
......@@ -536,7 +1072,7 @@ class ForceField(object):
boxVectors = topology.getPeriodicBoxVectors()
if boxVectors is not None:
sys.setDefaultPeriodicBoxVectors(boxVectors[0], boxVectors[1], boxVectors[2])
elif nonbondedMethod is not NoCutoff and nonbondedMethod is not CutoffNonPeriodic:
elif nonbondedMethod not in [NoCutoff, CutoffNonPeriodic]:
raise ValueError('Requested periodic boundary conditions for a Topology that does not specify periodic box dimensions')
# Make a list of all unique angles
......@@ -562,13 +1098,13 @@ class ForceField(object):
uniquePropers = set()
for angle in data.angles:
for atom in bondedToAtom[angle[0]]:
if atom != angle[1]:
if atom not in angle:
if atom < angle[2]:
uniquePropers.add((atom, angle[0], angle[1], angle[2]))
else:
uniquePropers.add((angle[2], angle[1], angle[0], atom))
for atom in bondedToAtom[angle[2]]:
if atom != angle[1]:
if atom not in angle:
if atom > angle[0]:
uniquePropers.add((angle[0], angle[1], angle[2], atom))
else:
......@@ -650,7 +1186,7 @@ class ForceField(object):
if removeCMMotion:
sys.addForce(mm.CMMotionRemover())
# Let generators do postprocessing
# Let force generators do postprocessing
for force in self._forces:
if 'postprocessSystem' in dir(force):
......@@ -690,7 +1226,6 @@ def _createResidueSignature(elements):
s += element.symbol+str(count)
return s
def _matchResidue(res, template, bondedToAtom):
"""Determine whether a residue matches a template and return a list of corresponding atoms.
......@@ -710,15 +1245,14 @@ def _matchResidue(res, template, bondedToAtom):
corresponds to, or None if it does not match the template
"""
atoms = list(res.atoms())
if len(atoms) != len(template.atoms):
numAtoms = len(atoms)
if numAtoms != len(template.atoms):
return None
matches = len(atoms)*[0]
hasMatch = len(atoms)*[False]
# Translate from global to local atom indices, and record the bonds for each atom.
renumberAtoms = {}
for i in range(len(atoms)):
for i in range(numAtoms):
renumberAtoms[atoms[i].index] = i
bondedTo = []
externalBonds = []
......@@ -744,41 +1278,268 @@ def _matchResidue(res, template, bondedToAtom):
templateTypeCount[key] += 1
if residueTypeCount != templateTypeCount:
return None
# Identify template atoms that could potentially be matches for each atom.
candidates = [[] for i in range(numAtoms)]
for i in range(numAtoms):
for j, atom in enumerate(template.atoms):
if (atom.element is not None and atom.element != atoms[i].element) or (atom.element is None and atom.name != atoms[i].name):
continue
if len(atom.bondedTo) != len(bondedTo[i]):
continue
if atom.externalBonds != externalBonds[i]:
continue
candidates[i].append(j)
# Find an optimal ordering for matching atoms. This means 1) start with the one that has the fewest options,
# and 2) follow with ones that are bonded to an already matched atom.
searchOrder = []
atomsToOrder = set(range(numAtoms))
efficientAtomSet = set()
efficientAtomHeap = []
while len(atomsToOrder) > 0:
if len(efficientAtomSet) == 0:
fewestNeighbors = numAtoms+1
for i in atomsToOrder:
if len(candidates[i]) < fewestNeighbors:
nextAtom = i
fewestNeighbors = len(candidates[i])
else:
nextAtom = heappop(efficientAtomHeap)[1]
efficientAtomSet.remove(nextAtom)
searchOrder.append(nextAtom)
atomsToOrder.remove(nextAtom)
for i in bondedTo[nextAtom]:
if i in atomsToOrder:
if i not in efficientAtomSet:
efficientAtomSet.add(i)
heappush(efficientAtomHeap, (len(candidates[i]), i))
inverseSearchOrder = [0]*numAtoms
for i in range(numAtoms):
inverseSearchOrder[searchOrder[i]] = i
bondedTo = [[inverseSearchOrder[bondedTo[i][j]] for j in range(len(bondedTo[i]))] for i in searchOrder]
candidates = [candidates[i] for i in searchOrder]
# Recursively match atoms.
if _findAtomMatches(atoms, template, bondedTo, externalBonds, matches, hasMatch, 0):
return matches
matches = numAtoms*[0]
hasMatch = numAtoms*[False]
if _findAtomMatches(template, bondedTo, matches, hasMatch, candidates, 0):
return [matches[inverseSearchOrder[i]] for i in range(numAtoms)]
return None
def _findAtomMatches(atoms, template, bondedTo, externalBonds, matches, hasMatch, position):
def _getAtomMatchCandidates(template, bondedTo, matches, candidates, position):
"""Get a list of template atoms that are potential matches for the next atom."""
for bonded in bondedTo[position]:
if bonded < position:
# This atom is bonded to another one for which we already have a match, so only consider
# template atoms that *that* one is bonded to.
return template.atoms[matches[bonded]].bondedTo
return candidates[position]
def _findAtomMatches(template, bondedTo, matches, hasMatch, candidates, position):
"""This is called recursively from inside _matchResidue() to identify matching atoms."""
if position == len(atoms):
if position == len(matches):
return True
elem = atoms[position].element
name = atoms[position].name
for i in range(len(atoms)):
for i in _getAtomMatchCandidates(template, bondedTo, matches, candidates, position):
atom = template.atoms[i]
if ((atom.element is not None and atom.element == elem) or (atom.element is None and atom.name == name)) and not hasMatch[i] and len(atom.bondedTo) == len(bondedTo[position]) and atom.externalBonds == externalBonds[position]:
if not hasMatch[i] and i in candidates[position]:
# See if the bonds for this identification are consistent
allBondsMatch = all((bonded > position or matches[bonded] in atom.bondedTo for bonded in bondedTo[position]))
if allBondsMatch:
# This is a possible match, so trying matching the rest of the residue.
# This is a possible match, so try matching the rest of the residue.
matches[position] = i
hasMatch[i] = True
if _findAtomMatches(atoms, template, bondedTo, externalBonds, matches, hasMatch, position+1):
if _findAtomMatches(template, bondedTo, matches, hasMatch, candidates, position+1):
return True
hasMatch[i] = False
return False
def _applyPatchesToMatchResidues(forcefield, data, residues, bondedToAtom):
"""Try to apply patches to find matches for residues."""
# Start by creating all templates than can be created by applying a combination of one-residue patches
# to a single template. The number of these is usually not too large, and they often cover a large fraction
# of residues.
patchedTemplateSignatures = {}
patchedTemplates = {}
for name, template in forcefield._templates.items():
if name in forcefield._templatePatches:
patches = [forcefield._patches[patchName] for patchName, patchResidueIndex in forcefield._templatePatches[name] if forcefield._patches[patchName].numResidues == 1]
if len(patches) > 0:
newTemplates = []
patchedTemplates[name] = newTemplates
_generatePatchedSingleResidueTemplates(template, patches, 0, newTemplates)
for patchedTemplate in newTemplates:
signature = _createResidueSignature([atom.element for atom in patchedTemplate.atoms])
if signature in patchedTemplateSignatures:
patchedTemplateSignatures[signature].append(patchedTemplate)
else:
patchedTemplateSignatures[signature] = [patchedTemplate]
# Now see if any of those templates matches any of the residues.
unmatchedResidues = []
for res in residues:
[template, matches] = forcefield._getResidueTemplateMatches(res, bondedToAtom, patchedTemplateSignatures)
if matches is None:
unmatchedResidues.append(res)
else:
data.recordMatchedAtomParameters(res, template, matches)
if len(unmatchedResidues) == 0:
return []
# We need to consider multi-residue patches. This can easily lead to a combinatorial explosion, so we make a simplifying
# assumption: that no residue is affected by more than one multi-residue patch (in addition to any number of single-residue
# patches). Record all multi-residue patches, and the templates they can be applied to.
patches = {}
maxPatchSize = 0
for patch in forcefield._patches.values():
if patch.numResidues > 1:
patches[patch.name] = [[] for i in range(patch.numResidues)]
maxPatchSize = max(maxPatchSize, patch.numResidues)
if maxPatchSize == 0:
return unmatchedResidues # There aren't any multi-residue patches
for templateName in forcefield._templatePatches:
for patchName, patchResidueIndex in forcefield._templatePatches[templateName]:
if patchName in patches:
# The patch should accept this template, *and* all patched versions of it generated above.
patches[patchName][patchResidueIndex].append(forcefield._templates[templateName])
if templateName in patchedTemplates:
patches[patchName][patchResidueIndex] += patchedTemplates[templateName]
# Record which unmatched residues are bonded to each other.
bonds = set()
topology = residues[0].chain.topology
for atom1, atom2 in topology.bonds():
if atom1.residue != atom2.residue:
res1 = atom1.residue
res2 = atom2.residue
if res1 in unmatchedResidues and res2 in unmatchedResidues:
bond = tuple(sorted((res1, res2), key=lambda x: x.index))
if bond not in bonds:
bonds.add(bond)
# Identify clusters of unmatched residues that are all bonded to each other. These are the ones we'll
# try to apply multi-residue patches to.
clusterSize = 2
clusters = bonds
while clusterSize <= maxPatchSize:
# Try to apply patches to clusters of this size.
for patchName in patches:
patch = forcefield._patches[patchName]
if patch.numResidues == clusterSize:
matchedClusters = _matchToMultiResiduePatchedTemplates(data, clusters, patch, patches[patchName], bondedToAtom)
for cluster in matchedClusters:
for residue in cluster:
unmatchedResidues.remove(residue)
bonds = set(bond for bond in bonds if bond[0] in unmatchedResidues and bond[1] in unmatchedResidues)
# Now extend the clusters to find ones of the next size up.
largerClusters = set()
for cluster in clusters:
for bond in bonds:
if bond[0] in cluster and bond[1] not in cluster:
newCluster = tuple(sorted(cluster+(bond[1],), key=lambda x: x.index))
largerClusters.add(newCluster)
elif bond[1] in cluster and bond[0] not in cluster:
newCluster = tuple(sorted(cluster+(bond[0],), key=lambda x: x.index))
largerClusters.add(newCluster)
if len(largerClusters) == 0:
# There are no clusters of this size or larger
break
clusters = largerClusters
clusterSize += 1
return unmatchedResidues
def _generatePatchedSingleResidueTemplates(template, patches, index, newTemplates):
"""Apply all possible combinations of a set of single-residue patches to a template."""
try:
patchedTemplate = patches[index].createPatchedTemplates([template])[0]
newTemplates.append(patchedTemplate)
except:
# This probably means the patch is inconsistent with another one that has already been applied,
# so just ignore it.
patchedTemplate = None
# Call this function recursively to generate combinations of patches.
if index+1 < len(patches):
_generatePatchedSingleResidueTemplates(template, patches, index+1, newTemplates)
if patchedTemplate is not None:
_generatePatchedSingleResidueTemplates(patchedTemplate, patches, index+1, newTemplates)
def _matchToMultiResiduePatchedTemplates(data, clusters, patch, residueTemplates, bondedToAtom):
"""Apply a multi-residue patch to templates, then try to match them against clusters of residues."""
matchedClusters = []
selectedTemplates = [None]*patch.numResidues
_applyMultiResiduePatch(data, clusters, patch, residueTemplates, selectedTemplates, 0, matchedClusters, bondedToAtom)
return matchedClusters
def _applyMultiResiduePatch(data, clusters, patch, candidateTemplates, selectedTemplates, index, matchedClusters, bondedToAtom):
"""This is called recursively to apply a multi-residue patch to all possible combinations of templates."""
if index < patch.numResidues:
for template in candidateTemplates[index]:
selectedTemplates[index] = template
_applyMultiResiduePatch(data, clusters, patch, candidateTemplates, selectedTemplates, index+1, matchedClusters, bondedToAtom)
else:
# We're at the deepest level of the recursion. We've selected a template for each residue, so apply the patch,
# then try to match it against clusters.
try:
patchedTemplates = patch.createPatchedTemplates(selectedTemplates)
except:
# This probably means the patch is inconsistent with another one that has already been applied,
# so just ignore it.
raise
return
newlyMatchedClusters = []
for cluster in clusters:
for residues in itertools.permutations(cluster):
residueMatches = []
for residue, template in zip(residues, patchedTemplates):
matches = _matchResidue(residue, template, bondedToAtom)
if matches is None:
residueMatches = None
break
else:
residueMatches.append(matches)
if residueMatches is not None:
# We successfully matched the template to the residues. Record the parameters.
for i in range(patch.numResidues):
data.recordMatchedAtomParameters(residues[i], patchedTemplates[i], residueMatches[i])
newlyMatchedClusters.append(cluster)
break
# Record which clusters were successfully matched.
matchedClusters += newlyMatchedClusters
for cluster in newlyMatchedClusters:
clusters.remove(cluster)
def _findMatchErrors(forcefield, res):
"""Try to guess why a residue failed to match any template and return an error message."""
residueCounts = _countResidueAtoms([atom.element for atom in res.atoms()])
numResidueAtoms = sum(residueCounts.itervalues())
numResidueAtoms = sum(residueCounts.values())
numResidueHeavyAtoms = sum(residueCounts[element] for element in residueCounts if element not in (None, elem.hydrogen))
# Loop over templates and see how closely each one might match.
......@@ -797,7 +1558,7 @@ def _findMatchErrors(forcefield, res):
# If there are too many missing atoms, discard this template.
numTemplateAtoms = sum(templateCounts.itervalues())
numTemplateAtoms = sum(templateCounts.values())
numTemplateHeavyAtoms = sum(templateCounts[element] for element in templateCounts if element not in (None, elem.hydrogen))
if numTemplateAtoms > numBestMatchAtoms:
continue
......@@ -835,6 +1596,34 @@ def _findMatchErrors(forcefield, res):
return 'The set of atoms is similar to %s, but it is missing %d atoms.' % (bestMatchName, numBestMatchAtoms-numResidueAtoms)
return 'This might mean your input topology is missing some atoms or bonds, or possibly that you are using the wrong force field.'
def _createResidueTemplate(residue):
"""Create a _TemplateData template from a Residue object.
Parameters
----------
residue : Residue
The Residue from which the template is to be constructed.
Returns
-------
template : _TemplateData
The residue template, with atom types set to None.
This method may be useful in creating new residue templates for residues without templates defined by the ForceField.
"""
template = ForceField._TemplateData(residue.name)
for atom in residue.atoms():
template.atoms.append(ForceField._TemplateAtomData(atom.name, None, atom.element))
for (atom1,atom2) in residue.internal_bonds():
template.addBondByName(atom1.name, atom2.name)
residue_atoms = [ atom for atom in residue.atoms() ]
for (atom1,atom2) in residue.external_bonds():
if atom1 in residue_atoms:
template.addExternalBondByName(atom1.name)
elif atom2 in residue_atoms:
template.addExternalBondByName(atom2.name)
return template
# The following classes are generators that know how to create Force subclasses and add them to a System that is being
# created. Each generator class must define two methods: 1) a static method that takes an etree Element and a ForceField,
......@@ -842,7 +1631,7 @@ def _findMatchErrors(forcefield, res):
# to the System. The static method should be added to the parsers map.
## @private
class HarmonicBondGenerator:
class HarmonicBondGenerator(object):
"""A HarmonicBondGenerator constructs a HarmonicBondForce."""
def __init__(self, forcefield):
......@@ -862,8 +1651,12 @@ class HarmonicBondGenerator:
@staticmethod
def parseElement(element, ff):
generator = HarmonicBondGenerator(ff)
ff.registerGenerator(generator)
existing = [f for f in ff._forces if isinstance(f, HarmonicBondGenerator)]
if len(existing) == 0:
generator = HarmonicBondGenerator(ff)
ff.registerGenerator(generator)
else:
generator = existing[0]
for bond in element.findall('Bond'):
generator.registerBond(bond.attrib)
......@@ -893,7 +1686,7 @@ parsers["HarmonicBondForce"] = HarmonicBondGenerator.parseElement
## @private
class HarmonicAngleGenerator:
class HarmonicAngleGenerator(object):
"""A HarmonicAngleGenerator constructs a HarmonicAngleForce."""
def __init__(self, forcefield):
......@@ -915,8 +1708,12 @@ class HarmonicAngleGenerator:
@staticmethod
def parseElement(element, ff):
generator = HarmonicAngleGenerator(ff)
ff.registerGenerator(generator)
existing = [f for f in ff._forces if isinstance(f, HarmonicAngleGenerator)]
if len(existing) == 0:
generator = HarmonicAngleGenerator(ff)
ff.registerGenerator(generator)
else:
generator = existing[0]
for angle in element.findall('Angle'):
generator.registerAngle(angle.attrib)
......@@ -966,7 +1763,7 @@ parsers["HarmonicAngleForce"] = HarmonicAngleGenerator.parseElement
## @private
class PeriodicTorsion:
class PeriodicTorsion(object):
"""A PeriodicTorsion records the information for a periodic torsion definition."""
def __init__(self, types):
......@@ -979,7 +1776,7 @@ class PeriodicTorsion:
self.k = []
## @private
class PeriodicTorsionGenerator:
class PeriodicTorsionGenerator(object):
"""A PeriodicTorsionGenerator constructs a PeriodicTorsionForce."""
def __init__(self, forcefield):
......@@ -999,8 +1796,12 @@ class PeriodicTorsionGenerator:
@staticmethod
def parseElement(element, ff):
generator = PeriodicTorsionGenerator(ff)
ff.registerGenerator(generator)
existing = [f for f in ff._forces if isinstance(f, PeriodicTorsionGenerator)]
if len(existing) == 0:
generator = PeriodicTorsionGenerator(ff)
ff.registerGenerator(generator)
else:
generator = existing[0]
for torsion in element.findall('Proper'):
generator.registerProperTorsion(torsion.attrib)
for torsion in element.findall('Improper'):
......@@ -1041,14 +1842,16 @@ class PeriodicTorsionGenerator:
type2 = data.atomType[data.atoms[torsion[1]]]
type3 = data.atomType[data.atoms[torsion[2]]]
type4 = data.atomType[data.atoms[torsion[3]]]
done = False
match = None
for tordef in self.improper:
if done:
break
types1 = tordef.types1
types2 = tordef.types2
types3 = tordef.types3
types4 = tordef.types4
hasWildcard = (wildcard in (types1, types2, types3, types4))
if match is not None and hasWildcard:
# Prefer specific definitions over ones with wildcards
continue
if type1 in types1:
for (t2, t3, t4) in itertools.permutations(((type2, 1), (type3, 2), (type4, 3))):
if t2[0] in types2 and t3[0] in types3 and t4[0] in types4:
......@@ -1063,17 +1866,19 @@ class PeriodicTorsionGenerator:
(a1, a2) = (a2, a1)
elif e1 != elem.carbon and (e2 == elem.carbon or e1.mass < e2.mass):
(a1, a2) = (a2, a1)
for i in range(len(tordef.phase)):
if tordef.k[i] != 0:
force.addTorsion(a1, a2, torsion[0], torsion[t4[1]], tordef.periodicity[i], tordef.phase[i], tordef.k[i])
done = True
match = (a1, a2, torsion[0], torsion[t4[1]], tordef)
break
if match is not None:
(a1, a2, a3, a4, tordef) = match
for i in range(len(tordef.phase)):
if tordef.k[i] != 0:
force.addTorsion(a1, a2, a3, a4, tordef.periodicity[i], tordef.phase[i], tordef.k[i])
parsers["PeriodicTorsionForce"] = PeriodicTorsionGenerator.parseElement
## @private
class RBTorsion:
class RBTorsion(object):
"""An RBTorsion records the information for a Ryckaert-Bellemans torsion definition."""
def __init__(self, types, c):
......@@ -1084,7 +1889,7 @@ class RBTorsion:
self.c = c
## @private
class RBTorsionGenerator:
class RBTorsionGenerator(object):
"""An RBTorsionGenerator constructs an RBTorsionForce."""
def __init__(self, forcefield):
......@@ -1094,8 +1899,12 @@ class RBTorsionGenerator:
@staticmethod
def parseElement(element, ff):
generator = RBTorsionGenerator(ff)
ff.registerGenerator(generator)
existing = [f for f in ff._forces if isinstance(f, RBTorsionGenerator)]
if len(existing) == 0:
generator = RBTorsionGenerator(ff)
ff.registerGenerator(generator)
else:
generator = existing[0]
for torsion in element.findall('Proper'):
types = ff._findAtomTypes(torsion.attrib, 4)
if None not in types:
......@@ -1138,18 +1947,20 @@ class RBTorsionGenerator:
type2 = data.atomType[data.atoms[torsion[1]]]
type3 = data.atomType[data.atoms[torsion[2]]]
type4 = data.atomType[data.atoms[torsion[3]]]
done = False
match = None
for tordef in self.improper:
if done:
break
types1 = tordef.types1
types2 = tordef.types2
types3 = tordef.types3
types4 = tordef.types4
hasWildcard = (wildcard in (types1, types2, types3, types4))
if match is not None and hasWildcard:
# Prefer specific definitions over ones with wildcards
continue
if type1 in types1:
for (t2, t3, t4) in itertools.permutations(((type2, 1), (type3, 2), (type4, 3))):
if t2[0] in types2 and t3[0] in types3 and t4[0] in types4:
if wildcard in (types1, types2, types3, types4):
if hasWildcard:
# Workaround to be more consistent with AMBER. It uses wildcards to define most of its
# impropers, which leaves the ordering ambiguous. It then follows some bizarre rules
# to pick the order.
......@@ -1161,18 +1972,20 @@ class RBTorsionGenerator:
(a1, a2) = (a2, a1)
elif e1 != elem.carbon and (e2 == elem.carbon or e1.mass < e2.mass):
(a1, a2) = (a2, a1)
force.addTorsion(a1, a2, torsion[0], torsion[t4[1]], tordef.c[0], tordef.c[1], tordef.c[2], tordef.c[3], tordef.c[4], tordef.c[5])
match = (a1, a2, torsion[0], torsion[t4[1]], tordef)
else:
# There are no wildcards, so the order is unambiguous.
force.addTorsion(torsion[0], torsion[t2[1]], torsion[t3[1]], torsion[t4[1]], tordef.c[0], tordef.c[1], tordef.c[2], tordef.c[3], tordef.c[4], tordef.c[5])
done = True
match = (torsion[0], torsion[t2[1]], torsion[t3[1]], torsion[t4[1]], tordef)
break
if match is not None:
(a1, a2, a3, a4, tordef) = match
force.addTorsion(a1, a2, a3, a4, tordef.c[0], tordef.c[1], tordef.c[2], tordef.c[3], tordef.c[4], tordef.c[5])
parsers["RBTorsionForce"] = RBTorsionGenerator.parseElement
## @private
class CMAPTorsion:
class CMAPTorsion(object):
"""A CMAPTorsion records the information for a CMAP torsion definition."""
def __init__(self, types, map):
......@@ -1184,7 +1997,7 @@ class CMAPTorsion:
self.map = map
## @private
class CMAPTorsionGenerator:
class CMAPTorsionGenerator(object):
"""A CMAPTorsionGenerator constructs a CMAPTorsionForce."""
def __init__(self, forcefield):
......@@ -1194,8 +2007,12 @@ class CMAPTorsionGenerator:
@staticmethod
def parseElement(element, ff):
generator = CMAPTorsionGenerator(ff)
ff.registerGenerator(generator)
existing = [f for f in ff._forces if isinstance(f, CMAPTorsionGenerator)]
if len(existing) == 0:
generator = CMAPTorsionGenerator(ff)
ff.registerGenerator(generator)
else:
generator = existing[0]
for map in element.findall('Map'):
values = [float(x) for x in map.text.split()]
size = sqrt(len(values))
......@@ -1264,9 +2081,11 @@ parsers["CMAPTorsionForce"] = CMAPTorsionGenerator.parseElement
## @private
class NonbondedGenerator:
class NonbondedGenerator(object):
"""A NonbondedGenerator constructs a NonbondedForce."""
SCALETOL = 1e-5
def __init__(self, forcefield, coulomb14scale, lj14scale):
self.ff = forcefield
self.coulomb14scale = coulomb14scale
......@@ -1285,7 +2104,8 @@ class NonbondedGenerator:
else:
# Multiple <NonbondedForce> tags were found, probably in different files. Simply add more types to the existing one.
generator = existing[0]
if generator.coulomb14scale != float(element.attrib['coulomb14scale']) or generator.lj14scale != float(element.attrib['lj14scale']):
if abs(generator.coulomb14scale - float(element.attrib['coulomb14scale'])) > NonbondedGenerator.SCALETOL or \
abs(generator.lj14scale - float(element.attrib['lj14scale'])) > NonbondedGenerator.SCALETOL:
raise ValueError('Found multiple NonbondedForce tags with different 1-4 scales')
generator.params.parseDefinitions(element)
......@@ -1344,7 +2164,174 @@ parsers["NonbondedForce"] = NonbondedGenerator.parseElement
## @private
class GBSAOBCGenerator:
class LennardJonesGenerator(object):
"""A NBFix generator to construct the L-J force with NBFIX implemented as a lookup table"""
def __init__(self, forcefield, lj14scale):
self.ff = forcefield
self.nbfixTypes = {}
self.lj14scale = lj14scale
self.ljTypes = ForceField._AtomTypeParameters(forcefield, 'LennardJonesForce', 'Atom', ('sigma', 'epsilon'))
def registerNBFIX(self, parameters):
types = self.ff._findAtomTypes(parameters, 2)
if None not in types:
type1 = types[0][0]
type2 = types[1][0]
epsilon = _convertParameterToNumber(parameters['epsilon'])
sigma = _convertParameterToNumber(parameters['sigma'])
self.nbfixTypes[(type1, type2)] = [sigma, epsilon]
self.nbfixTypes[(type2, type1)] = [sigma, epsilon]
def registerLennardJones(self, parameters):
self.ljTypes.registerAtom(parameters)
@staticmethod
def parseElement(element, ff):
existing = [f for f in ff._forces if isinstance(f, LennardJonesGenerator)]
if len(existing) == 0:
generator = LennardJonesGenerator(ff, float(element.attrib['lj14scale']))
ff.registerGenerator(generator)
else:
# Multiple <LennardJonesForce> tags were found, probably in different files
generator = existing[0]
if abs(generator.lj14scale - float(element.attrib['lj14scale'])) > NonbondedGenerator.SCALETOL:
raise ValueError('Found multiple LennardJonesForce tags with different 1-4 scales')
for LJ in element.findall('Atom'):
generator.registerLennardJones(LJ.attrib)
for Nbfix in element.findall('NBFixPair'):
generator.registerNBFIX(Nbfix.attrib)
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
# First derive the lookup tables
nbfixTypeSet = set().union(*self.nbfixTypes)
ljIndexList = [None]*len(data.atoms)
numLjTypes = 0
ljTypeList = []
typeMap = {}
for i, atom in enumerate(data.atoms):
atype = data.atomType[atom]
values = tuple(self.ljTypes.getAtomParameters(atom, data))
if values in typeMap and atype not in nbfixTypeSet:
# Only non-NBFIX types can be compressed
ljIndexList[i] = typeMap[values]
else:
typeMap[values] = numLjTypes
ljIndexList[i] = numLjTypes
numLjTypes += 1
ljTypeList.append(atype)
reverseMap = [0]*len(typeMap)
for typeValue in typeMap:
reverseMap[typeMap[typeValue]] = typeValue
# Now everything is assigned. Create the A- and B-coefficient arrays
acoef = [0]*(numLjTypes*numLjTypes)
bcoef = acoef[:]
for m in range(numLjTypes):
for n in range(numLjTypes):
pair = (ljTypeList[m], ljTypeList[n])
if pair in self.nbfixTypes:
epsilon = self.nbfixTypes[pair][1]
sigma = self.nbfixTypes[pair][0]
sigma6 = sigma**6
acoef[m+numLjTypes*n] = 4*epsilon*sigma6*sigma6
bcoef[m+numLjTypes*n] = 4*epsilon*sigma6
continue
else:
sigma = 0.5*(reverseMap[m][0]+reverseMap[n][0])
sigma6 = sigma**6
epsilon = math.sqrt(reverseMap[m][-1]*reverseMap[n][-1])
acoef[m+numLjTypes*n] = 4*epsilon*sigma6*sigma6
bcoef[m+numLjTypes*n] = 4*epsilon*sigma6
self.force = mm.CustomNonbondedForce('acoef(type1, type2)/r^12 - bcoef(type1, type2)/r^6;')
self.force.addTabulatedFunction('acoef', mm.Discrete2DFunction(numLjTypes, numLjTypes, acoef))
self.force.addTabulatedFunction('bcoef', mm.Discrete2DFunction(numLjTypes, numLjTypes, bcoef))
self.force.addPerParticleParameter('type')
if nonbondedMethod in [CutoffPeriodic, Ewald, PME]:
self.force.setNonbondedMethod(mm.CustomNonbondedForce.CutoffPeriodic)
elif nonbondedMethod is NoCutoff:
self.force.setNonbondedMethod(mm.CustomNonbondedForce.NoCutoff)
elif nonbondedMethod is CutoffNonPeriodic:
self.force.setNonbondedMethod(mm.CustomNonbondedForce.CutoffNonPeriodic)
else:
raise AssertionError('Unrecognized nonbonded method [%s]' % nonbondedMethod)
# Add the particles
for i in ljIndexList:
self.force.addParticle((i,))
self.force.setUseLongRangeCorrection(True)
self.force.setCutoffDistance(nonbondedCutoff)
sys.addForce(self.force)
def postprocessSystem(self, sys, data, args):
# Create exceptions based on bonds.
bondIndices = []
for bond in data.bonds:
bondIndices.append((bond.atom1, bond.atom2))
# If a virtual site does *not* share exclusions with another atom, add a bond between it and its first parent atom.
for i in range(sys.getNumParticles()):
if sys.isVirtualSite(i):
(site, atoms, excludeWith) = data.virtualSites[data.atoms[i]]
if excludeWith is None:
bondIndices.append((i, site.getParticle(0)))
# Certain particles, such as lone pairs and Drude particles, share exclusions with a parent atom.
# If the parent atom does not interact with an atom, the child particle does not either.
for atom1, atom2 in bondIndices:
for child1 in data.excludeAtomWith[atom1]:
bondIndices.append((child1, atom2))
for child2 in data.excludeAtomWith[atom2]:
bondIndices.append((child1, child2))
for child2 in data.excludeAtomWith[atom2]:
bondIndices.append((atom1, child2))
# Create the exceptions.
if self.lj14scale == 1:
# Just exclude the 1-2 and 1-3 interactions.
self.force.createExclusionsFromBonds(bondIndices, 2)
else:
forceCopy = deepcopy(self.force)
forceCopy.createExclusionsFromBonds(bondIndices, 2)
self.force.createExclusionsFromBonds(bondIndices, 3)
if self.force.getNumExclusions() > forceCopy.getNumExclusions() and self.lj14scale != 0:
# We need to create a CustomBondForce and use it to implement the scaled 1-4 interactions.
bonded = mm.CustomBondForce('%g*epsilon*((sigma/r)^12-(sigma/r)^6)' % (4*self.lj14scale))
bonded.addPerBondParameter('sigma')
bonded.addPerBondParameter('epsilon')
sys.addForce(bonded)
skip = set(tuple(forceCopy.getExclusionParticles(i)) for i in range(forceCopy.getNumExclusions()))
for i in range(self.force.getNumExclusions()):
p1,p2 = self.force.getExclusionParticles(i)
a1 = data.atoms[p1]
a2 = data.atoms[p2]
if (p1,p2) not in skip and (p2,p1) not in skip:
type1 = data.atomType[a1]
type2 = data.atomType[a2]
if (type1, type2) in self.nbfixTypes:
sigma, epsilon = self.nbfixTypes[(type1, type2)]
else:
values1 = self.ljTypes.getAtomParameters(a1, data)
values2 = self.ljTypes.getAtomParameters(a2, data)
sigma = 0.5*(values1[0]+values2[0])
epsilon = sqrt(values1[1]*values2[1])
bonded.addBond(p1, p2, (sigma, epsilon))
parsers["LennardJonesForce"] = LennardJonesGenerator.parseElement
## @private
class GBSAOBCGenerator(object):
"""A GBSAOBCGenerator constructs a GBSAOBCForce."""
def __init__(self, forcefield):
......@@ -1394,7 +2381,7 @@ parsers["GBSAOBCForce"] = GBSAOBCGenerator.parseElement
## @private
class CustomBondGenerator:
class CustomBondGenerator(object):
"""A CustomBondGenerator constructs a CustomBondForce."""
def __init__(self, forcefield):
......@@ -1442,7 +2429,7 @@ parsers["CustomBondForce"] = CustomBondGenerator.parseElement
## @private
class CustomAngleGenerator:
class CustomAngleGenerator(object):
"""A CustomAngleGenerator constructs a CustomAngleForce."""
def __init__(self, forcefield):
......@@ -1494,7 +2481,7 @@ parsers["CustomAngleForce"] = CustomAngleGenerator.parseElement
## @private
class CustomTorsion:
class CustomTorsion(object):
"""A CustomTorsion records the information for a custom torsion definition."""
def __init__(self, types, paramValues):
......@@ -1505,7 +2492,7 @@ class CustomTorsion:
self.paramValues = paramValues
## @private
class CustomTorsionGenerator:
class CustomTorsionGenerator(object):
"""A CustomTorsionGenerator constructs a CustomTorsionForce."""
def __init__(self, forcefield):
......@@ -1565,18 +2552,20 @@ class CustomTorsionGenerator:
type2 = data.atomType[data.atoms[torsion[1]]]
type3 = data.atomType[data.atoms[torsion[2]]]
type4 = data.atomType[data.atoms[torsion[3]]]
done = False
match = None
for tordef in self.improper:
if done:
break
types1 = tordef.types1
types2 = tordef.types2
types3 = tordef.types3
types4 = tordef.types4
hasWildcard = (wildcard in (types1, types2, types3, types4))
if match is not None and hasWildcard:
# Prefer specific definitions over ones with wildcards
continue
if type1 in types1:
for (t2, t3, t4) in itertools.permutations(((type2, 1), (type3, 2), (type4, 3))):
if t2[0] in types2 and t3[0] in types3 and t4[0] in types4:
if wildcard in (types1, types2, types3, types4):
if hasWildcard:
# Workaround to be more consistent with AMBER. It uses wildcards to define most of its
# impropers, which leaves the ordering ambiguous. It then follows some bizarre rules
# to pick the order.
......@@ -1588,18 +2577,20 @@ class CustomTorsionGenerator:
(a1, a2) = (a2, a1)
elif e1 != elem.carbon and (e2 == elem.carbon or e1.mass < e2.mass):
(a1, a2) = (a2, a1)
force.addTorsion(a1, a2, torsion[0], torsion[t4[1]], tordef.paramValues)
match = (a1, a2, torsion[0], torsion[t4[1]], tordef)
else:
# There are no wildcards, so the order is unambiguous.
force.addTorsion(torsion[0], torsion[t2[1]], torsion[t3[1]], torsion[t4[1]], tordef.paramValues)
done = True
match = (torsion[0], torsion[t2[1]], torsion[t3[1]], torsion[t4[1]], tordef)
break
if match is not None:
(a1, a2, a3, a4, tordef) = match
force.addTorsion(a1, a2, a3, a4, tordef.paramValues)
parsers["CustomTorsionForce"] = CustomTorsionGenerator.parseElement
## @private
class CustomNonbondedGenerator:
class CustomNonbondedGenerator(object):
"""A CustomNonbondedGenerator constructs a CustomNonbondedForce."""
def __init__(self, forcefield, energy, bondCutoff):
......@@ -1687,7 +2678,7 @@ parsers["CustomNonbondedForce"] = CustomNonbondedGenerator.parseElement
## @private
class CustomGBGenerator:
class CustomGBGenerator(object):
"""A CustomGBGenerator constructs a CustomGBForce."""
def __init__(self, forcefield):
......@@ -1768,7 +2759,7 @@ parsers["CustomGBForce"] = CustomGBGenerator.parseElement
## @private
class CustomManyParticleGenerator:
class CustomManyParticleGenerator(object):
"""A CustomManyParticleGenerator constructs a CustomManyParticleForce."""
def __init__(self, forcefield, particlesPerSet, energy, permutationMode, bondCutoff):
......@@ -1893,7 +2884,7 @@ def countConstraint(data):
print("Constraints bond=%d angle=%d total=%d" % (bondCount, angleCount, (bondCount+angleCount)))
## @private
class AmoebaBondGenerator:
class AmoebaBondGenerator(object):
#=============================================================================================
......@@ -1996,7 +2987,7 @@ def addAngleConstraint(angle, idealAngle, data, sys):
#=============================================================================================
## @private
class AmoebaAngleGenerator:
class AmoebaAngleGenerator(object):
#=============================================================================================
"""An AmoebaAngleGenerator constructs a AmoebaAngleForce."""
......@@ -2185,7 +3176,7 @@ parsers["AmoebaAngleForce"] = AmoebaAngleGenerator.parseElement
#=============================================================================================
## @private
class AmoebaOutOfPlaneBendGenerator:
class AmoebaOutOfPlaneBendGenerator(object):
#=============================================================================================
......@@ -2459,7 +3450,7 @@ parsers["AmoebaOutOfPlaneBendForce"] = AmoebaOutOfPlaneBendGenerator.parseElemen
#=============================================================================================
## @private
class AmoebaTorsionGenerator:
class AmoebaTorsionGenerator(object):
#=============================================================================================
"""An AmoebaTorsionGenerator constructs a AmoebaTorsionForce."""
......@@ -2568,7 +3559,7 @@ parsers["AmoebaTorsionForce"] = AmoebaTorsionGenerator.parseElement
#=============================================================================================
## @private
class AmoebaPiTorsionGenerator:
class AmoebaPiTorsionGenerator(object):
#=============================================================================================
......@@ -2682,7 +3673,7 @@ parsers["AmoebaPiTorsionForce"] = AmoebaPiTorsionGenerator.parseElement
#=============================================================================================
## @private
class AmoebaTorsionTorsionGenerator:
class AmoebaTorsionTorsionGenerator(object):
#=============================================================================================
"""An AmoebaTorsionTorsionGenerator constructs a AmoebaTorsionTorsionForce."""
......@@ -2924,7 +3915,7 @@ parsers["AmoebaTorsionTorsionForce"] = AmoebaTorsionTorsionGenerator.parseElemen
#=============================================================================================
## @private
class AmoebaStretchBendGenerator:
class AmoebaStretchBendGenerator(object):
#=============================================================================================
"""An AmoebaStretchBendGenerator constructs a AmoebaStretchBendForce."""
......@@ -3068,7 +4059,7 @@ parsers["AmoebaStretchBendForce"] = AmoebaStretchBendGenerator.parseElement
#=============================================================================================
## @private
class AmoebaVdwGenerator:
class AmoebaVdwGenerator(object):
"""A AmoebaVdwGenerator constructs a AmoebaVdwForce."""
......@@ -3234,7 +4225,7 @@ parsers["AmoebaVdwForce"] = AmoebaVdwGenerator.parseElement
#=============================================================================================
## @private
class AmoebaMultipoleGenerator:
class AmoebaMultipoleGenerator(object):
#=============================================================================================
......@@ -3601,6 +4592,8 @@ class AmoebaMultipoleGenerator:
polarizationType = args['polarization']
if (polarizationType.lower() == 'direct'):
force.setPolarizationType(mm.AmoebaMultipoleForce.Direct)
elif (polarizationType.lower() == 'extrapolated'):
force.setPolarizationType(mm.AmoebaMultipoleForce.Extrapolated)
else:
force.setPolarizationType(mm.AmoebaMultipoleForce.Mutual)
......@@ -3899,7 +4892,7 @@ parsers["AmoebaMultipoleForce"] = AmoebaMultipoleGenerator.parseElement
#=============================================================================================
## @private
class AmoebaWcaDispersionGenerator:
class AmoebaWcaDispersionGenerator(object):
"""A AmoebaWcaDispersionGenerator constructs a AmoebaWcaDispersionForce."""
......@@ -3972,7 +4965,7 @@ parsers["AmoebaWcaDispersionForce"] = AmoebaWcaDispersionGenerator.parseElement
#=============================================================================================
## @private
class AmoebaGeneralizedKirkwoodGenerator:
class AmoebaGeneralizedKirkwoodGenerator(object):
"""A AmoebaGeneralizedKirkwoodGenerator constructs a AmoebaGeneralizedKirkwoodForce."""
......@@ -4240,7 +5233,7 @@ parsers["AmoebaGeneralizedKirkwoodForce"] = AmoebaGeneralizedKirkwoodGenerator.p
#=============================================================================================
## @private
class AmoebaUreyBradleyGenerator:
class AmoebaUreyBradleyGenerator(object):
#=============================================================================================
"""An AmoebaUreyBradleyGenerator constructs a AmoebaUreyBradleyForce."""
......@@ -4314,7 +5307,7 @@ parsers["AmoebaUreyBradleyForce"] = AmoebaUreyBradleyGenerator.parseElement
## @private
class DrudeGenerator:
class DrudeGenerator(object):
"""A DrudeGenerator constructs a DrudeForce."""
def __init__(self, forcefield):
......@@ -4393,3 +5386,5 @@ class DrudeGenerator:
drude.addScreenedPair(drude1, drude2, thole1+thole2)
parsers["DrudeForce"] = DrudeGenerator.parseElement
#=============================================================================================
......@@ -6,7 +6,7 @@ Simbios, the NIH National Center for Physics-Based Simulation of
Biological Structures at Stanford, funded under the NIH Roadmap for
Medical Research, grant U54 GM072970. See https://simtk.org.
Portions copyright (c) 2012-2015 Stanford University and the Authors.
Portions copyright (c) 2012-2016 Stanford University and the Authors.
Authors: Peter Eastman
Contributors: Jason Swails
......@@ -259,7 +259,7 @@ class GromacsTopFile(object):
"""Process the [ defaults ] line."""
fields = line.split()
if len(fields) < 4:
raise ValueError('Too few fields in [ defaults ] line: '+line);
raise ValueError('Too few fields in [ defaults ] line: '+line)
if fields[0] != '1':
raise ValueError('Unsupported nonbonded type: '+fields[0])
if fields[1] != '2':
......@@ -272,7 +272,7 @@ class GromacsTopFile(object):
"""Process a line in the [ moleculetypes ] category."""
fields = line.split()
if len(fields) < 1:
raise ValueError('Too few fields in [ moleculetypes ] line: '+line);
raise ValueError('Too few fields in [ moleculetypes ] line: '+line)
type = GromacsTopFile._MoleculeType()
self._moleculeTypes[fields[0]] = type
self._currentMoleculeType = type
......@@ -281,7 +281,7 @@ class GromacsTopFile(object):
"""Process a line in the [ molecules ] category."""
fields = line.split()
if len(fields) < 2:
raise ValueError('Too few fields in [ molecules ] line: '+line);
raise ValueError('Too few fields in [ molecules ] line: '+line)
self._molecules.append((fields[0], int(fields[1])))
def _processAtom(self, line):
......@@ -290,7 +290,7 @@ class GromacsTopFile(object):
raise ValueError('Found [ atoms ] section before [ moleculetype ]')
fields = line.split()
if len(fields) < 5:
raise ValueError('Too few fields in [ atoms ] line: '+line);
raise ValueError('Too few fields in [ atoms ] line: '+line)
self._currentMoleculeType.atoms.append(fields)
def _processBond(self, line):
......@@ -299,9 +299,9 @@ class GromacsTopFile(object):
raise ValueError('Found [ bonds ] section before [ moleculetype ]')
fields = line.split()
if len(fields) < 3:
raise ValueError('Too few fields in [ bonds ] line: '+line);
raise ValueError('Too few fields in [ bonds ] line: '+line)
if fields[2] != '1':
raise ValueError('Unsupported function type in [ bonds ] line: '+line);
raise ValueError('Unsupported function type in [ bonds ] line: '+line)
self._currentMoleculeType.bonds.append(fields)
def _processAngle(self, line):
......@@ -310,9 +310,9 @@ class GromacsTopFile(object):
raise ValueError('Found [ angles ] section before [ moleculetype ]')
fields = line.split()
if len(fields) < 4:
raise ValueError('Too few fields in [ angles ] line: '+line);
raise ValueError('Too few fields in [ angles ] line: '+line)
if fields[3] not in ('1', '5'):
raise ValueError('Unsupported function type in [ angles ] line: '+line);
raise ValueError('Unsupported function type in [ angles ] line: '+line)
self._currentMoleculeType.angles.append(fields)
def _processDihedral(self, line):
......@@ -321,9 +321,9 @@ class GromacsTopFile(object):
raise ValueError('Found [ dihedrals ] section before [ moleculetype ]')
fields = line.split()
if len(fields) < 5:
raise ValueError('Too few fields in [ dihedrals ] line: '+line);
raise ValueError('Too few fields in [ dihedrals ] line: '+line)
if fields[4] not in ('1', '2', '3', '4', '9'):
raise ValueError('Unsupported function type in [ dihedrals ] line: '+line);
raise ValueError('Unsupported function type in [ dihedrals ] line: '+line)
self._currentMoleculeType.dihedrals.append(fields)
def _processExclusion(self, line):
......@@ -332,7 +332,7 @@ class GromacsTopFile(object):
raise ValueError('Found [ exclusions ] section before [ moleculetype ]')
fields = line.split()
if len(fields) < 2:
raise ValueError('Too few fields in [ exclusions ] line: '+line);
raise ValueError('Too few fields in [ exclusions ] line: '+line)
self._currentMoleculeType.exclusions.append(fields)
def _processPair(self, line):
......@@ -341,9 +341,9 @@ class GromacsTopFile(object):
raise ValueError('Found [ pairs ] section before [ moleculetype ]')
fields = line.split()
if len(fields) < 3:
raise ValueError('Too few fields in [ pairs ] line: '+line);
raise ValueError('Too few fields in [ pairs ] line: '+line)
if fields[2] != '1':
raise ValueError('Unsupported function type in [ pairs ] line: '+line);
raise ValueError('Unsupported function type in [ pairs ] line: '+line)
self._currentMoleculeType.pairs.append(fields)
def _processCmap(self, line):
......@@ -352,14 +352,14 @@ class GromacsTopFile(object):
raise ValueError('Found [ cmap ] section before [ moleculetype ]')
fields = line.split()
if len(fields) < 6:
raise ValueError('Too few fields in [ cmap ] line: '+line);
raise ValueError('Too few fields in [ cmap ] line: '+line)
self._currentMoleculeType.cmaps.append(fields)
def _processAtomType(self, line):
"""Process a line in the [ atomtypes ] category."""
fields = line.split()
if len(fields) < 6:
raise ValueError('Too few fields in [ atomtypes ] line: '+line);
raise ValueError('Too few fields in [ atomtypes ] line: '+line)
if len(fields[3]) == 1:
# Bonded type and atomic number are both missing.
fields.insert(1, None)
......@@ -377,27 +377,27 @@ class GromacsTopFile(object):
"""Process a line in the [ bondtypes ] category."""
fields = line.split()
if len(fields) < 5:
raise ValueError('Too few fields in [ bondtypes ] line: '+line);
raise ValueError('Too few fields in [ bondtypes ] line: '+line)
if fields[2] != '1':
raise ValueError('Unsupported function type in [ bondtypes ] line: '+line);
raise ValueError('Unsupported function type in [ bondtypes ] line: '+line)
self._bondTypes[tuple(fields[:2])] = fields
def _processAngleType(self, line):
"""Process a line in the [ angletypes ] category."""
fields = line.split()
if len(fields) < 6:
raise ValueError('Too few fields in [ angletypes ] line: '+line);
raise ValueError('Too few fields in [ angletypes ] line: '+line)
if fields[3] not in ('1', '5'):
raise ValueError('Unsupported function type in [ angletypes ] line: '+line);
raise ValueError('Unsupported function type in [ angletypes ] line: '+line)
self._angleTypes[tuple(fields[:3])] = fields
def _processDihedralType(self, line):
"""Process a line in the [ dihedraltypes ] category."""
fields = line.split()
if len(fields) < 7:
raise ValueError('Too few fields in [ dihedraltypes ] line: '+line);
raise ValueError('Too few fields in [ dihedraltypes ] line: '+line)
if fields[4] not in ('1', '2', '3', '4', '9'):
raise ValueError('Unsupported function type in [ dihedraltypes ] line: '+line);
raise ValueError('Unsupported function type in [ dihedraltypes ] line: '+line)
key = tuple(fields[:5])
if fields[4] == '9' and key in self._dihedralTypes:
# There are multiple dihedrals defined for these atom types.
......@@ -409,25 +409,25 @@ class GromacsTopFile(object):
"""Process a line in the [ implicit_genborn_params ] category."""
fields = line.split()
if len(fields) < 6:
raise ValueError('Too few fields in [ implicit_genborn_params ] line: '+line);
raise ValueError('Too few fields in [ implicit_genborn_params ] line: '+line)
self._implicitTypes[fields[0]] = fields
def _processPairType(self, line):
"""Process a line in the [ pairtypes ] category."""
fields = line.split()
if len(fields) < 5:
raise ValueError('Too few fields in [ pairtypes] line: '+line);
raise ValueError('Too few fields in [ pairtypes] line: '+line)
if fields[2] != '1':
raise ValueError('Unsupported function type in [ pairtypes ] line: '+line);
raise ValueError('Unsupported function type in [ pairtypes ] line: '+line)
self._pairTypes[tuple(fields[:2])] = fields
def _processCmapType(self, line):
"""Process a line in the [ cmaptypes ] category."""
fields = line.split()
if len(fields) < 8 or len(fields) < 8+int(fields[6])*int(fields[7]):
raise ValueError('Too few fields in [ cmaptypes ] line: '+line);
raise ValueError('Too few fields in [ cmaptypes ] line: '+line)
if fields[5] != '1':
raise ValueError('Unsupported function type in [ cmaptypes ] line: '+line);
raise ValueError('Unsupported function type in [ cmaptypes ] line: '+line)
self._cmapTypes[tuple(fields[:5])] = fields
def __init__(self, file, periodicBoxVectors=None, unitCellDimensions=None, includeDir=None, defines=None):
......@@ -814,7 +814,7 @@ class GromacsTopFile(object):
map = []
for i in range(mapSize):
for j in range(mapSize):
map.append(float(params[8+mapSize*((j+mapSize/2)%mapSize)+((i+mapSize/2)%mapSize)]))
map.append(float(params[8+mapSize*((j+mapSize//2)%mapSize)+((i+mapSize//2)%mapSize)]))
map = tuple(map)
if map not in mapIndices:
mapIndices[map] = cmap.addMap(mapSize, map)
......
......@@ -34,8 +34,7 @@ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
from __future__ import absolute_import
from __future__ import print_function
from __future__ import absolute_import, print_function
#=============================================================================================
# GLOBAL IMPORTS
......@@ -553,87 +552,6 @@ class PrmtopLoader(object):
self._excludedAtoms.append(atomList)
return self._excludedAtoms
def getGBParms(self, gbmodel, elements):
"""Return list giving GB params, Radius and screening factor"""
gb_List=[]
radii=[float(r) for r in self._raw_data["RADII"]]
screen=[float(s) for s in self._raw_data["SCREEN"]]
if gbmodel == 'GBn2':
alpha = [0 for i in self._raw_data['RADII']]
beta = [0 for i in self._raw_data['RADII']]
gamma = [0 for i in self._raw_data['RADII']]
# Update screening parameters for GBn if specified
if gbmodel == 'GBn':
if elements is None:
raise Exception('GBn model requires element information')
for i, element in enumerate(elements):
if element is elem.carbon:
screen[i] = 0.48435382330
elif element is elem.hydrogen:
screen[i] = 1.09085413633
elif element is elem.nitrogen:
screen[i] = 0.700147318409
elif element is elem.oxygen:
screen[i] = 1.06557401132
elif element is elem.sulfur:
screen[i] = 0.602256336067
else:
screen[i] = 0.5
# radii is currently in Angstroms right now. GBn lookup tables
# only support radii between 1.0 and 2.0
if radii[i] < 1.0 or radii[i] > 2.0:
raise ValueError('GBn requires intrinsic radii between 1 and '
'2 Angstroms (%.3f found for atom %d)' %
(radii[i], i))
if gbmodel == 'GBn2':
if elements is None:
raise Exception('GBn2 model requires element information')
for i, element in enumerate(elements):
if element is elem.carbon:
screen[i] = 1.058554
alpha[i] = 0.733756
beta[i] = 0.506378
gamma[i] = 0.205844
elif element is elem.hydrogen:
screen[i] = 1.425952
alpha[i] = 0.788440
beta[i] = 0.798699
gamma[i] = 0.437334
elif element is elem.nitrogen:
screen[i] = 0.733599
alpha[i] = 0.503364
beta[i] = 0.316828
gamma[i] = 0.192915
elif element is elem.oxygen:
screen[i] = 1.061039
alpha[i] = 0.867814
beta[i] = 0.876635
gamma[i] = 0.387882
elif element is elem.sulfur:
screen[i] = -0.703469
alpha[i] = 0.867814
beta[i] = 0.876635
gamma[i] = 0.387882
else: # not optimized
screen[i] = 0.5
alpha[i] = 1.0
beta[i] = 0.8
gamma[i] = 4.85
# radii is currently in Angstroms right now. GBn lookup tables
# only support radii between 1.0 and 2.0
if radii[i] < 1.0 or radii[i] > 2.0:
raise ValueError('GBn2 requires intrinsic radii between 1 and '
'2 Angstroms (%.3f found for atom %d)' %
(radii[i], i))
lengthConversionFactor = units.angstrom.conversion_factor_to(units.nanometer)
if gbmodel == 'GBn2':
for rad, scr, alp, bet, gam in zip(radii, screen, alpha, beta, gamma):
gb_List.append((rad*lengthConversionFactor, scr, alp, bet, gam))
else:
for rad, scr in zip(radii, screen):
gb_List.append((rad*lengthConversionFactor, scr))
return gb_List
def getBoxBetaAndDimensions(self):
"""Return periodic boundary box beta angle and dimensions"""
beta=float(self._raw_data["BOX_DIMENSIONS"][0])
......@@ -1053,9 +971,6 @@ def readAmberSystem(topology, prmtop_filename=None, prmtop_loader=None, shake=No
cutoff = nonbondedCutoff
if units.is_quantity(cutoff):
cutoff = cutoff.value_in_unit(units.nanometers)
gb_parms = prmtop.getGBParms(gbmodel, elements)
if gbmodel != 'OBC2' or implicitSolventKappa != 0:
gb_parms = customgb.convertParameters(gb_parms, gbmodel)
if gbmodel == 'HCT':
gb = customgb.GBSAHCTForce(solventDielectric, soluteDielectric, 'ACE', cutoff, implicitSolventKappa)
elif gbmodel == 'OBC1':
......@@ -1072,7 +987,29 @@ def readAmberSystem(topology, prmtop_filename=None, prmtop_loader=None, shake=No
elif gbmodel == 'GBn2':
gb = customgb.GBSAGBn2Force(solventDielectric, soluteDielectric, 'ACE', cutoff, implicitSolventKappa)
else:
raise Exception("Illegal value specified for implicit solvent model")
raise ValueError("Illegal value specified for implicit solvent model")
if isinstance(gb, mm.GBSAOBCForce):
# Built-in GBSAOBCForce does not have getStandardParameters, so use
# the one from the equivalent CustomGBForce
gb_parms = customgb.GBSAOBC2Force.getStandardParameters(topology)
else:
gb_parms = type(gb).getStandardParameters(topology)
# Replace radii and screen, but screen *only* gets replaced by the
# prmtop contents for HCT, OBC1, and OBC2. GBn and GBn2 both override
# the prmtop screen factors from LEaP in sander and pmemd
if gbmodel in ('HCT', 'OBC1', 'OBC2'):
screen = [float(s) for s in prmtop._raw_data['SCREEN']]
else:
screen = [gb_parm[1] for gb_parm in gb_parms]
radii = [float(r)/10 for r in prmtop._raw_data['RADII']]
warned = False
for i, (r, s) in enumerate(zip(radii, screen)):
if abs(r - gb_parms[i][0]) > 1e-4 or abs(s - gb_parms[i][1]) > 1e-4:
if not warned:
warnings.warn('Non-optimal GB parameters detected for GB '
'model %s' % gbmodel)
warned = True
gb_parms[i][0], gb_parms[i][1] = r, s
for charge, gb_parm in zip(charges, gb_parms):
if gbmodel == 'OBC2' and implicitSolventKappa == 0:
......
......@@ -29,10 +29,18 @@ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
from __future__ import division
from __future__ import absolute_import
from __future__ import division, absolute_import
from collections import defaultdict
import copy
from simtk.openmm.app import element as E
from simtk.openmm import CustomGBForce, Continuous2DFunction
import simtk.unit as u
def strip_unit(value, unit):
if not u.is_quantity(value):
return value
return value.value_in_unit(unit)
d0=[2.26685,2.32548,2.38397,2.44235,2.50057,2.55867,2.61663,2.67444,
2.73212,2.78965,2.84705,2.9043,2.96141,3.0184,3.07524,3.13196,
......@@ -189,10 +197,145 @@ for i in range (len(d0)):
d0[i]=d0[i]/10
m0[i]=m0[i]*10
def _get_bonded_atom_list(topology):
""" Returns a list of atoms bonded to each other atom in a dict """
bondeds = defaultdict(list)
for a1, a2 in topology.bonds():
bondeds[a1].append(a2)
bondeds[a2].append(a1)
return bondeds
def _is_carboxylateO(atom, all_bonds):
if atom is not E.oxygen: return False
bondeds = all_bonds[atom]
if len(bondeds) != 1:
return False
if bondeds[0].element is not E.carbon:
return False
bondedsC = all_bonds[bondeds[0]]
if len(bondedsC) != 3:
return False
for a3 in bondedsC:
if a3 is atom: continue
if a3.element is E.oxygen:
break
else:
return False
# If we got here, must be a carboxylate
return True
def _bondi_radii(topology):
""" Sets the bondi radii """
radii = [0.0 for atom in topology.atoms()]
for i, atom in enumerate(topology.atoms()):
if atom.element is E.carbon:
radii[i] = 1.7
elif atom.element in (E.hydrogen, E.deuterium):
radii[i] = 1.2
elif atom.element is E.nitrogen:
radii[i] = 1.55
elif atom.element is E.oxygen:
radii[i] = 1.5
elif atom.element is E.fluorine:
radii[i] = 1.5
elif atom.element is E.silicon:
radii[i] = 2.1
elif atom.element is E.phosphorus:
radii[i] = 1.85
elif atom.element is E.sulfur:
radii[i] = 1.8
elif atom.element is E.chlorine:
radii[i] = 1.5
else:
radii[i] = 1.5
return radii # converted to nanometers above
def _mbondi_radii(topology):
""" Sets the mbondi radii """
radii = [0.0 for atom in topology.atoms()]
all_bonds = _get_bonded_atom_list(topology)
for i, atom in enumerate(topology.atoms()):
# Radius of H atom depends on element it is bonded to
if atom.element in (E.hydrogen, E.deuterium):
bondeds = all_bonds[atom]
if bondeds[0].element in (E.carbon, E.nitrogen):
radii[i] = 1.3
elif bondeds[0].type.atomic_number in (E.oxygen, E.sulfur):
radii[i] = 0.8
else:
radii[i] = 1.2
# Radius of C atom depends on what type it is
elif atom.element is E.carbon:
radii[i] = 1.7
# All other elements have fixed radii for all types/partners
elif atom.element is E.nitrogen:
radii[i] = 1.55
elif atom.element is E.oxygen:
radii[i] = 1.5
elif atom.element is E.fluorine:
radii[i] = 1.5
elif atom.element is E.silicon:
radii[i] = 2.1
elif atom.element is E.phosphorus:
radii[i] = 1.85
elif atom.element is E.sulfur:
radii[i] = 1.8
elif atom.element is E.chlorine:
radii[i] = 1.5
else:
radii[i] = 1.5
return radii # converted to nanometers above
def _mbondi2_radii(topology):
""" Sets the mbondi2 radii """
radii = [0.0 for atom in topology.atoms()]
all_bonds = _get_bonded_atom_list(topology)
for i, atom in enumerate(topology.atoms()):
# Radius of H atom depends on element it is bonded to
if atom.element in (E.hydrogen, E.deuterium):
bondeds = all_bonds[atom]
if bondeds[0].element is E.nitrogen:
radii[i] = 1.3
else:
radii[i] = 1.2
# Radius of C atom depends on what type it is
elif atom.element is E.carbon:
radii[i] = 1.7
# All other elements have fixed radii for all types/partners
elif atom.element is E.nitrogen:
radii[i] = 1.55
elif atom.element is E.oxygen:
radii[i] = 1.5
elif atom.element is E.fluorine:
radii[i] = 1.5
elif atom.element is E.silicon:
radii[i] = 2.1
elif atom.element == E.phosphorus:
radii[i] = 1.85
elif atom.element == E.sulfur:
radii[i] = 1.8
elif atom.element == E.chlorine:
radii[i] = 1.5
else:
radii[i] = 1.5
return radii # Converted to nanometers above
def _mbondi3_radii(topology):
""" Sets the mbondi3 radii """
radii = _mbondi2_radii(topology)
all_bonds = _get_bonded_atom_list(topology)
for i, atom in enumerate(topology.atoms()):
# carboxylate and HH/HE (ARG)
if _is_carboxylateO(atom, all_bonds):
radii[i] = 1.4
elif atom.residue.name == 'ARG':
if atom.name.startswith('HH') or atom.name.startswith('HE'):
radii[i] = 1.17
return radii # Converted to nanometers above
def _createEnergyTerms(force, solventDielectric, soluteDielectric, SA, cutoff, kappa, offset):
# Add the energy terms to the CustomGBForce. These are identical for all the GB models.
params = "; solventDielectric=%.16g; soluteDielectric=%.16g; kappa=%.16g; offset=%.16g" % (solventDielectric, soluteDielectric, kappa, offset)
if cutoff is not None:
params += "; cutoff=%.16g" % cutoff
......@@ -224,159 +367,239 @@ def _createEnergyTerms(force, solventDielectric, soluteDielectric, SA, cutoff, k
force.addEnergyTerm("-138.935485*(1/soluteDielectric-1/solventDielectric)*q1*q2*(1/f-"+str(1/cutoff)+");"
"f=sqrt(r^2+B1*B2*exp(-r^2/(4*B1*B2)))"+params, CustomGBForce.ParticlePairNoExclusions)
_SCREEN_PARAMETERS = { # normal, GBn, GBn2
E.hydrogen : (0.85, 1.09085413633, 1.425952),
E.carbon : (0.72, 0.48435382330, 1.058554),
E.nitrogen : (0.79, 0.700147318409, 0.733599),
E.oxygen : (0.85, 1.06557401132, 1.061039),
E.fluorine : (0.88, 0.5, 0.5),
E.phosphorus : (0.86, 0.5, 0.5),
E.sulfur : (0.96, 0.602256336067, -0.703469),
None : (0.8, 0.5, 0.5) # default
}
_SCREEN_PARAMETERS[E.deuterium] = _SCREEN_PARAMETERS[E.hydrogen]
def _screen_parameter(atom):
if atom.element in _SCREEN_PARAMETERS:
return _SCREEN_PARAMETERS[atom.element]
return _SCREEN_PARAMETERS[None]
class CustomAmberGBForce(CustomGBForce):
OFFSET = 0.009
RADIUS_ARG_POSITION = 1
SCREEN_POSITION = 2
def __init__(self, *args, **kwargs):
raise NotImplementedError('Cannot instantiate ABC')
def addParticle(self, params):
params = copy.deepcopy(params)
params[self.RADIUS_ARG_POSITION] = strip_unit(params[self.RADIUS_ARG_POSITION], u.nanometer) - self.OFFSET
params[self.SCREEN_POSITION] *= params[self.RADIUS_ARG_POSITION]
CustomGBForce.addParticle(self, params)
return params
def setParticleParameters(self, idx, params):
params = copy.deepcopy(params)
params[self.RADIUS_ARG_POSITION] = strip_unit(params[self.RADIUS_ARG_POSITION], u.nanometer) - self.OFFSET
params[self.SCREEN_POSITION] *= params[self.RADIUS_ARG_POSITION]
CustomGBForce.addParticle(self, params)
return params
@staticmethod
def getStandardParameters(topology):
""" Gets list of standard parameters for this GB model based on an input Topology
Parameters
----------
topology : simtk.openmm.app.Topology
Topology of the system to get parameters for
Returns
-------
list of float
List of all parameters needed for this GB model. These can be passed
to addParticle or setParticleParameters after the charge is inserted
at the beginning of the list
"""
raise NotImplementedError('Must override getStandardParameters in subclasses')
"""
Amber Equivalent: igb = 1
"""
class GBSAHCTForce(CustomAmberGBForce):
def GBSAHCTForce(solventDielectric=78.5, soluteDielectric=1, SA=None,
def __init__(self, solventDielectric=78.5, soluteDielectric=1, SA=None,
cutoff=None, kappa=0.0):
custom = CustomGBForce()
custom.addPerParticleParameter("q")
custom.addPerParticleParameter("or") # Offset radius
custom.addPerParticleParameter("sr") # Scaled offset radius
custom.addComputedValue("I", "step(r+sr2-or1)*0.5*(1/L-1/U+0.25*(r-sr2^2/r)*(1/(U^2)-1/(L^2))+0.5*log(L/U)/r);"
"U=r+sr2;"
"L=max(or1, D);"
"D=abs(r-sr2)", CustomGBForce.ParticlePairNoExclusions)
custom.addComputedValue("B", "1/(1/or-I)", CustomGBForce.SingleParticle)
_createEnergyTerms(custom, solventDielectric, soluteDielectric, SA, cutoff, kappa, 0.009)
return custom
CustomGBForce.__init__(self)
self.addPerParticleParameter("q")
self.addPerParticleParameter("or") # Offset radius
self.addPerParticleParameter("sr") # Scaled offset radius
self.addComputedValue("I", "step(r+sr2-or1)*0.5*(1/L-1/U+0.25*(r-sr2^2/r)*(1/(U^2)-1/(L^2))+0.5*log(L/U)/r);"
"U=r+sr2;"
"L=max(or1, D);"
"D=abs(r-sr2)",
CustomGBForce.ParticlePairNoExclusions)
self.addComputedValue("B", "1/(1/or-I)", CustomGBForce.SingleParticle)
_createEnergyTerms(self, solventDielectric, soluteDielectric, SA, cutoff, kappa, 0.009)
@staticmethod
def getStandardParameters(topology):
radii = [[x/10] for x in _mbondi_radii(topology)]
for i, atom in enumerate(topology.atoms()):
radii[i].append(_screen_parameter(atom)[0])
return radii
"""
Amber Equivalents: igb = 2
"""
def GBSAOBC1Force(solventDielectric=78.5, soluteDielectric=1, SA=None,
cutoff=None, kappa=0.0):
class GBSAOBC1Force(CustomAmberGBForce):
def __init__(self, solventDielectric=78.5, soluteDielectric=1, SA=None,
cutoff=None, kappa=0.0):
custom = CustomGBForce()
CustomGBForce.__init__(self)
custom.addPerParticleParameter("q")
custom.addPerParticleParameter("or") # Offset radius
custom.addPerParticleParameter("sr") # Scaled offset radius
custom.addComputedValue("I", "step(r+sr2-or1)*0.5*(1/L-1/U+0.25*(r-sr2^2/r)*(1/(U^2)-1/(L^2))+0.5*log(L/U)/r);"
"U=r+sr2;"
"L=max(or1, D);"
"D=abs(r-sr2)", CustomGBForce.ParticlePairNoExclusions)
self.addPerParticleParameter("q")
self.addPerParticleParameter("or") # Offset radius
self.addPerParticleParameter("sr") # Scaled offset radius
self.addComputedValue("I", "step(r+sr2-or1)*0.5*(1/L-1/U+0.25*(r-sr2^2/r)*(1/(U^2)-1/(L^2))+0.5*log(L/U)/r);"
"U=r+sr2;"
"L=max(or1, D);"
"D=abs(r-sr2)", CustomGBForce.ParticlePairNoExclusions)
custom.addComputedValue("B", "1/(1/or-tanh(0.8*psi+2.909125*psi^3)/radius);"
"psi=I*or; radius=or+offset; offset=0.009", CustomGBForce.SingleParticle)
_createEnergyTerms(custom, solventDielectric, soluteDielectric, SA, cutoff, kappa, 0.009)
return custom
self.addComputedValue("B", "1/(1/or-tanh(0.8*psi+2.909125*psi^3)/radius);"
"psi=I*or; radius=or+offset; offset=0.009", CustomGBForce.SingleParticle)
_createEnergyTerms(self, solventDielectric, soluteDielectric, SA, cutoff, kappa, 0.009)
@staticmethod
def getStandardParameters(topology):
radii = [[x/10] for x in _mbondi2_radii(topology)]
for i, atom in enumerate(topology.atoms()):
radii[i].append(_screen_parameter(atom)[0])
return radii
"""
Amber Equivalents: igb = 5
"""
def GBSAOBC2Force(solventDielectric=78.5, soluteDielectric=1, SA=None,
cutoff=None, kappa=0.0):
class GBSAOBC2Force(GBSAOBC1Force):
def __init__(self, solventDielectric=78.5, soluteDielectric=1, SA=None,
cutoff=None, kappa=0.0):
custom = CustomGBForce()
CustomGBForce.__init__(self)
custom.addPerParticleParameter("q")
custom.addPerParticleParameter("or") # Offset radius
custom.addPerParticleParameter("sr") # Scaled offset radius
custom.addComputedValue("I", "step(r+sr2-or1)*0.5*(1/L-1/U+0.25*(r-sr2^2/r)*(1/(U^2)-1/(L^2))+0.5*log(L/U)/r);"
"U=r+sr2;"
"L=max(or1, D);"
"D=abs(r-sr2)", CustomGBForce.ParticlePairNoExclusions)
self.addPerParticleParameter("q")
self.addPerParticleParameter("or") # Offset radius
self.addPerParticleParameter("sr") # Scaled offset radius
self.addComputedValue("I", "step(r+sr2-or1)*0.5*(1/L-1/U+0.25*(r-sr2^2/r)*(1/(U^2)-1/(L^2))+0.5*log(L/U)/r);"
"U=r+sr2;"
"L=max(or1, D);"
"D=abs(r-sr2)", CustomGBForce.ParticlePairNoExclusions)
custom.addComputedValue("B", "1/(1/or-tanh(psi-0.8*psi^2+4.85*psi^3)/radius);"
"psi=I*or; radius=or+offset; offset=0.009", CustomGBForce.SingleParticle)
_createEnergyTerms(custom, solventDielectric, soluteDielectric, SA, cutoff, kappa, 0.009)
return custom
self.addComputedValue("B", "1/(1/or-tanh(psi-0.8*psi^2+4.85*psi^3)/radius);"
"psi=I*or; radius=or+offset; offset=0.009", CustomGBForce.SingleParticle)
_createEnergyTerms(self, solventDielectric, soluteDielectric, SA, cutoff, kappa, 0.009)
"""
Amber Equivalents: igb = 7
"""
def GBSAGBnForce(solventDielectric=78.5, soluteDielectric=1, SA=None,
cutoff=None, kappa=0.0):
class GBSAGBnForce(CustomAmberGBForce):
"""
Indexing for tables:
input: radius1, radius2
index = (radius2*200-20)*21 + (radius1*200-20)
output: index of desired value in row-by-row, 1D version of Tables 3 & 4
"""
custom = CustomGBForce()
custom.addPerParticleParameter("q")
custom.addPerParticleParameter("or") # Offset radius
custom.addPerParticleParameter("sr") # Scaled offset radius
custom.addTabulatedFunction("getd0", Continuous2DFunction(21, 21, d0, 0.1, 0.2, 0.1, 0.2))
custom.addTabulatedFunction("getm0", Continuous2DFunction(21, 21, m0, 0.1, 0.2, 0.1, 0.2))
custom.addComputedValue("I", "Ivdw+neckScale*Ineck;"
"Ineck=step(radius1+radius2+neckCut-r)*getm0(radius1,radius2)/(1+100*(r-getd0(radius1,radius2))^2+0.3*1000000*(r-getd0(radius1,radius2))^6);"
"Ivdw=step(r+sr2-or1)*0.5*(1/L-1/U+0.25*(r-sr2^2/r)*(1/(U^2)-1/(L^2))+0.5*log(L/U)/r);"
"U=r+sr2;"
"L=max(or1, D);"
"D=abs(r-sr2);"
"radius1=or1+offset; radius2=or2+offset;"
"neckScale=0.361825; neckCut=0.68; offset=0.009", CustomGBForce.ParticlePairNoExclusions)
def __init__(self, solventDielectric=78.5, soluteDielectric=1, SA=None,
cutoff=None, kappa=0.0):
custom.addComputedValue("B", "1/(1/or-tanh(1.09511284*psi-1.907992938*psi^2+2.50798245*psi^3)/radius);"
"psi=I*or; radius=or+offset; offset=0.009", CustomGBForce.SingleParticle)
_createEnergyTerms(custom, solventDielectric, soluteDielectric, SA, cutoff, kappa, 0.009)
return custom
CustomGBForce.__init__(self)
self.addPerParticleParameter("q")
self.addPerParticleParameter("or") # Offset radius
self.addPerParticleParameter("sr") # Scaled offset radius
self.addTabulatedFunction("getd0", Continuous2DFunction(21, 21, d0, 0.1, 0.2, 0.1, 0.2))
self.addTabulatedFunction("getm0", Continuous2DFunction(21, 21, m0, 0.1, 0.2, 0.1, 0.2))
self.addComputedValue("I", "Ivdw+neckScale*Ineck;"
"Ineck=step(radius1+radius2+neckCut-r)*getm0(radius1,radius2)/(1+100*(r-getd0(radius1,radius2))^2+0.3*1000000*(r-getd0(radius1,radius2))^6);"
"Ivdw=step(r+sr2-or1)*0.5*(1/L-1/U+0.25*(r-sr2^2/r)*(1/(U^2)-1/(L^2))+0.5*log(L/U)/r);"
"U=r+sr2;"
"L=max(or1, D);"
"D=abs(r-sr2);"
"radius1=or1+offset; radius2=or2+offset;"
"neckScale=0.361825; neckCut=0.68; offset=0.009", CustomGBForce.ParticlePairNoExclusions)
self.addComputedValue("B", "1/(1/or-tanh(1.09511284*psi-1.907992938*psi^2+2.50798245*psi^3)/radius);"
"psi=I*or; radius=or+offset; offset=0.009", CustomGBForce.SingleParticle)
_createEnergyTerms(self, solventDielectric, soluteDielectric, SA, cutoff, kappa, 0.009)
def addParticle(self, parameters):
parameters = CustomAmberGBForce.addParticle(self, parameters)
if parameters[1] + self.OFFSET < 0.1 or parameters[1] + self.OFFSET > 0.2:
raise ValueError('Radii must be between 1 and 2 Angstroms for neck lookup')
def setParticleParameters(self, idx, parameters):
parameters = CustomAmberGBForce.setParticleParameters(self, idx, parameters)
if parameters[1] < 0.1 or parameters[1] > 0.2:
raise ValueError('Radii must be between 1 and 2 Angstroms for neck lookup')
@staticmethod
def getStandardParameters(topology):
radii = [[x/10] for x in _bondi_radii(topology)]
for i, atom in enumerate(topology.atoms()):
radii[i].append(_screen_parameter(atom)[1])
return radii
"""
Amber Equivalents: igb = 8
"""
def GBSAGBn2Force(solventDielectric=78.5, soluteDielectric=1, SA=None,
cutoff=None, kappa=0.0):
"""
Indexing for tables:
input: radius1, radius2
index = (radius2*200-20)*21 + (radius1*200-20)
output: index of desired value in row-by-row, 1D version of Tables 3 & 4
"""
class GBSAGBn2Force(GBSAGBnForce):
OFFSET = 0.0195141
custom = CustomGBForce()
custom.addPerParticleParameter("q")
custom.addPerParticleParameter("or") # Offset radius
custom.addPerParticleParameter("sr") # Scaled offset radius
custom.addPerParticleParameter("alpha")
custom.addPerParticleParameter("beta")
custom.addPerParticleParameter("gamma")
custom.addTabulatedFunction("getd0", Continuous2DFunction(21, 21, d0, 0.1, 0.2, 0.1, 0.2))
custom.addTabulatedFunction("getm0", Continuous2DFunction(21, 21, m0, 0.1, 0.2, 0.1, 0.2))
custom.addComputedValue("I", "Ivdw+neckScale*Ineck;"
"Ineck=step(radius1+radius2+neckCut-r)*getm0(radius1,radius2)/(1+100*(r-getd0(radius1,radius2))^2+0.3*1000000*(r-getd0(radius1,radius2))^6);"
"Ivdw=step(r+sr2-or1)*0.5*(1/L-1/U+0.25*(r-sr2^2/r)*(1/(U^2)-1/(L^2))+0.5*log(L/U)/r);"
"U=r+sr2;"
"L=max(or1, D);"
"D=abs(r-sr2);"
"radius1=or1+offset; radius2=or2+offset;"
"neckScale=0.826836; neckCut=0.68; offset=0.0195141", CustomGBForce.ParticlePairNoExclusions)
custom.addComputedValue("B", "1/(1/or-tanh(alpha*psi-beta*psi^2+gamma*psi^3)/radius);"
"psi=I*or; radius=or+offset; offset=0.0195141", CustomGBForce.SingleParticle)
_createEnergyTerms(custom, solventDielectric, soluteDielectric, SA, cutoff, kappa, 0.0195141)
return custom
def convertParameters(params, gbmodel):
"""Convert the GB parameters from the file into the values expected by the appropriate CustomGBForce."""
if gbmodel == 'GBn2':
offset = 0.0195141
else:
offset = 0.009
def __init__(self, solventDielectric=78.5, soluteDielectric=1, SA=None,
cutoff=None, kappa=0.0):
for p in params:
newParam = list(p)
newParam[0] -= offset
newParam[1] *= newParam[0]
yield newParam
CustomGBForce.__init__(self)
self.addPerParticleParameter("q")
self.addPerParticleParameter("or") # Offset radius
self.addPerParticleParameter("sr") # Scaled offset radius
self.addPerParticleParameter("alpha")
self.addPerParticleParameter("beta")
self.addPerParticleParameter("gamma")
self.addTabulatedFunction("getd0", Continuous2DFunction(21, 21, d0, 0.1, 0.2, 0.1, 0.2))
self.addTabulatedFunction("getm0", Continuous2DFunction(21, 21, m0, 0.1, 0.2, 0.1, 0.2))
self.addComputedValue("I", "Ivdw+neckScale*Ineck;"
"Ineck=step(radius1+radius2+neckCut-r)*getm0(radius1,radius2)/(1+100*(r-getd0(radius1,radius2))^2+0.3*1000000*(r-getd0(radius1,radius2))^6);"
"Ivdw=step(r+sr2-or1)*0.5*(1/L-1/U+0.25*(r-sr2^2/r)*(1/(U^2)-1/(L^2))+0.5*log(L/U)/r);"
"U=r+sr2;"
"L=max(or1, D);"
"D=abs(r-sr2);"
"radius1=or1+offset; radius2=or2+offset;"
"neckScale=0.826836; neckCut=0.68; offset=0.0195141", CustomGBForce.ParticlePairNoExclusions)
self.addComputedValue("B", "1/(1/or-tanh(alpha*psi-beta*psi^2+gamma*psi^3)/radius);"
"psi=I*or; radius=or+offset; offset=0.0195141", CustomGBForce.SingleParticle)
_createEnergyTerms(self, solventDielectric, soluteDielectric, SA, cutoff, kappa, 0.0195141)
@staticmethod
def getStandardParameters(topology):
radii = [[x/10] for x in _mbondi3_radii(topology)]
for i, atom in enumerate(topology.atoms()):
radii[i].append(_screen_parameter(atom)[2])
if atom.element in (E.hydrogen, E.deuterium):
radii[i].extend([0.788440, 0.798699, 0.437334])
elif atom.element is E.carbon:
radii[i].extend([0.733756, 0.506378, 0.205844])
elif atom.element is E.nitrogen:
radii[i].extend([0.503364, 0.316828, 0.192915])
elif atom.element is E.oxygen:
radii[i].extend([0.867814, 0.876635, 0.387882])
elif atom.element is E.sulfur:
radii[i].extend([0.867814, 0.876635, 0.387882])
else:
radii[i].extend([0.8, 4.85, 0.5])
return radii
......@@ -123,7 +123,7 @@ class PdbStructure(object):
"""
def __init__(self, input_stream, load_all_models=False):
def __init__(self, input_stream, load_all_models=False, extraParticleIdentifier='EP'):
"""Create a PDB model from a PDB file stream.
Parameters
......@@ -135,9 +135,12 @@ class PdbStructure(object):
load_all_models : bool
Whether to load every model of an NMR structure or trajectory, or
just load the first model, to save memory.
extraParticleIdentifier : string='EP'
if this value appears in the element column for an ATOM record, the Atom's element will be set to 'EP' to mark it as an extra particle
"""
# initialize models
self.load_all_models = load_all_models
self.extraParticleIdentifier = extraParticleIdentifier
self.models = []
self._current_model = None
self.default_model = None
......@@ -157,7 +160,7 @@ class PdbStructure(object):
pdb_line = pdb_line.decode('utf-8')
# Look for atoms
if (pdb_line.find("ATOM ") == 0) or (pdb_line.find("HETATM") == 0):
self._add_atom(Atom(pdb_line, self))
self._add_atom(Atom(pdb_line, self, self.extraParticleIdentifier))
# Notice MODEL punctuation, for the next level of detail
# in the structure->model->chain->residue->atom->position hierarchy
elif (pdb_line.find("MODEL") == 0):
......@@ -682,7 +685,7 @@ class Residue(object):
class Atom(object):
"""Atom represents one atom in a PDB structure.
"""
def __init__(self, pdb_line, pdbstructure=None):
def __init__(self, pdb_line, pdbstructure=None, extraParticleIdentifier='EP'):
"""Create a new pdb.Atom from an ATOM or HETATM line.
Example line:
......@@ -795,11 +798,14 @@ class Atom(object):
try: self.formal_charge = int(pdb_line[78:80])
except ValueError: self.formal_charge = None
# figure out atom element
try:
# Try to find a sensible element symbol from columns 76-77
self.element = element.get_by_symbol(self.element_symbol)
except KeyError:
self.element = None
if self.element_symbol == extraParticleIdentifier:
self.element = 'EP'
else:
try:
# Try to find a sensible element symbol from columns 76-77
self.element = element.get_by_symbol(self.element_symbol)
except KeyError:
self.element = None
if pdbstructure is not None:
pdbstructure._next_atom_number = self.serial_number+1
pdbstructure._next_residue_number = self.residue_number+1
......
......@@ -6,7 +6,7 @@ Simbios, the NIH National Center for Physics-Based Simulation of
Biological Structures at Stanford, funded under the NIH Roadmap for
Medical Research, grant U54 GM072970. See https://simtk.org.
Portions copyright (c) 2012-2015 Stanford University and the Authors.
Portions copyright (c) 2012-2016 Stanford University and the Authors.
Authors: Peter Eastman
Contributors:
......@@ -35,7 +35,7 @@ __author__ = "Peter Eastman"
__version__ = "1.0"
from simtk.openmm.app import Topology, PDBFile, ForceField
from simtk.openmm.app.forcefield import HAngles, AllBonds, _createResidueSignature, _matchResidue, DrudeGenerator
from simtk.openmm.app.forcefield import HAngles, AllBonds, CutoffNonPeriodic, _createResidueSignature, _matchResidue, DrudeGenerator
from simtk.openmm.app.topology import Residue
from simtk.openmm.vec3 import Vec3
from simtk.openmm import System, Context, NonbondedForce, CustomNonbondedForce, HarmonicBondForce, HarmonicAngleForce, VerletIntegrator, LocalEnergyMinimizer
......@@ -264,7 +264,7 @@ class Modeller(object):
2. Water molecules are removed if their distance to any solute atom is less than the sum of their van der Waals radii.
3. If the solute is charged and neutralize=True, enough positive or negative ions are added to neutralize it. Each ion is added by
randomly selecting a water molecule and replacing it with the ion.
4. Ion pairs are added to give the requested total ionic strength.
4. Ion pairs are added to give the requested total ionic strength. Note that only monovalent ions are currently supported.
The box size can be specified in any of several ways:
......@@ -298,6 +298,7 @@ class Modeller(object):
ionicStrength : concentration=0*molar
the total concentration of ions (both positive and negative) to add. This
does not include ions that are added to neutralize the system.
Note that only monovalent ions are currently supported.
neutralize : bool=True
whether to add ions to neutralize the system
"""
......@@ -344,8 +345,11 @@ class Modeller(object):
elif padding is not None:
if is_quantity(padding):
padding = padding.value_in_unit(nanometer)
maxSize = max(max((pos[i] for pos in self.positions))-min((pos[i] for pos in self.positions)) for i in range(3))
maxSize = maxSize.value_in_unit(nanometer)
if len(self.positions) == 0:
maxSize = 0
else:
maxSize = max(max((pos[i] for pos in self.positions))-min((pos[i] for pos in self.positions)) for i in range(3))
maxSize = maxSize.value_in_unit(nanometer)
box = (maxSize+2*padding)*Vec3(1, 1, 1)
vectors = (Vec3(maxSize+2*padding, 0, 0), Vec3(0, maxSize+2*padding, 0), Vec3(0, 0, maxSize+2*padding))
else:
......@@ -522,7 +526,7 @@ class Modeller(object):
# Add ions based on the desired ionic strength.
numIons = len(addedWaters)*ionicStrength/(55.4*molar) # Pure water is about 55.4 molar (depending on temperature)
numPairs = int(floor(numIons/2+0.5))
numPairs = int(floor(numIons+0.5))
for i in range(numPairs):
addIon(positiveElement)
for i in range(numPairs):
......@@ -614,6 +618,7 @@ class Modeller(object):
HID: Neutral form with a hydrogen on the ND1 atom
HIE: Neutral form with a hydrogen on the NE2 atom
HIP: Positively charged form with hydrogens on both ND1 and NE2
HIN: Negatively charged form without a hydrogen on either ND1 or NE2
Lysine:
LYN: Neutral form with two hydrogens on the zeta nitrogen
......@@ -625,9 +630,14 @@ class Modeller(object):
2. Any Cysteine that participates in a disulfide bond uses the CYX variant regardless of pH.
3. For a neutral Histidine residue, the HID or HIE variant is selected based on which one forms a better hydrogen bond.
You can override these rules by explicitly specifying a variant for any residue. Also keep in mind that this
function will only add hydrogens. It will never remove ones that are already present in the model, regardless
of the specified pH.
You can override these rules by explicitly specifying a variant for any residue. To do that, provide a list for the
'variants' parameter, and set the corresponding element to the name of the variant to use.
A special case is when the model already contains a hydrogen that should not be present in the desired variant.
If you explicitly specify a variant using the 'variants' parameter, the residue will be modified to match the
desired variant, removing hydrogens if necessary. On the other hand, for residues whose variant is selected
automatically, this function will only add hydrogens. It will never remove ones that are already present in the
model, regardless of the specified pH.
Definitions for standard amino acids and nucleotides are built in. You can call loadHydrogenDefinitions() to load
additional definitions for other residue types.
......@@ -771,6 +781,7 @@ class Modeller(object):
if variant is not None and variant not in spec.variants:
raise ValueError('Illegal variant for %s residue: %s' % (residue.name, variant))
actualVariants[residue.index] = variant
removeExtraHydrogens = (variants[residue.index] is not None)
# Make a list of hydrogens that should be present in the residue.
......@@ -783,6 +794,11 @@ class Modeller(object):
# Loop over atoms in the residue, adding them to the new topology along with required hydrogens.
for parent in residue.atoms():
# Check whether this is a hydrogen that should be removed.
if removeExtraHydrogens and parent.element == elem.hydrogen and not any(parent.name == h.name for h in hydrogens):
continue
# Add the atom.
newAtom = newTopology.addAtom(parent.name, parent.element, newResidue)
......@@ -844,7 +860,7 @@ class Modeller(object):
if forcefield is not None:
# Use the ForceField the user specified.
system = forcefield.createSystem(newTopology, rigidWater=False)
system = forcefield.createSystem(newTopology, rigidWater=False, nonbondedMethod=CutoffNonPeriodic)
atoms = list(newTopology.atoms())
for i in range(system.getNumParticles()):
if atoms[i].element != elem.hydrogen:
......@@ -856,6 +872,8 @@ class Modeller(object):
system = System()
nonbonded = CustomNonbondedForce('100/((r/0.1)^4+1)')
nonbonded.setNonbondedMethod(CustomNonbondedForce.CutoffNonPeriodic);
nonbonded.setCutoffDistance(1*nanometer)
bonds = HarmonicBondForce()
angles = HarmonicAngleForce()
system.addForce(nonbonded)
......@@ -1091,7 +1109,7 @@ class Modeller(object):
else:
a2 = newAtoms[matchingAtoms[atom2]]
newTopology.addBond(a1, a2)
for bond in self.topology.bonds():
if bond[0] in newAtoms and bond[1] in newAtoms:
newTopology.addBond(newAtoms[bond[0]], newAtoms[bond[1]])
......@@ -1123,6 +1141,6 @@ class Modeller(object):
delta *= (distance-length)/length
newPositions[atom1] -= weights[0]*delta
newPositions[atom2] += weights[1]*delta
self.topology = newTopology
self.positions = newPositions
......@@ -6,7 +6,7 @@ Simbios, the NIH National Center for Physics-Based Simulation of
Biological Structures at Stanford, funded under the NIH Roadmap for
Medical Research, grant U54 GM072970. See https://simtk.org.
Portions copyright (c) 2012-2015 Stanford University and the Authors.
Portions copyright (c) 2012-2016 Stanford University and the Authors.
Authors: Peter Eastman
Contributors:
......@@ -58,8 +58,11 @@ class PDBFile(object):
_residueNameReplacements = {}
_atomNameReplacements = {}
_standardResidues = ['ALA', 'ASN', 'CYS', 'GLU', 'HIS', 'LEU', 'MET', 'PRO', 'THR', 'TYR',
'ARG', 'ASP', 'GLN', 'GLY', 'ILE', 'LYS', 'PHE', 'SER', 'TRP', 'VAL',
'A', 'G', 'C', 'U', 'I', 'DA', 'DG', 'DC', 'DT', 'DI', 'HOH']
def __init__(self, file):
def __init__(self, file, extraParticleIdentifier='EP'):
"""Load a PDB file.
The atom positions and Topology can be retrieved by calling getPositions() and getTopology().
......@@ -68,7 +71,13 @@ class PDBFile(object):
----------
file : string
the name of the file to load
extraParticleIdentifier : string='EP'
if this value appears in the element column for an ATOM record, the Atom's element will be set to None to mark it as an extra particle
"""
metalElements = ['Al','As','Ba','Ca','Cd','Ce','Co','Cs','Cu','Dy','Fe','Gd','Hg','Ho','In','Ir','K','Li','Mg',
'Mn','Mo','Na','Ni','Pb','Pd','Pt','Rb','Rh','Sm','Sr','Te','Tl','V','W','Yb','Zn']
top = Topology()
## The Topology read from the PDB file
self.topology = top
......@@ -83,7 +92,7 @@ class PDBFile(object):
if isinstance(file, str):
inputfile = open(file)
own_handle = True
pdb = PdbStructure(inputfile, load_all_models=True)
pdb = PdbStructure(inputfile, load_all_models=True, extraParticleIdentifier=extraParticleIdentifier)
if own_handle:
inputfile.close()
PDBFile._loadNameReplacementTables()
......@@ -108,7 +117,9 @@ class PDBFile(object):
atomName = atomReplacements[atomName]
atomName = atomName.strip()
element = atom.element
if element is None:
if element == 'EP':
element = None
elif element is None:
# Try to guess the element.
upper = atomName.upper()
......@@ -151,14 +162,22 @@ class PDBFile(object):
self.topology.createDisulfideBonds(self.positions)
self._numpyPositions = None
# Add bonds based on CONECT records.
# Add bonds based on CONECT records. Bonds between metals of elements specified in metalElements and residues in standardResidues are not added.
connectBonds = []
for connect in pdb.models[-1].connects:
i = connect[0]
for j in connect[1:]:
if i in atomByNumber and j in atomByNumber:
connectBonds.append((atomByNumber[i], atomByNumber[j]))
if i in atomByNumber and j in atomByNumber:
if atomByNumber[i].element is not None and atomByNumber[j].element is not None:
if atomByNumber[i].element.symbol not in metalElements and atomByNumber[j].element.symbol not in metalElements:
connectBonds.append((atomByNumber[i], atomByNumber[j]))
elif atomByNumber[i].element.symbol in metalElements and atomByNumber[j].residue.name not in PDBFile._standardResidues:
connectBonds.append((atomByNumber[i], atomByNumber[j]))
elif atomByNumber[j].element.symbol in metalElements and atomByNumber[i].residue.name not in PDBFile._standardResidues:
connectBonds.append((atomByNumber[i], atomByNumber[j]))
else:
connectBonds.append((atomByNumber[i], atomByNumber[j]))
if len(connectBonds) > 0:
# Only add bonds that don't already exist.
existingBonds = set(top.bonds())
......@@ -237,7 +256,7 @@ class PDBFile(object):
map[atom.attrib[id]] = name
@staticmethod
def writeFile(topology, positions, file=sys.stdout, keepIds=False):
def writeFile(topology, positions, file=sys.stdout, keepIds=False, extraParticleIdentifier=' '):
"""Write a PDB file containing a single model.
Parameters
......@@ -253,9 +272,11 @@ class PDBFile(object):
rather than generating new ones. Warning: It is up to the caller to
make sure these are valid IDs that satisfy the requirements of the
PDB format. Otherwise, the output file will be invalid.
extraParticleIdentifier : string=' '
String to write in the element column of the ATOM records for atoms whose element is None (extra particles)
"""
PDBFile.writeHeader(topology, file)
PDBFile.writeModel(topology, positions, file, keepIds=keepIds)
PDBFile.writeModel(topology, positions, file, keepIds=keepIds, extraParticleIdentifier=extraParticleIdentifier)
PDBFile.writeFooter(topology, file)
@staticmethod
......@@ -278,7 +299,7 @@ class PDBFile(object):
a*10, b*10, c*10, alpha*RAD_TO_DEG, beta*RAD_TO_DEG, gamma*RAD_TO_DEG), file=file)
@staticmethod
def writeModel(topology, positions, file=sys.stdout, modelIndex=None, keepIds=False):
def writeModel(topology, positions, file=sys.stdout, modelIndex=None, keepIds=False, extraParticleIdentifier=' '):
"""Write out a model to a PDB file.
Parameters
......@@ -297,6 +318,8 @@ class PDBFile(object):
rather than generating new ones. Warning: It is up to the caller to
make sure these are valid IDs that satisfy the requirements of the
PDB format. Otherwise, the output file will be invalid.
extraParticleIdentifier : string=' '
String to write in the element column of the ATOM records for atoms whose element is None (extra particles)
"""
if len(list(topology.atoms())) != len(positions):
......@@ -307,6 +330,8 @@ class PDBFile(object):
raise ValueError('Particle position is NaN')
if any(math.isinf(norm(pos)) for pos in positions):
raise ValueError('Particle position is infinite')
nonHeterogens = PDBFile._standardResidues[:]
nonHeterogens.remove('HOH')
atomIndex = 1
posIndex = 0
if modelIndex is not None:
......@@ -326,20 +351,24 @@ class PDBFile(object):
resId = res.id
else:
resId = "%4d" % ((resIndex+1)%10000)
if res.name in nonHeterogens:
recordName = "ATOM "
else:
recordName = "HETATM"
for atom in res.atoms():
if len(atom.name) < 4 and atom.name[:1].isalpha() and (atom.element is None or len(atom.element.symbol) < 2):
if atom.element is not None:
symbol = atom.element.symbol
else:
symbol = extraParticleIdentifier
if len(atom.name) < 4 and atom.name[:1].isalpha() and len(symbol) < 2:
atomName = ' '+atom.name
elif len(atom.name) > 4:
atomName = atom.name[:4]
else:
atomName = atom.name
coords = positions[posIndex]
if atom.element is not None:
symbol = atom.element.symbol
else:
symbol = ' '
line = "ATOM %5d %-4s %3s %s%4s %s%s%s 1.00 0.00 %2s " % (
atomIndex%100000, atomName, resName, chainName, resId, _format_83(coords[0]),
line = "%s%5d %-4s %3s %s%4s %s%s%s 1.00 0.00 %2s " % (
recordName, atomIndex%100000, atomName, resName, chainName, resId, _format_83(coords[0]),
_format_83(coords[1]), _format_83(coords[2]), symbol)
assert len(line) == 80, 'Fixed width overflow detected'
print(line, file=file)
......@@ -364,12 +393,9 @@ class PDBFile(object):
"""
# Identify bonds that should be listed as CONECT records.
standardResidues = ['ALA', 'ASN', 'CYS', 'GLU', 'HIS', 'LEU', 'MET', 'PRO', 'THR', 'TYR',
'ARG', 'ASP', 'GLN', 'GLY', 'ILE', 'LYS', 'PHE', 'SER', 'TRP', 'VAL',
'A', 'G', 'C', 'U', 'I', 'DA', 'DG', 'DC', 'DT', 'DI', 'HOH']
conectBonds = []
for atom1, atom2 in topology.bonds():
if atom1.residue.name not in standardResidues or atom2.residue.name not in standardResidues:
if atom1.residue.name not in PDBFile._standardResidues or atom2.residue.name not in PDBFile._standardResidues:
conectBonds.append((atom1, atom2))
elif atom1.name == 'SG' and atom2.name == 'SG' and atom1.residue.name == 'CYS' and atom2.residue.name == 'CYS':
conectBonds.append((atom1, atom2))
......
......@@ -36,6 +36,10 @@ import simtk.openmm as mm
import simtk.unit as unit
import sys
from datetime import datetime, timedelta
try:
string_types = (unicode, str)
except NameError:
string_types = (str,)
class Simulation(object):
"""Simulation provides a simplified API for running simulations with OpenMM and reporting results.
......@@ -52,40 +56,62 @@ class Simulation(object):
simulation.reporters.append(PDBReporter('output.pdb', 1000))
"""
def __init__(self, topology, system, integrator, platform=None, platformProperties=None):
def __init__(self, topology, system, integrator, platform=None, platformProperties=None, state=None):
"""Create a Simulation.
Parameters
----------
topology : Topology
A Topology describing the the system to simulate
system : System
The OpenMM System object to simulate
integrator : Integrator
The OpenMM Integrator to use for simulating the System
system : System or XML file name
The OpenMM System object to simulate (or the name of an XML file
with a serialized System)
integrator : Integrator or XML file name
The OpenMM Integrator to use for simulating the System (or the name
of an XML file with a serialized System)
platform : Platform=None
If not None, the OpenMM Platform to use
platformProperties : map=None
If not None, a set of platform-specific properties to pass to the
Context's constructor
state : XML file name=None
The name of an XML file containing a serialized State. If not None,
the information stored in state will be transferred to the generated
Simulation object.
"""
## The Topology describing the system being simulated
self.topology = topology
## The System being simulated
self.system = system
if isinstance(system, string_types):
with open(system, 'r') as f:
self.system = mm.XmlSerializer.deserialize(f.read())
else:
self.system = system
## The Integrator used to advance the simulation
self.integrator = integrator
if isinstance(integrator, string_types):
with open(integrator, 'r') as f:
self.integrator = mm.XmlSerializer.deserialize(f.read())
else:
self.integrator = integrator
## The index of the current time step
self.currentStep = 0
## A list of reporters to invoke during the simulation
self.reporters = []
if platform is None:
## The Context containing the current state of the simulation
self.context = mm.Context(system, integrator)
self.context = mm.Context(self.system, self.integrator)
elif platformProperties is None:
self.context = mm.Context(system, integrator, platform)
self.context = mm.Context(self.system, self.integrator, platform)
else:
self.context = mm.Context(system, integrator, platform, platformProperties)
self.context = mm.Context(self.system, self.integrator, platform, platformProperties)
if state is not None:
with open(state, 'r') as f:
self.context.setState(mm.XmlSerializer.deserialize(f.read()))
## Determines whether or not we are using PBC. Try from the System first,
## fall back to Topology if that doesn't work
try:
self._usesPBC = self.system.usesPeriodicBoundaryConditions()
except Exception: # OpenMM just raises Exception if it's not implemented everywhere
self._usesPBC = topology.getUnitCellDimensions() is not None
def minimizeEnergy(self, tolerance=10*unit.kilojoule/unit.mole, maxIterations=0):
"""Perform a local energy minimization on the system.
......@@ -186,7 +212,8 @@ class Simulation(object):
getForces = True
if next[4]:
getEnergy = True
state = self.context.getState(getPositions=getPositions, getVelocities=getVelocities, getForces=getForces, getEnergy=getEnergy, getParameters=True, enforcePeriodicBox=(self.topology.getUnitCellDimensions() is not None))
state = self.context.getState(getPositions=getPositions, getVelocities=getVelocities, getForces=getForces,
getEnergy=getEnergy, getParameters=True, enforcePeriodicBox=self._usesPBC)
for reporter, next in zip(self.reporters, nextReport):
if next[0] == nextSteps:
reporter.report(self, state)
......
......@@ -71,20 +71,17 @@ class Topology(object):
def getNumAtoms(self):
"""Return the number of atoms in the Topology.
"""
natom = self._numAtoms
return natom
return self._numAtoms
def getNumResidues(self):
"""Return the number of residues in the Topology.
"""
nres = self._numResidues
return nres
return self._numResidues
def getNumChains(self):
"""Return the number of chains in the Topology.
"""
nchain = len(self._chains)
return nchain
return len(self._chains)
def addChain(self, id=None):
"""Create a new Chain and add it to the Topology.
......@@ -371,6 +368,18 @@ class Residue(object):
"""Iterate over all Atoms in the Residue."""
return iter(self._atoms)
def bonds(self):
"""Iterate over all Bonds involving any atom in this residue."""
return ( bond for bond in self.chain.topology.bonds() if ((bond[0] in self._atoms) or (bond[1] in self._atoms)) )
def internal_bonds(self):
"""Iterate over all internal Bonds."""
return ( bond for bond in self.chain.topology.bonds() if ((bond[0] in self._atoms) and (bond[1] in self._atoms)) )
def external_bonds(self):
"""Iterate over all Bonds to external atoms."""
return ( bond for bond in self.chain.topology.bonds() if ((bond[0] in self._atoms) != (bond[1] in self._atoms)) )
def __len__(self):
return len(self._atoms)
......
......@@ -45,11 +45,9 @@ class MTSIntegrator(CustomIntegrator):
setForceGroup() on them) that should be evaluated at different frequencies. When
you create the integrator, you provide a tuple for each group specifying the index
of the force group and the frequency (as a fraction of the outermost time step) at
which to evaluate it. For example:
which to evaluate it. For example::
<pre>
integrator = MTSIntegrator(4*femtoseconds, [(0,1), (1,2), (2,8)])
</pre>
integrator = MTSIntegrator(4*femtoseconds, [(0,1), (1,2), (2,8)])
This specifies that the outermost time step is 4 fs, so each step of the integrator
will advance time by that much. It also says that force group 0 should be evaluated
......@@ -60,13 +58,11 @@ class MTSIntegrator(CustomIntegrator):
less often than the bonded and direct space nonbonded interactions. The following
example looks up the NonbondedForce, sets the reciprocal space interactions to their
own force group, and then creates an integrator that evaluates them once every 4 fs,
but all other interactions every 2 fs.
but all other interactions every 2 fs::
<pre>
nonbonded = [f for f in system.getForces() if isinstance(f, NonbondedForce)][0]
nonbonded.setReciprocalSpaceForceGroup(1)
integrator = MTSIntegrator(4*femtoseconds, [(1,1), (0,2)])
</pre>
nonbonded = [f for f in system.getForces() if isinstance(f, NonbondedForce)][0]
nonbonded.setReciprocalSpaceForceGroup(1)
integrator = MTSIntegrator(4*femtoseconds, [(1,1), (0,2)])
For details, see Tuckerman et al., J. Chem. Phys. 97(3) pp. 1990-2001 (1992).
"""
......@@ -94,7 +90,7 @@ class MTSIntegrator(CustomIntegrator):
def _createSubsteps(self, parentSubsteps, groups):
group, substeps = groups[0]
stepsPerParentStep = substeps/parentSubsteps
stepsPerParentStep = substeps//parentSubsteps
if stepsPerParentStep < 1 or stepsPerParentStep != int(stepsPerParentStep):
raise ValueError("The number for substeps for each group must be a multiple of the number for the previous group")
if group < 0 or group > 31:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment