def _create_view(self, system=None, view=None, gui=True): """Helper function to create the NGLview object. Parameters ---------- system : Sire.System.System A Sire molecular system. view : int The index of an existing view. gui : bool Whether to display the gui. """ if system is None and view is None: raise ValueError("Both 'system' and 'view' cannot be 'None'.") elif system is not None and view is not None: raise ValueError("One of 'system' or 'view' must be 'None'.") # Make sure gui flag is valid. if gui not in [True, False]: gui = True # Default to the most recent view. if view is None: index = self._num_views else: index = view # Create the file name. filename = "%s/view_%04d.pdb" % (self._work_dir, index) # Increment the number of views. if view is None: self._num_views += 1 # Create a PDB object and write to file. if system is not None: try: pdb = _SireIO.PDB2(system) pdb.writeToFile(filename) except Exception as e: msg = "Failed to write system to 'PDB' format." if _isVerbose(): print(msg) raise IOError(e) from None else: raise IOError(msg) from None # Import NGLView when it is used for the first time. import nglview as _nglview # Create the NGLview object. view = _nglview.show_file(filename) # Return the view and display it. return view.display(gui=gui)
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 _initialise_runner(self, system0, system1): """Internal helper function to initialise the process runner. Parameters ---------- system0 : :class:`System <BioSimSpace._SireWrappers.System>` The system for the first free energy leg. system1 : :class:`System <BioSimSpace._SireWrappers.System>` The system for the second free energy leg. """ if type(system0) is not _System: raise TypeError( "'system0' must be of type 'BioSimSpace._SireWrappers.System'") if type(system1) is not _System: raise TypeError( "'system1' must be of type 'BioSimSpace._SireWrappers.System'") # Initialise lists to store the processes for each leg. leg0 = [] leg1 = [] # Get the simulation type. sim_type = self.__class__.__name__ # Store the working directories for the legs. if sim_type == "Solvation": self._dir0 = "%s/free" % self._work_dir if self._is_dual: self._dir1 = "%s/vacuum" % self._work_dir elif sim_type == "Binding": self._dir0 = "%s/bound" % self._work_dir if self._is_dual: self._dir1 = "%s/free" % self._work_dir else: raise TypeError("Unsupported FreeEnergy simulation: '%s'" % sim_type) # Convert to an appropriate AMBER topology. (Required by SOMD.) if self._engine == "SOMD": # Try to get the water model used to solvate the system. try: water_model = system0._sire_object.property( "water_model").toString() waters0 = _SireIO.setAmberWater( system0._sire_object.search("water"), water_model) if self._is_dual: waters1 = _SireIO.setAmberWater( system1._sire_object.search("water"), water_model) # If the system wasn't solvated by BioSimSpace, e.g. read from file, then try # to guess the water model from the topology. except: num_point = system0.getWaterMolecules()[0].nAtoms() if num_point == 3: # TODO: Assume TIP3P. Not sure how to detect SPC/E. waters0 = _SireIO.setAmberWater( system0._sire_object.search("water"), "TIP3P") if self._is_dual: waters1 = _SireIO.setAmberWater( system1._sire_object.search("water"), "TIP3P") water_model = "tip3p" elif num_point == 4: waters0 = _SireIO.setAmberWater( system0._sire_object.search("water"), "TIP4P") if self._is_dual: waters1 = _SireIO.setAmberWater( system1._sire_object.search("water"), "TIP4P") water_model = "tip4p" elif num_point == 5: waters0 = _SireIO.setAmberWater( system0._sire_object.search("water"), "TIP5P") if self._is_dual: waters1 = _SireIO.setAmberWater( system1._sire_object.search("water"), "TIP5P") water_model = "tip5p" else: raise RuntimeError("Unsupported %d-point water model!" % num_point) # Warn the user that we've guessed the water topology. _warnings.warn("Guessed water topology: %r" % water_model) # Remove the existing water molecules from the systems. system0.removeWaterMolecules() if self._is_dual: system1.removeWaterMolecules() # Convert the waters to BioSimSpace molecule containers. waters0 = _Molecules(waters0.toMolecules()) if self._is_dual: waters1 = _Molecules(waters1.toMolecules()) # Add the updated water topology back into the systems. system0.addMolecules(waters0) if self._is_dual: system1.addMolecules(waters1) # Get the lambda values from the protocol. lam_vals = self._protocol.getLambdaValues() # Loop over all of the lambda values. for lam in lam_vals: # Update the protocol lambda values. self._protocol.setLambdaValues(lam=lam, lam_vals=lam_vals) # Create and append the required processes for each leg. # Nest the working directories inside self._work_dir. # SOMD. if self._engine == "SOMD": # Check for GPU support. if "CUDA_VISIBLE_DEVICES" in _os.environ: platform = "CUDA" else: platform = "CPU" leg0.append( _Process.Somd(system0, self._protocol, platform=platform, work_dir="%s/lambda_%5.4f" % (self._dir0, lam))) if self._is_dual: leg1.append( _Process.Somd(system1, self._protocol, platform=platform, work_dir="%s/lambda_%5.4f" % (self._dir1, lam))) # GROMACS. elif self._engine == "GROMACS": leg0.append( _Process.Gromacs(system0, self._protocol, work_dir="%s/lambda_%5.4f" % (self._dir0, lam))) if self._is_dual: leg1.append( _Process.Gromacs(system1, self._protocol, work_dir="%s/lambda_%5.4f" % (self._dir1, lam))) # Initialise the process runner. All processes have already been nested # inside the working directory so no need to re-nest. self._runner = _Process.ProcessRunner(leg0 + leg1, work_dir=self._work_dir, nest_dirs=False)
def _solvate(molecule, box, shell, model, num_point, ion_conc, is_neutral, work_dir=None, property_map={}): """Internal function to add solvent using 'gmx solvate'. Parameters ---------- molecule : :class:`Molecule <BioSimSpace._SireWrappers.Molecule>`, \ :class:`System <BioSimSpace._SireWrappers.System>` A molecule, or system of molecules. box : [:class:`Length <BioSimSpace.Types.Length>`] A list containing the box size in each dimension. shell : :class:`Length` <BioSimSpace.Types.Length>` Thickness of the water shell around the solute. model : str The name of the water model. num_point : int The number of atoms in the water model. ion_conc : float The ion concentration in (mol per litre). is_neutral : bool Whether to neutralise the system. work_dir : str The working directory for the process. property_map : dict A dictionary that maps system "properties" to their user defined values. This allows the user to refer to properties with their own naming scheme, e.g. { "charge" : "my-charge" } Returns ------- system : :class:`System <BioSimSpace._SireWrappers.System>` The solvated system. """ if molecule is not None: # Store the centre of the molecule. center = molecule._getAABox(property_map).center() # Work out the vector from the centre of the molecule to the centre of the # water box, converting the distance in each direction to Angstroms. vec = [] for x, y in zip(box, center): vec.append(0.5 * x.angstroms().magnitude() - y) # Translate the molecule. This allows us to create a water box # around the molecule. molecule.translate(vec, property_map) if type(molecule) is _System: # Reformat all of the water molecules so that they match the # expected GROMACS topology template. waters = _SireIO.setGromacsWater( molecule._sire_object.search("water"), model) # Convert to a BioSimSpace molecules container. waters = _Molecules(waters.toMolecules()) # Remove the old water molecules then add those with the updated topology. molecule.removeWaterMolecules() molecule.addMolecules(waters) # Create a temporary working directory and store the directory name. if work_dir is None: tmp_dir = _tempfile.TemporaryDirectory() work_dir = tmp_dir.name # Run the solvation in the working directory. with _Utils.cd(work_dir): # Create the gmx command. if num_point == 3: mod = "spc216" else: mod = model command = "%s solvate -cs %s" % (_gmx_exe, mod) if molecule is not None: # Write the molecule/system to a GRO files. _IO.saveMolecules("input", molecule, "gro87") _os.rename("input.gro87", "input.gro") # Update the command. command += " -cp input.gro" # Add the box information. if box is not None: command += " -box %f %f %f" % (box[0].nanometers().magnitude(), box[1].nanometers().magnitude(), box[2].nanometers().magnitude()) # Add the shell information. if shell is not None: command += " -shell %f" % shell.nanometers().magnitude() # Just add box information. else: command += " -box %f %f %f" % (box[0].nanometers().magnitude(), box[1].nanometers().magnitude(), box[2].nanometers().magnitude()) # Add the output file. command += " -o output.gro" with open("README.txt", "w") as file: # Write the command to file. file.write("# gmx solvate was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. stdout = open("solvate.out", "w") stderr = open("solvate.err", "w") # Run gmx solvate as a subprocess. proc = _subprocess.run(command, shell=True, stdout=stdout, stderr=stderr) stdout.close() stderr.close() # gmx doesn't return sensible error codes, so we need to check that # the expected output was generated. if not _os.path.isfile("output.gro"): raise RuntimeError("'gmx solvate failed to generate output!") # Extract the water lines from the GRO file. water_lines = [] with open("output.gro", "r") as file: for line in file: if _re.search("SOL", line): # Store the SOL atom record. water_lines.append(line) # Add any box information. This is the last line in the GRO file. water_lines.append(line) # Write a GRO file that contains only the water atoms. if len(water_lines) - 1 > 0: with open("water.gro", "w") as file: file.write("BioSimSpace %s water box\n" % model.upper()) file.write("%d\n" % (len(water_lines) - 1)) for line in water_lines: file.write("%s" % line) else: raise ValueError( "No water molecules were generated. Try increasing " "the 'box' size or 'shell' thickness.") # Create a TOP file for the water model. By default we use the Amber03 # force field to generate a dummy topology for the water model. with open("water_ions.top", "w") as file: file.write("#define FLEXIBLE 1\n\n") file.write("; Include AmberO3 force field\n") file.write('#include "amber03.ff/forcefield.itp"\n\n') file.write("; Include %s water topology\n" % model.upper()) file.write('#include "amber03.ff/%s.itp"\n\n' % model) file.write("; Include ions\n") file.write('#include "amber03.ff/ions.itp"\n\n') file.write("[ system ] \n") file.write("BioSimSpace %s water box\n\n" % model.upper()) file.write("[ molecules ] \n") file.write(";molecule name nr.\n") file.write("SOL %d\n" % ((len(water_lines) - 1) / num_point)) # Load the water box. water = _IO.readMolecules(["water.gro", "water_ions.top"]) # Create a new system by adding the water to the original molecule. if molecule is not None: # Translate the molecule and water back to the original position. vec = [-x for x in vec] molecule.translate(vec, property_map) water.translate(vec) if type(molecule) is _System: # Extract the non-water molecules from the original system. non_waters = _Molecules( molecule.search("not water")._sire_object.toMolecules()) # Create a system by adding these to the water molecules from # gmx solvate, which will include the original waters. system = non_waters.toSystem() + water else: system = molecule.toSystem() + water # Add all of the water box properties to the new system. for prop in water._sire_object.propertyKeys(): prop = property_map.get(prop, prop) # Add the space property from the water system. system._sire_object.setProperty( prop, water._sire_object.property(prop)) else: system = water # Now we add ions to the system and neutralise the charge. if ion_conc > 0 or is_neutral: # Write the molecule + water system to file. _IO.saveMolecules("solvated", system, "gro87") _IO.saveMolecules("solvated", system, "grotop") _os.rename("solvated.gro87", "solvated.gro") _os.rename("solvated.grotop", "solvated.top") # First write an mdp file. with open("ions.mdp", "w") as file: file.write("; Neighbour searching\n") file.write("cutoff-scheme = Verlet\n") file.write("rlist = 1.1\n") file.write("pbc = xyz\n") file.write("verlet-buffer-tolerance = -1\n") file.write("\n; Electrostatics\n") file.write("coulombtype = cut-off\n") file.write("\n; VdW\n") file.write("rvdw = 1.0\n") # Create the grompp command. command = "%s grompp -f ions.mdp -po ions.out.mdp -c solvated.gro -p solvated.top -o ions.tpr" % _gmx_exe with open("README.txt", "a") as file: # Write the command to file. file.write( "\n# gmx grompp was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. stdout = open("grommp.out", "w") stderr = open("grommp.err", "w") # Run grompp as a subprocess. proc = _subprocess.run(command, shell=True, stdout=stdout, stderr=stderr) stdout.close() stderr.close() # Flag whether to break out of the ion adding stage. is_break = False # Check for the tpr output file. if not _os.path.isfile("ions.tpr"): if shell is None: raise RuntimeError( "'gmx grommp' failed to generate output! " "Perhaps your box is too small?") else: is_break = True _warnings.warn( "Unable to achieve target ion concentration, try using " "'box' option instead of 'shell'.") # Only continue if grommp was successful. This allows us to skip the remainder # of the code if the ion addition failed when the 'shell' option was chosen, i.e. # because the estimated simulation box was too small. if not is_break: is_break = False # The ion concentration is unset. if ion_conc == 0: # Get the current molecular charge. charge = system.charge() # Round to the nearest integer value. charge = round(charge.magnitude()) # Create the genion command. command = "echo SOL | %s genion -s ions.tpr -o solvated_ions.gro -p solvated.top -neutral" % _gmx_exe # Add enough counter ions to neutralise the charge. if charge > 0: command += " -nn %d" % abs(charge) else: command += " -np %d" % abs(charge) else: # Create the genion command. command = "echo SOL | %s genion -s ions.tpr -o solvated_ions.gro -p solvated.top -%s -conc %f" \ % (_gmx_exe, "neutral" if is_neutral else "noneutral", ion_conc) with open("README.txt", "a") as file: # Write the command to file. file.write( "\n# gmx genion was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. stdout = open("genion.out", "w") stderr = open("genion.err", "w") # Run genion as a subprocess. proc = _subprocess.run(command, shell=True, stdout=stdout, stderr=stderr) stdout.close() stderr.close() # Check for the output GRO file. if not _os.path.isfile("solvated_ions.gro"): if shell is None: raise RuntimeError( "'gmx genion' failed to add ions! Perhaps your box is too small?" ) else: is_break = True _warnings.warn( "Unable to achieve target ion concentration, try using " "'box' option instead of 'shell'.") if not is_break: # Counters for the number of SOL, NA, and CL atoms. num_sol = 0 num_na = 0 num_cl = 0 # We now need to loop through the GRO file to extract the lines # corresponding to water or ion atoms. water_ion_lines = [] with open("solvated_ions.gro", "r") as file: for line in file: # This is a Sodium atom. if _re.search("NA", line): water_ion_lines.append(line) num_na += 1 # This is a Chlorine atom. if _re.search("CL", line): water_ion_lines.append(line) num_cl += 1 # This is a water atom. elif _re.search("SOL", line): water_ion_lines.append(line) num_sol += 1 # Add any box information. This is the last line in the GRO file. water_ion_lines.append(line) # Write a GRO file that contains only the water and ion atoms. if len(water_ion_lines) - 1 > 0: with open("water_ions.gro", "w") as file: file.write("BioSimSpace %s water box\n" % model.upper()) file.write("%d\n" % (len(water_ion_lines) - 1)) for line in water_ion_lines: file.write("%s" % line) # Ions have been added. Update the TOP file fo the water model # with the new atom counts. if num_na > 0 or num_cl > 0: with open("water_ions.top", "w") as file: file.write("#define FLEXIBLE 1\n\n") file.write("; Include AmberO3 force field\n") file.write( '#include "amber03.ff/forcefield.itp"\n\n') file.write("; Include %s water topology\n" % model.upper()) file.write('#include "amber03.ff/%s.itp"\n\n' % model) file.write("; Include ions\n") file.write('#include "amber03.ff/ions.itp"\n\n') file.write("[ system ] \n") file.write("BioSimSpace %s water box\n\n" % model.upper()) file.write("[ molecules ] \n") file.write(";molecule name nr.\n") file.write("SOL %d\n" % (num_sol / num_point)) if num_na > 0: file.write("NA %d\n" % num_na) if num_cl > 0: file.write("CL %d\n" % num_cl) # Load the water/ion box. water_ions = _IO.readMolecules( ["water_ions.gro", "water_ions.top"]) # Create a new system by adding the water to the original molecule. if molecule is not None: if type(molecule) is _System: # Extract the non-water molecules from the original system. non_waters = _Molecules( molecule.search( "not water")._sire_object.toMolecules()) # Create a system by adding these to the water and ion # molecules from gmx solvate, which will include the # original waters. system = non_waters.toSystem() + water_ions else: system = molecule.toSystem() + water_ions # Add all of the water molecules' properties to the new system. for prop in water_ions._sire_object.propertyKeys(): prop = property_map.get(prop, prop) # Add the space property from the water system. system._sire_object.setProperty( prop, water_ions._sire_object.property(prop)) else: system = water_ions # Store the name of the water model as a system property. system._sire_object.setProperty("water_model", _SireBase.wrap(model)) return system
def _setup(self): """Setup the input files and working directory ready for simulation.""" # Create the input files... # First create a copy of the system. system = self._system.copy() # If the we are performing a free energy simulation, then check that # the system contains a single perturbable molecule. If so, then create # and write a perturbation file to the work directory. if type(self._protocol) is _Protocol.FreeEnergy: if system.nPerturbableMolecules() == 1: # Extract the perturbable molecule. pert_mol = system.getPerturbableMolecules()[0] # Write the perturbation file and get the molecule corresponding # to the lambda = 0 state. pert_mol = pert_mol._toPertFile( self._pert_file, property_map=self._property_map) self._input_files.append(self._pert_file) # Remove the perturbable molecule. system._sire_object.remove(pert_mol.number()) # Recreate the system, putting the perturbable molecule with # renamed properties first. updated_system = _System(pert_mol) + _System(system) # Copy across all of the properties from the orginal system. for prop in system._sire_object.propertyKeys(): updated_system._sire_object.setProperty( prop, system._sire_object.property(prop)) # Copy the updated system object across. system = updated_system else: raise ValueError("'BioSimSpace.Protocol.FreeEnergy' requires a single " "perturbable molecule. The system has %d" \ % system.nPerturbableMolecules()) # If this is a different protocol and the system still contains a # perturbable molecule, then we'll warn the user and simulate the # lambda = 0 state. else: if system.nPerturbableMolecules() > 0: if not "is_lambda1" in self._property_map: is_lambda1 = False _warnings.warn( "The system contains a perturbable molecule but " "this isn't a 'FreeEnergy' protocol. We will assume " "that you intend to simulate the lambda = 0 state. " "If you want to simulate the lambda = 1 state, then " "pass {'is_lambda1' : True} in the 'property_map' " "argument.") else: is_lambda1 = self._property_map["is_lambda1"] self._property_map.pop("is_lambda1") # Loop over all perturbable molecules in the system and replace them # with a regular molecule and the chosen end state. for mol in system.getPerturbableMolecules(): system.updateMolecules( mol._toRegularMolecule(property_map=self._property_map, is_lambda1=is_lambda1)) # Copy across the properties from the original system. for prop in self._system._sire_object.propertyKeys(): system._sire_object.setProperty( prop, self._system._sire_object.property(prop)) # RST file (coordinates). try: rst = _SireIO.AmberRst7(system._sire_object, self._property_map) rst.writeToFile(self._rst_file) except Exception as e: msg = "Failed to write system to 'RST7' format." if _isVerbose(): raise IOError(msg) from e else: raise IOError(msg) from None # PRM file (topology). try: prm = _SireIO.AmberPrm(system._sire_object, self._property_map) prm.writeToFile(self._top_file) except Exception as e: msg = "Failed to write system to 'PRM7' format." if _isVerbose(): raise IOError(msg) from e else: raise IOError(msg) from None # Generate the SOMD configuration file. # Skip if the user has passed a custom config. if type(self._protocol) is _Protocol.Custom: self.setConfig(self._protocol.getConfig()) else: self._generate_config() self.writeConfig(self._config_file) # Generate the dictionary of command-line arguments. self._generate_args() # Return the list of input files. return self._input_files
def _setup(self): """Setup the input files and working directory ready for simulation.""" # Create the input files... # First create a copy of the system. system = _System(self._system) # If the we are performing a free energy simulation, then check that # the system contains a single perturbable molecule. If so, then create # and write a perturbation file to the work directory. if type(self._protocol) is _Protocol.FreeEnergy: if system.nPerturbableMolecules() == 1: # Extract the perturbable molecule. pert_mol = system.getPerturbableMolecules()[0] # Write the perturbation file and get the molecule corresponding # to the lambda = 0 state. pert_mol = pert_mol._toPertFile( self._pert_file, property_map=self._property_map) self._input_files.append(self._pert_file) # Remove the perturbable molecule. system._sire_system.remove(pert_mol.number()) # Recreate the system, putting the perturbable molecule with # renamed properties first. updated_system = _System(pert_mol) + _System(system) # Copy across all of the properties from the orginal system. for prop in system._sire_system.propertyKeys(): updated_system._sire_system.setProperty( prop, system._sire_system.property(prop)) # Copy the updated system object across. system = updated_system else: raise ValueError("'BioSimSpace.Protocol.FreeEnergy' requires a single " "perturbable molecule. The system has %d" \ % system.nPerturbableMolecules()) # Extract the updated Sire system. system = system._sire_system # RST file (coordinates). try: rst = _SireIO.AmberRst7(system, self._property_map) rst.writeToFile(self._rst_file) except: raise IOError("Failed to write system to 'RST7' format.") from None # PRM file (topology). try: prm = _SireIO.AmberPrm(system, self._property_map) prm.writeToFile(self._top_file) except: raise IOError("Failed to write system to 'PRM7' format.") from None # Generate the SOMD configuration file. # Skip if the user has passed a custom config. if type(self._protocol) is _Protocol.Custom: self.setConfig(self._protocol.getConfig()) else: self._generate_config() self.writeConfig(self._config_file) # Generate the dictionary of command-line arguments. self._generate_args() # Return the list of input files. return self._input_files
def _run_pdb2gmx(self, molecule, work_dir): """Run using pdb2gmx. Parameters ---------- molecule : BioSimSpace._SireWrappers.Molecule The molecule to apply the parameterisation protocol to. work_dir : str The working directory. """ # A list of supported force fields, mapping to their GROMACS ID string. # GROMACS supports a sub-set of the AMBER force fields. supported_ff = { "ff99": "amber99", "ff99SB": "amber99sb", "ff03": "amber03" } if self._forcefield not in supported_ff: raise _IncompatibleError( "'pdb2gmx' does not support the '%s' force field." % self._forcefield) # Create a new system and molecule group. s = _SireSystem.System("BioSimSpace System") m = _SireMol.MoleculeGroup("all") # Add the molecule. m.add(molecule._getSireObject()) s.add(m) # Create the file prefix. prefix = work_dir + "/" # Write the system to a PDB file. try: pdb = _SireIO.PDB2(s, self._property_map) pdb.writeToFile(prefix + "input.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 pdb2gmx command. command = "%s pdb2gmx -f input.pdb -o output.gro -p output.top -ignh -ff %s -water none" \ % (_gmx_exe, supported_ff[self._forcefield]) with open(prefix + "README.txt", "w") as file: # Write the command to file. file.write("# pdb2gmx was run with the following command:\n") file.write("%s\n" % command) # Create files for stdout/stderr. stdout = open(prefix + "pdb2gmx.out", "w") stderr = open(prefix + "pdb2gmx.err", "w") # Run pdb2gmx as a subprocess. proc = _subprocess.run(command, cwd=work_dir, shell=True, stdout=stdout, stderr=stderr) stdout.close() stderr.close() # Check for the expected output. if _os.path.isfile(prefix + "output.gro") and _os.path.isfile(prefix + "output.top"): return ["output.gro", "output.top"] else: raise _ParameterisationError("pdb2gmx failed!")
def _run_tleap(self, molecule, work_dir): """Run using tLEaP. Parameters ---------- molecule : BioSimSpace._SireWrappers.Molecule The molecule to apply the parameterisation protocol to. work_dir : str The working directory. """ # Create a new system and molecule group. s = _SireSystem.System("BioSimSpace System") m = _SireMol.MoleculeGroup("all") # Add the molecule. m.add(molecule._getSireObject()) s.add(m) # Create the file prefix. prefix = work_dir + "/" # Write the system to a PDB file. try: pdb = _SireIO.PDB2(s, self._property_map) pdb.writeToFile(prefix + "leap.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 # Try to find a force field file. ff = _find_force_field(self._forcefield) # Write the LEaP input file. with open(prefix + "leap.txt", "w") as file: file.write("source %s\n" % ff) file.write("mol = loadPdb leap.pdb\n") file.write("saveAmberParm mol leap.top leap.crd\n") file.write("quit") # Generate the tLEaP command. command = "%s -f leap.txt" % _tleap_exe with open(prefix + "README.txt", "w") as file: # Write the command to file. file.write("# 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"): return ["leap.top", "leap.crd"] else: raise _ParameterisationError("tLEaP failed!")
def _initialise_runner(self, system0, system1): """Internral helper function to initialise the process runner. Parameters ---------- system0 : :class:`System <BioSimSpace._SireWrappers.System>` The system for the first free energy leg. system1 : :class:`System <BioSimSpace._SireWrappers.System>` The system for the second free energy leg. """ if type(system0) is not _System: raise TypeError( "'system0' must be of type 'BioSimSpace._SireWrappers.System'") if type(system1) is not _System: raise TypeError( "'system1' must be of type 'BioSimSpace._SireWrappers.System'") # Initialise lists to store the processes for each leg. leg0 = [] leg1 = [] # Get the simulation type. sim_type = self.__class__.__name__ # Store the working directories for the legs. if sim_type == "Solvation": self._dir0 = "%s/free" % self._work_dir self._dir1 = "%s/vacuum" % self._work_dir elif sim_type == "Binding": self._dir0 = "%s/bound" % self._work_dir self._dir1 = "%s/free" % self._work_dir else: raise TypeError("Unsupported FreeEnergy simulation: '%s'" % sim_type) # Try to get the water model property of the system. try: water_model = system0._sire_system.property( "water_model").toString() # Default to TIP3P. except: water_model = "tip3p" if self._engine == "SOMD": # Reformat all of the water molecules so that they match the expected # AMBER topology template. (Required by SOMD.) waters0 = _SireIO.setAmberWater( system0._sire_system.search("water"), water_model) waters1 = _SireIO.setAmberWater( system1._sire_system.search("water"), water_model) # Loop over all of the renamed water molecules, delete the old one # from the system, then add the renamed one back in. # TODO: This is a hack since the "update" method of Sire.System # doesn't work properly at present. system0.removeWaterMolecules() system1.removeWaterMolecules() for wat in waters0: system0._sire_system.add(wat, _SireMol.MGName("all")) for wat in waters1: system1._sire_system.add(wat, _SireMol.MGName("all")) # Get the lambda values from the protocol. lam_vals = self._protocol.getLambdaValues() # Loop over all of the lambda values. for lam in lam_vals: # Update the protocol lambda values. self._protocol.setLambdaValues(lam=lam, lam_vals=lam_vals) # Create and append the required processes for each leg. # Nest the working directories inside self._work_dir. # SOMD. if self._engine == "SOMD": # TODO: This is currently hard-coded to use SOMD with the CUDA platform. leg0.append( _Process.Somd(system0, self._protocol, platform="CUDA", work_dir="%s/lambda_%5.4f" % (self._dir0, lam))) leg1.append( _Process.Somd(system1, self._protocol, platform="CUDA", work_dir="%s/lambda_%5.4f" % (self._dir1, lam))) # GROMACS. elif self._engine == "GROMACS": # TODO: This is currently hard-coded to use SOMD with the CUDA platform. leg0.append( _Process.Gromacs(system0, self._protocol, work_dir="%s/lambda_%5.4f" % (self._dir0, lam))) leg1.append( _Process.Gromacs(system1, self._protocol, work_dir="%s/lambda_%5.4f" % (self._dir1, lam))) # Initialise the process runner. All processes have already been nested # inside the working directory so no need to re-nest. self._runner = _Process.ProcessRunner(leg0 + leg1, work_dir=self._work_dir, nest_dirs=False)