Commit 0f1c973d authored by peastman's avatar peastman
Browse files

Merge pull request #1235 from peastman/neutralize

Added neutralize option to addSolvent()
parents f680718e 85b0ef02
...@@ -241,13 +241,13 @@ class Modeller(object): ...@@ -241,13 +241,13 @@ class Modeller(object):
self.topology = newTopology self.topology = newTopology
self.positions = newPositions self.positions = newPositions
def addSolvent(self, forcefield, model='tip3p', boxSize=None, boxVectors=None, padding=None, numAdded=None, positiveIon='Na+', negativeIon='Cl-', ionicStrength=0*molar): def addSolvent(self, forcefield, model='tip3p', boxSize=None, boxVectors=None, padding=None, numAdded=None, positiveIon='Na+', negativeIon='Cl-', ionicStrength=0*molar, neutralize=True):
"""Add solvent (both water and ions) to the model to fill a rectangular box. """Add solvent (both water and ions) to the model to fill a rectangular box.
The algorithm works as follows: The algorithm works as follows:
1. Water molecules are added to fill the box. 1. Water molecules are added to fill the box.
2. Water molecules are removed if their distance to any solute atom is less than the sum of their van der Waals radii. 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, enough positive or negative ions are added to neutralize it. Each ion is added by 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. 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.
...@@ -258,7 +258,7 @@ class Modeller(object): ...@@ -258,7 +258,7 @@ class Modeller(object):
3. You can give a padding distance. The largest dimension of the solute (along the x, y, or z axis) is determined, and a cubic 3. You can give a padding distance. The largest dimension of the solute (along the x, y, or z axis) is determined, and a cubic
box of size (largest dimension)+2*padding is used. box of size (largest dimension)+2*padding is used.
4. You can specify the total number of molecules (both waters and ions) to add. A cubic box is then created whose size is 4. You can specify the total number of molecules (both waters and ions) to add. A cubic box is then created whose size is
just large enough hold the specified amount of solvent. just large enough to hold the specified amount of solvent.
5. Finally, if none of the above options is specified, the existing Topology's box vectors are used. 5. Finally, if none of the above options is specified, the existing Topology's box vectors are used.
Parameters: Parameters:
...@@ -273,6 +273,7 @@ class Modeller(object): ...@@ -273,6 +273,7 @@ class Modeller(object):
that not all force fields support all ion types. that not all force fields support all ion types.
- ionicStrength (concentration=0*molar) the total concentration of ions (both positive and negative) to add. This - 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. does not include ions that are added to neutralize the system.
- neutralize (bool=True) whether to add ions to neutralize the system
""" """
if len([x for x in (boxSize, boxVectors, padding, numAdded) if x is not None]) > 1: if len([x for x in (boxSize, boxVectors, padding, numAdded) if x is not None]) > 1:
raise ValueError('At most one of the following arguments may be specified: boxSize, boxVectors, padding, numAdded') raise ValueError('At most one of the following arguments may be specified: boxSize, boxVectors, padding, numAdded')
...@@ -478,9 +479,6 @@ class Modeller(object): ...@@ -478,9 +479,6 @@ class Modeller(object):
# Add ions to neutralize the system. # Add ions to neutralize the system.
totalCharge = int(floor(0.5+sum((nonbonded.getParticleParameters(i)[0].value_in_unit(elementary_charge) for i in range(system.getNumParticles())))))
if abs(totalCharge) > len(addedWaters):
raise Exception('Cannot neutralize the system because the charge is greater than the number of available positions for ions')
def addIon(element): def addIon(element):
# Replace a water by an ion. # Replace a water by an ion.
index = random.randint(0, len(addedWaters)-1) index = random.randint(0, len(addedWaters)-1)
...@@ -488,6 +486,10 @@ class Modeller(object): ...@@ -488,6 +486,10 @@ class Modeller(object):
newTopology.addAtom(element.symbol, element, newResidue) newTopology.addAtom(element.symbol, element, newResidue)
newPositions.append(addedWaters[index][1]*nanometer) newPositions.append(addedWaters[index][1]*nanometer)
del addedWaters[index] del addedWaters[index]
if neutralize:
totalCharge = int(floor(0.5+sum((nonbonded.getParticleParameters(i)[0].value_in_unit(elementary_charge) for i in range(system.getNumParticles())))))
if abs(totalCharge) > len(addedWaters):
raise Exception('Cannot neutralize the system because the charge is greater than the number of available positions for ions')
for i in range(abs(totalCharge)): for i in range(abs(totalCharge)):
addIon(positiveElement if totalCharge < 0 else negativeElement) addIon(positiveElement if totalCharge < 0 else negativeElement)
......
...@@ -401,6 +401,7 @@ class TestModeller(unittest.TestCase): ...@@ -401,6 +401,7 @@ class TestModeller(unittest.TestCase):
topology_start = self.pdb.topology topology_start = self.pdb.topology
topology_start.setUnitCellDimensions(Vec3(3.5, 3.5, 3.5)*nanometers) topology_start.setUnitCellDimensions(Vec3(3.5, 3.5, 3.5)*nanometers)
for neutralize in (True, False):
# set up modeller with no solvent # set up modeller with no solvent
modeller = Modeller(topology_start, self.positions) modeller = Modeller(topology_start, self.positions)
modeller.deleteWater() modeller.deleteWater()
...@@ -416,7 +417,7 @@ class TestModeller(unittest.TestCase): ...@@ -416,7 +417,7 @@ class TestModeller(unittest.TestCase):
positions_toAdd = [Vec3(1.0,1.2,1.5), Vec3(1.7,1.0,1.4), Vec3(1.5,2.0,1.0), positions_toAdd = [Vec3(1.0,1.2,1.5), Vec3(1.7,1.0,1.4), Vec3(1.5,2.0,1.0),
Vec3(2.0,2.0,2.0), Vec3(2.0,1.5,1.0)]*nanometers Vec3(2.0,2.0,2.0), Vec3(2.0,1.5,1.0)]*nanometers
modeller.add(topology_toAdd, positions_toAdd) modeller.add(topology_toAdd, positions_toAdd)
modeller.addSolvent(self.forcefield, ionicStrength=1.0*molar) modeller.addSolvent(self.forcefield, ionicStrength=1.0*molar, neutralize=neutralize)
topology_after = modeller.getTopology() topology_after = modeller.getTopology()
water_count = 0 water_count = 0
...@@ -432,9 +433,10 @@ class TestModeller(unittest.TestCase): ...@@ -432,9 +433,10 @@ class TestModeller(unittest.TestCase):
total_water_ions = water_count+sodium_count+chlorine_count total_water_ions = water_count+sodium_count+chlorine_count
expected_ion_fraction = 1.0*molar/(55.4*molar) expected_ion_fraction = 1.0*molar/(55.4*molar)
expected_ions = math.floor((total_water_ions-10)*expected_ion_fraction/2+0.5)+5 expected_chlorine = math.floor((total_water_ions-10)*expected_ion_fraction/2+0.5)+5
self.assertEqual(sodium_count, expected_ions) expected_sodium = expected_chlorine if neutralize else expected_chlorine-5
self.assertEqual(chlorine_count, expected_ions) self.assertEqual(sodium_count, expected_sodium)
self.assertEqual(chlorine_count, expected_chlorine)
def test_addSolventPositiveSolvent(self): def test_addSolventPositiveSolvent(self):
""" Test the addSolvent() method; test adding ions to a positively charged solvent. """ """ Test the addSolvent() method; test adding ions to a positively charged solvent. """
...@@ -442,6 +444,7 @@ class TestModeller(unittest.TestCase): ...@@ -442,6 +444,7 @@ class TestModeller(unittest.TestCase):
topology_start = self.pdb.topology topology_start = self.pdb.topology
topology_start.setUnitCellDimensions(Vec3(3.5, 3.5, 3.5)*nanometers) topology_start.setUnitCellDimensions(Vec3(3.5, 3.5, 3.5)*nanometers)
for neutralize in (True, False):
# set up modeller with no solvent # set up modeller with no solvent
modeller = Modeller(topology_start, self.positions) modeller = Modeller(topology_start, self.positions)
modeller.deleteWater() modeller.deleteWater()
...@@ -459,7 +462,7 @@ class TestModeller(unittest.TestCase): ...@@ -459,7 +462,7 @@ class TestModeller(unittest.TestCase):
# positions_toAdd doesn't need to change # positions_toAdd doesn't need to change
modeller.add(topology_toAdd, positions_toAdd) modeller.add(topology_toAdd, positions_toAdd)
modeller.addSolvent(self.forcefield, ionicStrength=1.0*molar) modeller.addSolvent(self.forcefield, ionicStrength=1.0*molar, neutralize=neutralize)
topology_after = modeller.getTopology() topology_after = modeller.getTopology()
water_count = 0 water_count = 0
...@@ -475,9 +478,10 @@ class TestModeller(unittest.TestCase): ...@@ -475,9 +478,10 @@ class TestModeller(unittest.TestCase):
total_water_ions = water_count+sodium_count+chlorine_count total_water_ions = water_count+sodium_count+chlorine_count
expected_ion_fraction = 1.0*molar/(55.4*molar) expected_ion_fraction = 1.0*molar/(55.4*molar)
expected_ions = math.floor((total_water_ions-10)*expected_ion_fraction/2+0.5)+5 expected_sodium = math.floor((total_water_ions-10)*expected_ion_fraction/2+0.5)+5
self.assertEqual(sodium_count, expected_ions) expected_chlorine = expected_sodium if neutralize else expected_sodium-5
self.assertEqual(chlorine_count, expected_ions) self.assertEqual(sodium_count, expected_sodium)
self.assertEqual(chlorine_count, expected_chlorine)
def test_addSolventIons(self): def test_addSolventIons(self):
""" Test the addSolvent() method with all possible choices for positive and negative ions. """ """ Test the addSolvent() method with all possible choices for positive and negative ions. """
......
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