Exemplo n.º 1
0
def formalCharge(molecule):
    """Compute the formal charge on a molecule. This function requires that
       the molecule has explicit hydrogen atoms.

       Parameters
       ----------

       molecule : :class:`Molecule <BioSimSpace._SireWrappers.Molecule>`
           A molecule object.

       Returns
       -------

       formal_charge : :class:`Charge <BioSimSpace.Types.Charge>`
           The total formal charge on the molecule.
    """

    if type(molecule) is not _Molecule:
        raise TypeError(
            "'molecule' must be of type 'BioSimSpace._SireWrappers.Molecule'")

    from rdkit import Chem as _Chem

    # Create a temporary working directory.
    tmp_dir = _tempfile.TemporaryDirectory()
    work_dir = tmp_dir.name

    # Zero the total formal charge.
    formal_charge = 0

    # Stdout/stderr redirection doesn't work from within Jupyter.
    if _is_notebook:
        # Run in the working directory.
        with _Utils.cd(work_dir):

            # Save the molecule to a PDB file.
            _IO.saveMolecules("tmp", molecule, "PDB")

            # Read the ligand PDB into an RDKit molecule.
            mol = _Chem.MolFromPDBFile("tmp.pdb")

            # Compute the formal charge.
            formal_charge = _Chem.rdmolops.GetFormalCharge(mol)

    else:
        # Run in the working directory and redirect stderr from RDKit.
        with _Utils.cd(work_dir), _Utils.stderr_redirected():

            # Save the molecule to a PDB file.
            _IO.saveMolecules("tmp", molecule, "PDB")

            # Read the ligand PDB into an RDKit molecule.
            mol = _Chem.MolFromPDBFile("tmp.pdb")

            # Compute the formal charge.
            formal_charge = _Chem.rdmolops.GetFormalCharge(mol)

    return formal_charge * _electron_charge
Exemplo n.º 2
0
def formalCharge(molecule):
    """Compute the formal charge on a molecule. This function requires that
       the molecule has explicit hydrogen atoms.

       Parameters
       ----------

       molecule : :class:`Molecule <BioSimSpace._SireWrappers.Molecule>`
           A molecule object.

       Returns
       -------

       formal_charge : :class:`Charge <BioSimSpace.Types.Charge>`
           The total formal charge on the molecule.
    """

    if type(molecule) is not _Molecule:
        raise TypeError("'molecule' must be of type 'BioSimSpace._SireWrappers.Molecule'")

    from rdkit import Chem as _Chem
    from rdkit import RDLogger as _RDLogger

    # Disable RDKit warnings.
    _RDLogger.DisableLog('rdApp.*')

    # Create a temporary working directory.
    tmp_dir = _tempfile.TemporaryDirectory()
    work_dir = tmp_dir.name

    # Zero the total formal charge.
    formal_charge = 0

    # Run in the working directory.
    with _Utils.cd(work_dir):

        # Save the molecule to a PDB file.
        _IO.saveMolecules("tmp", molecule, "PDB")

        # Read the ligand PDB into an RDKit molecule.
        mol = _Chem.MolFromPDBFile("tmp.pdb")

        # Compute the formal charge.
        formal_charge = _Chem.rdmolops.GetFormalCharge(mol)

    return formal_charge * _electron_charge
Exemplo n.º 3
0
def matchAtoms(molecule0,
               molecule1,
               scoring_function="rmsd_align",
               matches=1,
               return_scores=False,
               prematch={},
               timeout=5 * _Units.Time.second,
               property_map0={},
               property_map1={}):
    """Find mappings between atom indices in molecule0 to those in molecule1.
       Molecules are aligned using a Maximum Common Substructure (MCS) search.
       When requesting more than one match, the mappings will be sorted using
       a scoring function and returned in order of best to worst score. (Note
       that, depending on the scoring function the "best" score may have the
       lowest value.)

       Parameters
       ----------

       molecule0 : :class:`Molecule <BioSimSpace._SireWrappers.Molecule>`
           The molecule of interest.

       molecule1 : :class:`Molecule <BioSimSpace._SireWrappers.Molecule>`
           The reference molecule.

       scoring_function : str
           The scoring function used to match atoms. Available options are:
             - "rmsd"
                 Calculate the root mean squared distance between the
                 coordinates of atoms in molecule0 to those that they
                 map to in molecule1.
             - "rmsd_align"
                 Align molecule0 to molecule1 based on the mapping before
                 computing the above RMSD score.
             - "rmsd_flex_align"
                 Flexibly align molecule0 to molecule1 based on the mapping
                 before computing the above RMSD score. (Requires the
                 'fkcombu'. package: http://strcomp.protein.osaka-u.ac.jp/kcombu)

       matches : int
           The maximum number of matches to return. (Sorted in order of score).

       return_scores : bool
           Whether to return a list containing the scores for each mapping.

       prematch : dict
           A dictionary of atom mappings that must be included in the match.

       timeout : BioSimSpace.Types.Time
           The timeout for the maximum common substructure search.

       property_map0 : dict
           A dictionary that maps "properties" in molecule0 to their user
           defined values. This allows the user to refer to properties
           with their own naming scheme, e.g. { "charge" : "my-charge" }

       property_map1 : dict
           A dictionary that maps "properties" in molecule1 to their user
           defined values.

       Returns
       -------

       matches : dict, [dict], ([dict], list)
           The best atom mapping, a list containing a user specified number of
           the best mappings ranked by their score, or a tuple containing the
           list of best mappings and a list of the corresponding scores.

       Examples
       --------

       Find the best maximum common substructure mapping between two molecules.

       >>> import BioSimSpace as BSS
       >>> mapping = BSS.Align.matchAtoms(molecule0, molecule1)

       Find the 5 best mappings.

       >>> import BioSimSpace as BSS
       >>> mappings = BSS.Align.matchAtoms(molecule0, molecule1, matches=5)

       Find the 5 best mappings along with their ranking scores.

       >>> import BioSimSpace as BSS
       >>> mappings, scores = BSS.Align.matchAtoms(molecule0, molecule1, matches=5, return_scores=True)

       Find the 5 best mappings along with their ranking scores. Score
       by flexibly aligning molecule0 to molecule1 based on each mapping
       and computing the root mean squared displacement of the matched
       atoms.

       >>> import BioSimSpace as BSS
       >>> mappings, scores = BSS.Align.matchAtoms(molecule0, molecule1, matches=5, return_scores=True, scoring_function="rmsd_flex_align")

       Find the best mapping that contains a prematch (this is a dictionary mapping
       atom indices in molecule0 to those in molecule1).

       >>> import BioSimSpace as BSS
       >>> mapping = BSS.Align.matchAtoms(molecule0, molecule1, prematch={0 : 10, 3 : 7})
    """

    # A list of supported scoring functions.
    scoring_functions = ["RMSD", "RMSDALIGN", "RMSDFLEXALIGN"]

    # Validate input.

    if type(molecule0) is not _Molecule:
        raise TypeError(
            "'molecule0' must be of type 'BioSimSpace._SireWrappers.Molecule'")

    if type(molecule1) is not _Molecule:
        raise TypeError(
            "'molecule1' must be of type 'BioSimSpace._SireWrappers.Molecule'")

    if type(scoring_function) is not str:
        raise TypeError("'scoring_function' must be of type 'str'")
    else:
        # Strip underscores and whitespace, then convert to upper case.
        _scoring_function = scoring_function.replace("_", "").upper()
        _scoring_function = _scoring_function.replace(" ", "").upper()
        if not _scoring_function in scoring_functions:
            raise ValueError(
                "Unsupported scoring function '%s'. Options are: %s" %
                (scoring_function, scoring_functions))

    if _scoring_function == "RMSDFLEXALIGN" and _fkcombu_exe is None:
        raise _MissingSoftwareError(
            "'rmsd_flex_align' option requires the 'fkcombu' program: "
            "http://strcomp.protein.osaka-u.ac.jp/kcombu")

    if type(matches) is not int:
        raise TypeError("'matches' must be of type 'int'")
    else:
        if matches < 0:
            raise ValueError("'matches' must be positive!")

    if type(return_scores) is not bool:
        raise TypeError("'return_matches' must be of type 'bool'")

    if type(prematch) is not dict:
        raise TypeError("'prematch' must be of type 'dict'")
    else:
        _validate_mapping(molecule0, molecule1, prematch, "prematch")

    if type(timeout) is not _Units.Time._Time:
        raise TypeError("'timeout' must be of type 'BioSimSpace.Types.Time'")

    if type(property_map0) is not dict:
        raise TypeError("'property_map0' must be of type 'dict'")

    if type(property_map1) is not dict:
        raise TypeError("'property_map1' must be of type 'dict'")

    # Extract the Sire molecule from each BioSimSpace molecule.
    mol0 = molecule0._getSireObject()
    mol1 = molecule1._getSireObject()

    # Convert the timeout to seconds and take the magnitude as an integer.
    timeout = int(timeout.seconds().magnitude())

    # Create a temporary working directory.
    tmp_dir = _tempfile.TemporaryDirectory()
    work_dir = tmp_dir.name

    # Use RDKkit to find the maximum common substructure.

    try:
        # Run inside a temporary directory.
        with _Utils.cd(work_dir):
            # Write both molecules to PDB files.
            _IO.saveMolecules("tmp0",
                              molecule0,
                              "PDB",
                              property_map=property_map0)
            _IO.saveMolecules("tmp1",
                              molecule1,
                              "PDB",
                              property_map=property_map1)

            # Load the molecules with RDKit.
            # Note that the C++ function overloading seems to be broken, so we
            # need to pass all arguments by position, rather than keyword.
            # The arguments are: "filename", "sanitize", "removeHs", "flavor"
            mols = [
                _Chem.MolFromPDBFile("tmp0.pdb", False, False, 0),
                _Chem.MolFromPDBFile("tmp1.pdb", False, False, 0)
            ]

            # Generate the MCS match.
            mcs = _rdFMCS.FindMCS(mols,
                                  atomCompare=_rdFMCS.AtomCompare.CompareAny,
                                  bondCompare=_rdFMCS.BondCompare.CompareAny,
                                  completeRingsOnly=True,
                                  ringMatchesRingOnly=True,
                                  matchChiralTag=False,
                                  matchValences=False,
                                  maximizeBonds=False,
                                  timeout=timeout)

            # Get the common substructure as a SMARTS string.
            mcs_smarts = _Chem.MolFromSmarts(mcs.smartsString)

    except:
        raise RuntimeError("RDKIT MCS mapping failed!")

    # Score the mappings and return them in sorted order (best to worst).
    mappings, scores = _score_rdkit_mappings(mol0, mol1, mols[0], mols[1],
                                             mcs_smarts, prematch,
                                             _scoring_function, property_map0,
                                             property_map1)

    # Sometimes RDKit fails to generate a mapping that includes the prematch.
    # If so, then try generating a mapping using the MCS routine from Sire.
    if len(mappings) == 1 and mappings[0] == prematch:

        # Convert timeout to a Sire Unit.
        timeout = timeout * _SireUnits.second

        # Regular match. Include light atoms, but don't allow matches between heavy
        # and light atoms.
        m0 = mol0.evaluate().findMCSmatches(
            mol1, _SireMol.AtomResultMatcher(_to_sire_mapping(prematch)),
            timeout, True, property_map0, property_map1, 6, False)

        # Include light atoms, and allow matches between heavy and light atoms.
        # This captures mappings such as O --> H in methane to methanol.
        m1 = mol0.evaluate().findMCSmatches(
            mol1, _SireMol.AtomResultMatcher(_to_sire_mapping(prematch)),
            timeout, True, property_map0, property_map1, 0, False)

        # Take the mapping with the larger number of matches.
        if len(m1) > 0:
            if len(m0) > 0:
                if len(m1[0]) > len(m0[0]):
                    mappings = m1
                else:
                    mappings = m0
            else:
                mappings = m1
        else:
            mappings = m0

        # Score the mappings and return them in sorted order (best to worst).
        mappings, scores = _score_sire_mappings(mol0, mol1, mappings, prematch,
                                                _scoring_function,
                                                property_map0, property_map1)

    if matches == 1:
        if return_scores:
            return (mappings[0], scores[0])
        else:
            return mappings[0]
    else:
        # Return a list of matches from best to worst.
        if return_scores:
            return (mappings[0:matches], scores[0:matches])
        # Return a tuple containing the list of matches from best to
        # worst along with the list of scores.
        else:
            return mappings[0:matches]
Exemplo n.º 4
0
def flexAlign(molecule0,
              molecule1,
              mapping=None,
              fkcombu_exe=None,
              property_map0={},
              property_map1={}):
    """Flexibly align atoms in molecule0 to those in molecule1 using the
       mapping between matched atom indices.

       Parameters
       ----------

       molecule0 : :class:`Molecule <BioSimSpace._SireWrappers.Molecule>`
           The molecule to align.

       molecule1 : :class:`Molecule <BioSimSpace._SireWrappers.Molecule>`
           The reference molecule.

       mapping : dict
           A dictionary mapping atoms in molecule0 to those in molecule1.

       fkcombu_exe : str
           Path to the fkcombu executable. If None is passed, then BioSimSpace
           will attempt to find fkcombu by searching your PATH.

       property_map0 : dict
           A dictionary that maps "properties" in molecule0 to their user
           defined values. This allows the user to refer to properties
           with their own naming scheme, e.g. { "charge" : "my-charge" }

       property_map1 : dict
           A dictionary that maps "properties" in molecule1 to their user
           defined values.

       Returns
       -------

       molecule : :class:`Molecule <BioSimSpace._SireWrappers.Molecule>`
           The aligned molecule.

       Examples
       --------

       Align molecule0 to molecule1 based on a precomputed mapping.

       >>> import BioSimSpace as BSS
       >>> molecule0 = BSS.Align.flexAlign(molecule0, molecule1, mapping)

       Align molecule0 to molecule1. Since no mapping is passed one will be
       autogenerated using :class:`matchAtoms <BioSimSpace.Align.matchAtoms>`
       with default options.

       >>> import BioSimSpace as BSS
       >>> molecule0 = BSS.Align.flexAlign(molecule0, molecule1)
    """

    # Check that we found fkcombu in the PATH.
    if fkcombu_exe is None:
        if _fkcombu_exe is None:
            raise _MissingSoftwareError(
                "'BioSimSpace.Align.flexAlign' requires the 'fkcombu' program: "
                "http://strcomp.protein.osaka-u.ac.jp/kcombu")
        else:
            fkcombu_exe = _fkcombu_exe
    # Check that the user supplied executable exists.
    else:
        if not _os.path.isfile(fkcombu_exe):
            raise IOError("'fkcombu' executable doesn't exist: '%s'" %
                          fkcombu_exe)

    if type(molecule0) is not _Molecule:
        raise TypeError(
            "'molecule0' must be of type 'BioSimSpace._SireWrappers.Molecule'")

    if type(molecule1) is not _Molecule:
        raise TypeError(
            "'molecule1' must be of type 'BioSimSpace._SireWrappers.Molecule'")

    if type(property_map0) is not dict:
        raise TypeError("'property_map0' must be of type 'dict'")

    if type(property_map1) is not dict:
        raise TypeError("'property_map1' must be of type 'dict'")

    # The user has passed an atom mapping.
    if mapping is not None:
        if type(mapping) is not dict:
            raise TypeError("'mapping' must be of type 'dict'.")
        else:
            _validate_mapping(molecule0, molecule1, mapping, "mapping")

    # Get the best match atom mapping.
    else:
        mapping = matchAtoms(molecule0,
                             molecule1,
                             property_map0=property_map0,
                             property_map1=property_map1)

    # Convert the mapping to AtomIdx key:value pairs.
    sire_mapping = _to_sire_mapping(mapping)

    # Create a temporary working directory.
    tmp_dir = _tempfile.TemporaryDirectory()
    work_dir = tmp_dir.name

    # Execute in the working directory.
    with _Utils.cd(work_dir):

        # Write the two molecules to PDB files.
        _IO.saveMolecules("molecule0",
                          molecule0,
                          "PDB",
                          property_map=property_map0)
        _IO.saveMolecules("molecule1",
                          molecule1,
                          "PDB",
                          property_map=property_map1)

        # Write the mapping to text. (Increment indices by one).
        with open("mapping.txt", "w") as file:
            for idx0, idx1 in sire_mapping.items():
                file.write("%d %d\n" % (idx0.value() + 1, idx1.value() + 1))

        # Create the fkcombu command string.
        command = "%s -T molecule0.pdb -R molecule1.pdb -alg F -iam mapping.txt -opdbT aligned.pdb" % fkcombu_exe

        # Run the command as a subprocess.
        proc = _subprocess.run(command,
                               shell=True,
                               stdout=_subprocess.PIPE,
                               stderr=_subprocess.PIPE)

        # Check that the output file exists.
        if not _os.path.isfile("aligned.pdb"):
            raise _AlignmentError(
                "Failed to align molecules based on mapping: %r" %
                mapping) from None

        # Load the aligned molecule.
        aligned = _IO.readMolecules("aligned.pdb")[0]

        # Get the "coordinates" property for molecule0.
        prop = property_map0.get("coordinates", "coordinates")

        # Copy the coordinates back into the original molecule.
        molecule0._sire_object = molecule0._sire_object.edit() \
            .setProperty(prop, aligned._sire_object.property("coordinates")).commit()

    # Return the aligned molecule.
    return _Molecule(molecule0)
Exemplo n.º 5
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