Example #1
0
    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)
Example #2
0
    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)
Example #4
0
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
Example #5
0
    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
Example #6
0
    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
Example #7
0
    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!")
Example #8
0
    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!")
Example #9
0
    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)