def genTop(self): self.processFile(self.infile) # Create the Topology from it. top = Topology() # The Topology read from the prmtop infile self.topology = top top.setUnitCellDimensions(self.unitCellDimensions) # PDBFile._loadNameReplacementTables() for moleculeName, moleculeCount in self.molecules: if moleculeName not in self.moleculeTypes: raise ValueError("Unknown molecule type: " + moleculeName) moleculeType = self.moleculeTypes[moleculeName] # Create the specified number of molecules of this type. for i in range(moleculeCount): atoms = [] lastResidue = None c = top.addChain() for index, fields in enumerate(moleculeType.atoms): resNumber = fields[2] if resNumber != lastResidue: lastResidue = resNumber resName = fields[3] r = top.addResidue(resName, c) atomName = fields[4] atoms.append(top.addAtom(atomName, None, r)) # Add bonds to the topology for fields in moleculeType.bonds: top.addBond(atoms[int(fields[0]) - 1], atoms[int(fields[1]) - 1])
def __init__(self, file): """Load a prmtop file.""" top = Topology() ## The Topology read from the prmtop file self.topology = top # Load the prmtop file prmtop = amber_file_parser.PrmtopLoader(file) self._prmtop = prmtop # Add atoms to the topology PDBFile._loadNameReplacementTables() lastResidue = None c = top.addChain() for index in range(prmtop.getNumAtoms()): resNumber = prmtop.getResidueNumber(index) if resNumber != lastResidue: lastResidue = resNumber resName = prmtop.getResidueLabel(iAtom=index).strip() if resName in PDBFile._residueNameReplacements: resName = PDBFile._residueNameReplacements[resName] r = top.addResidue(resName, c) if resName in PDBFile._atomNameReplacements: atomReplacements = PDBFile._atomNameReplacements[resName] else: atomReplacements = {} atomName = prmtop.getAtomName(index).strip() if atomName in atomReplacements: atomName = atomReplacements[atomName] # Try to guess the element. upper = atomName.upper() if upper.startswith('CL'): element = elem.chlorine elif upper.startswith('NA'): element = elem.sodium elif upper.startswith('MG'): element = elem.magnesium else: try: element = elem.get_by_symbol(atomName[0]) except KeyError: element = None top.addAtom(atomName, element, r) # Add bonds to the topology atoms = list(top.atoms()) for bond in prmtop.getBondsWithH(): top.addBond(atoms[bond[0]], atoms[bond[1]]) for bond in prmtop.getBondsNoH(): top.addBond(atoms[bond[0]], atoms[bond[1]]) # Set the periodic box size. if prmtop.getIfBox(): top.setUnitCellDimensions(tuple(x.value_in_unit(unit.nanometer) for x in prmtop.getBoxBetaAndDimensions()[1:4])*unit.nanometer)
def _createTopology(self): """Build the topology of the system """ top = Topology() positions = [] velocities = [] boxVectors = [] for x, y, z in self._conn.execute('SELECT x, y, z FROM global_cell'): boxVectors.append(mm.Vec3(x, y, z)) unitCellDimensions = [boxVectors[0][0], boxVectors[1][1], boxVectors[2][2]] top.setUnitCellDimensions(unitCellDimensions*angstrom) atoms = {} lastChain = None lastResId = None c = top.addChain() q = """SELECT id, name, anum, resname, resid, chain, x, y, z, vx, vy, vz FROM particle ORDER BY id""" for (atomId, atomName, atomNumber, resName, resId, chain, x, y, z, vx, vy, vz) in self._conn.execute(q): newChain = False if chain != lastChain: lastChain = chain c = top.addChain() newChain = True if resId != lastResId or newChain: lastResId = resId if resName in PDBFile._residueNameReplacements: resName = PDBFile._residueNameReplacements[resName] r = top.addResidue(resName, c) if resName in PDBFile._atomNameReplacements: atomReplacements = PDBFile._atomNameReplacements[resName] else: atomReplacements = {} if atomNumber == 0 and atomName.startswith('Vrt'): elem = None else: elem = Element.getByAtomicNumber(atomNumber) if atomName in atomReplacements: atomName = atomReplacements[atomName] atoms[atomId] = top.addAtom(atomName, elem, r) positions.append(mm.Vec3(x, y, z)) velocities.append(mm.Vec3(vx, vy, vz)) for p0, p1 in self._conn.execute('SELECT p0, p1 FROM bond'): top.addBond(atoms[p0], atoms[p1]) positions = positions*angstrom velocities = velocities*angstrom/femtosecond return top, positions, velocities
def _createTopology(self): '''Build the topology of the system ''' top = Topology() positions = [] boxVectors = [] for x, y, z in self._conn.execute('SELECT x, y, z FROM global_cell'): boxVectors.append(mm.Vec3(x, y, z)*angstrom) unitCellDimensions = [boxVectors[0][0], boxVectors[1][1], boxVectors[2][2]] top.setUnitCellDimensions(unitCellDimensions) atoms = {} lastChain = None lastResId = None c = top.addChain() q = '''SELECT id, name, anum, resname, resid, chain, x, y, z FROM particle''' for (atomId, atomName, atomNumber, resName, resId, chain, x, y, z) in self._conn.execute(q): if chain != lastChain: lastChain = chain c = top.addChain() if resId != lastResId: lastResId = resId if resName in PDBFile._residueNameReplacements: resName = PDBFile._residueNameReplacements[resName] r = top.addResidue(resName, c) if resName in PDBFile._atomNameReplacements: atomReplacements = PDBFile._atomNameReplacements[resName] else: atomReplacements = {} if atomName in atomReplacements: atomName = atomReplacements[atomName] elem = Element.getByAtomicNumber(atomNumber) atoms[atomId] = top.addAtom(atomName, elem, r) positions.append(mm.Vec3(x, y, z)*angstrom) for p0, p1 in self._conn.execute('SELECT p0, p1 FROM bond'): top.addBond(atoms[p0], atoms[p1]) return top, positions
def delete(self, toDelete): """Delete chains, residues, atoms, and bonds from the model. You can specify objects to delete at any granularity: atoms, residues, or chains. Passing in an Atom object causes that Atom to be deleted. Passing in a Residue object causes that Residue and all Atoms it contains to be deleted. Passing in a Chain object causes that Chain and all Residues and Atoms it contains to be deleted. In all cases, when an Atom is deleted, any bonds it participates in are also deleted. You also can specify a bond (as a tuple of Atom objects) to delete just that bond without deleting the Atoms it connects. Parameters: - toDelete (list) a list of Atoms, Residues, Chains, and bonds (specified as tuples of Atoms) to delete """ newTopology = Topology() newTopology.setUnitCellDimensions(deepcopy(self.topology.getUnitCellDimensions())) newAtoms = {} newPositions = []*nanometer deleteSet = set(toDelete) for chain in self.topology.chains(): if chain not in deleteSet: needNewChain = True; for residue in chain.residues(): if residue not in deleteSet: needNewResidue = True for atom in residue.atoms(): if atom not in deleteSet: if needNewChain: newChain = newTopology.addChain() needNewChain = False; if needNewResidue: newResidue = newTopology.addResidue(residue.name, newChain) needNewResidue = False; newAtom = newTopology.addAtom(atom.name, atom.element, newResidue) newAtoms[atom] = newAtom newPositions.append(deepcopy(self.positions[atom.index])) for bond in self.topology.bonds(): if bond[0] in newAtoms and bond[1] in newAtoms: if bond not in deleteSet and (bond[1], bond[0]) not in deleteSet: newTopology.addBond(newAtoms[bond[0]], newAtoms[bond[1]]) self.topology = newTopology self.positions = newPositions
def add(self, addTopology, addPositions): """Add chains, residues, atoms, and bonds to the model. Specify what to add by providing a new Topology object and the corresponding atomic positions. All chains, residues, atoms, and bonds contained in the Topology are added to the model. Parameters: - addTopoology (Topology) a Topology whose contents should be added to the model - addPositions (list) the positions of the atoms to add """ # Copy over the existing model. newTopology = Topology() newTopology.setUnitCellDimensions(deepcopy(self.topology.getUnitCellDimensions())) newAtoms = {} newPositions = []*nanometer for chain in self.topology.chains(): newChain = newTopology.addChain() for residue in chain.residues(): newResidue = newTopology.addResidue(residue.name, newChain) for atom in residue.atoms(): newAtom = newTopology.addAtom(atom.name, atom.element, newResidue) newAtoms[atom] = newAtom newPositions.append(deepcopy(self.positions[atom.index])) for bond in self.topology.bonds(): newTopology.addBond(newAtoms[bond[0]], newAtoms[bond[1]]) # Add the new model newAtoms = {} for chain in addTopology.chains(): newChain = newTopology.addChain() for residue in chain.residues(): newResidue = newTopology.addResidue(residue.name, newChain) for atom in residue.atoms(): newAtom = newTopology.addAtom(atom.name, atom.element, newResidue) newAtoms[atom] = newAtom newPositions.append(deepcopy(addPositions[atom.index])) for bond in addTopology.bonds(): newTopology.addBond(newAtoms[bond[0]], newAtoms[bond[1]]) self.topology = newTopology self.positions = newPositions
def __init__(self, file): """Load a prmtop file.""" top = Topology() ## The Topology read from the prmtop file self.topology = top self.elements = [] # Load the prmtop file prmtop = amber_file_parser.PrmtopLoader(file) self._prmtop = prmtop # Add atoms to the topology PDBFile._loadNameReplacementTables() lastResidue = None c = top.addChain() for index in range(prmtop.getNumAtoms()): resNumber = prmtop.getResidueNumber(index) if resNumber != lastResidue: lastResidue = resNumber resName = prmtop.getResidueLabel(iAtom=index).strip() if resName in PDBFile._residueNameReplacements: resName = PDBFile._residueNameReplacements[resName] r = top.addResidue(resName, c) if resName in PDBFile._atomNameReplacements: atomReplacements = PDBFile._atomNameReplacements[resName] else: atomReplacements = {} atomName = prmtop.getAtomName(index).strip() if atomName in atomReplacements: atomName = atomReplacements[atomName] # Get the element from the prmtop file if available if prmtop.has_atomic_number: try: element = elem.Element.getByAtomicNumber( int(prmtop._raw_data['ATOMIC_NUMBER'][index])) except KeyError: element = None else: # Try to guess the element from the atom name. upper = atomName.upper() if upper.startswith('CL'): element = elem.chlorine elif upper.startswith('NA'): element = elem.sodium elif upper.startswith('MG'): element = elem.magnesium elif upper.startswith('ZN'): element = elem.zinc else: try: element = elem.get_by_symbol(atomName[0]) except KeyError: element = None top.addAtom(atomName, element, r) self.elements.append(element) # Add bonds to the topology atoms = list(top.atoms()) for bond in prmtop.getBondsWithH(): top.addBond(atoms[bond[0]], atoms[bond[1]]) for bond in prmtop.getBondsNoH(): top.addBond(atoms[bond[0]], atoms[bond[1]]) # Set the periodic box size. if prmtop.getIfBox(): top.setUnitCellDimensions( tuple( x.value_in_unit(unit.nanometer) for x in prmtop.getBoxBetaAndDimensions()[1:4]) * unit.nanometer)
def __init__(self, file, unitCellDimensions=None, includeDir='/usr/local/gromacs/share/gromacs/top', defines={}): """Load a top file. Parameters: - file (string) the name of the file to load - unitCellDimensions (Vec3=None) the dimensions of the crystallographic unit cell - includeDir (string=/usr/local/gromacs/share/gromacs/top) a directory in which to look for other files included from the top file - defines (map={}) preprocessor definitions that should be predefined when parsing the file """ self._includeDirs = (os.path.dirname(file), includeDir) self._defines = defines # Parse the file. self._currentCategory = None self._ifStack = [] self._moleculeTypes = {} self._molecules = [] self._currentMoleculeType = None self._atomTypes = {} self._bondTypes = {} self._angleTypes = {} self._dihedralTypes = {} self._implicitTypes = {} self._pairTypes = {} self._cmapTypes = {} self._processFile(file) # Create the Topology from it. top = Topology() ## The Topology read from the prmtop file self.topology = top top.setUnitCellDimensions(unitCellDimensions) PDBFile._loadNameReplacementTables() for moleculeName, moleculeCount in self._molecules: if moleculeName not in self._moleculeTypes: raise ValueError("Unknown molecule type: " + moleculeName) moleculeType = self._moleculeTypes[moleculeName] # Create the specified number of molecules of this type. for i in range(moleculeCount): atoms = [] lastResidue = None c = top.addChain() for index, fields in enumerate(moleculeType.atoms): resNumber = fields[2] if resNumber != lastResidue: lastResidue = resNumber resName = fields[3] if resName in PDBFile._residueNameReplacements: resName = PDBFile._residueNameReplacements[resName] r = top.addResidue(resName, c) if resName in PDBFile._atomNameReplacements: atomReplacements = PDBFile._atomNameReplacements[ resName] else: atomReplacements = {} atomName = fields[4] if atomName in atomReplacements: atomName = atomReplacements[atomName] # Try to guess the element. upper = atomName.upper() if upper.startswith('CL'): element = elem.chlorine elif upper.startswith('NA'): element = elem.sodium elif upper.startswith('MG'): element = elem.magnesium else: try: element = elem.get_by_symbol(atomName[0]) except KeyError: element = None atoms.append(top.addAtom(atomName, element, r)) # Add bonds to the topology for fields in moleculeType.bonds: top.addBond(atoms[int(fields[0]) - 1], atoms[int(fields[1]) - 1])
def __init__(self, file, unitCellDimensions=None, includeDir='/usr/local/gromacs/share/gromacs/top', defines={}): """Load a top file. Parameters: - file (string) the name of the file to load - unitCellDimensions (Vec3=None) the dimensions of the crystallographic unit cell - includeDir (string=/usr/local/gromacs/share/gromacs/top) a directory in which to look for other files included from the top file - defines (map={}) preprocessor definitions that should be predefined when parsing the file """ self._includeDirs = (os.path.dirname(file), includeDir) self._defines = defines # Parse the file. self._currentCategory = None self._ifStack = [] self._moleculeTypes = {} self._molecules = [] self._currentMoleculeType = None self._atomTypes = {} self._bondTypes= {} self._angleTypes = {} self._dihedralTypes = {} self._implicitTypes = {} self._pairTypes = {} self._cmapTypes = {} self._processFile(file) # Create the Topology from it. top = Topology() ## The Topology read from the prmtop file self.topology = top top.setUnitCellDimensions(unitCellDimensions) PDBFile._loadNameReplacementTables() for moleculeName, moleculeCount in self._molecules: if moleculeName not in self._moleculeTypes: raise ValueError("Unknown molecule type: "+moleculeName) moleculeType = self._moleculeTypes[moleculeName] # Create the specified number of molecules of this type. for i in range(moleculeCount): atoms = [] lastResidue = None c = top.addChain() for index, fields in enumerate(moleculeType.atoms): resNumber = fields[2] if resNumber != lastResidue: lastResidue = resNumber resName = fields[3] if resName in PDBFile._residueNameReplacements: resName = PDBFile._residueNameReplacements[resName] r = top.addResidue(resName, c) if resName in PDBFile._atomNameReplacements: atomReplacements = PDBFile._atomNameReplacements[resName] else: atomReplacements = {} atomName = fields[4] if atomName in atomReplacements: atomName = atomReplacements[atomName] # Try to guess the element. upper = atomName.upper() if upper.startswith('CL'): element = elem.chlorine elif upper.startswith('NA'): element = elem.sodium elif upper.startswith('MG'): element = elem.magnesium else: try: element = elem.get_by_symbol(atomName[0]) except KeyError: element = None atoms.append(top.addAtom(atomName, element, r)) # Add bonds to the topology for fields in moleculeType.bonds: top.addBond(atoms[int(fields[0])-1], atoms[int(fields[1])-1])
def addExtraParticles(self, forcefield): """Add missing extra particles to the model that are required by a force field. Some force fields use "extra particles" that do not represent actual atoms, but still need to be included in the System. Examples include lone pairs, Drude particles, and the virtual sites used in some water models to adjust the charge distribution. Extra particles can be recognized by the fact that their element is None. This method is primarily used to add extra particles, but it can also remove them. It tries to match every residue in the Topology to a template in the force field. If there is no match, it will both add and remove extra particles as necessary to make it match. Parameters: - forcefield (ForceField) the ForceField defining what extra particles should be present """ # Create copies of all residue templates that have had all extra points removed. templatesNoEP = {} for resName, template in forcefield._templates.iteritems(): if any(atom.element is None for atom in template.atoms): index = 0 newIndex = {} newTemplate = ForceField._TemplateData(resName) for i, atom in enumerate(template.atoms): if atom.element is not None: newIndex[i] = index index += 1 newTemplate.atoms.append(ForceField._TemplateAtomData(atom.name, atom.type, atom.element)) for b1, b2 in template.bonds: if b1 in newIndex and b2 in newIndex: newTemplate.bonds.append((newIndex[b1], newIndex[b2])) newTemplate.atoms[newIndex[b1]].bondedTo.append(newIndex[b2]) newTemplate.atoms[newIndex[b2]].bondedTo.append(newIndex[b1]) for b in template.externalBonds: if b in newIndex: newTemplate.externalBonds.append(newIndex[b]) templatesNoEP[template] = newTemplate # Record which atoms are bonded to each other atom, with and without extra particles. bondedToAtom = [] bondedToAtomNoEP = [] for atom in self.topology.atoms(): bondedToAtom.append(set()) bondedToAtomNoEP.append(set()) for atom1, atom2 in self.topology.bonds(): bondedToAtom[atom1.index].add(atom2.index) bondedToAtom[atom2.index].add(atom1.index) if atom1.element is not None and atom2.element is not None: bondedToAtomNoEP[atom1.index].add(atom2.index) bondedToAtomNoEP[atom2.index].add(atom1.index) # If the force field has a DrudeForce, record the types of Drude particles and their parents since we'll # need them for picking particle positions. drudeTypeMap = {} for force in forcefield._forces: if isinstance(force, DrudeGenerator): for type in force.typeMap: drudeTypeMap[type] = force.typeMap[type][0] # Create the new Topology. newTopology = Topology() newTopology.setUnitCellDimensions(deepcopy(self.topology.getUnitCellDimensions())) newAtoms = {} newPositions = []*nanometer for chain in self.topology.chains(): newChain = newTopology.addChain() for residue in chain.residues(): newResidue = newTopology.addResidue(residue.name, newChain) # Look for a matching template. matchFound = False signature = _createResidueSignature([atom.element for atom in residue.atoms()]) if signature in forcefield._templateSignatures: for t in forcefield._templateSignatures[signature]: if _matchResidue(residue, t, bondedToAtom) is not None: matchFound = True if matchFound: # Just copy the residue over. for atom in residue.atoms(): newAtom = newTopology.addAtom(atom.name, atom.element, newResidue) newAtoms[atom] = newAtom newPositions.append(deepcopy(self.positions[atom.index])) else: # There's no matching template. Try to find one that matches based on everything except # extra points. template = None residueNoEP = Residue(residue.name, residue.index, residue.chain) residueNoEP._atoms = [atom for atom in residue.atoms() if atom.element is not None] if signature in forcefield._templateSignatures: for t in forcefield._templateSignatures[signature]: if t in templatesNoEP: matches = _matchResidue(residueNoEP, templatesNoEP[t], bondedToAtomNoEP) if matches is not None: template = t; # Record the corresponding atoms. matchingAtoms = {} for atom, match in zip(residueNoEP.atoms(), matches): templateAtomName = t.atoms[match].name for templateAtom in template.atoms: if templateAtom.name == templateAtomName: matchingAtoms[templateAtom] = atom break if template is None: raise ValueError('Residue %d (%s) does not match any template defined by the ForceField.' % (residue.index+1, residue.name)) # Add the regular atoms. for atom in residue.atoms(): if atom.element is not None: newAtoms[atom] = newTopology.addAtom(atom.name, atom.element, newResidue) newPositions.append(deepcopy(self.positions[atom.index])) # Add the extra points. templateAtomPositions = len(template.atoms)*[None] for index, atom in enumerate(template.atoms): if atom in matchingAtoms: templateAtomPositions[index] = self.positions[matchingAtoms[atom].index].value_in_unit(nanometer) for index, atom in enumerate(template.atoms): if atom.element is None: newTopology.addAtom(atom.name, None, newResidue) position = None for site in template.virtualSites: if site.index == index: # This is a virtual site. Compute its position by the correct rule. if site.type == 'average2': position = site.weights[0]*templateAtomPositions[index+site.atoms[0]] + site.weights[1]*templateAtomPositions[index+site.atoms[1]] elif site.type == 'average3': position = site.weights[0]*templateAtomPositions[index+site.atoms[0]] + site.weights[1]*templateAtomPositions[index+site.atoms[1]] + site.weights[2]*templateAtomPositions[index+site.atoms[2]] elif site.type == 'outOfPlane': v1 = templateAtomPositions[index+site.atoms[1]] - templateAtomPositions[index+site.atoms[0]] v2 = templateAtomPositions[index+site.atoms[2]] - templateAtomPositions[index+site.atoms[0]] cross = Vec3(v1[1]*v2[2]-v1[2]*v2[1], v1[2]*v2[0]-v1[0]*v2[2], v1[0]*v2[1]-v1[1]*v2[0]) position = templateAtomPositions[index+site.atoms[0]] + site.weights[0]*v1 + site.weights[1]*v2 + site.weights[2]*cross if position is None and atom.type in drudeTypeMap: # This is a Drude particle. Put it on top of its parent atom. for atom2, pos in zip(template.atoms, templateAtomPositions): if atom2.type in drudeTypeMap[atom.type]: position = deepcopy(pos) if position is None: # We couldn't figure out the correct position. As a wild guess, just put it at the center of the residue # and hope that energy minimization will fix it. knownPositions = [x for x in templateAtomPositions if x is not None] position = sum(knownPositions)/len(knownPositions) newPositions.append(position*nanometer) for bond in self.topology.bonds(): if bond[0] in newAtoms and bond[1] in newAtoms: newTopology.addBond(newAtoms[bond[0]], newAtoms[bond[1]]) self.topology = newTopology self.positions = newPositions
def addSolvent(self, forcefield, model='tip3p', boxSize=None, padding=None, positiveIon='Na+', negativeIon='Cl-', ionicStrength=0*molar): """Add solvent (both water and ions) to the model to fill a rectangular box. The algorithm works as follows: 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. 3. If the solute is charged, 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. The box size can be specified in three ways. First, you can explicitly give a box size to use. Alternatively, 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. Finally, if neither a box size nor a padding distance is specified, the existing Topology's unit cell dimensions are used. Parameters: - forcefield (ForceField) the ForceField to use for determining van der Waals radii and atomic charges - model (string='tip3p') the water model to use. Supported values are 'tip3p', 'spce', 'tip4pew', and 'tip5p'. - boxSize (Vec3=None) the size of the box to fill with water - padding (distance=None) the padding distance to use - positiveIon (string='Na+') the type of positive ion to add. Allowed values are 'Cs+', 'K+', 'Li+', 'Na+', and 'Rb+' - negativeIon (string='Cl-') the type of negative ion to add. Allowed values are 'Cl-', 'Br-', 'F-', and 'I-'. Be aware that not all force fields support all ion types. - ionicString (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. """ # Pick a unit cell size. if boxSize is not None: if is_quantity(boxSize): boxSize = boxSize.value_in_unit(nanometer) box = Vec3(boxSize[0], boxSize[1], boxSize[2])*nanometer elif padding is not None: maxSize = max(max((pos[i] for pos in self.positions))-min((pos[i] for pos in self.positions)) for i in range(3)) box = (maxSize+2*padding)*Vec3(1, 1, 1) else: box = self.topology.getUnitCellDimensions() if box is None: raise ValueError('Neither the box size nor padding was specified, and the Topology does not define unit cell dimensions') box = box.value_in_unit(nanometer) invBox = Vec3(1.0/box[0], 1.0/box[1], 1.0/box[2]) # Identify the ion types. posIonElements = {'Cs+':elem.cesium, 'K+':elem.potassium, 'Li+':elem.lithium, 'Na+':elem.sodium, 'Rb+':elem.rubidium} negIonElements = {'Cl-':elem.chlorine, 'Br-':elem.bromine, 'F-':elem.fluorine, 'I-':elem.iodine} if positiveIon not in posIonElements: raise ValueError('Illegal value for positive ion: %s' % positiveIon) if negativeIon not in negIonElements: raise ValueError('Illegal value for negative ion: %s' % negativeIon) positiveElement = posIonElements[positiveIon] negativeElement = negIonElements[negativeIon] # Load the pre-equilibrated water box. vdwRadiusPerSigma = 0.5612310241546864907 if model == 'tip3p': waterRadius = 0.31507524065751241*vdwRadiusPerSigma elif model == 'spce': waterRadius = 0.31657195050398818*vdwRadiusPerSigma elif model == 'tip4pew': waterRadius = 0.315365*vdwRadiusPerSigma elif model == 'tip5p': waterRadius = 0.312*vdwRadiusPerSigma else: raise ValueError('Unknown water model: %s' % model) pdb = PDBFile(os.path.join(os.path.dirname(__file__), 'data', model+'.pdb')) pdbTopology = pdb.getTopology() pdbPositions = pdb.getPositions().value_in_unit(nanometer) pdbResidues = list(pdbTopology.residues()) pdbBoxSize = pdbTopology.getUnitCellDimensions().value_in_unit(nanometer) # Have the ForceField build a System for the solute from which we can determine van der Waals radii. system = forcefield.createSystem(self.topology) nonbonded = None for i in range(system.getNumForces()): if isinstance(system.getForce(i), NonbondedForce): nonbonded = system.getForce(i) if nonbonded is None: raise ValueError('The ForceField does not specify a NonbondedForce') cutoff = [nonbonded.getParticleParameters(i)[1].value_in_unit(nanometer)*vdwRadiusPerSigma+waterRadius for i in range(system.getNumParticles())] waterCutoff = waterRadius if len(cutoff) == 0: maxCutoff = waterCutoff else: maxCutoff = max(waterCutoff, max(cutoff)) # Copy the solute over. newTopology = Topology() newTopology.setUnitCellDimensions(box) newAtoms = {} newPositions = []*nanometer for chain in self.topology.chains(): newChain = newTopology.addChain() for residue in chain.residues(): newResidue = newTopology.addResidue(residue.name, newChain) for atom in residue.atoms(): newAtom = newTopology.addAtom(atom.name, atom.element, newResidue) newAtoms[atom] = newAtom newPositions.append(deepcopy(self.positions[atom.index])) for bond in self.topology.bonds(): newTopology.addBond(newAtoms[bond[0]], newAtoms[bond[1]]) # Sort the solute atoms into cells for fast lookup. if len(self.positions) == 0: positions = [] else: positions = self.positions.value_in_unit(nanometer) cells = {} numCells = tuple((max(1, int(floor(box[i]/maxCutoff))) for i in range(3))) cellSize = tuple((box[i]/numCells[i] for i in range(3))) for i in range(len(positions)): cell = tuple((int(floor(positions[i][j]/cellSize[j]))%numCells[j] for j in range(3))) if cell in cells: cells[cell].append(i) else: cells[cell] = [i] # Create a generator that loops over atoms close to a position. def neighbors(pos): centralCell = tuple((int(floor(pos[i]/cellSize[i])) for i in range(3))) offsets = (-1, 0, 1) for i in offsets: for j in offsets: for k in offsets: cell = ((centralCell[0]+i+numCells[0])%numCells[0], (centralCell[1]+j+numCells[1])%numCells[1], (centralCell[2]+k+numCells[2])%numCells[2]) if cell in cells: for atom in cells[cell]: yield atom # Define a function to compute the distance between two points, taking periodic boundary conditions into account. def periodicDistance(pos1, pos2): delta = pos1-pos2 delta = [delta[i]-floor(delta[i]*invBox[i]+0.5)*box[i] for i in range(3)] return norm(delta) # Find the list of water molecules to add. newChain = newTopology.addChain() if len(positions) == 0: center = Vec3(0, 0, 0) else: center = [(max((pos[i] for pos in positions))+min((pos[i] for pos in positions)))/2 for i in range(3)] center = Vec3(center[0], center[1], center[2]) numBoxes = [int(ceil(box[i]/pdbBoxSize[i])) for i in range(3)] addedWaters = [] for boxx in range(numBoxes[0]): for boxy in range(numBoxes[1]): for boxz in range(numBoxes[2]): offset = Vec3(boxx*pdbBoxSize[0], boxy*pdbBoxSize[1], boxz*pdbBoxSize[2]) for residue in pdbResidues: oxygen = [atom for atom in residue.atoms() if atom.element == elem.oxygen][0] atomPos = pdbPositions[oxygen.index]+offset if not any((atomPos[i] > box[i] for i in range(3))): # This molecule is inside the box, so see how close to it is to the solute. atomPos += center-box/2 for i in neighbors(atomPos): if periodicDistance(atomPos, positions[i]) < cutoff[i]: break else: # Record this water molecule as one to add. addedWaters.append((residue.index, atomPos)) # There could be clashes between water molecules at the box edges. Find ones to remove. upperCutoff = center+box/2-Vec3(waterCutoff, waterCutoff, waterCutoff) lowerCutoff = center-box/2+Vec3(waterCutoff, waterCutoff, waterCutoff) lowerSkinPositions = [pos for index, pos in addedWaters if pos[0] < lowerCutoff[0] or pos[1] < lowerCutoff[1] or pos[2] < lowerCutoff[2]] filteredWaters = [] cells = {} for i in range(len(lowerSkinPositions)): cell = tuple((int(floor(lowerSkinPositions[i][j]/cellSize[j]))%numCells[j] for j in range(3))) if cell in cells: cells[cell].append(i) else: cells[cell] = [i] for entry in addedWaters: pos = entry[1] if pos[0] < upperCutoff[0] and pos[1] < upperCutoff[1] and pos[2] < upperCutoff[2]: filteredWaters.append(entry) else: if not any((periodicDistance(lowerSkinPositions[i], pos) < waterCutoff and norm(lowerSkinPositions[i]-pos) > waterCutoff for i in neighbors(pos))): filteredWaters.append(entry) addedWaters = filteredWaters # 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): # Replace a water by an ion. index = random.randint(0, len(addedWaters)-1) newResidue = newTopology.addResidue(element.symbol.upper(), newChain) newTopology.addAtom(element.symbol, element, newResidue) newPositions.append(addedWaters[index][1]*nanometer) del addedWaters[index] for i in range(abs(totalCharge)): addIon(positiveElement if totalCharge < 0 else negativeElement) # 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)) for i in range(numPairs): addIon(positiveElement) for i in range(numPairs): addIon(negativeElement) # Add the water molecules. for index, pos in addedWaters: newResidue = newTopology.addResidue(residue.name, newChain) residue = pdbResidues[index] oxygen = [atom for atom in residue.atoms() if atom.element == elem.oxygen][0] oPos = pdbPositions[oxygen.index] molAtoms = [] for atom in residue.atoms(): molAtoms.append(newTopology.addAtom(atom.name, atom.element, newResidue)) newPositions.append((pos+pdbPositions[atom.index]-oPos)*nanometer) for atom1 in molAtoms: if atom1.element == elem.oxygen: for atom2 in molAtoms: if atom2.element == elem.hydrogen: newTopology.addBond(atom1, atom2) newTopology.setUnitCellDimensions(deepcopy(box)*nanometer) self.topology = newTopology self.positions = newPositions
def convertWater(self, model='tip3p'): """Convert all water molecules to a different water model. Parameters: - model (string='tip3p') the water model to convert to. Supported values are 'tip3p', 'spce', 'tip4pew', and 'tip5p'. @deprecated Use addExtraParticles() instead. It performs the same function but in a more general way. """ if model in ('tip3p', 'spce'): sites = 3 elif model == 'tip4pew': sites = 4 elif model == 'tip5p': sites = 5 else: raise ValueError('Unknown water model: %s' % model) newTopology = Topology() newTopology.setUnitCellDimensions(deepcopy(self.topology.getUnitCellDimensions())) newAtoms = {} newPositions = []*nanometer for chain in self.topology.chains(): newChain = newTopology.addChain() for residue in chain.residues(): newResidue = newTopology.addResidue(residue.name, newChain) if residue.name == "HOH": # Copy the oxygen and hydrogens oatom = [atom for atom in residue.atoms() if atom.element == elem.oxygen] hatoms = [atom for atom in residue.atoms() if atom.element == elem.hydrogen] if len(oatom) != 1 or len(hatoms) != 2: raise ValueError('Illegal water molecule (residue %d): contains %d oxygen(s) and %d hydrogen(s)' % (residue.index, len(oatom), len(hatoms))) o = newTopology.addAtom(oatom[0].name, oatom[0].element, newResidue) h1 = newTopology.addAtom(hatoms[0].name, hatoms[0].element, newResidue) h2 = newTopology.addAtom(hatoms[1].name, hatoms[1].element, newResidue) newAtoms[oatom[0]] = o newAtoms[hatoms[0]] = h1 newAtoms[hatoms[1]] = h2 po = deepcopy(self.positions[oatom[0].index]) ph1 = deepcopy(self.positions[hatoms[0].index]) ph2 = deepcopy(self.positions[hatoms[1].index]) newPositions.append(po) newPositions.append(ph1) newPositions.append(ph2) # Add virtual sites. if sites == 4: newTopology.addAtom('M', None, newResidue) newPositions.append(0.786646558*po + 0.106676721*ph1 + 0.106676721*ph2) elif sites == 5: newTopology.addAtom('M1', None, newResidue) newTopology.addAtom('M2', None, newResidue) v1 = (ph1-po).value_in_unit(nanometer) v2 = (ph2-po).value_in_unit(nanometer) cross = Vec3(v1[1]*v2[2]-v1[2]*v2[1], v1[2]*v2[0]-v1[0]*v2[2], v1[0]*v2[1]-v1[1]*v2[0]) newPositions.append(po - (0.34490826*v1 - 0.34490826*v2 - 6.4437903*cross)*nanometer) newPositions.append(po - (0.34490826*v1 - 0.34490826*v2 + 6.4437903*cross)*nanometer) else: # Just copy the residue over. for atom in residue.atoms(): newAtom = newTopology.addAtom(atom.name, atom.element, newResidue) newAtoms[atom] = newAtom newPositions.append(deepcopy(self.positions[atom.index])) for bond in self.topology.bonds(): if bond[0] in newAtoms and bond[1] in newAtoms: newTopology.addBond(newAtoms[bond[0]], newAtoms[bond[1]]) self.topology = newTopology self.positions = newPositions
def _createTopology(self): """Build the topology of the system """ top = Topology() positions = [] velocities = [] boxVectors = [] #assume cell dimensions are set in the first file #the other molecules inherit the same cell conn = self._conn[0] self.pbc = False if self._hasTable('global_cell', self._tables[0]): for x, y, z in conn.execute('SELECT x, y, z FROM global_cell'): boxVectors.append(mm.Vec3(x, y, z)) unitCellDimensions = [boxVectors[0][0], boxVectors[1][1], boxVectors[2][2]] top.setUnitCellDimensions(unitCellDimensions*angstrom) self.pbc = True #process each file nfiles = len(self._conn) for (fcounter, conn, tables) in zip(range(0,nfiles),self._conn,self._tables): """ resdb = {} chaindb = {} """ atoms = {} lastChain = None lastResId = None c = top.addChain() q = """SELECT id, name, anum, resname, resid, chain, x, y, z, vx, vy, vz FROM particle ORDER BY id""" for (atomId, atomName, atomNumber, resName, resId, chain, x, y, z, vx, vy, vz) in conn.execute(q): """ #more elegant way to assign atoms to residues #but it works only if atoms in residues are contiguous #due to the fact that openmm does not support non-contiguous residues resuid = "%s:%s:%s" % (resName, resId, chain) if resuid in resdb.keys(): r = resdb[resuid] c = chaindb[chain] else: if chain in chaindb.keys(): c = chaindb[chain] else: c = top.addChain() chaindb[chain] = c r = top.addResidue(resName, c) resdb[resuid] = r """ newChain = False if chain != lastChain: lastChain = chain c = top.addChain() newChain = True if resId != lastResId or newChain: lastResId = resId if resName in PDBFile._residueNameReplacements: resName = PDBFile._residueNameReplacements[resName] r = top.addResidue(resName, c) if resName in PDBFile._atomNameReplacements: atomReplacements = PDBFile._atomNameReplacements[resName] else: atomReplacements = {} if atomNumber == 0 and atomName.startswith('Vrt'): elem = None else: elem = Element.getByAtomicNumber(atomNumber) if atomName in atomReplacements: atomName = atomReplacements[atomName] atoms[atomId] = top.addAtom(atomName, elem, r) positions.append(mm.Vec3(x, y, z)) velocities.append(mm.Vec3(vx, vy, vz)) self._natoms[fcounter] = len(atoms) for p0, p1 in conn.execute('SELECT p0, p1 FROM bond'): top.addBond(atoms[p0], atoms[p1]) positions = positions*angstrom velocities = velocities*angstrom/picosecond return top, positions, velocities
def __init__(self, file, periodicBoxVectors=None, unitCellDimensions=None, includeDir=None, defines=None): """Load a top file. Parameters ---------- file : str the name of the file to load periodicBoxVectors : tuple of Vec3=None the vectors defining the periodic box unitCellDimensions : Vec3=None the dimensions of the crystallographic unit cell. For non-rectangular unit cells, specify periodicBoxVectors instead. includeDir : string=None A directory in which to look for other files included from the top file. If not specified, we will attempt to locate a gromacs installation on your system. When gromacs is installed in /usr/local, this will resolve to /usr/local/gromacs/share/gromacs/top defines : dict={} preprocessor definitions that should be predefined when parsing the file """ if includeDir is None: includeDir = _defaultGromacsIncludeDir() self._includeDirs = (os.path.dirname(file), includeDir) # Most of the gromacs water itp files for different forcefields, # unless the preprocessor #define FLEXIBLE is given, don't define # bonds between the water hydrogen and oxygens, but only give the # constraint distances and exclusions. self._defines = OrderedDict() self._defines['FLEXIBLE'] = True self._genpairs = True if defines is not None: for define, value in defines.iteritems(): self._defines[define] = value # Parse the file. self._currentCategory = None self._ifStack = [] self._elseStack = [] self._moleculeTypes = {} self._molecules = [] self._currentMoleculeType = None self._atomTypes = {} self._bondTypes= {} self._angleTypes = {} self._dihedralTypes = {} self._implicitTypes = {} self._pairTypes = {} self._cmapTypes = {} self._processFile(file) # Create the Topology from it. top = Topology() ## The Topology read from the prmtop file self.topology = top if periodicBoxVectors is not None: if unitCellDimensions is not None: raise ValueError("specify either periodicBoxVectors or unitCellDimensions, but not both") top.setPeriodicBoxVectors(periodicBoxVectors) else: top.setUnitCellDimensions(unitCellDimensions) PDBFile._loadNameReplacementTables() for moleculeName, moleculeCount in self._molecules: if moleculeName not in self._moleculeTypes: raise ValueError("Unknown molecule type: "+moleculeName) moleculeType = self._moleculeTypes[moleculeName] if moleculeCount > 0 and moleculeType.has_virtual_sites: raise ValueError('Virtual sites not yet supported by Gromacs parsers') # Create the specified number of molecules of this type. for i in range(moleculeCount): atoms = [] lastResidue = None c = top.addChain() for index, fields in enumerate(moleculeType.atoms): resNumber = fields[2] if resNumber != lastResidue: lastResidue = resNumber resName = fields[3] if resName in PDBFile._residueNameReplacements: resName = PDBFile._residueNameReplacements[resName] r = top.addResidue(resName, c) if resName in PDBFile._atomNameReplacements: atomReplacements = PDBFile._atomNameReplacements[resName] else: atomReplacements = {} atomName = fields[4] if atomName in atomReplacements: atomName = atomReplacements[atomName] # Try to guess the element. upper = atomName.upper() if upper.startswith('CL'): element = elem.chlorine elif upper.startswith('NA'): element = elem.sodium elif upper.startswith('MG'): element = elem.magnesium else: try: element = elem.get_by_symbol(atomName[0]) except KeyError: element = None atoms.append(top.addAtom(atomName, element, r)) # Add bonds to the topology for fields in moleculeType.bonds: top.addBond(atoms[int(fields[0])-1], atoms[int(fields[1])-1])
class OpenMMAmberParm(AmberParm): """ OpenMM-compatible subclass of AmberParm. This object should still work with the ParmEd API while also being compatible with OpenMM's environment """ # Define default force groups for all of the bonded terms. This allows them # to be turned on and off selectively. This is a way to implement per-term # energy decomposition to compare individual components BOND_FORCE_GROUP = 0 ANGLE_FORCE_GROUP = 1 DIHEDRAL_FORCE_GROUP = 2 NONBONDED_FORCE_GROUP = 3 GB_FORCE_GROUP = 3 def openmm_LJ(self): """ Same as fill_LJ, except it uses 0.5 for the LJ radius for H-atoms with no vdW parameters (per OpenMM's standard) Returns: list, list : The 1st list is the list of all Rmin/2 terms. The 2nd is the list of all epsilon (or well depth) terms. """ LJ_radius = [] # empty LJ_radii so it can be re-filled LJ_depth = [] # empty LJ_depths so it can be re-filled one_sixth = 1 / 6 # we need to raise some numbers to the 1/6th power ntypes = self.pointers['NTYPES'] acoef = self.parm_data['LENNARD_JONES_ACOEF'] bcoef = self.parm_data['LENNARD_JONES_BCOEF'] for i in range(ntypes): lj_index = self.parm_data["NONBONDED_PARM_INDEX"][ntypes*i+i] - 1 if acoef[lj_index] < 1.0e-10: LJ_radius.append(0.5) LJ_depth.append(0) else: factor = (2 * acoef[lj_index] / bcoef[lj_index]) LJ_radius.append(pow(factor, one_sixth) * 0.5) LJ_depth.append(bcoef[lj_index] / 2 / factor) # Now check that we haven't modified any off-diagonals, since that will # not work with OpenMM for i in range(ntypes): for j in range(ntypes): idx = self.parm_data['NONBONDED_PARM_INDEX'][ntypes*i+j] - 1 rij = LJ_radius[i] + LJ_radius[j] wdij = sqrt(LJ_depth[i] * LJ_depth[j]) a = acoef[idx] b = bcoef[idx] if a == 0 or b == 0: if a != 0 or b != 0 or (wdij != 0 and rij != 0): raise OpenMMError('Off-diagonal LJ modifications ' 'detected. These are incompatible ' 'with the OpenMM API') elif (abs((a - (wdij * rij**12)) / a) > 1e-6 or abs((b - (2 * wdij * rij**6)) / b) > 1e-6): raise OpenMMError( 'Off-diagonal LJ modifications detected. These are ' 'incompatible with the OpenMM API. Acoef=%s; ' 'computed=%s. Bcoef=%s; computed=%s' % (acoef, wdij*rij**12, bcoef, 2*wdij*rij**6) ) return LJ_radius, LJ_depth def openmm_14_LJ(self): """ Returns the radii and depths for the LJ interactions between 1-4 pairs. For Amber topology files this is the same as the normal LJ parameters, but is done here so that OpenMMChamberParm can inherit and override this behavior without having to duplicate all of the system creation code. """ return self.openmm_LJ() @property def topology(self): """ The OpenMM Topology object. Cached when possible, but any changes to the topology object lists results in the topology being deleted and rebuilt """ # If anything changed, rebuild the topology if not self._topology_changed(): try: return self._topology except AttributeError: pass else: self.remake_parm() self._topology = Topology() # Add all of the atoms to the topology file in the same chain chain = self._topology.addChain() last_residue = None for i, atm in enumerate(self.atom_list): resnum = atm.residue.idx if last_residue != resnum: last_residue = resnum resname = atm.residue.resname res = self._topology.addResidue(resname, chain) elem = element.get_by_symbol(pt.Element[atm.element]) self._topology.addAtom(atm.atname, elem, res) # Add bonds to the topology (both with and without hydrogen) atoms = list(self._topology.atoms()) for bnd in self.bonds_inc_h + self.bonds_without_h: self._topology.addBond(atoms[bnd.atom1.starting_index], atoms[bnd.atom2.starting_index]) # Set the box dimensions if self.ptr('ifbox'): if hasattr(self, 'rst7'): self._topology.setUnitCellDimensions( self.rst7.box[:3]*u.angstrom ) else: self._topology.setUnitCellDimensions( self.parm_data['BOX_DIMENSIONS'][1:4]*u.angstrom ) return self._topology def _get_gb_params(self, gb_model=HCT): """ Gets the GB parameters. Need this method to special-case GB neck """ if gb_model is GBn: screen = [0.5 for atom in self.atom_list] for i, atom in enumerate(self.atom_list): if atom.element == 6: screen[i] = 0.48435382330 elif atom.element == 1: screen[i] = 1.09085413633 elif atom.element == 7: screen[i] = 0.700147318409 elif atom.element == 8: screen[i] = 1.06557401132 elif atom.element == 16: screen[i] = 0.602256336067 elif gb_model is GBn2: # 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.element == 6: screen[i] = 1.058554 alpha[i] = 0.733756 beta[i] = 0.506378 gamma[i] = 0.205844 elif atom.element == 1: screen[i] = 1.425952 alpha[i] = 0.788440 beta[i] = 0.798699 gamma[i] = 0.437334 elif atom.element == 7: screen[i] = 0.733599 alpha[i] = 0.503364 beta[i] = 0.316828 gamma[i] = 0.192915 elif atom.element == 8: screen[i] = 1.061039 alpha[i] = 0.867814 beta[i] = 0.876635 gamma[i] = 0.387882 elif atom.element == 16: screen[i] = -0.703469 alpha[i] = 0.867814 beta[i] = 0.876635 gamma[i] = 0.387882 else: screen = self.parm_data['SCREEN'] length_conv = u.angstrom.conversion_factor_to(u.nanometer) radii = [rad * length_conv for rad in self.parm_data['RADII']] if gb_model is GBn2: return zip(radii, screen, alpha, beta, gamma) return zip(radii, screen) def createSystem(self, nonbondedMethod=ff.NoCutoff, nonbondedCutoff=1.0*u.nanometer, constraints=None, rigidWater=True, implicitSolvent=None, implicitSolventKappa=None, implicitSolventSaltConc=0.0*u.moles/u.liter, temperature=298.15*u.kelvin, soluteDielectric=1.0, solventDielectric=78.5, removeCMMotion=True, hydrogenMass=None, ewaldErrorTolerance=0.0005, flexibleConstraints=True, verbose=False): """ Construct an OpenMM System representing the topology described by the prmtop file. Parameters: - nonbondedMethod (object=NoCutoff) The method to use for nonbonded interactions. Allowed values are NoCutoff, CutoffNonPeriodic, CutoffPeriodic, Ewald, or PME. - nonbondedCutoff (distance=1*nanometer) The cutoff distance to use for nonbonded interactions. - constraints (object=None) Specifies which bonds or angles should be implemented with constraints. Allowed values are None, HBonds, AllBonds, or HAngles. - rigidWater (boolean=True) If true, water molecules will be fully rigid regardless of the value passed for the constraints argument - implicitSolvent (object=None) If not None, the implicit solvent model to use. Allowed values are HCT, OBC1, OBC2, or GBn - implicitSolventKappa (float=None): Debye screening parameter to model salt concentrations in GB solvent. - implicitSolventSaltConc (float=0.0*u.moles/u.liter): Salt concentration for GB simulations. Converted to Debye length `kappa' - temperature (float=298.15*u.kelvin): Temperature used in the salt concentration-to-kappa conversion for GB salt concentration term - soluteDielectric (float=1.0) The solute dielectric constant to use in the implicit solvent model. - solventDielectric (float=78.5) The solvent dielectric constant to use in the implicit solvent model. - removeCMMotion (boolean=True) If true, a CMMotionRemover will be added to the System. - hydrogenMass (mass=None) 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. - ewaldErrorTolerance (float=0.0005) The error tolerance to use if the nonbonded method is Ewald or PME. - flexibleConstraints (bool=True) Are our constraints flexible or not? - verbose (bool=False) Optionally prints out a running progress report """ # Rebuild the topology file if necessary, and flush the atom property # data to the atom list if self._topology_changed(): self.remake_parm() else: self.atom_list.refresh_data() LJ_radius, LJ_depth = self.openmm_LJ() # Get our LJ parameters LJ_14_radius, LJ_14_depth = self.openmm_14_LJ() # Set the cutoff distance in nanometers cutoff = None if nonbondedMethod is not ff.NoCutoff: cutoff = nonbondedCutoff # Remove units from cutoff if u.is_quantity(cutoff): cutoff = cutoff.value_in_unit(u.nanometers) if nonbondedMethod not in (ff.NoCutoff, ff.CutoffNonPeriodic, ff.CutoffPeriodic, ff.Ewald, ff.PME): raise ValueError('Illegal value for nonbonded method') if self.ptr('ifbox') == 0 and nonbondedMethod in (ff.CutoffPeriodic, ff.Ewald, ff.PME): raise ValueError('Illegal nonbonded method for a ' 'non-periodic system') if implicitSolvent not in (HCT, OBC1, OBC2, GBn, GBn2, None): raise ValueError('Illegal implicit solvent model choice.') if not constraints in (None, ff.HAngles, ff.HBonds, ff.AllBonds): raise ValueError('Illegal constraints choice') # Define conversion factors length_conv = u.angstrom.conversion_factor_to(u.nanometer) _ambfrc = u.kilocalorie_per_mole/(u.angstrom*u.angstrom) _openmmfrc = u.kilojoule_per_mole/(u.nanometer*u.nanometer) bond_frc_conv = _ambfrc.conversion_factor_to(_openmmfrc) _ambfrc = u.kilocalorie_per_mole/(u.radians*u.radians) _openmmfrc = u.kilojoule_per_mole/(u.radians*u.radians) angle_frc_conv = _ambfrc.conversion_factor_to(_openmmfrc) dihe_frc_conv = u.kilocalorie_per_mole.conversion_factor_to( u.kilojoule_per_mole) ene_conv = dihe_frc_conv # Create the system system = mm.System() if verbose: print('Adding particles...') for mass in self.parm_data['MASS']: system.addParticle(mass) # Set up the constraints if verbose and (constraints is not None and not rigidWater): print('Adding constraints...') if constraints in (ff.HBonds, ff.AllBonds, ff.HAngles): for bond in self.bonds_inc_h: system.addConstraint(bond.atom1.starting_index, bond.atom2.starting_index, bond.bond_type.req*length_conv) if constraints in (ff.AllBonds, ff.HAngles): for bond in self.bonds_without_h: system.addConstraint(bond.atom1.starting_index, bond.atom2.starting_index, bond.bond_type.req*length_conv) if rigidWater and constraints is None: for bond in self.bonds_inc_h: if (bond.atom1.residue.resname in WATNAMES and bond.atom2.residue.resname in WATNAMES): system.addConstraint(bond.atom1.starting_index, bond.atom2.starting_index, bond.bond_type.req*length_conv) # Add Bond forces if verbose: print('Adding bonds...') force = mm.HarmonicBondForce() force.setForceGroup(self.BOND_FORCE_GROUP) if flexibleConstraints or (constraints not in (ff.HBonds, ff.AllBonds, ff.HAngles)): for bond in self.bonds_inc_h: force.addBond(bond.atom1.starting_index, bond.atom2.starting_index, bond.bond_type.req*length_conv, 2*bond.bond_type.k*bond_frc_conv) if flexibleConstraints or (constraints not in (ff.AllBonds,ff.HAngles)): for bond in self.bonds_without_h: force.addBond(bond.atom1.starting_index, bond.atom2.starting_index, bond.bond_type.req*length_conv, 2*bond.bond_type.k*bond_frc_conv) system.addForce(force) # Add Angle forces if verbose: print('Adding angles...') force = mm.HarmonicAngleForce() force.setForceGroup(self.ANGLE_FORCE_GROUP) if constraints is ff.HAngles: num_constrained_bonds = system.getNumConstraints() atom_constraints = [[]] * system.getNumParticles() for i in range(num_constrained_bonds): c = system.getConstraintParameters(i) dist = c[2].value_in_unit(u.nanometer) atom_constraints[c[0]].append((c[1], dist)) atom_constraints[c[1]].append((c[0], dist)) for angle in self.angles_inc_h: if constraints is ff.HAngles: a1 = angle.atom1.element a2 = angle.atom2.element a3 = angle.atom3.element nh = int(a1==1) + int(a2==1) + int(a3==1) constrained = (nh >= 2 or (nh == 1 and a2 == 8)) else: constrained = False # no constraints if constrained: l1 = l2 = None for bond in angle.atom2.bonds: if bond.atom1 is angle.atom1 or bond.atom2 is angle.atom1: l1 = bond.bond_type.req * length_conv elif bond.atom1 is angle.atom3 or bond.atom2 is angle.atom3: l2 = bond.bond_type.req * length_conv # Compute the distance between the atoms and add a constraint length = sqrt(l1*l1 + l2*l2 - 2*l1*l2* cos(angle.angle_type.theteq)) system.addConstraint(bond.atom1.starting_index, bond.atom2.starting_index, length) if flexibleConstraints or not constrained: force.addAngle(angle.atom1.starting_index, angle.atom2.starting_index, angle.atom3.starting_index, angle.angle_type.theteq, 2*angle.angle_type.k*angle_frc_conv) for angle in self.angles_without_h: force.addAngle(angle.atom1.starting_index, angle.atom2.starting_index, angle.atom3.starting_index, angle.angle_type.theteq, 2*angle.angle_type.k*angle_frc_conv) system.addForce(force) # Add dihedral forces if verbose: print('Adding torsions...') force = mm.PeriodicTorsionForce() force.setForceGroup(self.DIHEDRAL_FORCE_GROUP) for tor in self.dihedrals_inc_h + self.dihedrals_without_h: force.addTorsion(tor.atom1.starting_index, tor.atom2.starting_index, tor.atom3.starting_index, tor.atom4.starting_index, int(tor.dihed_type.per), tor.dihed_type.phase, tor.dihed_type.phi_k*dihe_frc_conv) system.addForce(force) # Add nonbonded terms now if verbose: print('Adding nonbonded interactions...') force = mm.NonbondedForce() force.setForceGroup(self.NONBONDED_FORCE_GROUP) if self.ptr('ifbox') == 0: # non-periodic if nonbondedMethod is ff.NoCutoff: force.setNonbondedMethod(mm.NonbondedForce.NoCutoff) elif nonbondedMethod is ff.CutoffNonPeriodic: if cutoff is None: raise ValueError('No cutoff value specified') force.setNonbondedMethod(mm.NonbondedForce.CutoffNonPeriodic) force.setCutoffDistance(cutoff) else: raise ValueError('Illegal nonbonded method for non-periodic ' 'system') else: # periodic # Set up box vectors (from inpcrd if available, or fall back to # prmtop definitions system.setDefaultPeriodicBoxVectors(*self.box_vectors) # Set cutoff if cutoff is None: # Compute cutoff automatically box = self.box_lengths min_box_width = min((box[0]/u.nanometers, box[1]/u.nanometers, box[2]/u.nanometers)) CLEARANCE_FACTOR = 0.97 cutoff = u.Quantity((min_box_width*CLEARANCE_FACTOR)/2.0, u.nanometers) if nonbondedMethod is not ff.NoCutoff: force.setCutoffDistance(cutoff) # Set nonbonded method. if nonbondedMethod is ff.NoCutoff: force.setNonbondedMethod(mm.NonbondedForce.NoCutoff) elif nonbondedMethod is ff.CutoffNonPeriodic: force.setNonbondedMethod(mm.NonbondedForce.CutoffNonPeriodic) elif nonbondedMethod is ff.CutoffPeriodic: force.setNonbondedMethod(mm.NonbondedForce.CutoffPeriodic) elif nonbondedMethod is ff.Ewald: force.setNonbondedMethod(mm.NonbondedForce.Ewald) elif nonbondedMethod is ff.PME: force.setNonbondedMethod(mm.NonbondedForce.PME) else: raise ValueError('Cutoff method is not understood') if ewaldErrorTolerance is not None: force.setEwaldErrorTolerance(ewaldErrorTolerance) # Add per-particle nonbonded parameters (LJ params) sigma_scale = 2**(-1/6) * 2 for i, atm in enumerate(self.atom_list): force.addParticle(atm.charge, sigma_scale*LJ_radius[atm.nb_idx-1]*length_conv, LJ_depth[atm.nb_idx-1]*ene_conv) # Add 1-4 interactions excluded_atom_pairs = set() # save these pairs so we don't zero them out sigma_scale = 2**(-1/6) for tor in self.dihedrals_inc_h + self.dihedrals_without_h: if min(tor.signs) < 0: continue # multi-terms and impropers charge_prod = (tor.atom1.charge * tor.atom4.charge / tor.dihed_type.scee) epsilon = (sqrt(LJ_14_depth[tor.atom1.nb_idx-1] * ene_conv * LJ_14_depth[tor.atom4.nb_idx-1] * ene_conv) / tor.dihed_type.scnb) sigma = (LJ_14_radius[tor.atom1.nb_idx-1] + LJ_14_radius[tor.atom4.nb_idx-1])*length_conv*sigma_scale force.addException(tor.atom1.starting_index, tor.atom4.starting_index, charge_prod, sigma, epsilon) excluded_atom_pairs.add( min( (tor.atom1.starting_index, tor.atom4.starting_index), (tor.atom4.starting_index, tor.atom1.starting_index) ) ) # Add excluded atoms for atom in self.atom_list: # Exclude all bonds and angles for atom2 in atom.bond_partners: if atom2.starting_index > atom.starting_index: force.addException(atom.starting_index, atom2.starting_index, 0.0, 0.1, 0.0) for atom2 in atom.angle_partners: if atom2.starting_index > atom.starting_index: force.addException(atom.starting_index, atom2.starting_index, 0.0, 0.1, 0.0) for atom2 in atom.exclusion_partners: if atom2.starting_index > atom.starting_index: force.addException(atom.starting_index, atom2.starting_index, 0.0, 0.1, 0.0) for atom2 in atom.dihedral_partners: if atom2.starting_index <= atom.starting_index: continue if ((atom.starting_index, atom2.starting_index) in excluded_atom_pairs): continue force.addException(atom.starting_index, atom2.starting_index, 0.0, 0.1, 0.0) system.addForce(force) # Add virtual sites for water # First tag the residues that have an extra point in them for res in self.residue_list: res.has_ep = False ep = [atom for atom in self.atom_list if atom.atname in EPNAMES] for atom in ep: atom.residue.has_ep = True if len(ep) > 0: numRes = ep[-1].residue.idx + 1 waterO = [[] for i in range(numRes)] waterH = [[] for i in range(numRes)] waterEP = [[] for i in range(numRes)] for atom in self.atom_list: if atom.residue.has_ep: if atom.element == 8: waterO[res].append(atom) elif atom.element == 1: waterH[res].append(atom) elif atom.element == 0: waterEP[res].append(atom) # Record bond lengths for faster access distOH = [None] * numRes distHH = [None] * numRes distOE = [None] * numRes for bond in self.bonds_inc_h + self.bonds_without_h: a1 = bond.atom1 a2 = bond.atom2 if a1.residue.has_ep: res = a1.residue.idx if a1.element == 1 or a2.element == 1: if a1.element == 1 and a2.element == 1: distHH[res] = bond.bond_type.req * u.angstroms if a1.element == 8 or a2.element == 8: distOH[res] = bond.bond_type.req * u.angstroms elif ((a1.element == 8 or a2.element == 8) and (a1.element == 0 or a2.element == 0)): distOE[res] = bond.bond_type.req * u.angstroms # Loop over residues and add the virtual points out_of_plane_angle = 54.735 * u.degrees cosOOP = u.cos(out_of_plane_angle) sinOOP = u.sin(out_of_plane_angle) for residue in self.residue_list: if not residue.has_ep: continue res = residue.idx if len(waterO[res]) == 1 and len(waterH[res]) == 2: if len(waterEP[res]) == 1: # 4-point water weightH = distOE[res] / sqrt(distOH[res] * distOH[res] - 0.25 * distHH[res] * distHH[res]) system.setVirtualSite( waterEP[res][0], mm.ThreeParticleAverageSite(waterO[res][0], waterH[res][0], waterH[res][1], 1-weightH, weightH/2, weightH/2) ) elif len(waterEP[res]) == 2: # 5-point water weightH = (cosOOP * distOE[res] / sqrt(distOH[res] * distOH[res] - 0.25 * distHH[res] * distHH[res]) ) angleHOH = 2 * asin(0.5 * distHH[res] / distOH[res]) lenCross = distOH[res] * distOH[res] * sin(angleHOH) weightCross = sinOOP * distOE[res] / lenCross site1 = mm.OutOfPlaneSite(waterO[res][0], waterH[res][0], waterH[res][1], weightH/2, weightH/2, weightCross) site2 = mm.OutOfPlaneSite(waterO[res][0], waterH[res][0], waterH[res][1], weightH/2, weightH/2, -weightCross) system.setVirtualSite(waterEP[res][0], site1) system.setVirtualSite(waterEP[res][1], site2) # 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) # If implicitSolventKappa is None, compute it from salt # concentration if implicitSolventKappa is None: if u.is_quantity(implicitSolventSaltConc): sc = implicitSolventSaltConc.value_in_unit(u.moles/u.liter) implicitSolventSaltConc = sc if u.is_quantity(temperature): temperature = temperature.value_in_unit(u.kelvin) # The constant is 1 / sqrt( epsilon_0 * kB / (2 * NA * q^2 * # 1000) ) where NA is avogadro's number, epsilon_0 is the # permittivity of free space, q is the elementary charge (this # number matches sander/pmemd's kappa conversion factor) implicitSolventKappa = 50.33355 * sqrt(implicitSolventSaltConc / solventDielectric / temperature) # Multiply by 0.73 to account for ion exclusions, and multiply # by 10 to convert to 1/nm from 1/angstroms implicitSolventKappa *= 7.3 elif implicitSolvent is None: implicitSolventKappa = 0.0 if u.is_quantity(implicitSolventKappa): implicitSolventKappa = implicitSolventKappa.value_in_unit( (1.0/u.nanometer).unit) if implicitSolvent is HCT: gb = GBSAHCTForce(solventDielectric, soluteDielectric, None, cutoff, kappa=implicitSolventKappa) elif implicitSolvent is OBC1: gb = GBSAOBC1Force(solventDielectric, soluteDielectric, None, cutoff, kappa=implicitSolventKappa) elif implicitSolvent is OBC2: gb = GBSAOBC2Force(solventDielectric, soluteDielectric, None, cutoff, kappa=implicitSolventKappa) elif implicitSolvent is GBn: gb = GBSAGBnForce(solventDielectric, soluteDielectric, None, cutoff, kappa=implicitSolventKappa) elif implicitSolvent is GBn2: gb = GBSAGBn2Force(solventDielectric, soluteDielectric, None, cutoff, kappa=implicitSolventKappa) for i, atom in enumerate(self.atom_list): gb.addParticle([atom.charge] + list(gb_parms[i])) # Set cutoff method if nonbondedMethod is ff.NoCutoff: gb.setNonbondedMethod(mm.NonbondedForce.NoCutoff) elif nonbondedMethod is ff.CutoffNonPeriodic: gb.setNonbondedMethod(mm.NonbondedForce.CutoffNonPeriodic) gb.setCutoffDistance(cutoff) elif nonbondedMethod is ff.CutoffPeriodic: gb.setNonbondedMethod(mm.NonbondedForce.CutoffPeriodic) gb.setCutoffDistance(cutoff) else: raise ValueError('Illegal nonbonded method for use with GBSA') gb.setForceGroup(self.GB_FORCE_GROUP) system.addForce(gb) force.setReactionFieldDielectric(1.0) # applies to NonbondedForce # See if we repartition the hydrogen masses if hydrogenMass is not None: for bond in self.bonds_inc_h: atom1, atom2 = bond.atom1, bond.atom2 if atom1.element == 1: atom1, atom2 = atom2, atom1 # now atom2 is hydrogen for sure if atom1.element != 1: transfer_mass = hydrogenMass - atom2.mass new_mass1 = (system.getParticleMass(atom1.index) - transfer_mass) system.setParticleMass(atom2.index, hydrogenMass) system.setParticleMass(atom1.index, new_mass1) # See if we want to remove COM motion if removeCMMotion: system.addForce(mm.CMMotionRemover()) # Cache our system for easy access self._system = system return system @property def system(self): """ Return the cached system class -- it needs to be initialized via "createSystem" first! """ try: return self._system except AttributeError: raise APIError('You must initialize the system with createSystem ' 'before accessing the cached object.') @property def positions(self): """ Return the cached positions or create new ones from the atoms """ try: if len(self._positions) == len(self.atom_list): return self._positions except AttributeError: pass self._positions = tuple([Vec3(a.xx, a.xy, a.xz) for a in self.atom_list]) * u.angstroms return self._positions @positions.setter def positions(self, stuff): """ Update the cached positions and assign the coordinates to the atoms """ self._positions = stuff for i, pos in enumerate(stuff.value_in_unit(u.angstroms)): i3 = i * 3 atom = self.atom_list[i] atom.xx, atom.xy, atom.xz = pos self.coords[i3], self.coords[i3+1], self.coords[i3+2] = pos @property def velocities(self): """ Same as for positions, but for velocities """ try: if len(self._velocities) == len(self.atom_list): return self._velocities except AttributeError: pass self._velocities = tuple([Vec3(a.vx, a.vy, a.vz) for a in self.atom_list]) * (u.angstroms/u.picosecond) return self._velocities @velocities.setter def velocities(self, stuff): self._velocities = stuff for atom, vel in zip(self.atom_list, stuff): atom.vx, atom.vy, atom.vz = vel.value_in_unit( u.angstroms/u.picoseconds) @property def box_vectors(self): """ Return tuple of box vectors """ if hasattr(self, 'rst7'): box = [x*u.angstrom for x in self.rst7.box[:3]] ang = [self.rst7.box[3], self.rst7.box[4], self.rst7.box[5]] return _box_vectors_from_lengths_angles(box[0], box[1], box[2], ang[0], ang[1], ang[2]) else: box = [x*u.angstrom for x in self.parm_data['BOX_DIMENSIONS'][1:]] ang = [self.parm_data['BOX_DIMENSIONS'][0]] * 3 return _box_vectors_from_lengths_angles(box[0], box[1], box[2], ang[0], ang[1], ang[2]) @property def box_lengths(self): """ Return tuple of 3 units """ if hasattr(self, 'rst7'): box = [x*u.angstrom for x in self.rst7.box[:3]] else: box = [x*u.angstrom for x in self.parm_data['BOX_DIMENSIONS'][1:]] return tuple(box)
def addHydrogens(self, forcefield, pH=7.0, variants=None, platform=None): """Add missing hydrogens to the model. Some residues can exist in multiple forms depending on the pH and properties of the local environment. These variants differ in the presence or absence of particular hydrogens. In particular, the following variants are supported: Aspartic acid: ASH: Neutral form with a hydrogen on one of the delta oxygens ASP: Negatively charged form without a hydrogen on either delta oxygen Cysteine: CYS: Neutral form with a hydrogen on the sulfur CYX: No hydrogen on the sulfur (either negatively charged, or part of a disulfide bond) Glutamic acid: GLH: Neutral form with a hydrogen on one of the epsilon oxygens GLU: Negatively charged form without a hydrogen on either epsilon oxygen Histidine: 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 Lysine: LYN: Neutral form with two hydrogens on the zeta nitrogen LYS: Positively charged form with three hydrogens on the zeta nitrogen The variant to use for each residue is determined by the following rules: 1. The most common variant at the specified pH is selected. 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. Definitions for standard amino acids and nucleotides are built in. You can call loadHydrogenDefinitions() to load additional definitions for other residue types. Parameters: - forcefield (ForceField) the ForceField to use for determining the positions of hydrogens - pH (float=7.0) the pH based on which to select variants - variants (list=None) an optional list of variants to use. If this is specified, its length must equal the number of residues in the model. variants[i] is the name of the variant to use for residue i (indexed starting at 0). If an element is None, the standard rules will be followed to select a variant for that residue. - platform (Platform=None) the Platform to use when computing the hydrogen atom positions. If this is None, the default Platform will be used. Returns: a list of what variant was actually selected for each residue, in the same format as the variants parameter """ # Check the list of variants. residues = list(self.topology.residues()) if variants is not None: if len(variants) != len(residues): raise ValueError("The length of the variants list must equal the number of residues") else: variants = [None]*len(residues) actualVariants = [None]*len(residues) # Load the residue specifications. if not Modeller._hasLoadedStandardHydrogens: Modeller.loadHydrogenDefinitions(os.path.join(os.path.dirname(__file__), 'data', 'hydrogens.xml')) # Make a list of atoms bonded to each atom. bonded = {} for atom in self.topology.atoms(): bonded[atom] = [] for atom1, atom2 in self.topology.bonds(): bonded[atom1].append(atom2) bonded[atom2].append(atom1) # Define a function that decides whether a set of atoms form a hydrogen bond, using fairly tolerant criteria. def isHbond(d, h, a): if norm(d-a) > 0.35*nanometer: return False deltaDH = h-d deltaHA = a-h deltaDH /= norm(deltaDH) deltaHA /= norm(deltaHA) return acos(dot(deltaDH, deltaHA)) < 50*degree # Loop over residues. newTopology = Topology() newTopology.setUnitCellDimensions(deepcopy(self.topology.getUnitCellDimensions())) newAtoms = {} newPositions = []*nanometer newIndices = [] acceptors = [atom for atom in self.topology.atoms() if atom.element in (elem.oxygen, elem.nitrogen)] for chain in self.topology.chains(): newChain = newTopology.addChain() for residue in chain.residues(): newResidue = newTopology.addResidue(residue.name, newChain) isNTerminal = (residue == chain._residues[0]) isCTerminal = (residue == chain._residues[-1]) if residue.name in Modeller._residueHydrogens: # Add hydrogens. First select which variant to use. spec = Modeller._residueHydrogens[residue.name] variant = variants[residue.index] if variant is None: if residue.name == 'CYS': # If this is part of a disulfide, use CYX. sulfur = [atom for atom in residue.atoms() if atom.element == elem.sulfur] if len(sulfur) == 1 and any((atom.residue != residue for atom in bonded[sulfur[0]])): variant = 'CYX' if residue.name == 'HIS' and pH > 6.5: # See if either nitrogen already has a hydrogen attached. nd1 = [atom for atom in residue.atoms() if atom.name == 'ND1'] ne2 = [atom for atom in residue.atoms() if atom.name == 'NE2'] if len(nd1) != 1 or len(ne2) != 1: raise ValueError('HIS residue (%d) has the wrong set of atoms' % residue.index) nd1 = nd1[0] ne2 = ne2[0] nd1HasHydrogen = any((atom.element == elem.hydrogen for atom in bonded[nd1])) ne2HasHydrogen = any((atom.element == elem.hydrogen for atom in bonded[ne2])) if nd1HasHydrogen and ne2HasHydrogen: variant = 'HIP' elif nd1HasHydrogen: variant = 'HID' elif ne2HasHydrogen: variant = 'HIE' else: # Estimate the hydrogen positions. nd1Pos = self.positions[nd1.index] ne2Pos = self.positions[ne2.index] hd1Delta = Vec3(0, 0, 0)*nanometer for other in bonded[nd1]: hd1Delta += nd1Pos-self.positions[other.index] hd1Delta *= 0.1*nanometer/norm(hd1Delta) hd1Pos = nd1Pos+hd1Delta he2Delta = Vec3(0, 0, 0)*nanometer for other in bonded[ne2]: he2Delta += ne2Pos-self.positions[other.index] he2Delta *= 0.1*nanometer/norm(he2Delta) he2Pos = ne2Pos+he2Delta # See whether either hydrogen would form a hydrogen bond. nd1IsBonded = False ne2IsBonded = False for acceptor in acceptors: if acceptor.residue != residue: acceptorPos = self.positions[acceptor.index] if isHbond(nd1Pos, hd1Pos, acceptorPos): nd1IsBonded = True break if isHbond(ne2Pos, he2Pos, acceptorPos): ne2IsBonded = True if ne2IsBonded and not nd1IsBonded: variant = 'HIE' else: variant = 'HID' elif residue.name == 'HIS': variant = 'HIP' 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 # Make a list of hydrogens that should be present in the residue. parents = [atom for atom in residue.atoms() if atom.element != elem.hydrogen] parentNames = [atom.name for atom in parents] hydrogens = [h for h in spec.hydrogens if (variant is None and pH <= h.maxph) or (h.variants is None and pH <= h.maxph) or (h.variants is not None and variant in h.variants)] hydrogens = [h for h in hydrogens if h.terminal is None or (isNTerminal and h.terminal == 'N') or (isCTerminal and h.terminal == 'C')] hydrogens = [h for h in hydrogens if h.parent in parentNames] # Loop over atoms in the residue, adding them to the new topology along with required hydrogens. for parent in residue.atoms(): # Add the atom. newAtom = newTopology.addAtom(parent.name, parent.element, newResidue) newAtoms[parent] = newAtom newPositions.append(deepcopy(self.positions[parent.index])) if parent in parents: # Match expected hydrogens with existing ones and find which ones need to be added. existing = [atom for atom in bonded[parent] if atom.element == elem.hydrogen] expected = [h for h in hydrogens if h.parent == parent.name] if len(existing) < len(expected): # Try to match up existing hydrogens to expected ones. matches = [] for e in existing: match = [h for h in expected if h.name == e.name] if len(match) > 0: matches.append(match[0]) expected.remove(match[0]) else: matches.append(None) # If any hydrogens couldn't be matched by name, just match them arbitrarily. for i in range(len(matches)): if matches[i] is None: matches[i] = expected[-1] expected.remove(expected[-1]) # Add the missing hydrogens. for h in expected: newH = newTopology.addAtom(h.name, elem.hydrogen, newResidue) newIndices.append(newH.index) delta = Vec3(0, 0, 0)*nanometer if len(bonded[parent]) > 0: for other in bonded[parent]: delta += self.positions[parent.index]-self.positions[other.index] else: delta = Vec3(random.random(), random.random(), random.random())*nanometer delta *= 0.1*nanometer/norm(delta) delta += 0.05*Vec3(random.random(), random.random(), random.random())*nanometer delta *= 0.1*nanometer/norm(delta) newPositions.append(self.positions[parent.index]+delta) newTopology.addBond(newAtom, newH) else: # Just copy over the residue. for atom in residue.atoms(): newAtom = newTopology.addAtom(atom.name, atom.element, newResidue) newAtoms[atom] = newAtom newPositions.append(deepcopy(self.positions[atom.index])) for bond in self.topology.bonds(): if bond[0] in newAtoms and bond[1] in newAtoms: newTopology.addBond(newAtoms[bond[0]], newAtoms[bond[1]]) # The hydrogens were added at random positions. Now use the ForceField to fix them up. system = forcefield.createSystem(newTopology, rigidWater=False) atoms = list(newTopology.atoms()) for i in range(system.getNumParticles()): if atoms[i].element != elem.hydrogen: # This is a heavy atom, so make it immobile. system.setParticleMass(i, 0) if platform is None: context = Context(system, VerletIntegrator(0.0)) else: context = Context(system, VerletIntegrator(0.0), platform) context.setPositions(newPositions) LocalEnergyMinimizer.minimize(context) self.topology = newTopology self.positions = context.getState(getPositions=True).getPositions() return actualVariants