def setUnitCellDimensions(self, dimensions): """Set the dimensions of the crystallographic unit cell. This method is an alternative to setPeriodicBoxVectors() for the case of a rectangular box. It sets the box vectors to be orthogonal to each other and to have the specified lengths.""" if dimensions is None: self._periodicBoxVectors = None else: if is_quantity(dimensions): dimensions = dimensions.value_in_unit(nanometers) self._periodicBoxVectors = (Vec3(dimensions[0], 0, 0), Vec3(0, dimensions[1], 0), Vec3(0, 0, dimensions[2]))*nanometers
def run_simulation(self): """ Run a simulation to calculate the current configuration's energy level. Note that the atoms will likely move somewhat during the calculation, since energy minimization is used. Returns: A tuple of the form (potential_energy, updated_positions) """ # Delete solvent that's based on previous positions cartesian = self.zmat.get_cartesian().sort_index() self.simulation.context.setPositions([ Vec3(x, y, z) for x, y, z in zip(cartesian['x'], cartesian['y'], cartesian['z']) ]) # self._add_backbone_restraint() # self._fix_backbone() self.modeller.addSolvent(self.forcefield, padding=1.0 * u.nanometer) self.simulation.minimizeEnergy(maxIterations=200) state = self.simulation.context.getState(getEnergy=True, getPositions=True) p_energy = state.getPotentialEnergy() positions = state.getPositions(asNumpy=True) # Clean up - remove solvent and backbone restraint (for next iteration) self.modeller.deleteWater() # self._remove_backbone_restraint() return p_energy, positions
def _load(self, input_stream): # Read one line at a time for pdb_line in input_stream: # Look for atoms if (pdb_line.find("ATOM ") == 0) or (pdb_line.find("HETATM") == 0): self._add_atom(Atom(pdb_line)) # Notice MODEL punctuation, for the next level of detail # in the structure->model->chain->residue->atom->position hierarchy elif (pdb_line.find("MODEL") == 0): model_number = int(pdb_line[10:14]) self._add_model(Model(model_number)) elif (pdb_line.find("ENDMDL") == 0): self._current_model._finalize() if not self.load_all_models: break elif (pdb_line.find("END") == 0): self._current_model._finalize() if not self.load_all_models: break elif (pdb_line.find("TER") == 0 and pdb_line.split()[0] == "TER"): self._current_model._current_chain._add_ter_record() elif (pdb_line.find("CRYST1") == 0): self._unit_cell_dimensions = Vec3( float(pdb_line[6:15]), float(pdb_line[15:24]), float(pdb_line[24:33])) * unit.angstroms elif (pdb_line.find("CONECT") == 0): atoms = [int(pdb_line[6:11])] for pos in (11, 16, 21, 26): try: atoms.append(int(pdb_line[pos:pos + 5])) except: pass self._current_model.connects.append(atoms) self._finalize()
def getUnitCellDimensions(self): """Get the dimensions of the crystallographic unit cell. The return value may be None if this Topology does not represent a periodic structure. """ if self._periodicBoxVectors is None: return None xsize = self._periodicBoxVectors[0][0].value_in_unit(nanometers) ysize = self._periodicBoxVectors[1][1].value_in_unit(nanometers) zsize = self._periodicBoxVectors[2][2].value_in_unit(nanometers) return Vec3(xsize, ysize, zsize) * nanometers
def _parse(self, fname): with open(fname, 'r') as crdfile: line = crdfile.readline() while len(line.strip()) == 0: # Skip whitespace, as a precaution line = crdfile.readline() intitle = True while intitle: self.title.append(line.strip()) line = crdfile.readline() if len(line.strip()) == 0: intitle = False elif line.strip()[0] != '*': intitle = False else: intitle = True while len(line.strip()) == 0: # Skip whitespace line = crdfile.readline() try: self.natom = int(line.strip().split()[0]) for _ in range(self.natom): line = crdfile.readline().strip().split() self.atomno.append(int(line[0])) self.resno.append(int(line[1])) self.resname.append(line[2]) self.attype.append(line[3]) pos = Vec3(float(line[4]), float(line[5]), float(line[6])) self.positions.append(pos) self.segid.append(line[7]) self.weighting.append(float(line[9])) if self.natom != len(self.positions): raise CharmmFileError("Error parsing CHARMM .crd file: %d " "atoms requires %d positions (not %d)" % (self.natom, self.natom, len(self.positions)) ) except (ValueError, IndexError): raise CharmmFileError('Error parsing CHARMM coordinate file') # Apply units to the positions now. Do it this way to allow for # (possible) numpy functionality in the future. self.positions = u.Quantity(self.positions, u.angstroms)
def _parse(self, fname): crdfile = open(fname, 'r') line = crdfile.readline() while len(line.strip()) == 0: # Skip whitespace, as a precaution line = crdfile.readline() intitle = True while intitle: self.title.append(line.strip()) line = crdfile.readline() if len(line.strip()) == 0: intitle = False elif line.strip()[0] != '*': intitle = False else: intitle = True while len(line.strip()) == 0: # Skip whitespace line = crdfile.readline() try: self.natom = int(line.strip().split()[0]) for row in range(self.natom): line = crdfile.readline().strip().split() self.atomno.append(int(line[0])) self.resno.append(int(line[1])) self.resname.append(line[2]) self.attype.append(line[3]) pos = Vec3(float(line[4]), float(line[5]), float(line[6])) self.positions.append(pos) self.segid.append(line[7]) self.resid.append(int(line[8])) self.weighting.append(float(line[9])) if self.natom != len(self.positions): raise CharmmFileError( "Error parsing CHARMM .crd file: %d " "atoms requires %d positions (not %d)" % (self.natom, self.natom, len(self.positions))) except (ValueError, IndexError), e: raise CharmmFileError('Error parsing CHARMM coordinate file')
def _load(self, input_stream): self._reset_atom_numbers() self._reset_residue_numbers() # Read one line at a time for pdb_line in input_stream: # Look for atoms if (pdb_line.find("ATOM ") == 0) or (pdb_line.find("HETATM") == 0): self._add_atom(Atom(pdb_line, self)) # Notice MODEL punctuation, for the next level of detail # in the structure->model->chain->residue->atom->position hierarchy elif (pdb_line.find("MODEL") == 0): model_number = int(pdb_line[10:14]) self._add_model(Model(model_number)) self._reset_atom_numbers() self._reset_residue_numbers() elif (pdb_line.find("ENDMDL") == 0): self._current_model._finalize() if not self.load_all_models: break elif (pdb_line.find("END") == 0): self._current_model._finalize() if not self.load_all_models: break elif (pdb_line.find("TER") == 0 and pdb_line.split()[0] == "TER"): self._current_model._current_chain._add_ter_record() self._reset_residue_numbers() elif (pdb_line.find("CRYST1") == 0): self._unit_cell_dimensions = Vec3(float(pdb_line[6:15]), float(pdb_line[15:24]), float(pdb_line[24:33]))*unit.angstroms elif (pdb_line.find("CONECT") == 0): atoms = [int(pdb_line[6:11])] for pos in (11,16,21,26): try: atoms.append(int(pdb_line[pos:pos+5])) except: pass self._current_model.connects.append(atoms) elif (pdb_line.find("SEQRES") == 0): chain_id = pdb_line[11] if len(self.sequences) == 0 or chain_id != self.sequences[-1].chain_id: self.sequences.append(Sequence(chain_id)) self.sequences[-1].residues.extend(pdb_line[19:].split()) elif (pdb_line.find("MODRES") == 0): self.modified_residues.append(ModifiedResidue(pdb_line[16], int(pdb_line[18:22]), pdb_line[12:15].strip(), pdb_line[24:27].strip())) self._finalize()
def _get_formatted_crds(self, crdfile, crds): for row in range(self.natom): line = crdfile.readline() if not line: raise CharmmFileError('Premature end of file') if len(line) < 3*CHARMMLEN: raise CharmmFileError("Less than 3 coordinates present in " "coordinate row or positions may be " "truncated.") line = line.replace('D','E') # CHARMM uses 'D' for exponentials # CHARMM uses fixed format (len = CHARMMLEN = 22) for crds in .rst's c = Vec3(float(line[0:CHARMMLEN]), float(line[CHARMMLEN:2*CHARMMLEN]), float(line[2*CHARMMLEN:3*CHARMMLEN])) crds.append(c)
def addHydrogensPageCallback(parameters, handler): if 'addhydrogens' in parameters: pH = float(parameters.getfirst('ph')) fixer.addMissingHydrogens(pH) if 'addwater' in parameters: padding, boxSize, boxVectors = None, None, None if parameters.getfirst('boxType') == 'geometry': geompadding = float( parameters.getfirst('geomPadding')) * unit.nanometer geometry = parameters.getfirst('geometryDropdown') base_size = float( parameters.getfirst('maxMolecularAxis')) * unit.nanometer if geometry == 'cube': padding = geompadding elif geometry == 'truncatedOctahedron': vectors = Vec3(1, 0, 0), Vec3(1 / 3, 2 * sqrt(2) / 3, 0), Vec3(-1 / 3, sqrt(2) / 3, sqrt(6) / 3) boxVectors = [(base_size + geompadding) * v for v in vectors] elif geometry == 'rhombicDodecahedron': vectors = Vec3(1, 0, 0), Vec3(0, 1, 0), Vec3(0.5, 0.5, sqrt(2) / 2) boxVectors = [(base_size + geompadding) * v for v in vectors] else: boxSize = (float(parameters.getfirst('boxx')), float(parameters.getfirst('boxy')), float(parameters.getfirst('boxz'))) * unit.nanometer ionicStrength = float( parameters.getfirst('ionicstrength')) * unit.molar positiveIon = parameters.getfirst('positiveion') + '+' negativeIon = parameters.getfirst('negativeion') + '-' fixer.addSolvent(boxSize, padding, boxVectors, positiveIon, negativeIon, ionicStrength) if 'addmembrane' in parameters: lipidType = parameters.getfirst('lipidType') padding = float( parameters.getfirst('membranePadding')) * unit.nanometer ionicStrength = float( parameters.getfirst('ionicstrength')) * unit.molar positiveIon = parameters.getfirst('positiveion') + '+' negativeIon = parameters.getfirst('negativeion') + '-' fixer.addMembrane(lipidType, 0 * unit.nanometer, padding, positiveIon, negativeIon, ionicStrength) displaySaveFilePage()
def __sub__(self, other): """Add two Vec3s.""" return Vec3(self.x - other[0], self.y - other[1], self.z - other[2])
def __radd__(self, other): """Add two Vec3s.""" return Vec3(self.x + other[0], self.y + other[1], self.z + other[2])
def __radd__(self, other): """Add two Vec3s.""" return Vec3(self[0] + other[0], self[1] + other[1], self[2] + other[2])
def __rsub__(self, other): """Add two Vec3s.""" return Vec3(other[0] - self[0], other[1] - self[1], other[2] - self[2])
def __neg__(self): return Vec3(-self.x, -self.y, -self.z)
def __div__(self, other): """Divide a Vec3 by a constant.""" return Vec3(self.x / other, self.y / other, self.z / other)
def __rsub__(self, other): """Add two Vec3s.""" return Vec3(other[0] - self.x, other[1] - self.y, other[2] - self.z)
def __mul__(self, other): """Multiply a Vec3 by a constant.""" if unit.is_unit(other): return unit.Quantity(self, other) return Vec3(other * self[0], other * self[1], other * self[2])
def __deepcopy__(self, memo): return Vec3(self[0], self[1], self[2])
def __div__(self, other): """Divide a Vec3 by a constant.""" return Vec3(self[0] / other, self[1] / other, self[2] / other)
def __rmul__(self, other): """Multiply a Vec3 by a constant.""" if unit.is_unit(other): return unit.Quantity(self, other) return Vec3(other * self.x, other * self.y, other * self.z)
def dodecahedral_box_vectors(base_size, padding=0.0): vectors = Vec3(1, 0, 0), Vec3(0, 1, 0), Vec3(0.5, 0.5, sqrt(2) / 2) return (float(base_size) + padding) * (vectors * nm)
def __deepcopy__(self, memo): return Vec3(self.x, self.y, self.z)
def compute_sterics_dvdl(sigma_1, sigma_2, epsilon_1, epsilon_2, r, l=0.5): sigma = 0.5 * (sigma_1 + sigma_2) epsilon = np.sqrt(epsilon_1 * epsilon_2) reff = (0.5 * l * sigma ** 6 + r ** 6) ** (1 / 6) x = (sigma / reff) ** 6 dreffdl = 1 / 6 * (0.5 * l * sigma ** 6 + r ** 6) ** (-5 / 6) * 0.5 * sigma ** 6 dxdl = -6 * (sigma / reff) ** 5 * sigma / (reff ** 2) * dreffdl dudl = -4 * epsilon * x * (x - 1) + (1 - l) * 4 * epsilon * (dxdl * (x - 1) + x * dxdl) return dudl for r in distance: context.setPositions([Vec3(x=0.0, y=0.0, z=0.0), Vec3(x=r, y=0.0, z=0.0)]) state = context.getState(getEnergy=True, getParameterDerivatives=True) energy = state.getPotentialEnergy() energy_derivs = state.getEnergyParameterDerivatives() energy_deriv = energy_derivs['lambda'] print (energy_deriv, compute_sterics_dvdl(sigmas[0]._value, sigmas[1]._value, epsilons[0]._value, epsilons[1]._value, r))
def __init__(self, pdb_line, pdbstructure=None, extraParticleIdentifier='EP'): """Create a new pdb.Atom from an ATOM or HETATM line. Example line: ATOM 2209 CB TYR A 299 6.167 22.607 20.046 1.00 8.12 C 00000000011111111112222222222333333333344444444445555555555666666666677777777778 12345678901234567890123456789012345678901234567890123456789012345678901234567890 ATOM line format description from http://deposit.rcsb.org/adit/docs/pdb_atom_format.html: COLUMNS DATA TYPE CONTENTS -------------------------------------------------------------------------------- 1 - 6 Record name "ATOM " 7 - 11 Integer Atom serial number. 13 - 16 Atom Atom name. 17 Character Alternate location indicator. 18 - 20 Residue name Residue name. 22 Character Chain identifier. 23 - 26 Integer Residue sequence number. 27 AChar Code for insertion of residues. 31 - 38 Real(8.3) Orthogonal coordinates for X in Angstroms. 39 - 46 Real(8.3) Orthogonal coordinates for Y in Angstroms. 47 - 54 Real(8.3) Orthogonal coordinates for Z in Angstroms. 55 - 60 Real(6.2) Occupancy (Default = 1.0). 61 - 66 Real(6.2) Temperature factor (Default = 0.0). 73 - 76 LString(4) Segment identifier, left-justified. 77 - 78 LString(2) Element symbol, right-justified. 79 - 80 LString(2) Charge on the atom. """ # We might modify first/final status during _finalize() methods self.is_first_atom_in_chain = False self.is_final_atom_in_chain = False self.is_first_residue_in_chain = False self.is_final_residue_in_chain = False # Start parsing fields from pdb line self.record_name = pdb_line[0:6].strip() if pdbstructure is not None and pdbstructure._atom_numbers_are_hex: self.serial_number = int(pdb_line[6:11], 16) else: try: self.serial_number = int(pdb_line[6:11]) except: try: self.serial_number = int(pdb_line[6:11], 16) pdbstructure._atom_numbers_are_hex = True except: # Just give it the next number in sequence. self.serial_number = pdbstructure._next_atom_number self.name_with_spaces = pdb_line[12:16] alternate_location_indicator = pdb_line[16] self.residue_name_with_spaces = pdb_line[17:20] # In some MD codes, notably ffamber in gromacs, residue name has a fourth character in # column 21 possible_fourth_character = pdb_line[20:21] if possible_fourth_character != " ": # Fourth character should only be there if official 3 are already full if len(self.residue_name_with_spaces.strip()) != 3: raise ValueError('Misaligned residue name: %s' % pdb_line) self.residue_name_with_spaces += possible_fourth_character self.residue_name = self.residue_name_with_spaces.strip() self.chain_id = pdb_line[21] if pdbstructure is not None and pdbstructure._residue_numbers_are_hex: self.residue_number = int(pdb_line[22:26], 16) else: try: self.residue_number = int(pdb_line[22:26]) except: try: self.residue_number = int(pdb_line[22:26], 16) pdbstructure._residue_numbers_are_hex = True except: # When VMD runs out of hex values it starts filling the residue ID field with ****. # Look at the most recent atoms to figure out whether this is a new residue or not. if pdbstructure._current_model is None or pdbstructure._current_model._current_chain is None or pdbstructure._current_model._current_chain._current_residue is None: # This is the first residue in the model. self.residue_number = pdbstructure._next_residue_number else: currentRes = pdbstructure._current_model._current_chain._current_residue if currentRes.name_with_spaces != self.residue_name_with_spaces: # The residue name has changed. self.residue_number = pdbstructure._next_residue_number elif self.name_with_spaces in currentRes.atoms_by_name: # There is already an atom with this name. self.residue_number = pdbstructure._next_residue_number else: self.residue_number = currentRes.number self.insertion_code = pdb_line[26] # coordinates, occupancy, and temperature factor belong in Atom.Location object x = float(pdb_line[30:38]) y = float(pdb_line[38:46]) z = float(pdb_line[46:54]) try: occupancy = float(pdb_line[54:60]) except: occupancy = 1.0 try: temperature_factor = float(pdb_line[60:66]) * unit.angstroms * unit.angstroms except: temperature_factor = 0.0 * unit.angstroms * unit.angstroms self.locations = {} loc = Atom.Location(alternate_location_indicator, Vec3(x,y,z) * unit.angstroms, occupancy, temperature_factor, self.residue_name_with_spaces) self.locations[alternate_location_indicator] = loc self.default_location_id = alternate_location_indicator # segment id, element_symbol, and formal_charge are not always present self.segment_id = pdb_line[72:76].strip() self.element_symbol = pdb_line[76:78].strip() try: self.formal_charge = int(pdb_line[78:80]) except ValueError: self.formal_charge = None # figure out atom element 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
def addHydrogens(self, forcefield=None, pH=None, variants=None, platform=None): """Add missing hydrogens to the model. This function automatically changes compatible residues into their constant-pH variant if no variant is specified.: Aspartic acid: AS4: Form with a 2 hydrogens on each one of the delta oxygens (syn,anti) It has 5 titration states. Alternative: AS2: Has 2 hydrogens (syn, anti) on one of the delta oxygens It has 3 titration states. 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: GL4: Form with a 2 hydrogens on each one of the epsilon oxygens (syn,anti) It has 5 titration states. Histidine: HIP: Positively charged form with hydrogens on both ND1 and NE2 It has 3 titration states. The variant to use for each residue is determined by the following rules: 1. Any Cysteine that participates in a disulfide bond uses the CYX variant regardless of pH. 2. Other residues are all set to maximally protonated state, which can be updated using a proton drive 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. 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=None the ForceField to use for determining the positions of hydrogens. If this is None, positions will be picked which are generally reasonable but not optimized for any particular ForceField. pH : None, Kept for compatibility reasons. Has no effect. 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 ------- list a list of what variant was actually selected for each residue, in the same format as the variants parameter Notes ----- This function does not use a pH specification. The argument is kept for compatibility reasons. """ # Check the list of variants. if pH is not None: print("Ignored pH argument provided for constant-pH residues.") 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-amber10-constph.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.setPeriodicBoxVectors( self.topology.getPeriodicBoxVectors()) 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(chain.id) for residue in chain.residues(): newResidue = newTopology.addResidue(residue.name, newChain, residue.id) 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": variant = "HIP" if residue.name == "GLU": variant = "GL4" if residue.name == "ASP": variant = "AS4" 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. 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) or (h.variants is None) 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(): # 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) 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 perform an energy minimization to fix them up. if forcefield is not None: # Use the ForceField the user specified. 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) else: # Create a System that restrains the distance of each hydrogen from its parent atom # and causes hydrogens to spread out evenly. system = System() nonbonded = CustomNonbondedForce("100/((r/0.1)^4+1)") bonds = HarmonicBondForce() angles = HarmonicAngleForce() system.addForce(nonbonded) system.addForce(bonds) system.addForce(angles) bondedTo = [] for atom in newTopology.atoms(): nonbonded.addParticle([]) if atom.element != elem.hydrogen: system.addParticle(0.0) else: system.addParticle(1.0) bondedTo.append([]) for atom1, atom2 in newTopology.bonds(): if atom1.element == elem.hydrogen or atom2.element == elem.hydrogen: bonds.addBond(atom1.index, atom2.index, 0.1, 100_000.0) bondedTo[atom1.index].append(atom2) bondedTo[atom2.index].append(atom1) for residue in newTopology.residues(): if residue.name == "HOH": # Add an angle term to make the water geometry correct. atoms = list(residue.atoms()) oindex = [ i for i in range(len(atoms)) if atoms[i].element == elem.oxygen ] if len(atoms) == 3 and len(oindex) == 1: hindex = list(set([0, 1, 2]) - set(oindex)) angles.addAngle( atoms[hindex[0]].index, atoms[oindex[0]].index, atoms[hindex[1]].index, 1.824, 836.8, ) else: # Add angle terms for any hydroxyls. for atom in residue.atoms(): index = atom.index if (atom.element == elem.oxygen and len(bondedTo[index]) == 2 and elem.hydrogen in (a.element for a in bondedTo[index])): angles.addAngle( bondedTo[index][0].index, index, bondedTo[index][1].index, 1.894, 460.24, ) if platform is None: context = Context(system, VerletIntegrator(0.0)) else: context = Context(system, VerletIntegrator(0.0), platform) context.setPositions(newPositions) LocalEnergyMinimizer.minimize(context, 1.0, 50) self.topology = newTopology self.positions = context.getState(getPositions=True).getPositions() del context return actualVariants