def generateTopologyFromOEMol(molecule):
    """
    Generate an OpenMM Topology object from an OEMol molecule.

    Parameters
    ----------
    molecule : openeye.oechem.OEMol
        The molecule from which a Topology object is to be generated.

    Returns
    -------
    topology : simtk.openmm.app.Topology
        The Topology object generated from `molecule`.

    """
    # Create a Topology object with one Chain and one Residue.
    from simtk.openmm.app import Topology
    topology = Topology()
    chain = topology.addChain()
    resname = molecule.GetTitle()
    residue = topology.addResidue(resname, chain)

    # Create atoms in the residue.
    for atom in molecule.GetAtoms():
        name = atom.GetName()
        element = Element.getByAtomicNumber(atom.GetAtomicNum())
        atom = topology.addAtom(name, element, residue)

    # Create bonds.
    atoms = { atom.name : atom for atom in topology.atoms() }
    for bond in molecule.GetBonds():
        topology.addBond(atoms[bond.GetBgn().GetName()], atoms[bond.GetEnd().GetName()])

    return topology
示例#2
0
def generateTopologyFromOEMol(molecule):
    """
    Generate an OpenMM Topology object from an OEMol molecule.

    Parameters
    ----------
    molecule : openeye.oechem.OEMol
        The molecule from which a Topology object is to be generated.

    Returns
    -------
    topology : simtk.openmm.app.Topology
        The Topology object generated from `molecule`.

    """
    # Create a Topology object with one Chain and one Residue.
    from simtk.openmm.app import Topology
    topology = Topology()
    chain = topology.addChain()
    resname = molecule.GetTitle()
    residue = topology.addResidue(resname, chain)

    # Create atoms in the residue.
    for atom in molecule.GetAtoms():
        name = atom.GetName()
        element = Element.getByAtomicNumber(atom.GetAtomicNum())
        atom = topology.addAtom(name, element, residue)

    # Create bonds.
    atoms = {atom.name: atom for atom in topology.atoms()}
    for bond in molecule.GetBonds():
        topology.addBond(atoms[bond.GetBgn().GetName()],
                         atoms[bond.GetEnd().GetName()])

    return topology
示例#3
0
 def _find_nonsolvent_atoms(self, topology: app.Topology) -> List[int]:
     solvent_residue_names = ["WAT", "SOL", "H2O", "HOH"]
     nonsolvent_atoms = []
     for atom in topology.atoms():
         if not atom.residue.name in solvent_residue_names:
             nonsolvent_atoms.append(atom.index)
     return nonsolvent_atoms
示例#4
0
    def __init__(self, file):
        """Load a prmtop file."""
        top = Topology()
        ## The Topology read from the prmtop file
        self.topology = top

        # Load the prmtop file

        prmtop = amber_file_parser.PrmtopLoader(file)
        self._prmtop = prmtop

        # Add atoms to the topology

        PDBFile._loadNameReplacementTables()
        lastResidue = None
        c = top.addChain()
        for index in range(prmtop.getNumAtoms()):
            resNumber = prmtop.getResidueNumber(index)
            if resNumber != lastResidue:
                lastResidue = resNumber
                resName = prmtop.getResidueLabel(iAtom=index).strip()
                if resName in PDBFile._residueNameReplacements:
                    resName = PDBFile._residueNameReplacements[resName]
                r = top.addResidue(resName, c)
                if resName in PDBFile._atomNameReplacements:
                    atomReplacements = PDBFile._atomNameReplacements[resName]
                else:
                    atomReplacements = {}
            atomName = prmtop.getAtomName(index).strip()
            if atomName in atomReplacements:
                atomName = atomReplacements[atomName]

            # Try to guess the element.

            upper = atomName.upper()
            if upper.startswith('CL'):
                element = elem.chlorine
            elif upper.startswith('NA'):
                element = elem.sodium
            elif upper.startswith('MG'):
                element = elem.magnesium
            else:
                try:
                    element = elem.get_by_symbol(atomName[0])
                except KeyError:
                    element = None
            top.addAtom(atomName, element, r)

        # Add bonds to the topology

        atoms = list(top.atoms())
        for bond in prmtop.getBondsWithH():
            top.addBond(atoms[bond[0]], atoms[bond[1]])
        for bond in prmtop.getBondsNoH():
            top.addBond(atoms[bond[0]], atoms[bond[1]])

        # Set the periodic box size.

        if prmtop.getIfBox():
            top.setUnitCellDimensions(tuple(x.value_in_unit(unit.nanometer) for x in prmtop.getBoxBetaAndDimensions()[1:4])*unit.nanometer)
示例#5
0
def generateTopologyFromOEMol(molecule):
    """
    Generate an OpenMM Topology object from an OEMol molecule.

    Parameters
    ----------
    molecule : openeye.oechem.OEMol
        The molecule from which a Topology object is to be generated.

    Returns
    -------
    topology : simtk.openmm.app.Topology
        The Topology object generated from `molecule`.

    """
    # Avoid manipulating the molecule
    mol = OEMol(molecule)

    # Create a Topology object with one Chain and one Residue.
    from simtk.openmm.app import Topology
    topology = Topology()
    chain = topology.addChain()
    resname = mol.GetTitle()
    residue = topology.addResidue(resname, chain)

    # Make sure the atoms have names, otherwise bonds won't be created properly below
    if any([atom.GetName() == '' for atom in mol.GetAtoms()]):
        oechem.OETriposAtomNames(mol)
    # Check names are unique; non-unique names will also cause a problem
    atomnames = [atom.GetName() for atom in mol.GetAtoms()]
    if any(atomnames.count(atom.GetName()) > 1 for atom in mol.GetAtoms()):
        raise Exception(
            "Error: Reference molecule must have unique atom names in order to create a Topology."
        )

    # Create atoms in the residue.
    for atom in mol.GetAtoms():
        name = atom.GetName()
        element = elem.Element.getByAtomicNumber(atom.GetAtomicNum())
        openmm_atom = topology.addAtom(name, element, residue)

    # Create bonds.
    atoms = {atom.name: atom for atom in topology.atoms()}
    for bond in mol.GetBonds():
        aromatic = None
        if bond.IsAromatic(): aromatic = 'Aromatic'
        # Add bond, preserving order assessed by OEChem
        topology.addBond(atoms[bond.GetBgn().GetName()],
                         atoms[bond.GetEnd().GetName()],
                         type=aromatic,
                         order=bond.GetOrder())

    return topology
示例#6
0
class OpenMMAmberParm(AmberParm):
    """
    OpenMM-compatible subclass of AmberParm. This object should still work with
    the ParmEd API while also being compatible with OpenMM's environment
    """
   
    # Define default force groups for all of the bonded terms. This allows them
    # to be turned on and off selectively. This is a way to implement per-term
    # energy decomposition to compare individual components

    BOND_FORCE_GROUP = 0
    ANGLE_FORCE_GROUP = 1
    DIHEDRAL_FORCE_GROUP = 2
    NONBONDED_FORCE_GROUP = 3
    GB_FORCE_GROUP = 3

    def openmm_LJ(self):
        """
        Same as fill_LJ, except it uses 0.5 for the LJ radius for H-atoms with
        no vdW parameters (per OpenMM's standard)

        Returns:
            list, list : The 1st list is the list of all Rmin/2 terms. The
                         2nd is the list of all epsilon (or well depth) terms.
        """
        LJ_radius = []  # empty LJ_radii so it can be re-filled
        LJ_depth = []   # empty LJ_depths so it can be re-filled
        one_sixth = 1 / 6    # we need to raise some numbers to the 1/6th power

        ntypes = self.pointers['NTYPES']
        acoef = self.parm_data['LENNARD_JONES_ACOEF']
        bcoef = self.parm_data['LENNARD_JONES_BCOEF']

        for i in range(ntypes):
            lj_index = self.parm_data["NONBONDED_PARM_INDEX"][ntypes*i+i] - 1
            if acoef[lj_index] < 1.0e-10:
                LJ_radius.append(0.5)
                LJ_depth.append(0)
            else:
                factor = (2 * acoef[lj_index] / bcoef[lj_index])
                LJ_radius.append(pow(factor, one_sixth) * 0.5)
                LJ_depth.append(bcoef[lj_index] / 2 / factor)
      
        # Now check that we haven't modified any off-diagonals, since that will
        # not work with OpenMM
        for i in range(ntypes):
            for j in range(ntypes):
                idx = self.parm_data['NONBONDED_PARM_INDEX'][ntypes*i+j] - 1
                rij = LJ_radius[i] + LJ_radius[j]
                wdij = sqrt(LJ_depth[i] * LJ_depth[j])
                a = acoef[idx]
                b = bcoef[idx]
                if a == 0 or b == 0:
                    if a != 0 or b != 0 or (wdij != 0 and rij != 0):
                        raise OpenMMError('Off-diagonal LJ modifications '
                                          'detected. These are incompatible '
                                          'with the OpenMM API')
                elif (abs((a - (wdij * rij**12)) / a) > 1e-6 or
                      abs((b - (2 * wdij * rij**6)) / b) > 1e-6):
                    raise OpenMMError(
                            'Off-diagonal LJ modifications detected. These are '
                            'incompatible with the OpenMM API. Acoef=%s; '
                            'computed=%s. Bcoef=%s; computed=%s' %
                            (acoef, wdij*rij**12, bcoef, 2*wdij*rij**6)
                    )

        return LJ_radius, LJ_depth

    def openmm_14_LJ(self):
        """
        Returns the radii and depths for the LJ interactions between 1-4 pairs.
        For Amber topology files this is the same as the normal LJ parameters,
        but is done here so that OpenMMChamberParm can inherit and override this
        behavior without having to duplicate all of the system creation code.
        """
        return self.openmm_LJ()

    @property
    def topology(self):
        """
        The OpenMM Topology object. Cached when possible, but any changes to the
        topology object lists results in the topology being deleted and rebuilt
        """
        # If anything changed, rebuild the topology
        if not self._topology_changed():
            try:
                return self._topology
            except AttributeError:
                pass
        else:
            self.remake_parm()
      
        self._topology = Topology()

        # Add all of the atoms to the topology file in the same chain
        chain = self._topology.addChain()
        last_residue = None
        for i, atm in enumerate(self.atom_list):
            resnum = atm.residue.idx
            if last_residue != resnum:
                last_residue = resnum
                resname = atm.residue.resname
                res = self._topology.addResidue(resname, chain)
            elem = element.get_by_symbol(pt.Element[atm.element])
            self._topology.addAtom(atm.atname, elem, res)

        # Add bonds to the topology (both with and without hydrogen)
        atoms = list(self._topology.atoms())
        for bnd in self.bonds_inc_h + self.bonds_without_h:
            self._topology.addBond(atoms[bnd.atom1.starting_index],
                                   atoms[bnd.atom2.starting_index])
      
        # Set the box dimensions
        if self.ptr('ifbox'):
            if hasattr(self, 'rst7'):
                self._topology.setUnitCellDimensions(
                        self.rst7.box[:3]*u.angstrom
                )
            else:
                self._topology.setUnitCellDimensions(
                        self.parm_data['BOX_DIMENSIONS'][1:4]*u.angstrom
                )

        return self._topology
   
    def _get_gb_params(self, gb_model=HCT):
        """ Gets the GB parameters. Need this method to special-case GB neck """
        if gb_model is GBn:
            screen = [0.5 for atom in self.atom_list]
            for i, atom in enumerate(self.atom_list):
                if atom.element == 6:
                    screen[i] = 0.48435382330
                elif atom.element == 1:
                    screen[i] = 1.09085413633
                elif atom.element == 7:
                    screen[i] = 0.700147318409
                elif atom.element == 8:
                    screen[i] = 1.06557401132
                elif atom.element == 16:
                    screen[i] = 0.602256336067
        elif gb_model is GBn2:
            # Add non-optimized values as defaults
            alpha = [1.0 for i in self.atom_list]
            beta = [0.8 for i in self.atom_list]
            gamma = [4.85 for i in self.atom_list]
            screen = [0.5 for i in self.atom_list]
            for i, atom in enumerate(self.atom_list):
                if atom.element == 6:
                    screen[i] = 1.058554
                    alpha[i] = 0.733756
                    beta[i] = 0.506378
                    gamma[i] = 0.205844
                elif atom.element == 1:
                    screen[i] = 1.425952
                    alpha[i] = 0.788440
                    beta[i] = 0.798699
                    gamma[i] = 0.437334
                elif atom.element == 7:
                    screen[i] = 0.733599
                    alpha[i] = 0.503364
                    beta[i] = 0.316828
                    gamma[i] = 0.192915
                elif atom.element == 8:
                    screen[i] = 1.061039
                    alpha[i] = 0.867814
                    beta[i] = 0.876635
                    gamma[i] = 0.387882
                elif atom.element == 16:
                    screen[i] = -0.703469
                    alpha[i] = 0.867814
                    beta[i] = 0.876635
                    gamma[i] = 0.387882
        else:
            screen = self.parm_data['SCREEN']

        length_conv = u.angstrom.conversion_factor_to(u.nanometer)
        radii = [rad * length_conv for rad in self.parm_data['RADII']]

        if gb_model is GBn2:
            return zip(radii, screen, alpha, beta, gamma)
        return zip(radii, screen)

    def createSystem(self, nonbondedMethod=ff.NoCutoff,
                     nonbondedCutoff=1.0*u.nanometer,
                     constraints=None,
                     rigidWater=True,
                     implicitSolvent=None,
                     implicitSolventKappa=None,
                     implicitSolventSaltConc=0.0*u.moles/u.liter,
                     temperature=298.15*u.kelvin,
                     soluteDielectric=1.0,
                     solventDielectric=78.5,
                     removeCMMotion=True,
                     hydrogenMass=None,
                     ewaldErrorTolerance=0.0005,
                     flexibleConstraints=True,
                     verbose=False):
        """
        Construct an OpenMM System representing the topology described by the
        prmtop file.

        Parameters:
         -  nonbondedMethod (object=NoCutoff) The method to use for nonbonded
               interactions. Allowed values are NoCutoff, CutoffNonPeriodic,
               CutoffPeriodic, Ewald, or PME.
         -  nonbondedCutoff (distance=1*nanometer) The cutoff distance to use
               for nonbonded interactions.
         -  constraints (object=None) Specifies which bonds or angles should be
               implemented with constraints. Allowed values are None, HBonds,
               AllBonds, or HAngles.
         -  rigidWater (boolean=True) If true, water molecules will be fully
               rigid regardless of the value passed for the constraints argument
         -  implicitSolvent (object=None) If not None, the implicit solvent
               model to use. Allowed values are HCT, OBC1, OBC2, or GBn
         -  implicitSolventKappa (float=None): Debye screening parameter to
               model salt concentrations in GB solvent.
         -  implicitSolventSaltConc (float=0.0*u.moles/u.liter): Salt
               concentration for GB simulations. Converted to Debye length
               `kappa'
         -  temperature (float=298.15*u.kelvin): Temperature used in the salt
               concentration-to-kappa conversion for GB salt concentration term
         -  soluteDielectric (float=1.0) The solute dielectric constant to use
               in the implicit solvent model.
         -  solventDielectric (float=78.5) The solvent dielectric constant to
               use in the implicit solvent model.
         -  removeCMMotion (boolean=True) If true, a CMMotionRemover will be
               added to the System.
         -  hydrogenMass (mass=None) The mass to use for hydrogen atoms bound to
               heavy atoms. Any mass added to a hydrogen is subtracted from the
               heavy atom to keep their total mass the same.
         -  ewaldErrorTolerance (float=0.0005) The error tolerance to use if the
               nonbonded method is Ewald or PME.
         -  flexibleConstraints (bool=True) Are our constraints flexible or not?
         -  verbose (bool=False) Optionally prints out a running progress report
        """
        # Rebuild the topology file if necessary, and flush the atom property
        # data to the atom list
        if self._topology_changed():
            self.remake_parm()
        else:
            self.atom_list.refresh_data()
        LJ_radius, LJ_depth = self.openmm_LJ() # Get our LJ parameters
        LJ_14_radius, LJ_14_depth = self.openmm_14_LJ()

        # Set the cutoff distance in nanometers
        cutoff = None
        if nonbondedMethod is not ff.NoCutoff:
            cutoff = nonbondedCutoff
            # Remove units from cutoff
            if u.is_quantity(cutoff):
                cutoff = cutoff.value_in_unit(u.nanometers)

        if nonbondedMethod not in (ff.NoCutoff, ff.CutoffNonPeriodic,
                                   ff.CutoffPeriodic, ff.Ewald, ff.PME):
            raise ValueError('Illegal value for nonbonded method')
        if self.ptr('ifbox') == 0 and nonbondedMethod in (ff.CutoffPeriodic,
                                                          ff.Ewald, ff.PME):
            raise ValueError('Illegal nonbonded method for a '
                             'non-periodic system')
        if implicitSolvent not in (HCT, OBC1, OBC2, GBn, GBn2, None):
            raise ValueError('Illegal implicit solvent model choice.')
        if not constraints in (None, ff.HAngles, ff.HBonds, ff.AllBonds):
            raise ValueError('Illegal constraints choice')
      
        # Define conversion factors
        length_conv = u.angstrom.conversion_factor_to(u.nanometer)
        _ambfrc = u.kilocalorie_per_mole/(u.angstrom*u.angstrom)
        _openmmfrc = u.kilojoule_per_mole/(u.nanometer*u.nanometer)
        bond_frc_conv = _ambfrc.conversion_factor_to(_openmmfrc)
        _ambfrc = u.kilocalorie_per_mole/(u.radians*u.radians)
        _openmmfrc = u.kilojoule_per_mole/(u.radians*u.radians)
        angle_frc_conv = _ambfrc.conversion_factor_to(_openmmfrc)
        dihe_frc_conv = u.kilocalorie_per_mole.conversion_factor_to(
                            u.kilojoule_per_mole)
        ene_conv = dihe_frc_conv
      
        # Create the system
        system = mm.System()
        if verbose: print('Adding particles...')
        for mass in self.parm_data['MASS']:
            system.addParticle(mass)
        # Set up the constraints
        if verbose and (constraints is not None and not rigidWater):
            print('Adding constraints...')
        if constraints in (ff.HBonds, ff.AllBonds, ff.HAngles):
            for bond in self.bonds_inc_h:
                system.addConstraint(bond.atom1.starting_index,
                                     bond.atom2.starting_index,
                                     bond.bond_type.req*length_conv)
        if constraints in (ff.AllBonds, ff.HAngles):
            for bond in self.bonds_without_h:
                system.addConstraint(bond.atom1.starting_index,
                                     bond.atom2.starting_index,
                                     bond.bond_type.req*length_conv)
        if rigidWater and constraints is None:
            for bond in self.bonds_inc_h:
                if (bond.atom1.residue.resname in WATNAMES and
                    bond.atom2.residue.resname in WATNAMES):
                    system.addConstraint(bond.atom1.starting_index,
                                         bond.atom2.starting_index,
                                         bond.bond_type.req*length_conv)
        # Add Bond forces
        if verbose: print('Adding bonds...')
        force = mm.HarmonicBondForce()
        force.setForceGroup(self.BOND_FORCE_GROUP)
        if flexibleConstraints or (constraints not in (ff.HBonds, ff.AllBonds,
                                                       ff.HAngles)):
            for bond in self.bonds_inc_h:
                force.addBond(bond.atom1.starting_index,
                              bond.atom2.starting_index,
                              bond.bond_type.req*length_conv,
                              2*bond.bond_type.k*bond_frc_conv)
        if flexibleConstraints or (constraints not in (ff.AllBonds,ff.HAngles)):
            for bond in self.bonds_without_h:
                force.addBond(bond.atom1.starting_index,
                              bond.atom2.starting_index,
                              bond.bond_type.req*length_conv,
                              2*bond.bond_type.k*bond_frc_conv)
        system.addForce(force)
        # Add Angle forces
        if verbose: print('Adding angles...')
        force = mm.HarmonicAngleForce()
        force.setForceGroup(self.ANGLE_FORCE_GROUP)
        if constraints is ff.HAngles:
            num_constrained_bonds = system.getNumConstraints()
            atom_constraints = [[]] * system.getNumParticles()
            for i in range(num_constrained_bonds):
                c = system.getConstraintParameters(i)
                dist = c[2].value_in_unit(u.nanometer)
                atom_constraints[c[0]].append((c[1], dist))
                atom_constraints[c[1]].append((c[0], dist))
        for angle in self.angles_inc_h:
            if constraints is ff.HAngles:
                a1 = angle.atom1.element
                a2 = angle.atom2.element
                a3 = angle.atom3.element
                nh = int(a1==1) + int(a2==1) + int(a3==1)
                constrained = (nh >= 2 or (nh == 1 and a2 == 8))
            else:
                constrained = False # no constraints
            if constrained:
                l1 = l2 = None
                for bond in angle.atom2.bonds:
                    if bond.atom1 is angle.atom1 or bond.atom2 is angle.atom1:
                        l1 = bond.bond_type.req * length_conv
                    elif bond.atom1 is angle.atom3 or bond.atom2 is angle.atom3:
                        l2 = bond.bond_type.req * length_conv
                # Compute the distance between the atoms and add a constraint
                length = sqrt(l1*l1 + l2*l2 - 2*l1*l2*
                              cos(angle.angle_type.theteq))
                system.addConstraint(bond.atom1.starting_index,
                                     bond.atom2.starting_index, length)
            if flexibleConstraints or not constrained:
                force.addAngle(angle.atom1.starting_index,
                               angle.atom2.starting_index,
                               angle.atom3.starting_index,
                               angle.angle_type.theteq,
                               2*angle.angle_type.k*angle_frc_conv)
        for angle in self.angles_without_h:
            force.addAngle(angle.atom1.starting_index,
                           angle.atom2.starting_index,
                           angle.atom3.starting_index,
                           angle.angle_type.theteq,
                           2*angle.angle_type.k*angle_frc_conv)
        system.addForce(force)
        # Add dihedral forces
        if verbose: print('Adding torsions...')
        force = mm.PeriodicTorsionForce()
        force.setForceGroup(self.DIHEDRAL_FORCE_GROUP)
        for tor in self.dihedrals_inc_h + self.dihedrals_without_h:
            force.addTorsion(tor.atom1.starting_index,
                             tor.atom2.starting_index,
                             tor.atom3.starting_index,
                             tor.atom4.starting_index,
                             int(tor.dihed_type.per),
                             tor.dihed_type.phase,
                             tor.dihed_type.phi_k*dihe_frc_conv)
        system.addForce(force)

        # Add nonbonded terms now
        if verbose: print('Adding nonbonded interactions...')
        force = mm.NonbondedForce()
        force.setForceGroup(self.NONBONDED_FORCE_GROUP)
        if self.ptr('ifbox') == 0: # non-periodic
            if nonbondedMethod is ff.NoCutoff:
                force.setNonbondedMethod(mm.NonbondedForce.NoCutoff)
            elif nonbondedMethod is ff.CutoffNonPeriodic:
                if cutoff is None:
                    raise ValueError('No cutoff value specified')
                force.setNonbondedMethod(mm.NonbondedForce.CutoffNonPeriodic)
                force.setCutoffDistance(cutoff)
            else:
                raise ValueError('Illegal nonbonded method for non-periodic '
                                 'system')
        else: # periodic
            # Set up box vectors (from inpcrd if available, or fall back to
            # prmtop definitions
            system.setDefaultPeriodicBoxVectors(*self.box_vectors)

            # Set cutoff
            if cutoff is None:
                # Compute cutoff automatically
                box = self.box_lengths
                min_box_width = min((box[0]/u.nanometers,
                                     box[1]/u.nanometers,
                                     box[2]/u.nanometers))
                CLEARANCE_FACTOR = 0.97
                cutoff = u.Quantity((min_box_width*CLEARANCE_FACTOR)/2.0,
                                    u.nanometers)
            if nonbondedMethod is not ff.NoCutoff:
                force.setCutoffDistance(cutoff)

            # Set nonbonded method.
            if nonbondedMethod is ff.NoCutoff:
                force.setNonbondedMethod(mm.NonbondedForce.NoCutoff)
            elif nonbondedMethod is ff.CutoffNonPeriodic:
                force.setNonbondedMethod(mm.NonbondedForce.CutoffNonPeriodic)
            elif nonbondedMethod is ff.CutoffPeriodic:
                force.setNonbondedMethod(mm.NonbondedForce.CutoffPeriodic)
            elif nonbondedMethod is ff.Ewald:
                force.setNonbondedMethod(mm.NonbondedForce.Ewald)
            elif nonbondedMethod is ff.PME:
                force.setNonbondedMethod(mm.NonbondedForce.PME)
            else:
                raise ValueError('Cutoff method is not understood')

            if ewaldErrorTolerance is not None:
                force.setEwaldErrorTolerance(ewaldErrorTolerance)

        # Add per-particle nonbonded parameters (LJ params)
        sigma_scale = 2**(-1/6) * 2
        for i, atm in enumerate(self.atom_list):
            force.addParticle(atm.charge,
                              sigma_scale*LJ_radius[atm.nb_idx-1]*length_conv,
                              LJ_depth[atm.nb_idx-1]*ene_conv)

        # Add 1-4 interactions
        excluded_atom_pairs = set() # save these pairs so we don't zero them out
        sigma_scale = 2**(-1/6)
        for tor in self.dihedrals_inc_h + self.dihedrals_without_h:
            if min(tor.signs) < 0: continue # multi-terms and impropers
            charge_prod = (tor.atom1.charge * tor.atom4.charge /
                           tor.dihed_type.scee)
            epsilon = (sqrt(LJ_14_depth[tor.atom1.nb_idx-1] * ene_conv *
                            LJ_14_depth[tor.atom4.nb_idx-1] * ene_conv) /
                            tor.dihed_type.scnb)
            sigma = (LJ_14_radius[tor.atom1.nb_idx-1] +
                     LJ_14_radius[tor.atom4.nb_idx-1])*length_conv*sigma_scale
            force.addException(tor.atom1.starting_index,
                               tor.atom4.starting_index,
                               charge_prod, sigma, epsilon)
            excluded_atom_pairs.add(
                    min( (tor.atom1.starting_index, tor.atom4.starting_index),
                         (tor.atom4.starting_index, tor.atom1.starting_index) )
            )

        # Add excluded atoms
        for atom in self.atom_list:
            # Exclude all bonds and angles
            for atom2 in atom.bond_partners:
                if atom2.starting_index > atom.starting_index:
                    force.addException(atom.starting_index,
                                       atom2.starting_index, 0.0, 0.1, 0.0)
            for atom2 in atom.angle_partners:
                if atom2.starting_index > atom.starting_index:
                    force.addException(atom.starting_index,
                                       atom2.starting_index, 0.0, 0.1, 0.0)
            for atom2 in atom.exclusion_partners:
                if atom2.starting_index > atom.starting_index:
                    force.addException(atom.starting_index,
                                       atom2.starting_index, 0.0, 0.1, 0.0)
            for atom2 in atom.dihedral_partners:
                if atom2.starting_index <= atom.starting_index: continue
                if ((atom.starting_index, atom2.starting_index) in
                    excluded_atom_pairs):
                    continue
                force.addException(atom.starting_index,
                                   atom2.starting_index, 0.0, 0.1, 0.0)
        system.addForce(force)

        # Add virtual sites for water
        # First tag the residues that have an extra point in them
        for res in self.residue_list: res.has_ep = False
        ep = [atom for atom in self.atom_list if atom.atname in EPNAMES]
        for atom in ep: atom.residue.has_ep = True
        if len(ep) > 0:
            numRes = ep[-1].residue.idx + 1
            waterO = [[] for i in range(numRes)]
            waterH = [[] for i in range(numRes)]
            waterEP = [[] for i in range(numRes)]
            for atom in self.atom_list:
                if atom.residue.has_ep:
                    if atom.element == 8:
                        waterO[res].append(atom)
                    elif atom.element == 1:
                        waterH[res].append(atom)
                    elif atom.element == 0:
                        waterEP[res].append(atom)
            # Record bond lengths for faster access
            distOH = [None] * numRes
            distHH = [None] * numRes
            distOE = [None] * numRes
            for bond in self.bonds_inc_h + self.bonds_without_h:
                a1 = bond.atom1
                a2 = bond.atom2
                if a1.residue.has_ep:
                    res = a1.residue.idx
                    if a1.element == 1 or a2.element == 1:
                        if a1.element == 1 and a2.element == 1:
                            distHH[res] = bond.bond_type.req * u.angstroms
                        if a1.element == 8 or a2.element == 8:
                            distOH[res] = bond.bond_type.req * u.angstroms
                    elif ((a1.element == 8 or a2.element == 8) and
                          (a1.element == 0 or a2.element == 0)):
                        distOE[res] = bond.bond_type.req * u.angstroms
            # Loop over residues and add the virtual points
            out_of_plane_angle = 54.735 * u.degrees
            cosOOP = u.cos(out_of_plane_angle)
            sinOOP = u.sin(out_of_plane_angle)
            for residue in self.residue_list:
                if not residue.has_ep: continue
                res = residue.idx
                if len(waterO[res]) == 1 and len(waterH[res]) == 2:
                    if len(waterEP[res]) == 1:
                        # 4-point water
                        weightH = distOE[res] / sqrt(distOH[res] * distOH[res] -
                                               0.25 * distHH[res] * distHH[res])
                        system.setVirtualSite(
                                waterEP[res][0],
                                mm.ThreeParticleAverageSite(waterO[res][0],
                                waterH[res][0], waterH[res][1],
                                1-weightH, weightH/2, weightH/2)
                        )
                elif len(waterEP[res]) == 2:
                    # 5-point water
                    weightH = (cosOOP * distOE[res] /
                               sqrt(distOH[res] * distOH[res] -
                                 0.25 * distHH[res] * distHH[res])
                    )
                    angleHOH = 2 * asin(0.5 * distHH[res] / distOH[res])
                    lenCross = distOH[res] * distOH[res] * sin(angleHOH)
                    weightCross = sinOOP * distOE[res] / lenCross
                    site1 = mm.OutOfPlaneSite(waterO[res][0], waterH[res][0],
                            waterH[res][1], weightH/2, weightH/2, weightCross)
                    site2 = mm.OutOfPlaneSite(waterO[res][0], waterH[res][0],
                            waterH[res][1], weightH/2, weightH/2, -weightCross)
                    system.setVirtualSite(waterEP[res][0], site1)
                    system.setVirtualSite(waterEP[res][1], site2)

        # Add GB model if we're doing one
        if implicitSolvent is not None:
            if verbose: print('Adding GB parameters...')
            gb_parms = self._get_gb_params(implicitSolvent)

            # If implicitSolventKappa is None, compute it from salt
            # concentration
            if implicitSolventKappa is None:
                if u.is_quantity(implicitSolventSaltConc):
                    sc = implicitSolventSaltConc.value_in_unit(u.moles/u.liter)
                    implicitSolventSaltConc = sc
                if u.is_quantity(temperature):
                    temperature = temperature.value_in_unit(u.kelvin)
                # The constant is 1 / sqrt( epsilon_0 * kB / (2 * NA * q^2 *
                # 1000) ) where NA is avogadro's number, epsilon_0 is the
                # permittivity of free space, q is the elementary charge (this
                # number matches sander/pmemd's kappa conversion factor)
                implicitSolventKappa = 50.33355 * sqrt(implicitSolventSaltConc /
                                                solventDielectric / temperature)
                # Multiply by 0.73 to account for ion exclusions, and multiply
                # by 10 to convert to 1/nm from 1/angstroms
                implicitSolventKappa *= 7.3
            elif implicitSolvent is None:
                implicitSolventKappa = 0.0

            if u.is_quantity(implicitSolventKappa):
                implicitSolventKappa = implicitSolventKappa.value_in_unit(
                                            (1.0/u.nanometer).unit)
            if implicitSolvent is HCT:
                gb = GBSAHCTForce(solventDielectric, soluteDielectric, None,
                                  cutoff, kappa=implicitSolventKappa)
            elif implicitSolvent is OBC1:
                gb = GBSAOBC1Force(solventDielectric, soluteDielectric, None,
                                   cutoff, kappa=implicitSolventKappa)
            elif implicitSolvent is OBC2:
                gb = GBSAOBC2Force(solventDielectric, soluteDielectric, None,
                                   cutoff, kappa=implicitSolventKappa)
            elif implicitSolvent is GBn:
                gb = GBSAGBnForce(solventDielectric, soluteDielectric, None,
                                  cutoff, kappa=implicitSolventKappa)
            elif implicitSolvent is GBn2:
                gb = GBSAGBn2Force(solventDielectric, soluteDielectric, None,
                                   cutoff, kappa=implicitSolventKappa)
            for i, atom in enumerate(self.atom_list):
                gb.addParticle([atom.charge] + list(gb_parms[i]))
            # Set cutoff method
            if nonbondedMethod is ff.NoCutoff:
                gb.setNonbondedMethod(mm.NonbondedForce.NoCutoff)
            elif nonbondedMethod is ff.CutoffNonPeriodic:
                gb.setNonbondedMethod(mm.NonbondedForce.CutoffNonPeriodic)
                gb.setCutoffDistance(cutoff)
            elif nonbondedMethod is ff.CutoffPeriodic:
                gb.setNonbondedMethod(mm.NonbondedForce.CutoffPeriodic)
                gb.setCutoffDistance(cutoff)
            else:
                raise ValueError('Illegal nonbonded method for use with GBSA')
            gb.setForceGroup(self.GB_FORCE_GROUP)
            system.addForce(gb)
            force.setReactionFieldDielectric(1.0) # applies to NonbondedForce

        # See if we repartition the hydrogen masses
        if hydrogenMass is not None:
            for bond in self.bonds_inc_h:
                atom1, atom2 = bond.atom1, bond.atom2
                if atom1.element == 1:
                    atom1, atom2 = atom2, atom1 # now atom2 is hydrogen for sure
                if atom1.element != 1:
                    transfer_mass = hydrogenMass - atom2.mass
                    new_mass1 = (system.getParticleMass(atom1.index) -
                                 transfer_mass)
                    system.setParticleMass(atom2.index, hydrogenMass)
                    system.setParticleMass(atom1.index, new_mass1)
        # See if we want to remove COM motion
        if removeCMMotion:
            system.addForce(mm.CMMotionRemover())

        # Cache our system for easy access
        self._system = system

        return system

    @property
    def system(self):
        """
        Return the cached system class -- it needs to be initialized via
        "createSystem" first!
        """
        try:
            return self._system
        except AttributeError:
            raise APIError('You must initialize the system with createSystem '
                           'before accessing the cached object.')

    @property
    def positions(self):
        """
        Return the cached positions or create new ones from the atoms
        """
        try:
            if len(self._positions) == len(self.atom_list):
                return self._positions
        except AttributeError:
            pass

        self._positions = tuple([Vec3(a.xx, a.xy, a.xz)
                               for a in self.atom_list]) * u.angstroms
        return self._positions

    @positions.setter
    def positions(self, stuff):
        """
        Update the cached positions and assign the coordinates to the atoms
        """
        self._positions = stuff
        for i, pos in enumerate(stuff.value_in_unit(u.angstroms)):
            i3 = i * 3
            atom = self.atom_list[i]
            atom.xx, atom.xy, atom.xz = pos
            self.coords[i3], self.coords[i3+1], self.coords[i3+2] = pos

    @property
    def velocities(self):
        """ Same as for positions, but for velocities """
        try:
            if len(self._velocities) == len(self.atom_list):
                return self._velocities
        except AttributeError:
            pass

        self._velocities = tuple([Vec3(a.vx, a.vy, a.vz)
                    for a in self.atom_list]) * (u.angstroms/u.picosecond) 
        return self._velocities

    @velocities.setter
    def velocities(self, stuff):
        self._velocities = stuff
        for atom, vel in zip(self.atom_list, stuff):
            atom.vx, atom.vy, atom.vz = vel.value_in_unit(
                    u.angstroms/u.picoseconds)

    @property
    def box_vectors(self):
        """ Return tuple of box vectors """
        if hasattr(self, 'rst7'):
            box = [x*u.angstrom for x in self.rst7.box[:3]]
            ang = [self.rst7.box[3], self.rst7.box[4], self.rst7.box[5]]
            return _box_vectors_from_lengths_angles(box[0], box[1], box[2],
                                                    ang[0], ang[1], ang[2])
        else:
            box = [x*u.angstrom for x in self.parm_data['BOX_DIMENSIONS'][1:]]
            ang = [self.parm_data['BOX_DIMENSIONS'][0]] * 3
            return _box_vectors_from_lengths_angles(box[0], box[1], box[2],
                                                    ang[0], ang[1], ang[2])

    @property
    def box_lengths(self):
        """ Return tuple of 3 units """
        if hasattr(self, 'rst7'):
            box = [x*u.angstrom for x in self.rst7.box[:3]]
        else:
            box = [x*u.angstrom for x in self.parm_data['BOX_DIMENSIONS'][1:]]
        return tuple(box)
示例#7
0
    def __init__(self, file):
        """Load a prmtop file."""
        top = Topology()
        ## The Topology read from the prmtop file
        self.topology = top
        self.elements = []

        # Load the prmtop file

        prmtop = amber_file_parser.PrmtopLoader(file)
        self._prmtop = prmtop

        # Add atoms to the topology

        PDBFile._loadNameReplacementTables()
        lastResidue = None
        c = top.addChain()
        for index in range(prmtop.getNumAtoms()):
            resNumber = prmtop.getResidueNumber(index)
            if resNumber != lastResidue:
                lastResidue = resNumber
                resName = prmtop.getResidueLabel(iAtom=index).strip()
                if resName in PDBFile._residueNameReplacements:
                    resName = PDBFile._residueNameReplacements[resName]
                r = top.addResidue(resName, c)
                if resName in PDBFile._atomNameReplacements:
                    atomReplacements = PDBFile._atomNameReplacements[resName]
                else:
                    atomReplacements = {}
            atomName = prmtop.getAtomName(index).strip()
            if atomName in atomReplacements:
                atomName = atomReplacements[atomName]

            # Get the element from the prmtop file if available
            if prmtop.has_atomic_number:
                try:
                    element = elem.Element.getByAtomicNumber(
                        int(prmtop._raw_data['ATOMIC_NUMBER'][index]))
                except KeyError:
                    element = None
            else:
                # Try to guess the element from the atom name.

                upper = atomName.upper()
                if upper.startswith('CL'):
                    element = elem.chlorine
                elif upper.startswith('NA'):
                    element = elem.sodium
                elif upper.startswith('MG'):
                    element = elem.magnesium
                elif upper.startswith('ZN'):
                    element = elem.zinc
                else:
                    try:
                        element = elem.get_by_symbol(atomName[0])
                    except KeyError:
                        element = None

            top.addAtom(atomName, element, r)
            self.elements.append(element)

        # Add bonds to the topology

        atoms = list(top.atoms())
        for bond in prmtop.getBondsWithH():
            top.addBond(atoms[bond[0]], atoms[bond[1]])
        for bond in prmtop.getBondsNoH():
            top.addBond(atoms[bond[0]], atoms[bond[1]])

        # Set the periodic box size.

        if prmtop.getIfBox():
            top.setUnitCellDimensions(
                tuple(
                    x.value_in_unit(unit.nanometer)
                    for x in prmtop.getBoxBetaAndDimensions()[1:4]) *
                unit.nanometer)
示例#8
0
    def __init__(self, file):
        """Load a prmtop file."""
        top = Topology()
        ## The Topology read from the prmtop file
        self.topology = top
        self.elements = []

        # Load the prmtop file

        prmtop = amber_file_parser.PrmtopLoader(file)
        self._prmtop = prmtop

        # Add atoms to the topology

        PDBFile._loadNameReplacementTables()
        lastResidue = None
        c = top.addChain()
        for index in range(prmtop.getNumAtoms()):
            resNumber = prmtop.getResidueNumber(index)
            if resNumber != lastResidue:
                lastResidue = resNumber
                resName = prmtop.getResidueLabel(iAtom=index).strip()
                if resName in PDBFile._residueNameReplacements:
                    resName = PDBFile._residueNameReplacements[resName]
                r = top.addResidue(resName, c)
                if resName in PDBFile._atomNameReplacements:
                    atomReplacements = PDBFile._atomNameReplacements[resName]
                else:
                    atomReplacements = {}
            atomName = prmtop.getAtomName(index).strip()
            if atomName in atomReplacements:
                atomName = atomReplacements[atomName]

            # Get the element from the prmtop file if available
            if prmtop.has_atomic_number:
                try:
                    element = elem.Element.getByAtomicNumber(int(prmtop._raw_data['ATOMIC_NUMBER'][index]))
                except KeyError:
                    element = None
            else:
                # Try to guess the element from the atom name.

                upper = atomName.upper()
                if upper.startswith('CL'):
                    element = elem.chlorine
                elif upper.startswith('NA'):
                    element = elem.sodium
                elif upper.startswith('MG'):
                    element = elem.magnesium
                elif upper.startswith('ZN'):
                    element = elem.zinc
                else:
                    try:
                        element = elem.get_by_symbol(atomName[0])
                    except KeyError:
                        element = None

            top.addAtom(atomName, element, r)
            self.elements.append(element)

        # Add bonds to the topology

        atoms = list(top.atoms())
        for bond in prmtop.getBondsWithH():
            top.addBond(atoms[bond[0]], atoms[bond[1]])
        for bond in prmtop.getBondsNoH():
            top.addBond(atoms[bond[0]], atoms[bond[1]])

        # Set the periodic box size.

        if prmtop.getIfBox():
            box = prmtop.getBoxBetaAndDimensions()
            top.setPeriodicBoxVectors(computePeriodicBoxVectors(*(box[1:4] + box[0:1]*3)))
示例#9
0
    def remove_molecules(self, simulation_old, system_options, ensemble_options):
        topology_old = simulation_old.topology
        system_old = simulation_old.system
        state = simulation_old.context.getState(getPositions=True, getVelocities=True)
        positions_old = state.getPositions(asNumpy=True)
        velocities_old = state.getVelocities(asNumpy=True)
        periodic_box_vectors = state.getPeriodicBoxVectors(asNumpy=True)

        # randomly determine which molecules are removed
        removed_molecules = random.sample(range(topology_old.getNumChains()), self.numVoid)

        # create new topology and dictionary mapping old atom indices to new atom indices
        removed_atoms = []
        topology_new = Topology()
        old_to_new = {}
        atom_index_new = 0
        for chain_index, chain_old in enumerate(topology_old.chains()):
            if chain_index not in removed_molecules:
                chain_id = chain_old.id.split('-', 1)[-1]
                chain_new = topology_new.addChain(id="{}-{}".format(topology_new.getNumChains()+1, chain_id))
                for residue_old in chain_old.residues():
                    residue_new = topology_new.addResidue(residue_old.name, chain_new)
                    for atom_old in residue_old.atoms():
                        topology_new.addAtom(atom_old.name, atom_old.element, residue_new)
                        old_to_new[atom_old.index] = atom_index_new
                        atom_index_new += 1
            else:
                for atom_old in chain_old.atoms():
                    removed_atoms.append(atom_old.index)

        # add bonds to new topology
        atoms_new = list(topology_new.atoms())
        for bond_old in topology_old.bonds():
            atom1_index_old = bond_old[0].index
            atom2_index_old = bond_old[1].index
            try:
                atom1_new = atoms_new[old_to_new[atom1_index_old]]
                atom2_new = atoms_new[old_to_new[atom2_index_old]]
                topology_new.addBond(atom1_new, atom2_new)
            except KeyError:
                pass

        # set box vectors for topology
        topology_new.setPeriodicBoxVectors(periodic_box_vectors)

        # create system
        system_new = system_options.create_system_with_new_topology(topology_new)

        # check if old system had barostat and add to new system if applicable
        if self._has_barostat(system_old):
            barostat_old = self._get_barostat(system_old)
            defaultPressure = barostat_old.getDefaultPressure()
            defaultTemperature = barostat_old.getDefaultTemperature()
            frequency = barostat_old.getFrequency()
            if isinstance(barostat_old, MonteCarloAnisotropicBarostat):
                scaleX = barostat_old.getScaleX()
                scaleY = barostat_old.getScaleY()
                scaleZ = barostat_old.getScaleZ()
                barostat_new = MonteCarloAnisotropicBarostat(defaultPressure, defaultTemperature,
                                                             scaleX, scaleY, scaleZ, frequency)
            else:
                barostat_new = MonteCarloBarostat(defaultPressure, defaultTemperature, frequency)
            system_new.addForce(barostat_new)
            barostat_new.setForceGroup(system_new.getNumForces() - 1)

        # create integrator
        integrator = ensemble_options.create_integrator()

        # create new positions and velocities arrays
        positions_new = np.delete(positions_old, removed_atoms, axis=0)
        velocities_new = np.delete(velocities_old, removed_atoms, axis=0)

        # create new simulation
        simulation_new = Simulation(topology_new, system_new, integrator)
        simulation_new.context.setPositions(positions_new)
        simulation_new.context.setVelocities(velocities_new)
        simulation_new.context.setPeriodicBoxVectors(*periodic_box_vectors)

        # move reporters from old simulation to new simulation
        while simulation_old.reporters:
            simulation_new.reporters.append(simulation_old.reporters.pop(0))

        # create pdb file if specified
        if self.file is not None:
            PDBFile.writeFile(topology_new, positions_new, open(self.file, 'w'))

        return simulation_new
示例#10
0
    def addHydrogens(self, forcefield, pH=7.0, variants=None, platform=None):
        """Add missing hydrogens to the model.

        Some residues can exist in multiple forms depending on the pH and properties of the local environment.  These
        variants differ in the presence or absence of particular hydrogens.  In particular, the following variants
        are supported:

        Aspartic acid:
            ASH: Neutral form with a hydrogen on one of the delta oxygens
            ASP: Negatively charged form without a hydrogen on either delta oxygen

        Cysteine:
            CYS: Neutral form with a hydrogen on the sulfur
            CYX: No hydrogen on the sulfur (either negatively charged, or part of a disulfide bond)

        Glutamic acid:
            GLH: Neutral form with a hydrogen on one of the epsilon oxygens
            GLU: Negatively charged form without a hydrogen on either epsilon oxygen

        Histidine:
            HID: Neutral form with a hydrogen on the ND1 atom
            HIE: Neutral form with a hydrogen on the NE2 atom
            HIP: Positively charged form with hydrogens on both ND1 and NE2

        Lysine:
            LYN: Neutral form with two hydrogens on the zeta nitrogen
            LYS: Positively charged form with three hydrogens on the zeta nitrogen

        The variant to use for each residue is determined by the following rules:

        1. The most common variant at the specified pH is selected.
        2. Any Cysteine that participates in a disulfide bond uses the CYX variant regardless of pH.
        3. For a neutral Histidine residue, the HID or HIE variant is selected based on which one forms a better hydrogen bond.

        You can override these rules by explicitly specifying a variant for any residue.  Also keep in mind that this
        function will only add hydrogens.  It will never remove ones that are already present in the model, regardless
        of the specified pH.

        Definitions for standard amino acids and nucleotides are built in.  You can call loadHydrogenDefinitions() to load
        additional definitions for other residue types.

        Parameters:
         - forcefield (ForceField) the ForceField to use for determining the positions of hydrogens
         - pH (float=7.0) the pH based on which to select variants
         - variants (list=None) an optional list of variants to use.  If this is specified, its length must equal the number
           of residues in the model.  variants[i] is the name of the variant to use for residue i (indexed starting at 0).
           If an element is None, the standard rules will be followed to select a variant for that residue.
         - platform (Platform=None) the Platform to use when computing the hydrogen atom positions.  If this is None,
           the default Platform will be used.
        Returns: a list of what variant was actually selected for each residue, in the same format as the variants parameter
        """
        # Check the list of variants.

        residues = list(self.topology.residues())
        if variants is not None:
            if len(variants) != len(residues):
                raise ValueError("The length of the variants list must equal the number of residues")
        else:
            variants = [None]*len(residues)
        actualVariants = [None]*len(residues)

        # Load the residue specifications.

        if not Modeller._hasLoadedStandardHydrogens:
            Modeller.loadHydrogenDefinitions(os.path.join(os.path.dirname(__file__), 'data', 'hydrogens.xml'))

        # Make a list of atoms bonded to each atom.

        bonded = {}
        for atom in self.topology.atoms():
            bonded[atom] = []
        for atom1, atom2 in self.topology.bonds():
            bonded[atom1].append(atom2)
            bonded[atom2].append(atom1)

        # Define a function that decides whether a set of atoms form a hydrogen bond, using fairly tolerant criteria.

        def isHbond(d, h, a):
            if norm(d-a) > 0.35*nanometer:
                return False
            deltaDH = h-d
            deltaHA = a-h
            deltaDH /= norm(deltaDH)
            deltaHA /= norm(deltaHA)
            return acos(dot(deltaDH, deltaHA)) < 50*degree

        # Loop over residues.

        newTopology = Topology()
        newTopology.setUnitCellDimensions(deepcopy(self.topology.getUnitCellDimensions()))
        newAtoms = {}
        newPositions = []*nanometer
        newIndices = []
        acceptors = [atom for atom in self.topology.atoms() if atom.element in (elem.oxygen, elem.nitrogen)]
        for chain in self.topology.chains():
            newChain = newTopology.addChain()
            for residue in chain.residues():
                newResidue = newTopology.addResidue(residue.name, newChain)
                isNTerminal = (residue == chain._residues[0])
                isCTerminal = (residue == chain._residues[-1])
                if residue.name in Modeller._residueHydrogens:
                    # Add hydrogens.  First select which variant to use.

                    spec = Modeller._residueHydrogens[residue.name]
                    variant = variants[residue.index]
                    if variant is None:
                        if residue.name == 'CYS':
                            # If this is part of a disulfide, use CYX.

                            sulfur = [atom for atom in residue.atoms() if atom.element == elem.sulfur]
                            if len(sulfur) == 1 and any((atom.residue != residue for atom in bonded[sulfur[0]])):
                                variant = 'CYX'
                        if residue.name == 'HIS' and pH > 6.5:
                            # See if either nitrogen already has a hydrogen attached.

                            nd1 = [atom for atom in residue.atoms() if atom.name == 'ND1']
                            ne2 = [atom for atom in residue.atoms() if atom.name == 'NE2']
                            if len(nd1) != 1 or len(ne2) != 1:
                                raise ValueError('HIS residue (%d) has the wrong set of atoms' % residue.index)
                            nd1 = nd1[0]
                            ne2 = ne2[0]
                            nd1HasHydrogen = any((atom.element == elem.hydrogen for atom in bonded[nd1]))
                            ne2HasHydrogen = any((atom.element == elem.hydrogen for atom in bonded[ne2]))
                            if nd1HasHydrogen and ne2HasHydrogen:
                                variant = 'HIP'
                            elif nd1HasHydrogen:
                                variant = 'HID'
                            elif ne2HasHydrogen:
                                variant = 'HIE'
                            else:
                                # Estimate the hydrogen positions.

                                nd1Pos = self.positions[nd1.index]
                                ne2Pos = self.positions[ne2.index]
                                hd1Delta = Vec3(0, 0, 0)*nanometer
                                for other in bonded[nd1]:
                                    hd1Delta += nd1Pos-self.positions[other.index]
                                hd1Delta *= 0.1*nanometer/norm(hd1Delta)
                                hd1Pos = nd1Pos+hd1Delta
                                he2Delta = Vec3(0, 0, 0)*nanometer
                                for other in bonded[ne2]:
                                    he2Delta += ne2Pos-self.positions[other.index]
                                he2Delta *= 0.1*nanometer/norm(he2Delta)
                                he2Pos = ne2Pos+he2Delta

                                # See whether either hydrogen would form a hydrogen bond.

                                nd1IsBonded = False
                                ne2IsBonded = False
                                for acceptor in acceptors:
                                    if acceptor.residue != residue:
                                        acceptorPos = self.positions[acceptor.index]
                                        if isHbond(nd1Pos, hd1Pos, acceptorPos):
                                            nd1IsBonded = True
                                            break
                                        if isHbond(ne2Pos, he2Pos, acceptorPos):
                                            ne2IsBonded = True
                                if ne2IsBonded and not nd1IsBonded:
                                    variant = 'HIE'
                                else:
                                    variant = 'HID'
                        elif residue.name == 'HIS':
                            variant = 'HIP'
                    if variant is not None and variant not in spec.variants:
                        raise ValueError('Illegal variant for %s residue: %s' % (residue.name, variant))
                    actualVariants[residue.index] = variant

                    # Make a list of hydrogens that should be present in the residue.

                    parents = [atom for atom in residue.atoms() if atom.element != elem.hydrogen]
                    parentNames = [atom.name for atom in parents]
                    hydrogens = [h for h in spec.hydrogens if (variant is None and pH <= h.maxph) or (h.variants is None and pH <= h.maxph) or (h.variants is not None and variant in h.variants)]
                    hydrogens = [h for h in hydrogens if h.terminal is None or (isNTerminal and h.terminal == 'N') or (isCTerminal and h.terminal == 'C')]
                    hydrogens = [h for h in hydrogens if h.parent in parentNames]

                    # Loop over atoms in the residue, adding them to the new topology along with required hydrogens.

                    for parent in residue.atoms():
                        # Add the atom.

                        newAtom = newTopology.addAtom(parent.name, parent.element, newResidue)
                        newAtoms[parent] = newAtom
                        newPositions.append(deepcopy(self.positions[parent.index]))
                        if parent in parents:
                            # Match expected hydrogens with existing ones and find which ones need to be added.

                            existing = [atom for atom in bonded[parent] if atom.element == elem.hydrogen]
                            expected = [h for h in hydrogens if h.parent == parent.name]
                            if len(existing) < len(expected):
                                # Try to match up existing hydrogens to expected ones.

                                matches = []
                                for e in existing:
                                    match = [h for h in expected if h.name == e.name]
                                    if len(match) > 0:
                                        matches.append(match[0])
                                        expected.remove(match[0])
                                    else:
                                        matches.append(None)

                                # If any hydrogens couldn't be matched by name, just match them arbitrarily.

                                for i in range(len(matches)):
                                    if matches[i] is None:
                                        matches[i] = expected[-1]
                                        expected.remove(expected[-1])

                                # Add the missing hydrogens.

                                for h in expected:
                                    newH = newTopology.addAtom(h.name, elem.hydrogen, newResidue)
                                    newIndices.append(newH.index)
                                    delta = Vec3(0, 0, 0)*nanometer
                                    if len(bonded[parent]) > 0:
                                        for other in bonded[parent]:
                                            delta += self.positions[parent.index]-self.positions[other.index]
                                    else:
                                        delta = Vec3(random.random(), random.random(), random.random())*nanometer
                                    delta *= 0.1*nanometer/norm(delta)
                                    delta += 0.05*Vec3(random.random(), random.random(), random.random())*nanometer
                                    delta *= 0.1*nanometer/norm(delta)
                                    newPositions.append(self.positions[parent.index]+delta)
                                    newTopology.addBond(newAtom, newH)
                else:
                    # Just copy over the residue.

                    for atom in residue.atoms():
                        newAtom = newTopology.addAtom(atom.name, atom.element, newResidue)
                        newAtoms[atom] = newAtom
                        newPositions.append(deepcopy(self.positions[atom.index]))
        for bond in self.topology.bonds():
            if bond[0] in newAtoms and bond[1] in newAtoms:
                newTopology.addBond(newAtoms[bond[0]], newAtoms[bond[1]])

        # The hydrogens were added at random positions.  Now use the ForceField to fix them up.

        system = forcefield.createSystem(newTopology, rigidWater=False)
        atoms = list(newTopology.atoms())
        for i in range(system.getNumParticles()):
            if atoms[i].element != elem.hydrogen:
                # This is a heavy atom, so make it immobile.
                system.setParticleMass(i, 0)
        if platform is None:
            context = Context(system, VerletIntegrator(0.0))
        else:
            context = Context(system, VerletIntegrator(0.0), platform)
        context.setPositions(newPositions)
        LocalEnergyMinimizer.minimize(context)
        self.topology = newTopology
        self.positions = context.getState(getPositions=True).getPositions()
        return actualVariants