def _reset_mappings(self): """Internal function to reset index mapping dictionaries.""" # Clear dictionaries. self._molecule_index = {} self._atom_index_tally = {} self._residue_index_tally = {} # Rebuild the MolNum to index mapping. for idx in range(0, self.nMolecules()): self._molecule_index[self._sire_object[_SireMol.MolIdx(idx)].number()] = idx
def run(self, molecule, work_dir=None, queue=None): """Run the parameterisation protocol. Parameters ---------- molecule : BioSimSpace._SireWrappers.Molecule The molecule to apply the parameterisation protocol to. work_dir : str The working directory. queue : queue.Queue The thread queue is which this method has been run. Returns ------- molecule : BioSimSpace._SireWrappers.Molecule The parameterised molecule. """ if type(molecule) is not _Molecule: raise TypeError( "'molecule' must be of type 'BioSimSpace._SireWrappers.Molecule'" ) if type(work_dir) is not None and type(work_dir) is not str: raise TypeError("'work_dir' must be of type 'str'") if type(queue) is not None and type(queue) is not _queue.Queue: raise TypeError("'queue' must be of type 'queue.Queue'") # Set work_dir to the current directory. if work_dir is None: work_dir = _os.getcwd() # Create the file prefix. prefix = work_dir + "/" # Create a copy of the molecule. new_mol = molecule.copy() # Use the net molecular charge passed as an option. if self._net_charge is not None: charge = self._net_charge else: # The user will likely have passed a bare PDB or Mol2 file. # Antechamber expects the molecule to be uncharged, or integer # charged (where the charge, or number of electrons, is passed with # the -nc flag). # Get the total charge on the molecule. if "charge" in self._property_map: _property_map = {"charge": self._property_map["charge"]} prop = self._property_map["charge"] else: _property_map = {"charge": "charge"} prop = "charge" # The molecule has a charge property. if new_mol._getSireObject().hasProperty(prop): charge = new_mol.charge(property_map=_property_map).magnitude() # Charge is non-integer, try to fix it. if abs(round(charge) - charge) > 0: new_mol._fixCharge(property_map=_property_map) charge = round(charge) else: charge = None # Only try "formal_charge" when "charge" is missing. Unlikely to have # both if this is a bare molecule, but the user could be re-parameterising # an existing molecule. if charge is None: # Get the total formal charge on the molecule. if "formal_charge" in self._property_map: _property_map = { "charge": self._property_map["formal_charge"] } prop = self._property_map["charge"] else: _property_map = {"charge": "formal_charge"} prop = "formal_charge" if new_mol._getSireObject().hasProperty(prop): charge = new_mol.charge( property_map=_property_map).magnitude() # Compute the formal charge ourselves to check that it is consistent. formal_charge = _formalCharge(molecule).magnitude() if charge != formal_charge: _warnings.warn( "The formal charge on the molecule is %d " "but we estimate it to be %d" % (charge, formal_charge)) else: msg = ( "The molecule has no 'charge' or 'formal_charge' information, and " "no 'net_charge' option has been passed. You can use the " "'BioSimSpace.Parameters.formalCharge' function to compute the " "formal charge") raise _ParameterisationError(msg) # Create a new system and molecule group. s = _SireSystem.System("BioSimSpace System") m = _SireMol.MoleculeGroup("all") # Add the molecule. m.add(new_mol._getSireObject()) s.add(m) # Write the system to a PDB file. try: pdb = _SireIO.PDB2(s) pdb.writeToFile(prefix + "antechamber.pdb") except Exception as e: msg = "Failed to write system to 'PDB' format." if _isVerbose(): raise IOError(msg) from e else: raise IOError(msg) from None # Generate the Antechamber command. command = ("%s -at %d -i antechamber.pdb -fi pdb " + "-o antechamber.mol2 -fo mol2 -c %s -s 2 -nc %d") % ( _protocol._antechamber_exe, self._version, self._charge_method.lower(), charge) with open(prefix + "README.txt", "w") as file: # Write the command to file. file.write("# Antechamber was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. stdout = open(prefix + "antechamber.out", "w") stderr = open(prefix + "antechamber.err", "w") # Run Antechamber as a subprocess. proc = _subprocess.run(command, cwd=work_dir, shell=True, stdout=stdout, stderr=stderr) stdout.close() stderr.close() # Antechamber doesn't return sensible error codes, so we need to check that # the expected output was generated. if _os.path.isfile(prefix + "antechamber.mol2"): # Run parmchk to check for missing parameters. command = ("%s -s %d -i antechamber.mol2 -f mol2 " + "-o antechamber.frcmod") % (_protocol._parmchk_exe, self._version) with open(prefix + "README.txt", "a") as file: # Write the command to file. file.write("\n# ParmChk was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. stdout = open(prefix + "parmchk.out", "w") stderr = open(prefix + "parmchk.err", "w") # Run parmchk as a subprocess. proc = _subprocess.run(command, cwd=work_dir, shell=True, stdout=stdout, stderr=stderr) stdout.close() stderr.close() # The frcmod file was created. if _os.path.isfile(prefix + "antechamber.frcmod"): # Now call tLEaP using the partially parameterised molecule and the frcmod file. # tLEap will run in the same working directory, using the Mol2 file generated by # Antechamber. # Try to find a force field file. if self._version == 1: ff = _protocol._find_force_field("gaff") else: ff = _protocol._find_force_field("gaff2") # Write the LEaP input file. with open(prefix + "leap.txt", "w") as file: file.write("source %s\n" % ff) file.write("mol = loadMol2 antechamber.mol2\n") file.write("loadAmberParams antechamber.frcmod\n") file.write("saveAmberParm mol leap.top leap.crd\n") file.write("quit") # Generate the tLEaP command. command = "%s -f leap.txt" % _protocol._tleap_exe with open(prefix + "README.txt", "a") as file: # Write the command to file. file.write( "\n# tLEaP was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. stdout = open(prefix + "tleap.out", "w") stderr = open(prefix + "tleap.err", "w") # Run tLEaP as a subprocess. proc = _subprocess.run(command, cwd=work_dir, shell=True, stdout=stdout, stderr=stderr) stdout.close() stderr.close() # tLEaP doesn't return sensible error codes, so we need to check that # the expected output was generated. if _os.path.isfile(prefix + "leap.top") and _os.path.isfile(prefix + "leap.crd"): # Load the parameterised molecule. try: par_mol = _Molecule( _IO.readMolecules([ prefix + "leap.top", prefix + "leap.crd" ])._getSireObject()[_SireMol.MolIdx(0)]) except Exception as e: msg = "Failed to read molecule from: 'leap.top', 'leap.crd'" if _isVerbose(): raise IOError(msg) from e else: raise IOError(msg) from None # Make the molecule 'mol' compatible with 'par_mol'. This will create # a mapping between atom indices in the two molecules and add all of # the new properties from 'par_mol' to 'mol'. new_mol._makeCompatibleWith( par_mol, property_map=self._property_map, overwrite=True, verbose=False) # Record the forcefield used to parameterise the molecule. new_mol._forcefield = ff else: raise _ParameterisationError("tLEaP failed!") else: raise _ParameterisationError("Parmchk failed!") else: raise _ParameterisationError("Antechamber failed!") if queue is not None: queue.put(new_mol) return new_mol
def getIndex(self, item): """Convert indices of atoms and residues to their absolute values in this system. Parameters ---------- item : :class:`Atom <BioSimSpace._SireWrappers.Atom>`, \ :class:`Residue <BioSimSpace._SireWrappers.Residue>` \ :class:`Molecule <BioSimSpace._SireWrappers.Molecule>` An Atom, Residue, or Molecule object from the System, or a list containing objects of these types. Returns ------- index : int, [int] The absolute index of the atom/residue/molecule in the system. """ # Convert single object to list. if type(item) is not tuple and type(item) is not list: item = [item] # Create a list to hold the indices. indices = [] # Loop over all of the items. for x in item: # Atom. if type(x) is _Atom: # Only compute the atom index mapping if it hasn't already # been created. if len(self._atom_index_tally) == 0: # Loop over all molecules in the system and keep track # of the cumulative number of atoms. num_atoms = 0 for num in self._sire_object.molNums(): self._atom_index_tally[num] = num_atoms num_atoms += self._sire_object.molecule(num).nAtoms() # Get the AtomIdx and MolNum from the Atom. index = x.index() mol_num = x._sire_object.molecule().number() try: index += self._atom_index_tally[mol_num] except KeyError: raise KeyError("The atom belongs to molecule '%s' that is not part of " "this system!" % mol_num) indices.append(index) # Residue. elif type(x) is _Residue: # Only compute the residue index mapping if it hasn't already # been created. if len(self._residue_index_tally) == 0: # Loop over all molecules in the system and keep track # of the cumulative number of residues. num_residues = 0 for num in self._sire_object.molNums(): self._residue_index_tally[num] = num_residues num_residues += self._sire_object.molecule(num).nResidues() # Get the ResIdx and MolNum from the Residue. index = x.index() mol_num = x._sire_object.molecule().number() try: index += self._residue_index_tally[mol_num] except KeyError: raise KeyError("The residue belongs to molecule '%s' that is not part of " "this system!" % mol_num) indices.append(index) # Residue. elif type(x) is _Molecule: # Only compute the molecule index mapping if it hasn't already # been created. if len(self._molecule_index) == 0: # Loop over all molecules in the system by index and map # the MolNum to index. for idx in range(0, self.nMolecules()): self._molecule_index[self._sire_object[_SireMol.MolIdx(idx)].number()] = idx # Get the MolNum from the molecule. mol_num = x._sire_object.molecule().number() try: index = self._molecule_index[mol_num] except KeyError: raise KeyError("The molecule '%s' is not part of this system!" % mol_num) indices.append(index) # Unsupported. else: raise TypeError("'item' must be of type 'BioSimSpace._SireWrappers.Atom' " "or 'BioSimSpace._SireWrappers.Residue'") # If this was a single object, then return a single index. if len(indices) == 1: return indices[0] # Return the list of indices. else: return indices
def _updateCoordinates(self, system, property_map0={}, property_map1={}, is_lambda1=False): """Update the coordinates of atoms in the system. Parameters ---------- system : :class:`System <BioSimSpace._SireWrappers.System>` A system containing the updated coordinates. property_map0 : dict A dictionary that maps system "properties" to their user defined values in this system. property_map1 : dict A dictionary that maps system "properties" to their user defined values in the passed system. is_lambda1 : bool Whether to update coordinates of perturbed molecules at lambda = 1. By default, coordinates at lambda = 0 are used. """ # Validate the system. if type(system) is not System: raise TypeError("'system' must be of type 'BioSimSpace._SireWrappers.System'") # Check that the passed system contains the same number of molecules. if self.nMolecules() != system.nMolecules(): raise _IncompatibleError("The passed 'system' contains a different number of " "molecules. Expected '%d', found '%d'" % (self.nMolecules(), system.nMolecules())) # Check that each molecule in the system contains the same number of atoms. for idx in range(0, self.nMolecules()): # Extract the number of atoms in the molecules. num_atoms0 = self._sire_object.molecule(_SireMol.MolIdx(idx)).nAtoms() num_atoms1 = self._sire_object.molecule(_SireMol.MolIdx(idx)).nAtoms() if num_atoms0 != num_atoms1: raise _IncompatibleError("Mismatch in atom count for molecule '%d': " "Expected '%d', found '%d'" % (num_atoms0, num_atoms1)) # Work out the name of the "coordinates" property. prop0 = property_map0.get("coordinates0", "coordinates") prop1 = property_map1.get("coordinates1", "coordinates") # Loop over all molecules and update the coordinates. for idx in range(0, self.nMolecules()): # Extract the molecules from each system. mol0 = self._sire_object.molecule(_SireMol.MolIdx(idx)) mol1 = system._sire_object.molecule(_SireMol.MolIdx(idx)) # Check whether the molecule is perturbable. if mol0.hasProperty("is_perturbable"): if is_lambda1: prop = "coordinates1" else: prop = "coordinates0" else: prop = prop0 # Try to update the coordinates property. try: mol0 = mol0.edit().setProperty(prop, mol1.property(prop1)).molecule().commit() except: raise _IncompatibleError("Unable to update 'coordinates' for molecule index '%d'" % idx) # Update the molecule in the original system. self._sire_object.update(mol0)
def run(self, molecule, work_dir=None, queue=None): """Run the parameterisation protocol. Parameters ---------- molecule : BioSimSpace._SireWrappers.Molecule The molecule to apply the parameterisation protocol to. work_dir : str The working directory. queue : queue.Queue The thread queue is which this method has been run. Returns ------- molecule : BioSimSpace._SireWrappers.Molecule The parameterised molecule. """ if type(molecule) is not _Molecule: raise TypeError( "'molecule' must be of type 'BioSimSpace._SireWrappers.Molecule'" ) if type(work_dir) is not None and type(work_dir) is not str: raise TypeError("'work_dir' must be of type 'str'") if type(queue) is not None and type(queue) is not _queue.Queue: raise TypeError("'queue' must be of type 'queue.Queue'") # Set work_dir to the current directory. if work_dir is None: work_dir = _os.getcwd() # Create the file prefix. prefix = work_dir + "/" # Create a copy of the molecule. new_mol = molecule.copy() # Choose the program to run with depending on the force field compatibility. # If tLEaP and pdb2gmx are supported, default to tLEaP, then use pdb2gmx if # tLEaP fails to produce output. # First, try parameterise using tLEaP. if self._tleap: if _tleap_exe is not None: output = self._run_tleap(molecule, work_dir) # Otherwise, try using pdb2gmx. elif self._pdb2gmx: if _gmx_exe is not None: output = self._run_pdb2gmx(molecule, work_dir) else: raise _MissingSoftwareError( "Cannot parameterise. Missing AmberTools and GROMACS.") # Parameterise using pdb2gmx. elif self._pdb2gmx: if _gmx_exe is not None: output = self._run_pdb2gmx(molecule, work_dir) else: raise _MissingSoftwareError( "Cannot use pdb2gmx since GROMACS is not installed!") # Prepend the working directory to the output file names. output = [prefix + output[0], prefix + output[1]] try: # Load the parameterised molecule. par_mol = _Molecule( _IO.readMolecules(output)._getSireObject()[_SireMol.MolIdx(0)]) except Exception as e: msg = "Failed to read molecule from: '%s', '%s'" % (output[0], output[1]) if _isVerbose(): raise IOError(msg) from e else: raise IOError(msg) from None # Make the molecule 'mol' compatible with 'par_mol'. This will create # a mapping between atom indices in the two molecules and add all of # the new properties from 'par_mol' to 'mol'. new_mol._makeCompatibleWith(par_mol, property_map=self._property_map, overwrite=True, verbose=False) # Record the forcefield used to parameterise the molecule. new_mol._forcefield = self._forcefield if queue is not None: queue.put(new_mol) return new_mol
def rmsd(self, frame=None, atoms=None, molecule=None): """Compute the root mean squared displacement. Parameters ---------- frame : int The index of the reference frame. atoms : [int] A list of reference atom indices. molecule : int The index of the reference molecule. Returns ------- rmsd : [float] A list containing the RMSD value at each time point. """ # Default to the first frame. if frame is None: frame = 0 else: if type(frame) is not int: raise TypeError("'frame' must be of type 'int'") else: # Store the number of frames. n_frames = self._trajectory.n_frames # Make sure the frame index is within range. if frame > 0 and frame >= n_frames: raise ValueError( "Frame index (%d) of of range (0 to %d)." % s(frame, n_frames - 1)) elif frame < -n_frames: raise ValueError( "Frame index (%d) of of range (-1 to -%d)." % s(frame, n_frames)) if molecule is not None and atoms is not None: _warnings.warn( "Cannot have a reference molecule and list of atoms. Defaulting to atoms." ) molecule = None # Extract the molecule from the reference trajectory frame and generate a # list of atom indices. if molecule is not None: # Integer molecule index. if type(molecule) is int: try: molecule = self.getFrames(frame)[0]._getSireSystem()[ _SireMol.MolIdx(molecule)] except: raise ValueError("Missing molecule index '%d' in System" % molecule) # Sire.Mol.MolIdx index. elif type(molecule) is _SireMol.MolIdx: try: molecule = self.getFrames( frame)[0]._getSireSystem()[molecule] except: raise ValueError("Missing '%s' in System" % molecule.toString()) # A BioSimSpace Molecule object. elif type(molecule) is _Molecule: molecule = molecule._getSireMolecule() # A Sire.Mol.Molecule object. elif type(molecule) is _SireMol.Molecule: pass else: raise TypeError( "'molecule' must be of type 'int', 'BioSimSpace.Molecue', " "'Sire.Mol.MolIdx', or 'Sire.Mol.Molecule'") # Initialise the list of atom indices. atoms = [] # Loop over all of the atoms and add them to the list. # Atoms are numbered from one in Sire, so subtract one to get the python index. for atom in molecule.atoms(): atoms.append(atom.number().value() - 1) if atoms is not None: # Check that all of the atom indices are integers. if not all(isinstance(x, int) for x in atoms): raise TypeError("'atom' indices must be of type 'int'") # Use MDTraj to compute the RMSD. try: rmsd = _mdtraj.rmsd(self._trajectory, self._trajectory, frame, atoms) except: raise ValueError("Atom indices not found in the system.") # Convert to a list and return. return list(rmsd)