Ejemplo n.º 1
0
def grosberg_polymer_bonds(sim_object, bonds, k=30, name="grosberg_polymer"):
    """Adds FENE bonds according to Halverson-Grosberg paper.
    (Halverson, Jonathan D., et al. "Molecular dynamics simulation study of
     nonconcatenated ring polymers in a melt. I. Statics."
     The Journal of chemical physics 134 (2011): 204904.)

    This method has a repulsive potential build-in,
    so that Grosberg bonds could be used with truncated potentials.
    Is of no use unless you really need to simulate Grosberg-type system.

    Parameters
    ----------
    k : float, optional
        Arbitrary parameter; default value as in Grosberg paper.

     """

    equation = "- 0.5 * k * r0 * r0 * log(1-(r/r0)* (r / r0))"
    force = openmm.CustomBondForce(equation)
    force.name = name

    force.addGlobalParameter(
        "k", k * sim_object.kT / (sim_object.conlen * sim_object.conlen))
    force.addGlobalParameter("r0", sim_object.conlen * 1.5)

    for bond_idx, (i, j) in enumerate(bonds):
        if (i >= sim_object.N) or (j >= sim_object.N):
            raise ValueError("\nCannot add bond with monomers %d,%d that"
                             "are beyound the polymer length %d" %
                             (i, j, sim_object.N))

        force.addBond(int(i), int(j))

    return force
Ejemplo n.º 2
0
def applyLigandChunkRestraint(system, k2, k3, R2, R3, R4,
                              indexOfAffectedAtoms):

    forceConstant_k2 = u.Quantity(value=k2,
                                  unit=u.kilocalorie /
                                  (u.mole * u.angstrom * u.angstrom))
    forceConstant_k3 = u.Quantity(value=k3,
                                  unit=u.kilocalorie /
                                  (u.mole * u.angstrom * u.angstrom))

    restraint_force = mm.CustomBondForce(
        'step(R2 - r) * f1 + step(r - R3) * select( step(r - R4), f3, f2);'
        'f1 = k2 * (r - R2)^2;'
        'f2 = k3 * (r - R3)^2;'
        'f3 = k3 * (R4 - R3) * (2 * r - R4 - R3)')

    restraint_force.addGlobalParameter(
        "k2",
        forceConstant_k2.in_units_of(u.kilojoule /
                                     (u.mole * u.nanometer * u.nanometer)))
    restraint_force.addGlobalParameter(
        "k3",
        forceConstant_k3.in_units_of(u.kilojoule /
                                     (u.mole * u.nanometer * u.nanometer)))

    restraint_force.addGlobalParameter("R2", R2)
    restraint_force.addGlobalParameter("R3", R3)
    restraint_force.addGlobalParameter("R4", R4)

    restraint_force.addBond(indexOfAffectedAtoms[0], indexOfAffectedAtoms[1])

    system.addForce(restraint_force)
Ejemplo n.º 3
0
 def test_restorable_openmm_object_failure(self):
     """An exception is raised if the class has a restorable hash but the class can't be found."""
     force = openmm.CustomBondForce('0.0')
     force_hash_parameter_name = self.dummy_force._hash_parameter_name
     force.addGlobalParameter(force_hash_parameter_name, 15.0)
     with nose.tools.assert_raises(RestorableOpenMMObjectError):
         RestorableOpenMMObject.restore_interface(force)
Ejemplo n.º 4
0
    def _create_restraint_force(self, particle1, particle2):
        """
        Create a new restraint force between specified atoms.

        Parameters
        ----------
        particle1 : int
           Index of first atom in restraint
        particle2 : int
           Index of second atom in restraint

        Returns
        -------
        force : simtk.openmm.CustomBondForce
           The created restraint force.

        """
        force = openmm.CustomBondForce(self._energy_function)
        force.addGlobalParameter('lambda_restraints', 1.0)
        is_periodic = self._system.usesPeriodicBoundaryConditions()
        try:  # This was added in OpenMM 7.1
            force.setUsesPeriodicBoundaryConditions(is_periodic)
        except AttributeError:
            pass
        for parameter in self._bond_parameter_names:
            force.addPerBondParameter(parameter)
        try:
            force.addBond(particle1, particle2, self._bond_parameters)
        except Exception as e:
            print('particle1: %s' % str(particle1))
            print('particle2: %s' % str(particle1))
            print('bond_parameters: %s' % str(self._bond_parameters))
            raise(e)
        return force
Ejemplo n.º 5
0
    def _createRestraintForce(self, particle1, particle2, mm=None):
        """
        Create a new copy of the receptor-ligand restraint force.

        Parameters
        ----------
        particle1 : int
           Index of first particle for which restraint is to be applied.
        particle2 : int
           Index of second particle for which restraint is to be applied
        mm : simtk.openmm compliant interface, optional, default=None
           If specified, use an alternative OpenMM API implementation.
           Otherwise, use simtk.openmm.

        Returns
        -------
        force : simtk.openmm.CustomBondForce
           A restraint force object

        """

        if mm is None: mm = openmm

        force = openmm.CustomBondForce(self.energy_function)
        force.addGlobalParameter('lambda_restraints', 1.0)
        for parameter in self.bond_parameter_names:
            force.addPerBondParameter(parameter)
        force.addBond(particle1, particle2, self.bond_parameters)

        return force
Ejemplo n.º 6
0
    def test_restorable_openmm_object(self):
        """Test RestorableOpenMMObject classes can be serialized and copied correctly."""

        # Each test case is a pair (object, is_restorable).
        test_cases = [(copy.deepcopy(self.dummy_force), True),
                      (copy.deepcopy(self.dummy_integrator), True),
                      (openmm.CustomBondForce('K'), False)]

        for openmm_object, is_restorable in test_cases:
            assert RestorableOpenMMObject.is_restorable(
                openmm_object) is is_restorable
            err_msg = '{}: {}, {}'.format(
                openmm_object,
                RestorableOpenMMObject.restore_interface(openmm_object),
                is_restorable)
            assert RestorableOpenMMObject.restore_interface(
                openmm_object) is is_restorable, err_msg

            # Serializing/deserializing restore the class correctly.
            serialization = openmm.XmlSerializer.serialize(openmm_object)
            deserialized_object = RestorableOpenMMObject.deserialize_xml(
                serialization)
            if is_restorable:
                assert type(deserialized_object) is type(openmm_object)

            # Copying keep the Python class and attributes.
            deserialized_object._monkey_patching = True
            copied_object = copy.deepcopy(deserialized_object)
            if is_restorable:
                assert type(copied_object) is type(openmm_object)
                assert hasattr(copied_object, '_monkey_patching')
def addLigandBox(topology, positions, system, resname, dummy, radius, worker):
    masses = []
    coords = np.ndarray(shape=(0, 3))
    ligand_atoms = []
    for atom in topology.atoms():
        if atom.residue.name == resname and atom.element.symbol != "H":
            masses.append(atom.element.mass.value_in_unit(unit=unit.dalton))
            coords = np.vstack(
                (coords,
                 positions[atom.index].value_in_unit(unit=unit.nanometer)))
            ligand_atoms.append(atom)
    masses = np.array(masses)
    masses /= masses.sum()
    mass_center = coords.astype('float64').T.dot(masses)
    atomClosestToMassCenter = min(
        ligand_atoms,
        key=lambda x: np.linalg.norm(mass_center - positions[x.index].
                                     value_in_unit(unit=unit.nanometer)))
    if worker == 0:
        utilities.print_unbuffered(
            "Ligand atom selected to check distance to the box",
            atomClosestToMassCenter.residue.name, atomClosestToMassCenter.name,
            atomClosestToMassCenter.index)
    ligand_atom = atomClosestToMassCenter.index
    forceFB = mm.CustomBondForce('step(r-r0)*(k_box/2) * (r-r0)^2')
    forceFB.addPerBondParameter("k_box")
    forceFB.addPerBondParameter("r0")
    forceFB.addBond(dummy, ligand_atom, [
        5.0 * unit.kilocalories_per_mole / unit.angstroms**2,
        radius * unit.angstroms
    ])
    system.addForce(forceFB)
Ejemplo n.º 8
0
def FENE_bonds(
    sim_object,
    bonds,
    bondWiggleDistance=0.05,
    bondLength=1.0,
    name="FENE_bonds",
    override_checks=False,
):
    """Adds harmonic bonds

    Parameters
    ----------
    
    bonds : iterable of (int, int)
        Pairs of particle indices to be connected with a bond.
    bondWiggleDistance : float
        Average displacement from the equilibrium bond distance.
        Can be provided per-particle.
    bondLength : float
        The length of the bond.
        Can be provided per-particle.
    override_checks: bool
        If True then do not check that no bonds are repeated.
        False by default.
    """

    # check for repeated bonds
    if not override_checks:
        _check_bonds(bonds, sim_object.N)

    energy = (f"(1. / wiggle) * univK * "
              f"(sqrt((r-r0 * conlen)* "
              f" (r - r0 * conlen) + a * a) - a)")
    force = openmm.CustomBondForce(energy)
    force.name = name

    force.addPerBondParameter("wiggle")
    force.addPerBondParameter("r0")
    force.addGlobalParameter("univK", sim_object.kT / sim_object.conlen)
    force.addGlobalParameter("a", 0.02 * sim_object.conlen)
    force.addGlobalParameter("conlen", sim_object.conlen)

    bondLength = _to_array_1d(bondLength, len(bonds)) * sim_object.length_scale
    bondWiggleDistance = (_to_array_1d(bondWiggleDistance, len(bonds)) *
                          sim_object.length_scale)

    for bond_idx, (i, j) in enumerate(bonds):
        if (i >= sim_object.N) or (j >= sim_object.N):
            raise ValueError("\nCannot add bond with monomers %d,%d that"
                             "are beyound the polymer length %d" %
                             (i, j, sim_object.N))

        force.addBond(
            int(i),
            int(j),
            [float(bondWiggleDistance[bond_idx]),
             float(bondLength[bond_idx])],
        )

    return force
Ejemplo n.º 9
0
def add_harmonic_restraints(system: mm.System, args: ListOfArgs):
    """Restraints format is different here than in SM webservice.
    Example record: :10 :151\n
    or
    :10 :151 0.1 30000\n
    :i :j distance energy
    """
    print("      Adding harmonic restraints")
    if args.HR_USE_FLAT_BOTTOM_FORCE:
        contact_force = mm.CustomBondForce('step(r-r0) * (k/2) * (r-r0)^2')
        contact_force.addPerBondParameter('r0')
        contact_force.addPerBondParameter('k')
    else:
        contact_force = mm.HarmonicBondForce()
    system.addForce(contact_force)

    with open(args.HR_RESTRAINTS_PATH) as input_file:
        counter = 0
        for line in input_file:
            columns = line.split()
            atom_index_i = int(columns[0][1:]) - 1
            atom_index_j = int(columns[1][1:]) - 1
            try:
                r0 = float(columns[2])
                k = float(columns[3]) * args.HR_K_SCALE
            except IndexError:
                r0 = args.HR_R0_PARAM
                k = args.HR_K_PARAM
            if args.HR_USE_FLAT_BOTTOM_FORCE:
                contact_force.addBond(atom_index_i, atom_index_j, [r0, k])
            else:
                contact_force.addBond(atom_index_i, atom_index_j, r0, k)
            counter += 1
    print(f"         {counter} restraints added.")
Ejemplo n.º 10
0
 def __init__(self, group):
     RgSq = openmm.CustomBondForce('r^2')
     RgSq.setUsesPeriodicBoundaryConditions(False)
     for i, j in itertools.combinations(group, 2):
         RgSq.addBond(i, j)
     super().__init__(f'sqrt(RgSq)/{len(group)}')
     self.addCollectiveVariable('RgSq', RgSq)
Ejemplo n.º 11
0
    def test_parsing(self, ethane_system_topology):
        custom_bond = openmm.CustomBondForce('a+b*r')
        custom_bond.addPerBondParameter('a')
        custom_bond.addPerBondParameter('b')
        custom_bond.addBond(0, 1, (10, 20))
        my_ommp = Ommperator(ethane_system_topology[0], ethane_system_topology[1])
        my_bond_ommp = CustomBondForceOmmperator(my_ommp, custom_bond, 0)

        assert my_bond_ommp.particle1 == custom_bond.getBondParameters(0)[0]
        assert my_bond_ommp.particle2 == custom_bond.getBondParameters(0)[1]
        assert my_bond_ommp.parameters == custom_bond.getBondParameters(0)[2]
Ejemplo n.º 12
0
def _get_internal_from_omm(atom_coords, bond_coords, angle_coords, torsion_coords):
    import copy
    #master system, will be used for all three
    sys = openmm.System()
    platform = openmm.Platform.getPlatformByName("Reference")
    for i in range(4):
        sys.addParticle(1.0*unit.amu)

    #first, the bond length:
    bond_sys = openmm.System()
    bond_sys.addParticle(1.0*unit.amu)
    bond_sys.addParticle(1.0*unit.amu)
    bond_force = openmm.CustomBondForce("r")
    bond_force.addBond(0, 1, [])
    bond_sys.addForce(bond_force)
    bond_integrator = openmm.VerletIntegrator(1*unit.femtoseconds)
    bond_context = openmm.Context(bond_sys, bond_integrator, platform)
    bond_context.setPositions([atom_coords, bond_coords])
    bond_state = bond_context.getState(getEnergy=True)
    r = bond_state.getPotentialEnergy()
    del bond_sys, bond_context, bond_integrator

    #now, the angle:
    angle_sys = copy.deepcopy(sys)
    angle_force = openmm.CustomAngleForce("theta")
    angle_force.addAngle(0,1,2,[])
    angle_sys.addForce(angle_force)
    angle_integrator = openmm.VerletIntegrator(1*unit.femtoseconds)
    angle_context = openmm.Context(angle_sys, angle_integrator, platform)
    angle_context.setPositions([atom_coords, bond_coords, angle_coords, torsion_coords])
    angle_state = angle_context.getState(getEnergy=True)
    theta = angle_state.getPotentialEnergy()
    del angle_sys, angle_context, angle_integrator

    #finally, the torsion:
    torsion_sys = copy.deepcopy(sys)
    torsion_force = openmm.CustomTorsionForce("theta")
    torsion_force.addTorsion(0,1,2,3,[])
    torsion_sys.addForce(torsion_force)
    torsion_integrator = openmm.VerletIntegrator(1*unit.femtoseconds)
    torsion_context = openmm.Context(torsion_sys, torsion_integrator, platform)
    torsion_context.setPositions([atom_coords, bond_coords, angle_coords, torsion_coords])
    torsion_state = torsion_context.getState(getEnergy=True)
    phi = torsion_state.getPotentialEnergy()
    del torsion_sys, torsion_context, torsion_integrator

    return r, theta, phi
Ejemplo n.º 13
0
    def _add_bond_force_terms(self):
        core_energy_expression = '(K/2)*(r-length)^2;'
        core_energy_expression += 'K = k*scale_factor;'  # linearly interpolate spring constant
        core_energy_expression += self.scaling_expression()

        # Create the force and add the relevant parameters
        custom_core_force = openmm.CustomBondForce(core_energy_expression)
        custom_core_force.addPerBondParameter('length')
        custom_core_force.addPerBondParameter('k')
        custom_core_force.addPerBondParameter('identifier')

        custom_core_force.addGlobalParameter('solute_scale', 1.0)
        custom_core_force.addGlobalParameter('inter_scale', 1.0)

        self._out_system.addForce(custom_core_force)
        self._out_system_forces[
            custom_core_force.__class__.__name__] = custom_core_force
Ejemplo n.º 14
0
def WCADimerVacuum(mm=None,
                   mass=mass,
                   epsilon=epsilon,
                   sigma=sigma,
                   h=h,
                   r0=r0,
                   w=w):
    """
    Create a bistable dimer.

    OPTIONAL ARGUMENTS

    """

    # Choose OpenMM package.
    if mm is None:
        mm = openmm

    # Create system
    system = mm.System()

    # Add particles to system.
    for n in range(2):
        system.addParticle(mass)

    # Add dimer potential to first two particles.
    dimer_force = openmm.CustomBondForce('h*(1-((r-r0-w)/w)^2)^2;')
    dimer_force.addGlobalParameter('h', h)  # barrier height
    dimer_force.addGlobalParameter('r0', r0)  # compact state separation
    dimer_force.addGlobalParameter('w', w)  # second minimum is at r0 + 2*w
    dimer_force.addBond(0, 1, [])
    system.addForce(dimer_force)

    # Create initial coordinates using random positions.
    coordinates = units.Quantity(numpy.zeros([2, 3], numpy.float64),
                                 units.nanometer)

    # Reposition dimer particles at compact minimum.
    coordinates[0, :] *= 0.0
    coordinates[1, :] *= 0.0
    coordinates[1, 0] = r0

    # Return system and coordinates.
    return [system, coordinates]
Ejemplo n.º 15
0
    def _createRestraintForce(self, particle1, particle2, mm=None):
        """
        Create a new copy of the receptor-ligand restraint force.

        RETURNS

        force (simtk.openmm.CustomBondForce) - a restraint force object

        """

        if mm is None: mm = openmm

        force = openmm.CustomBondForce(self.energy_function)
        force.addGlobalParameter('restraint_lambda', 1.0)
        for parameter in self.bond_parameter_names:
            force.addPerBondParameter(parameter)
        force.addBond(particle1, particle2, self.bond_parameters)

        return force
Ejemplo n.º 16
0
    def modify_harmonic_bonds(self):
        """
        turn the harmonic bonds into a custom bond force

        lambda_protocol :
            - 'lambda_MM_bonds' : 1 -> 0
            - 'lambda_scale' : beta / beta0
        """
        self._alchemical_to_old_bonds = {}
        bond_expression = 'lambda_MM_bonds * lambda_scale * (k/2)*(r-r0)^2;'
        custom_bond_force = openmm.CustomBondForce(bond_expression)

        #add the global params
        custom_bond_force.addGlobalParameter('lambda_MM_bonds', 1.)
        custom_bond_force.addGlobalParameter('lambda_scale', 1.)

        #add the perbondparams
        custom_bond_force.addPerBondParameter('r0')
        custom_bond_force.addPerBondParameter('k')

        #now to iterate over the bonds.
        for idx in range(self._system_forces['HarmonicBondForce'].getNumBonds()):
            p1, p2, length, k = self._system_forces['HarmonicBondForce'].getBondParameters(idx)
            if self.is_in_alchemical_region({p1, p2}): #then this bond is in the transforming residue

                #first thing to do is to zero the force from the `HarmonicBondForce`
                self._system_forces['HarmonicBondForce'].setBondParameters(idx, p1, p2, length, k*0.0)
                self._endstate_system_forces['HarmonicBondForce'].setBondParameters(idx, p1, p2, length, k*0.0) #for bookkeeping

                #then add it to the custom bond force
                custom_bond_idx = custom_bond_force.addBond(p1, p2, [length, k])

                #add to the alchemical bonds dict for bookkeeping
                self._alchemical_to_old_bonds[custom_bond_idx] = idx

        #then add the custom bond force to the system

        if self._system_forces['HarmonicBondForce'].usesPeriodicBoundaryConditions():
            custom_bond_force.setUsesPeriodicBoundaryConditions(True)

        self._system.addForce(custom_bond_force)
Ejemplo n.º 17
0
    def get_custom_bond_force(self):
        """
        make a custom bond force object
        """
        from openmmtools.constants import ONE_4PI_EPS0 # constant for coulomb (implicitly in md_unit_system units)

        custom_expression = "lambda_scale * (U_electrostatics + U_sterics);" #name the energy contributions
        custom_expression += "U_sterics = 4*epsilon*x*(x-1.0); x = (sigma/reff_sterics)^6;" #name sterics expression

        #custom_expression += "reff_sterics = r;"
        custom_expression += "reff_sterics = sigma*((softcore_alpha_sterics * lambda_nonbonded_MM_sterics + (r/sigma)^6))^(1/6);" # effective softcore distance for sterics
        custom_expression += "epsilon = (1 - lambda_nonbonded_MM_sterics) * epsilonA + lambda_nonbonded_MM_sterics * epsilonB;"
        custom_expression += "sigma = (1 - lambda_nonbonded_MM_sterics) * sigmaA + lambda_nonbonded_MM_sterics * sigmaB;"

        custom_expression += "U_electrostatics = chargeProd * ONE_4PI_EPS0 / reff_electrostatics;"
        custom_expression += f"ONE_4PI_EPS0 = {ONE_4PI_EPS0};"
        #custom_expression += f"reff_electrostatics = r;"
        custom_expression += "reff_electrostatics = ((softcore_alpha_electrostatics * lambda_nonbonded_MM_electrostatics + (r)^6))^(1/6);"
        custom_expression += "chargeProd = (1 - lambda_nonbonded_MM_electrostatics) * chargeProdA + lambda_nonbonded_MM_electrostatics * chargeProdB;"

        custom_expression += f"softcore_alpha_sterics = {self._softcore_alpha_sterics};"
        custom_expression += f"softcore_alpha_electrostatics = {self._softcore_alpha_electrostatics};"

        custom_bond_force = openmm.CustomBondForce(custom_expression)

        global_params = ['lambda_scale','lambda_nonbonded_MM_sterics', 'lambda_nonbonded_MM_electrostatics']
        per_bond_params = ['epsilonA', 'epsilonB', 'sigmaA', 'sigmaB', 'chargeProdA', 'chargeProdB']

        for global_param in global_params:
            if global_param == 'lambda_scale': # 'lambda_scale' is the exception; it defaults to 0.0
                custom_bond_force.addGlobalParameter(global_param, 1.)
            else:
                custom_bond_force.addGlobalParameter(global_param, 0.)

        for per_bond_param in per_bond_params:
            custom_bond_force.addPerBondParameter(per_bond_param)

        self._system.addForce(custom_bond_force)

        return custom_bond_force
Ejemplo n.º 18
0
def createUnfoldedSurrogate3(topology,
                             reference_system,
                             locality=5,
                             cutoff=6.0 * unit.angstrom,
                             switch_width=1.0 * unit.angstrom):
    # Create system deep copy.
    system = copy.deepcopy(reference_system)

    # Modify forces as appropriate, copying other forces without modification.
    forces_to_remove = list()
    nforces = reference_system.getNumForces()
    for force_index in range(nforces):
        reference_force = reference_system.getForce(force_index)
        force_name = reference_force.__class__.__name__
        print force_name

        if force_name == 'NonbondedForce':
            forces_to_remove.append(force_index)

            # Create CustomNonbondedForce instead.
            energy_expression = "islocal * (E_LJ + E_Coulomb);"
            energy_expression += "E_LJ = 4*epsilon*((sigma/r)^12 - (sigma/r)^6);"
            energy_expression += "E_Coulomb = 138.935456*chargeprod/r;"
            energy_expression += "islocal = step(locality - abs(resid1 - resid2) + 0.001);"
            custom_force = openmm.CustomNonbondedForce(energy_expression)
            custom_force.setNonbondedMethod(
                reference_force.getNonbondedMethod())  # TODO: Fix me
            custom_force.setCutoffDistance(reference_force.getCutoffDistance())
            custom_force.setSwitchingDistance(
                reference_force.getSwitchingDistance())
            custom_force.setUseSwitchingFunction(
                reference_force.getUseSwitchingFunction())
            custom_force.addGlobalParameter("locality", locality)
            custom_force.addPerParticleParameter("charge")  # charge product
            custom_force.addPerParticleParameter(
                "sigma")  # Lennard-Jones sigma
            custom_force.addPerParticleParameter(
                "epsilon")  # Lennard-Jones epsilon
            custom_force.addPerParticleParameter("resid")  # residue index
            system.addForce(custom_force)

            custom_force.setCutoffDistance(cutoff)
            custom_force.setNonbondedMethod(custom_force.CutoffNonPeriodic)
            custom_force.setUseSwitchingFunction(True)
            custom_force.setSwitchingDistance(
                custom_force.getCutoffDistance() - switch_width)

            # Create CustomBondForce
            energy_expression = "E_LJ + E_Coulomb;"
            energy_expression += "E_LJ = 4*epsilon*((sigma/r)^12 - (sigma/r)^6);"
            energy_expression += "E_Coulomb = 138.935456*chargeprod/r;"
            custom_bond_force = openmm.CustomBondForce(energy_expression)
            custom_bond_force.addPerBondParameter(
                "chargeprod")  # charge product
            custom_bond_force.addPerBondParameter(
                "sigma")  # Lennard-Jones sigma
            custom_bond_force.addPerBondParameter(
                "epsilon")  # Lennard-Jones epsilon
            system.addForce(custom_bond_force)

            # Add exceptions
            for index in range(reference_force.getNumExceptions()):
                [atom1_index, atom2_index, chargeprod, sigma,
                 epsilon] = reference_force.getExceptionParameters(index)
                custom_force.addExclusion(atom1_index, atom2_index)
                custom_bond_force.addBond(atom1_index, atom2_index,
                                          [chargeprod, sigma, epsilon])

            # Add terms.
            for atom in topology.atoms():
                [charge, sigma,
                 epsilon] = reference_force.getParticleParameters(atom.index)
                custom_force.addParticle(
                    [charge, sigma, epsilon, atom.residue.index])

        elif force_name == 'GBSAOBCForce':
            #forces_to_remove.append(force_index)
            reference_force.setNonbondedMethod(
                reference_force.CutoffNonPeriodic)
            reference_force.setCutoffDistance(cutoff)

    # Remove forces scheduled for removal.
    print("Removing forces:")
    print(forces_to_remove)
    for force_index in forces_to_remove[::-1]:
        system.removeForce(force_index)

    return system
Ejemplo n.º 19
0
def createUnfoldedSurrogate2(topology, reference_system, locality=5):
    # Create system deep copy.
    system = copy.deepcopy(reference_system)

    # Modify forces as appropriate, copying other forces without modification.
    forces_to_remove = list()
    nforces = reference_system.getNumForces()
    for force_index in range(nforces):
        reference_force = reference_system.getForce(force_index)
        force_name = reference_force.__class__.__name__
        print force_name

        if force_name == 'NonbondedForce':
            forces_to_remove.append(force_index)

            # Create CustomBondForce instead.
            energy_expression = "E_LJ + E_Coulomb + E_GB;"
            energy_expression += "E_LJ = 4*epsilon*((sigma/r)^12 - (sigma/r)^6);"
            energy_expression += "E_Coulomb = 138.935456*chargeprod/r;"
            energy_expression += "E_GB = -0.5*(1/epsilon_solute - 1/epsilon_solvent)*chargeprod/f_GB;"
            energy_expression += "f_GB = (r^2 + RiRj*exp(-r/(4*RiRj)))^0.5;"
            energy_expression += "RiRj = 0.25;"
            custom_bond_force = openmm.CustomBondForce(energy_expression)
            custom_bond_force.addPerBondParameter(
                "chargeprod")  # charge product
            custom_bond_force.addPerBondParameter(
                "sigma")  # Lennard-Jones sigma
            custom_bond_force.addPerBondParameter(
                "epsilon")  # Lennard-Jones epsilon
            custom_bond_force.addGlobalParameter("epsilon_solvent", 78.5)
            custom_bond_force.addGlobalParameter("epsilon_solute", 1)
            system.addForce(custom_bond_force)

            # Add exclusions.
            print("Building exclusions...")
            from sets import Set
            exceptions = Set()
            for index in range(reference_force.getNumExceptions()):
                [atom1_index, atom2_index, chargeprod, sigma,
                 epsilon] = reference_force.getExceptionParameters(index)
                custom_bond_force.addBond(atom1_index, atom2_index,
                                          [chargeprod, sigma, epsilon])

                if atom2_index < atom1_index:
                    exceptions.add((atom2_index, atom1_index))
                else:
                    exceptions.add((atom1_index, atom2_index))

            # Add local interactions.
            print("Adding local interactions...")
            for atom1 in topology.atoms():
                [charge1, sigma1,
                 epsilon1] = reference_force.getParticleParameters(atom1.index)
                for atom2 in topology.atoms():
                    if (atom1.index < atom2.index) and (
                            abs(atom1.residue.index - atom2.residue.index) <=
                            locality) and ((atom1.index, atom2.index)
                                           not in exceptions):
                        [charge2, sigma2, epsilon2
                         ] = reference_force.getParticleParameters(atom1.index)
                        chargeprod = charge1 * charge2
                        sigma = 0.5 * (sigma1 + sigma2)
                        epsilon = unit.sqrt(epsilon1 * epsilon2)
                        custom_bond_force.addBond(atom1.index, atom2.index,
                                                  [chargeprod, sigma, epsilon])

            print("%d custom bond terms added." %
                  custom_bond_force.getNumBonds())

        elif force_name == 'GBSAOBCForce':
            forces_to_remove.append(force_index)

    # Remove forces scheduled for removal.
    print("Removing forces:")
    print(forces_to_remove)
    for force_index in forces_to_remove[::-1]:
        system.removeForce(force_index)

    return system
Ejemplo n.º 20
0
def main(args):
  #Get the structure and topology files from the command line
  #ParmEd accepts a wide range of file types (Amber, GROMACS, CHARMM, OpenMM... but not LAMMPS) 
  try:
    topFile = args[0]
    strucFile = args[1]
  except IndexError:
    print("Specify topology and structure files from the command line.")
    sys.exit(2)
  
  print("Using topology file: %s" % topFile)
  print("Using structure file: %s" % strucFile)
  
  print("\nSetting up system...")
  
  #Load in the files for initial simulations
  top = pmd.load_file(topFile)
  struc = pmd.load_file(strucFile)
  
  #Transfer unit cell information to topology object
  top.box = struc.box[:]
  
  #Set up some global features to use in all simulations
  temperature = 298.15*u.kelvin
  
  #Define the platform (i.e. hardware and drivers) to use for running the simulation
  #This can be CUDA, OpenCL, CPU, or Reference 
  #CUDA is for NVIDIA GPUs
  #OpenCL is for CPUs or GPUs, but must be used for old CPUs (not SSE4.1 compatible)
  #CPU only allows single precision (CUDA and OpenCL allow single, mixed, or double)
  #Reference is a clear, stable reference for other code development and is very slow, using double precision by default
  platform = mm.Platform.getPlatformByName('CUDA')
  prop = {#'Threads': '2', #number of threads for CPU - all definitions must be strings (I think)
          'Precision': 'mixed', #for CUDA or OpenCL, select the precision (single, mixed, or double)
          'DeviceIndex': '0', #selects which GPUs to use - set this to zero if using CUDA_VISIBLE_DEVICES
          'DeterministicForces': 'True' #Makes sure forces with CUDA and PME are deterministic
         }
  
  #Create the OpenMM system that can be used as a reference
  systemRef = top.createSystem(
                               nonbondedMethod=app.PME, #Uses PME for long-range electrostatics, simple cut-off for LJ
                               nonbondedCutoff=12.0*u.angstroms, #Defines cut-off for non-bonded interactions
                               rigidWater=True, #Use rigid water molecules
                               constraints=app.HBonds, #Constrains all bonds involving hydrogens
                               flexibleConstraints=False, #Whether to include energies for constrained DOFs
                               removeCMMotion=True, #Whether or not to remove COM motion (don't want to if part of system frozen)
  )

  #Set up the integrator to use as a reference
  integratorRef = mm.LangevinIntegrator(
                                        temperature, #Temperature for Langevin
                                        1.0/u.picoseconds, #Friction coefficient
                                        2.0*u.femtoseconds, #Integration timestep
  )
  integratorRef.setConstraintTolerance(1.0E-08)

  #Get solute atoms and solute heavy atoms separately
  soluteIndices = []
  for res in top.residues:
    if res.name not in ['OTM', 'CTM', 'STM', 'NTM', 'SOL']:
      for atom in res.atoms:
        soluteIndices.append(atom.idx)

  #And define lambda states of interest
  lambdaVec = np.array(#electrostatic lambda - 1.0 is fully interacting, 0.0 is non-interacting
                       [[1.00, 0.75, 0.50, 0.25, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], 
                       #LJ lambdas - 1.0 is fully interacting, 0.0 is non-interacting
                        [1.00, 1.00, 1.00, 1.00, 1.00, 0.90, 0.80, 0.70, 0.60, 0.50, 0.40, 0.35, 0.30, 0.25, 0.20, 0.15, 0.10, 0.05, 0.00] 
                       ])

  #JUST for boric acid, add a custom bonded force
  #Couldn't find a nice, compatible force field, but did find A forcefield, so using it
  #But has no angle terms on O-B-O and instead a weird bond repulsion term
  #This term also prevents out of plane bending
  #Simple in our case because boric acid is symmetric, so only need one parameter
  #Parameters come from Otkidach and Pletnev, 2001
  #Here, Ad = (A^2) / (d^6) since Ai and Aj and di and dj are all the same
  #In the original paper, B-OH bond had A = 1.72 and d = 0.354
  #Note that d is dimensionless and A should have units of (Angstrom^3)*(kcal/mol)^(1/2)
  #These units are inferred just to make things work out with kcal/mol and the given distance dependence
  bondRepulsionFunction = 'Ad*(1.0/r)^6'
  BondRepulsionForce = mm.CustomBondForce(bondRepulsionFunction)
  BondRepulsionForce.addPerBondParameter('Ad') #Units are technically kJ/mol * nm^6
  baOxInds = []
  for aind in soluteIndices:
    if top.atoms[aind].type == 'oh':
      baOxInds.append(aind)
  for i in range(len(baOxInds)):
    for j in range(i+1, len(baOxInds)):
      BondRepulsionForce.addBond(baOxInds[i], baOxInds[j], [0.006289686]) 

  systemRef.addForce(BondRepulsionForce)

  #We need to add a custom non-bonded force for the solute being alchemically changed
  #Will be helpful to have handle on non-bonded force handling LJ and coulombic interactions
  NBForce = None
  for frc in systemRef.getForces():
    if (isinstance(frc, mm.NonbondedForce)):
      NBForce = frc

  #Turn off dispersion correction since have interface
  NBForce.setUseDispersionCorrection(False)

  forceLabelsRef = getForceLabels(systemRef)

  decompEnergy(systemRef, struc.positions, labels=forceLabelsRef)

  #Separate out alchemical and regular particles using set objects
  alchemicalParticles = set(soluteIndices)
  chemicalParticles = set(range(systemRef.getNumParticles())) - alchemicalParticles

  #Define the soft-core function for turning on/off LJ interactions
  #In energy expressions for CustomNonbondedForce, r is a special variable and refers to the distance between particles
  #All other variables must be defined somewhere in the function.
  #The exception are variables like sigma1 and sigma2.
  #It is understood that a parameter will be added called 'sigma' and that the '1' and '2' are to specify the combining rule.
  softCoreFunction = '4.0*lambdaLJ*epsilon*x*(x-1.0); x = (1.0/reff_sterics);'
  softCoreFunction += 'reff_sterics = (0.5*(1.0-lambdaLJ) + ((r/sigma)^6));'
  softCoreFunction += 'sigma=0.5*(sigma1+sigma2); epsilon = sqrt(epsilon1*epsilon2)'
  #Define the system force for this function and its parameters
  SoftCoreForce = mm.CustomNonbondedForce(softCoreFunction)
  SoftCoreForce.addGlobalParameter('lambdaLJ', 1.0) #Throughout, should follow convention that lambdaLJ=1.0 is fully-interacting state
  SoftCoreForce.addPerParticleParameter('sigma')
  SoftCoreForce.addPerParticleParameter('epsilon')

  #Will turn off electrostatics completely in the original non-bonded force
  #In the end-state, only want electrostatics inside the alchemical molecule
  #To do this, just turn ON a custom force as we turn OFF electrostatics in the original force
  ONE_4PI_EPS0 = 138.935456 #in kJ/mol nm/e^2
  soluteCoulFunction = '(1.0-(lambdaQ^2))*ONE_4PI_EPS0*charge/r;'
  soluteCoulFunction += 'ONE_4PI_EPS0 = %.16e;' % (ONE_4PI_EPS0)
  soluteCoulFunction += 'charge = charge1*charge2'
  SoluteCoulForce = mm.CustomNonbondedForce(soluteCoulFunction)
  #Note this lambdaQ will be different than for soft core (it's also named differently, which is CRITICAL)
  #This lambdaQ corresponds to the lambda that scales the charges to zero
  #To turn on this custom force at the same rate, need to multiply by (1.0-lambdaQ**2), which we do
  SoluteCoulForce.addGlobalParameter('lambdaQ', 1.0) 
  SoluteCoulForce.addPerParticleParameter('charge')

  #Also create custom force for intramolecular alchemical LJ interactions
  #Could include with electrostatics, but nice to break up
  #We could also do this with a separate NonbondedForce object, but it would be a little more work, actually
  soluteLJFunction = '4.0*epsilon*x*(x-1.0); x = (sigma/r)^6;'
  soluteLJFunction += 'sigma=0.5*(sigma1+sigma2); epsilon=sqrt(epsilon1*epsilon2)'
  SoluteLJForce = mm.CustomNonbondedForce(soluteLJFunction)
  SoluteLJForce.addPerParticleParameter('sigma')
  SoluteLJForce.addPerParticleParameter('epsilon')
  
  #Loop over all particles and add to custom forces
  #As we go, will also collect full charges on the solute particles
  #AND we will set up the solute-solute interaction forces
  alchemicalCharges = [[0]]*len(soluteIndices)
  for ind in range(systemRef.getNumParticles()):
    #Get current parameters in non-bonded force
    [charge, sigma, epsilon] = NBForce.getParticleParameters(ind)
    #Make sure that sigma is not set to zero! Fine for some ways of writing LJ energy, but NOT OK for soft-core!
    if sigma/u.nanometer == 0.0:
      newsigma = 0.3*u.nanometer #This 0.3 is what's used by GROMACS as a default value for sc-sigma
    else:
      newsigma = sigma
    #Add the particle to the soft-core force (do for ALL particles)
    SoftCoreForce.addParticle([newsigma, epsilon])
    #Also add the particle to the solute only forces
    SoluteCoulForce.addParticle([charge])
    SoluteLJForce.addParticle([sigma, epsilon])
    #If the particle is in the alchemical molecule, need to set it's LJ interactions to zero in original force
    if ind in soluteIndices:
      NBForce.setParticleParameters(ind, charge, sigma, epsilon*0.0)
      #And keep track of full charge so we can scale it right by lambda
      alchemicalCharges[soluteIndices.index(ind)] = charge

  #Now we need to handle exceptions carefully
  for ind in range(NBForce.getNumExceptions()):
    [p1, p2, excCharge, excSig, excEps] = NBForce.getExceptionParameters(ind)
    #For consistency, must add exclusions where we have exceptions for custom forces
    SoftCoreForce.addExclusion(p1, p2)
    SoluteCoulForce.addExclusion(p1, p2)
    SoluteLJForce.addExclusion(p1, p2)

  #Only compute interactions between the alchemical and other particles for the soft-core force
  SoftCoreForce.addInteractionGroup(alchemicalParticles, chemicalParticles)

  #And only compute alchemical/alchemical interactions for other custom forces
  SoluteCoulForce.addInteractionGroup(alchemicalParticles, alchemicalParticles)
  SoluteLJForce.addInteractionGroup(alchemicalParticles, alchemicalParticles)

  #Set other soft-core parameters as needed
  SoftCoreForce.setCutoffDistance(12.0*u.angstroms)
  SoftCoreForce.setNonbondedMethod(mm.CustomNonbondedForce.CutoffPeriodic)
  SoftCoreForce.setUseLongRangeCorrection(False) 
  systemRef.addForce(SoftCoreForce)

  #Set other parameters as needed - note that for the solute force would like to set no cutoff
  #However, OpenMM won't allow a bunch of potentials with cutoffs then one without...
  #So as long as the solute is smaller than the cut-off, won't have any problems!
  SoluteCoulForce.setCutoffDistance(12.0*u.angstroms)
  SoluteCoulForce.setNonbondedMethod(mm.CustomNonbondedForce.CutoffPeriodic)
  SoluteCoulForce.setUseLongRangeCorrection(False) 
  systemRef.addForce(SoluteCoulForce)

  SoluteLJForce.setCutoffDistance(12.0*u.angstroms)
  SoluteLJForce.setNonbondedMethod(mm.CustomNonbondedForce.CutoffPeriodic)
  SoluteLJForce.setUseLongRangeCorrection(False) 
  systemRef.addForce(SoluteLJForce)

  #For the TRAPPE model of octanol, need to constrain ALL bonds, not just hydrogens
  #So loop through bonds and if it's associated with an octonal atom that's not a hydrogen, add constraint
  octHeavyInds = []
  for res in top.residues:
    if res.name == 'SOL':
      for atom in res.atoms:
        if 'H' not in atom.name[0]:
          octHeavyInds.append(atom.idx)

  print("With only hydrogens constrained have %i constraints."%systemRef.getNumConstraints())

  bondForce = None
  for frc in systemRef.getForces():
    if (isinstance(frc, mm.HarmonicBondForce)):
      bondForce = frc
  for k in range(bondForce.getNumBonds()):
    p1, p2, dist, kspring = bondForce.getBondParameters(k)
    if (p1 in octHeavyInds) and (p2 in octHeavyInds):
      systemRef.addConstraint(p1, p2, dist)
      #Turn off the bond potential energy if adding the constraint
      bondForce.setBondParameters(k, p1, p2, dist, 0.0) 

  print("With ALL octanol bonds constrained have %i constraints."%systemRef.getNumConstraints())

  forceLabelsRef = getForceLabels(systemRef)

  decompEnergy(systemRef, struc.positions, labels=forceLabelsRef)

  #Do NVT simulation
  stateFileNVT, stateNVT = doSimNVT(top, systemRef, integratorRef, platform, prop, temperature, pos=struc.positions)

  #And do NPT simulation using state information from NVT
  stateFileNPT, stateNPT = doSimNPT(top, systemRef, integratorRef, platform, prop, temperature, inBulk=True, state=stateFileNVT)

  #And do production run in expanded ensemble!
  stateFileProd, stateProd, weightsVec = doSimExpanded(top, systemRef, integratorRef, platform, prop, temperature, 0, lambdaVec, soluteIndices, alchemicalCharges, inBulk=True, state=stateFileNPT, nSteps=6000000)
Ejemplo n.º 21
0
def main(args):
    #Get the structure and topology files from the command line
    #ParmEd accepts a wide range of file types (Amber, GROMACS, CHARMM, OpenMM... but not LAMMPS)
    try:
        topFile = args[0]
        strucFile = args[1]
    except IndexError:
        print("Specify topology and structure files from the command line.")
        sys.exit(2)

    print("Using topology file: %s" % topFile)
    print("Using structure file: %s" % strucFile)

    print("\nSetting up system...")

    #Load in the files for initial simulations
    top = pmd.load_file(topFile)
    struc = pmd.load_file(strucFile)

    #Transfer unit cell information to topology object
    top.box = struc.box[:]

    #Set up some global features to use in all simulations
    temperature = 298.15 * u.kelvin

    #Define the platform (i.e. hardware and drivers) to use for running the simulation
    #This can be CUDA, OpenCL, CPU, or Reference
    #CUDA is for NVIDIA GPUs
    #OpenCL is for CPUs or GPUs, but must be used for old CPUs (not SSE4.1 compatible)
    #CPU only allows single precision (CUDA and OpenCL allow single, mixed, or double)
    #Reference is a clear, stable reference for other code development and is very slow, using double precision by default
    platform = mm.Platform.getPlatformByName('CUDA')
    prop = {  #'Threads': '2', #number of threads for CPU - all definitions must be strings (I think)
        'Precision':
        'mixed',  #for CUDA or OpenCL, select the precision (single, mixed, or double)
        'DeviceIndex':
        '0',  #selects which GPUs to use - set this to zero if using CUDA_VISIBLE_DEVICES
        'DeterministicForces':
        'True'  #Makes sure forces with CUDA and PME are deterministic
    }

    #Create the OpenMM system that can be used as a reference
    systemRef = top.createSystem(
        nonbondedMethod=app.
        PME,  #Uses PME for long-range electrostatics, simple cut-off for LJ
        nonbondedCutoff=12.0 *
        u.angstroms,  #Defines cut-off for non-bonded interactions
        rigidWater=True,  #Use rigid water molecules
        constraints=app.HBonds,  #Constrains all bonds involving hydrogens
        flexibleConstraints=
        False,  #Whether to include energies for constrained DOFs
        removeCMMotion=
        False,  #Whether or not to remove COM motion (don't want to if part of system frozen)
    )

    #Set up the integrator to use as a reference
    integratorRef = mm.LangevinIntegrator(
        temperature,  #Temperature for Langevin
        1.0 / u.picoseconds,  #Friction coefficient
        2.0 * u.femtoseconds,  #Integration timestep
    )
    integratorRef.setConstraintTolerance(1.0E-08)

    #To freeze atoms, set mass to zero (does not apply to virtual sites, termed "extra particles" in OpenMM)
    #Here assume (correctly, I think) that the topology indices for atoms correspond to those in the system
    for i, atom in enumerate(top.atoms):
        if atom.type in ('SU'):  #, 'CU', 'CUO'):
            systemRef.setParticleMass(i, 0 * u.dalton)

    #Get solute atoms and solute heavy atoms separately
    #Do this for as many solutes as we have so get list for each
    soluteIndices = []
    heavyIndices = []
    for res in top.residues:
        if res.name not in ['OTM', 'CTM', 'STM', 'NTM', 'SOL']:
            thissolinds = []
            thisheavyinds = []
            for atom in res.atoms:
                thissolinds.append(atom.idx)
                if 'H' not in atom.name[0]:
                    thisheavyinds.append(atom.idx)
            soluteIndices.append(thissolinds)
            heavyIndices.append(thisheavyinds)

    #For convenience, also create flattened version of soluteIndices
    allSoluteIndices = []
    for inds in soluteIndices:
        allSoluteIndices += inds

    #Also get surface SU atoms and surface CU atoms at top and bottom of surface
    surfIndices = []
    for atom in top.atoms:
        if atom.type == 'SU':
            surfIndices.append(atom.idx)

    print("\nSolute indices: %s" % str(soluteIndices))
    print("(all together - %s)" % str(allSoluteIndices))
    print("Solute heavy atom indices: %s" % str(heavyIndices))
    print("Surface SU atom indices: %s" % str(surfIndices))

    #Will now add a custom bonded force between heavy atoms of each solute and surface SU atoms
    #Should be in units of kJ/mol*nm^2, but should check this
    #Also, note that here we are using a flat-bottom restraint to keep close to surface
    #AND to keep from penetrating into surface when it's in the decoupled state
    refZlo = 1.4 * u.nanometer  #in nm, the distance between the SU atoms and the solute centroid
    refZhi = 1.7 * u.nanometer
    restraintExpression = '0.5*k*step(refZlo - (z2 - z1))*(((z2 - z1) - refZlo)^2)'
    restraintExpression += '+ 0.5*k*step((z2 - z1) - refZhi)*(((z2 - z1) - refZhi)^2)'
    restraintForce = mm.CustomCentroidBondForce(2, restraintExpression)
    restraintForce.addPerBondParameter('k')
    restraintForce.addPerBondParameter('refZlo')
    restraintForce.addPerBondParameter('refZhi')
    restraintForce.addGroup(surfIndices, np.ones(
        len(surfIndices)))  #Don't weight with masses
    #To assign flat-bottom restraint correctly, need to know if each solute is above or below interface
    #Will need surface z-positions for this
    suZpos = np.average(struc.coordinates[surfIndices, 2])
    for i in range(len(heavyIndices)):
        restraintForce.addGroup(heavyIndices[i], np.ones(len(heavyIndices[i])))
        solZpos = np.average(struc.coordinates[heavyIndices[i], 2])
        if (solZpos - suZpos) > 0:
            restraintForce.addBond([0, i + 1], [10000.0, refZlo, refZhi])
        else:
            #A little confusing, but have to negate and switch for when z2-z1 is always going to be negative
            restraintForce.addBond([0, i + 1], [10000.0, -refZhi, -refZlo])
    systemRef.addForce(restraintForce)

    #We also will need to force the solute to sample the entire surface
    #We will do this with flat-bottom restraints in x and y
    #But NOT on the centroid, because this is hard with CustomExternalForce
    #Instead, just apply it to the first heavy atom of each solute
    #Won't worry about keeping each solute in the same region of the surface on both faces...
    #Actually better if define regions on both faces differently
    initRefX = 0.25 * top.box[
        0] * u.angstrom  #Used parmed to load, and that program uses angstroms
    initRefY = 0.25 * top.box[1] * u.angstrom
    refDistX = 0.25 * top.box[0] * u.angstrom
    refDistY = 0.25 * top.box[1] * u.angstrom
    print('Default X reference distance: %s' % str(initRefX))
    print('Default Y reference distance: %s' % str(initRefY))
    restraintExpressionXY = '0.5*k*step(periodicdistance(x,0,0,refX,0,0) - distX)*((periodicdistance(x,0,0,refX,0,0) - distX)^2)'
    restraintExpressionXY += '+ 0.5*k*step(periodicdistance(0,y,0,0,refY,0) - distY)*((periodicdistance(0,y,0,0,refY,0) - distY)^2)'
    restraintXYForce = mm.CustomExternalForce(restraintExpressionXY)
    restraintXYForce.addPerParticleParameter('k')
    restraintXYForce.addPerParticleParameter('distX')
    restraintXYForce.addPerParticleParameter('distY')
    restraintXYForce.addGlobalParameter('refX', initRefX)
    restraintXYForce.addGlobalParameter('refY', initRefY)
    for i in range(len(heavyIndices)):
        restraintXYForce.addParticle(
            heavyIndices[i][0],
            [1000.0, refDistX, refDistY])  #k in units kJ/mol*nm^2
    systemRef.addForce(restraintXYForce)

    #And define lambda states of interest
    lambdaVec = np.array(  #electrostatic lambda - 1.0 is fully interacting, 0.0 is non-interacting
        [
            [
                1.00, 0.75, 0.50, 0.25, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00,
                0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00
            ],
            #LJ lambdas - 1.0 is fully interacting, 0.0 is non-interacting
            [
                1.00, 1.00, 1.00, 1.00, 1.00, 0.90, 0.80, 0.70, 0.60, 0.50,
                0.40, 0.35, 0.30, 0.25, 0.20, 0.15, 0.10, 0.05, 0.00
            ]
        ])

    #JUST for boric acid, add a custom bonded force
    #Couldn't find a nice, compatible force field, but did find A forcefield, so using it
    #But has no angle terms on O-B-O and instead a weird bond repulsion term
    #This term also prevents out of plane bending
    #Simple in our case because boric acid is symmetric, so only need one parameter
    #Parameters come from Otkidach and Pletnev, 2001
    #Here, Ad = (A^2) / (d^6) since Ai and Aj and di and dj are all the same
    #In the original paper, B-OH bond had A = 1.72 and d = 0.354
    #Note that d is dimensionless and A should have units of (Angstrom^3)*(kcal/mol)^(1/2)
    #These units are inferred just to make things work out with kcal/mol and the given distance dependence
    bondRepulsionFunction = 'Ad*(1.0/r)^6'
    BondRepulsionForce = mm.CustomBondForce(bondRepulsionFunction)
    BondRepulsionForce.addPerBondParameter(
        'Ad')  #Units are technically kJ/mol * nm^6
    solOxInds = []
    for solInds in soluteIndices:
        baOxInds = []
        for aind in solInds:
            if top.atoms[aind].type == 'oh':
                baOxInds.append(aind)
        solOxInds.append(baOxInds)
    for baOxInds in solOxInds:
        for i in range(len(baOxInds)):
            for j in range(i + 1, len(baOxInds)):
                BondRepulsionForce.addBond(baOxInds[i], baOxInds[j],
                                           [0.006289686])

    systemRef.addForce(BondRepulsionForce)

    #We need to add a custom non-bonded force for the solute being alchemically changed
    #Will be helpful to have handle on non-bonded force handling LJ and coulombic interactions
    NBForce = None
    for frc in systemRef.getForces():
        if (isinstance(frc, mm.NonbondedForce)):
            NBForce = frc

    #Turn off dispersion correction since have interface
    NBForce.setUseDispersionCorrection(False)

    forceLabelsRef = getForceLabels(systemRef)

    decompEnergy(systemRef,
                 struc.positions,
                 labels=forceLabelsRef,
                 verbose=True)

    #Separate out alchemical and regular particles using set objects
    alchemicalParticles = set(allSoluteIndices)
    chemicalParticles = set(range(
        systemRef.getNumParticles())) - alchemicalParticles

    #Define the soft-core function for turning on/off LJ interactions
    #In energy expressions for CustomNonbondedForce, r is a special variable and refers to the distance between particles
    #All other variables must be defined somewhere in the function.
    #The exception are variables like sigma1 and sigma2.
    #It is understood that a parameter will be added called 'sigma' and that the '1' and '2' are to specify the combining rule.
    softCoreFunction = '4.0*lambdaLJ*epsilon*x*(x-1.0); x = (1.0/reff_sterics);'
    softCoreFunction += 'reff_sterics = (0.5*(1.0-lambdaLJ) + ((r/sigma)^6));'
    softCoreFunction += 'sigma=0.5*(sigma1+sigma2); epsilon = sqrt(epsilon1*epsilon2)'
    #Define the system force for this function and its parameters
    SoftCoreForce = mm.CustomNonbondedForce(softCoreFunction)
    SoftCoreForce.addGlobalParameter(
        'lambdaLJ', 1.0
    )  #Throughout, should follow convention that lambdaLJ=1.0 is fully-interacting state
    SoftCoreForce.addPerParticleParameter('sigma')
    SoftCoreForce.addPerParticleParameter('epsilon')

    #Will turn off electrostatics completely in the original non-bonded force
    #In the end-state, only want electrostatics inside the alchemical molecule
    #To do this, just turn ON a custom force as we turn OFF electrostatics in the original force
    ONE_4PI_EPS0 = 138.935456  #in kJ/mol nm/e^2
    soluteCoulFunction = '(1.0-(lambdaQ^2))*ONE_4PI_EPS0*charge/r;'
    soluteCoulFunction += 'ONE_4PI_EPS0 = %.16e;' % (ONE_4PI_EPS0)
    soluteCoulFunction += 'charge = charge1*charge2'
    SoluteCoulForce = mm.CustomNonbondedForce(soluteCoulFunction)
    #Note this lambdaQ will be different than for soft core (it's also named differently, which is CRITICAL)
    #This lambdaQ corresponds to the lambda that scales the charges to zero
    #To turn on this custom force at the same rate, need to multiply by (1.0-lambdaQ**2), which we do
    SoluteCoulForce.addGlobalParameter('lambdaQ', 1.0)
    SoluteCoulForce.addPerParticleParameter('charge')

    #Also create custom force for intramolecular alchemical LJ interactions
    #Could include with electrostatics, but nice to break up
    #We could also do this with a separate NonbondedForce object, but it would be a little more work, actually
    soluteLJFunction = '4.0*epsilon*x*(x-1.0); x = (sigma/r)^6;'
    soluteLJFunction += 'sigma=0.5*(sigma1+sigma2); epsilon=sqrt(epsilon1*epsilon2)'
    SoluteLJForce = mm.CustomNonbondedForce(soluteLJFunction)
    SoluteLJForce.addPerParticleParameter('sigma')
    SoluteLJForce.addPerParticleParameter('epsilon')

    #Loop over all particles and add to custom forces
    #As we go, will also collect full charges on the solute particles
    #AND we will set up the solute-solute interaction forces
    alchemicalCharges = [[0]] * len(allSoluteIndices)
    for ind in range(systemRef.getNumParticles()):
        #Get current parameters in non-bonded force
        [charge, sigma, epsilon] = NBForce.getParticleParameters(ind)
        #Make sure that sigma is not set to zero! Fine for some ways of writing LJ energy, but NOT OK for soft-core!
        if sigma / u.nanometer == 0.0:
            newsigma = 0.3 * u.nanometer  #This 0.3 is what's used by GROMACS as a default value for sc-sigma
        else:
            newsigma = sigma
        #Add the particle to the soft-core force (do for ALL particles)
        SoftCoreForce.addParticle([newsigma, epsilon])
        #Also add the particle to the solute only forces
        SoluteCoulForce.addParticle([charge])
        SoluteLJForce.addParticle([sigma, epsilon])
        #If the particle is in the alchemical molecule, need to set it's LJ interactions to zero in original force
        if ind in allSoluteIndices:
            NBForce.setParticleParameters(ind, charge, sigma, epsilon * 0.0)
            #And keep track of full charge so we can scale it right by lambda
            alchemicalCharges[allSoluteIndices.index(ind)] = charge

    #Now we need to handle exceptions carefully
    for ind in range(NBForce.getNumExceptions()):
        [p1, p2, excCharge, excSig,
         excEps] = NBForce.getExceptionParameters(ind)
        #For consistency, must add exclusions where we have exceptions for custom forces
        SoftCoreForce.addExclusion(p1, p2)
        SoluteCoulForce.addExclusion(p1, p2)
        SoluteLJForce.addExclusion(p1, p2)

    #Only compute interactions between the alchemical and other particles for the soft-core force
    SoftCoreForce.addInteractionGroup(alchemicalParticles, chemicalParticles)

    #And only compute alchemical/alchemical interactions for other custom forces
    SoluteCoulForce.addInteractionGroup(alchemicalParticles,
                                        alchemicalParticles)
    SoluteLJForce.addInteractionGroup(alchemicalParticles, alchemicalParticles)

    #Set other soft-core parameters as needed
    SoftCoreForce.setCutoffDistance(12.0 * u.angstroms)
    SoftCoreForce.setNonbondedMethod(mm.CustomNonbondedForce.CutoffPeriodic)
    SoftCoreForce.setUseLongRangeCorrection(False)
    systemRef.addForce(SoftCoreForce)

    #Set other parameters as needed - note that for the solute force would like to set no cutoff
    #However, OpenMM won't allow a bunch of potentials with cutoffs then one without...
    #So as long as the solute is smaller than the cut-off, won't have any problems!
    SoluteCoulForce.setCutoffDistance(12.0 * u.angstroms)
    SoluteCoulForce.setNonbondedMethod(mm.CustomNonbondedForce.CutoffPeriodic)
    SoluteCoulForce.setUseLongRangeCorrection(False)
    systemRef.addForce(SoluteCoulForce)

    SoluteLJForce.setCutoffDistance(12.0 * u.angstroms)
    SoluteLJForce.setNonbondedMethod(mm.CustomNonbondedForce.CutoffPeriodic)
    SoluteLJForce.setUseLongRangeCorrection(False)
    systemRef.addForce(SoluteLJForce)

    forceLabelsRef = getForceLabels(systemRef)

    decompEnergy(systemRef, struc.positions, labels=forceLabelsRef)

    #Run set of simulations for particle restrained in x and y to each quadrant of the surface
    #For best results, the configuration read in should have the solute right in the center of the surface
    #Will store the lambda biasing weights as we got to help speed convergence (hopefully)
    for xfrac in [0.25, 0.75]:
        for yfrac in [0.25, 0.75]:

            os.mkdir('Quad_%1.2fX_%1.2fY' % (xfrac, yfrac))
            os.chdir('Quad_%1.2fX_%1.2fY' % (xfrac, yfrac))

            restraintXYForce.setGlobalParameterDefaultValue(
                0, xfrac * top.box[0] * u.angstrom)
            restraintXYForce.setGlobalParameterDefaultValue(
                1, yfrac * top.box[1] * u.angstrom)

            decompEnergy(systemRef, struc.positions, labels=forceLabelsRef)

            #Do NVT simulation
            stateFileNVT, stateNVT = doSimNVT(top,
                                              systemRef,
                                              integratorRef,
                                              platform,
                                              prop,
                                              temperature,
                                              pos=struc.positions)

            #And do NPT simulation using state information from NVT
            stateFileNPT, stateNPT = doSimNPT(top,
                                              systemRef,
                                              integratorRef,
                                              platform,
                                              prop,
                                              temperature,
                                              state=stateFileNVT)

            #And do production run in expanded ensemble!
            stateFileProd, stateProd, weightsVec = doSimExpanded(
                top,
                systemRef,
                integratorRef,
                platform,
                prop,
                temperature,
                0,
                lambdaVec,
                allSoluteIndices,
                alchemicalCharges,
                state=stateFileNPT)

            #Save final simulation configuration in .gro format (mainly for genetic algorithm)
            finalStruc = copy.deepcopy(struc)
            boxVecs = np.array(stateProd.getPeriodicBoxVectors().value_in_unit(
                u.angstrom))  # parmed works with angstroms
            finalStruc.box = np.array([
                boxVecs[0, 0], boxVecs[1, 1], boxVecs[2, 2], 90.0, 90.0, 90.0
            ])
            finalStruc.coordinates = np.array(
                stateProd.getPositions().value_in_unit(u.angstrom))
            finalStruc.save('prod.gro')

            os.chdir('../')
Ejemplo n.º 22
0
    def add_restraints(self, system):
        # Bond Restraints
        if self.restrain_bondfile is not None:
            nifty.printcool(" Adding bonding restraints!")
            # Harmonic constraint

            flat_bottom_force = openmm.CustomBondForce(
                'step(r-r0) * (k/2) * (r-r0)^2')
            flat_bottom_force.addPerBondParameter('r0')
            flat_bottom_force.addPerBondParameter('k')
            system.addForce(flat_bottom_force)

            with open(self.restrain_bondfile, 'r') as input_file:
                for line in input_file:
                    print(line)
                    columns = line.split()
                    atom_index_i = int(columns[0])
                    atom_index_j = int(columns[1])
                    r0 = float(columns[2])
                    k = float(columns[3])
                    flat_bottom_force.addBond(atom_index_i, atom_index_j,
                                              [r0, k])

        # Torsion restraint
        if self.restrain_torfile is not None:
            nifty.printcool(" Adding torsional restraints!")

            # Harmonic constraint
            tforce = openmm.CustomTorsionForce(
                "0.5*k*min(dtheta, 2*pi-dtheta)^2; dtheta = abs(theta-theta0); pi = 3.1415926535"
            )
            tforce.addPerTorsionParameter("k")
            tforce.addPerTorsionParameter("theta0")
            system.addForce(tforce)

            xyz = manage_xyz.xyz_to_np(self.geom)
            with open(self.restrain_torfile, 'r') as input_file:
                for line in input_file:
                    columns = line.split()
                    a = int(columns[0])
                    b = int(columns[1])
                    c = int(columns[2])
                    d = int(columns[3])
                    k = float(columns[4])
                    dih = Dihedral(a, b, c, d)
                    theta0 = dih.value(xyz)
                    tforce.addTorsion(a, b, c, d, [k, theta0])

        # Translation restraint
        if self.restrain_tranfile is not None:
            nifty.printcool(" Adding translational restraints!")
            trforce = openmm.CustomExternalForce(
                "k*periodicdistance(x, y, z, x0, y0, z0)^2")
            trforce.addPerParticleParameter("k")
            trforce.addPerParticleParameter("x0")
            trforce.addPerParticleParameter("y0")
            trforce.addPerParticleParameter("z0")
            system.addForce(trforce)

            xyz = manage_xyz.xyz_to_np(self.geom)
            with open(self.restrain_tranfile, 'r') as input_file:
                for line in input_file:
                    columns = line.split()
                    a = int(columns[0])
                    k = float(columns[1])
                    x0 = xyz[a, 0] * 0.1  # Units are in nm
                    y0 = xyz[a, 1] * 0.1  # Units are in nm
                    z0 = xyz[a, 2] * 0.1  # Units are in nm
                    trforce.addParticle(a, [k, x0, y0, z0])
Ejemplo n.º 23
0
def constant_force_bonds(
    sim_object,
    bonds,
    bondWiggleDistance=0.05,
    bondLength=1.0,
    quadraticPart = 0.02,
    name="abs_bonds",
    override_checks=False,
):
    """
    
    Constant force bond force. Energy is roughly linear with estension 
    after r=quadraticPart; before it is quadratic to make sure the force
    is differentiable. 
    
    Force is parametrized using the same approach as bond force:
    it reaches U=kT at extension = bondWiggleDistance 
    
    Note that, just as with bondForce, mean squared extension 
    is actually larger than wiggleDistance by sqrt(2) factor. 
    
    Parameters
    ----------
    
    bonds : iterable of (int, int)
        Pairs of particle indices to be connected with a bond.
    bondWiggleDistance : float
        Displacement at which bond energy equals 1 kT. 
        Can be provided per-particle.
    bondLength : float
        The length of the bond.
        Can be provided per-particle.
    override_checks: bool
        If True then do not check that no bonds are repeated.
        False by default.
    """

    # check for repeated bonds
    if not override_checks:
        _check_bonds(bonds, sim_object.N)

    energy = (
        f"(1. / wiggle) * univK * "
        f"(sqrt((r-r0 * conlen)* "
        f" (r - r0 * conlen) + a * a) - a)"
    )
    force = openmm.CustomBondForce(energy)
    force.name = name

    force.addPerBondParameter("wiggle")
    force.addPerBondParameter("r0")
    force.addGlobalParameter("univK", sim_object.kT / sim_object.conlen)
    force.addGlobalParameter("a", quadraticPart * sim_object.conlen)
    force.addGlobalParameter("conlen", sim_object.conlen)

    bondLength = _to_array_1d(bondLength, len(bonds)) * sim_object.length_scale
    bondWiggleDistance = (
        _to_array_1d(bondWiggleDistance, len(bonds)) * sim_object.length_scale
    )

    for bond_idx, (i, j) in enumerate(bonds):
        if (i >= sim_object.N) or (j >= sim_object.N):
            raise ValueError(
                "\nCannot add bond with monomers %d,%d that"
                "are beyound the polymer length %d" % (i, j, sim_object.N)
            )

        force.addBond(
            int(i),
            int(j),
            [float(bondWiggleDistance[bond_idx]), float(bondLength[bond_idx])],
        )

    return force
Ejemplo n.º 24
0
def _process_nonbonded_forces(openff_sys,
                              openmm_sys,
                              combine_nonbonded_forces=False):
    """Process the vdW and Electrostatics sections of an OpenFF Interchange into a corresponding openmm.NonbondedForce
    or a collection of other forces (NonbondedForce, CustomNonbondedForce, CustomBondForce)"""
    if "vdW" in openff_sys.handlers:
        vdw_handler = openff_sys.handlers["vdW"]

        vdw_cutoff = vdw_handler.cutoff.m_as(off_unit.angstrom) * unit.angstrom
        vdw_method = vdw_handler.method.lower()

        electrostatics_handler = openff_sys.handlers["Electrostatics"]
        electrostatics_method = electrostatics_handler.method.lower()

        if vdw_handler.mixing_rule != "lorentz-berthelot":
            if combine_nonbonded_forces:
                raise UnsupportedExportError(
                    "OpenMM's default NonbondedForce only supports Lorentz-Berthelot mixing rules."
                    "Try setting `combine_nonbonded_forces=False`.")
            else:
                raise NotImplementedError(
                    f"Mixing rule `{vdw_handler.mixing_rule}` not compatible with current OpenMM export."
                    "The only supported values is `lorentez-berthelot`.")

        if vdw_handler.mixing_rule == "lorentz-berthelot":
            if not combine_nonbonded_forces:
                mixing_rule_expression = (
                    "sigma=(sigma1+sigma2)/2; epsilon=sqrt(epsilon1*epsilon2); "
                )

        if combine_nonbonded_forces:
            non_bonded_force = openmm.NonbondedForce()
            openmm_sys.addForce(non_bonded_force)

            for _ in openff_sys.topology.mdtop.atoms:
                non_bonded_force.addParticle(0.0, 1.0, 0.0)

            if vdw_method == "cutoff" and electrostatics_method == "pme":
                if openff_sys.box is not None:
                    non_bonded_force.setNonbondedMethod(
                        openmm.NonbondedForce.PME)
                    non_bonded_force.setUseDispersionCorrection(True)
                    non_bonded_force.setCutoffDistance(vdw_cutoff)
                    non_bonded_force.setEwaldErrorTolerance(1.0e-4)
                else:
                    raise UnsupportedCutoffMethodError
            elif vdw_method == "pme" and electrostatics_method == "pme":
                if openff_sys.box is not None:
                    non_bonded_force.setNonbondedMethod(
                        openmm.NonbondedForce.LJPME)
                    non_bonded_force.setEwaldErrorTolerance(1.0e-4)
                else:
                    raise UnsupportedCutoffMethodError
            else:
                raise UnimplementedCutoffMethodError(
                    f"Combination of non-bonded cutoff methods {vdw_cutoff} (vdW) and "
                    f"{electrostatics_method} (Electrostatics) not currently supported with "
                    f"`combine_nonbonded_forces={combine_nonbonded_forces}")

        else:
            vdw_expression = vdw_handler.expression
            vdw_expression = vdw_expression.replace("**", "^")

            vdw_force = openmm.CustomNonbondedForce(vdw_expression + "; " +
                                                    mixing_rule_expression)
            openmm_sys.addForce(vdw_force)
            vdw_force.addPerParticleParameter("sigma")
            vdw_force.addPerParticleParameter("epsilon")

            # TODO: Add virtual particles
            for _ in openff_sys.topology.mdtop.atoms:
                vdw_force.addParticle([1.0, 0.0])

            if vdw_method == "cutoff":
                if openff_sys.box is None:
                    vdw_force.setNonbondedMethod(
                        openmm.NonbondedForce.CutoffNonPeriodic)
                else:
                    vdw_force.setNonbondedMethod(
                        openmm.NonbondedForce.CutoffPeriodic)
                vdw_force.setUseLongRangeCorrection(True)
                vdw_force.setCutoffDistance(vdw_cutoff)
                if getattr(vdw_handler, "switch_width", None) is not None:
                    if vdw_handler.switch_width == 0.0:
                        vdw_force.setUseSwitchingFunction(False)
                    else:
                        switching_distance = (vdw_handler.cutoff -
                                              vdw_handler.switch_width)
                        if switching_distance.m < 0:
                            raise UnsupportedCutoffMethodError(
                                "Found a 'switch_width' greater than the cutoff distance. It's not clear "
                                "what this means and it's probably invalid. Found "
                                f"switch_width{vdw_handler.switch_width} and cutoff {vdw_handler.cutoff}"
                            )

                        switching_distance = (
                            switching_distance.m_as(off_unit.angstrom) *
                            unit.angstrom)

                        vdw_force.setUseSwitchingFunction(True)
                        vdw_force.setSwitchingDistance(switching_distance)

            elif vdw_method == "pme":
                if openff_sys.box is None:
                    raise UnsupportedCutoffMethodError(
                        "vdW method pme/ljpme is not valid for non-periodic systems."
                    )
                else:
                    # TODO: Fully flesh out this implementation - cutoffs, other settings
                    vdw_force.setNonbondedMethod(openmm.NonbondedForce.PME)

            electrostatics_force = openmm.NonbondedForce()
            openmm_sys.addForce(electrostatics_force)

            for _ in openff_sys.topology.mdtop.atoms:
                electrostatics_force.addParticle(0.0, 1.0, 0.0)

            if electrostatics_method == "reaction-field":
                if openff_sys.box is None:
                    # TODO: Should this state be prevented from happening?
                    raise UnsupportedCutoffMethodError(
                        f"Electrostatics method {electrostatics_method} is not valid for a non-periodic interchange."
                    )
                else:
                    raise UnimplementedCutoffMethodError(
                        f"Electrostatics method {electrostatics_method} is not yet implemented."
                    )
            elif electrostatics_method == "pme":
                electrostatics_force.setNonbondedMethod(
                    openmm.NonbondedForce.PME)
                electrostatics_force.setEwaldErrorTolerance(1.0e-4)
                electrostatics_force.setUseDispersionCorrection(True)
            elif electrostatics_method == "cutoff":
                raise UnsupportedCutoffMethodError(
                    "OpenMM does not clearly support cut-off electrostatics with no reaction-field attenuation."
                )
            else:
                raise UnsupportedCutoffMethodError(
                    f"Electrostatics method {electrostatics_method} not supported"
                )

        partial_charges = electrostatics_handler.charges

        for top_key, pot_key in vdw_handler.slot_map.items():
            atom_idx = top_key.atom_indices[0]

            partial_charge = partial_charges[top_key]
            # partial_charge = partial_charge.m_as(off_unit.elementary_charge)
            vdw_potential = vdw_handler.potentials[pot_key]
            # these are floats, implicitly angstrom and kcal/mol
            sigma, epsilon = _lj_params_from_potential(vdw_potential)
            sigma = sigma.m_as(off_unit.nanometer)
            epsilon = epsilon.m_as(off_unit.kilojoule / off_unit.mol)

            if combine_nonbonded_forces:
                non_bonded_force.setParticleParameters(
                    atom_idx,
                    partial_charge.m_as(off_unit.e),
                    sigma,
                    epsilon,
                )
            else:
                vdw_force.setParticleParameters(atom_idx, [sigma, epsilon])
                electrostatics_force.setParticleParameters(
                    atom_idx, partial_charge.m_as(off_unit.e), 0.0, 0.0)

    elif "Buckingham-6" in openff_sys.handlers:
        buck_handler = openff_sys.handlers["Buckingham-6"]

        non_bonded_force = openmm.CustomNonbondedForce(
            "A * exp(-B * r) - C * r ^ -6; A = sqrt(A1 * A2); B = 2 / (1 / B1 + 1 / B2); C = sqrt(C1 * C2)"
        )
        non_bonded_force.addPerParticleParameter("A")
        non_bonded_force.addPerParticleParameter("B")
        non_bonded_force.addPerParticleParameter("C")
        openmm_sys.addForce(non_bonded_force)

        for _ in openff_sys.topology.mdtop.atoms:
            non_bonded_force.addParticle([0.0, 0.0, 0.0])

        if openff_sys.box is None:
            non_bonded_force.setNonbondedMethod(openmm.NonbondedForce.NoCutoff)
        else:
            non_bonded_force.setNonbondedMethod(
                openmm.NonbondedForce.CutoffPeriodic)
            non_bonded_force.setCutoffDistance(buck_handler.cutoff *
                                               unit.angstrom)

        for top_key, pot_key in buck_handler.slot_map.items():
            atom_idx = top_key.atom_indices[0]

            # TODO: Add electrostatics
            params = buck_handler.potentials[pot_key].parameters
            a = pint_to_simtk(params["A"])
            b = pint_to_simtk(params["B"])
            c = pint_to_simtk(params["C"])
            non_bonded_force.setParticleParameters(atom_idx, [a, b, c])

        return

    if not combine_nonbonded_forces:
        # Attempting to match the value used internally by OpenMM; The source of this value is likely
        # https://github.com/openmm/openmm/issues/1149#issuecomment-250299854
        # 1 / * (4pi * eps0) * elementary_charge ** 2 / nanometer ** 2
        coul_const = 138.935456  # kJ/nm

        vdw_14_force = openmm.CustomBondForce(
            "4*epsilon*((sigma/r)^12-(sigma/r)^6)")
        vdw_14_force.addPerBondParameter("sigma")
        vdw_14_force.addPerBondParameter("epsilon")
        vdw_14_force.setUsesPeriodicBoundaryConditions(True)
        coul_14_force = openmm.CustomBondForce(f"{coul_const}*qq/r")
        coul_14_force.addPerBondParameter("qq")
        coul_14_force.setUsesPeriodicBoundaryConditions(True)

        openmm_sys.addForce(vdw_14_force)
        openmm_sys.addForce(coul_14_force)

    # Need to create 1-4 exceptions, just to have a baseline for splitting out/modifying
    # It might be simpler to iterate over 1-4 pairs directly
    bonds = [(b.atom1.index, b.atom2.index)
             for b in openff_sys.topology.mdtop.bonds]

    if combine_nonbonded_forces:
        non_bonded_force.createExceptionsFromBonds(
            bonds=bonds,
            coulomb14Scale=electrostatics_handler.scale_14,
            lj14Scale=vdw_handler.scale_14,
        )
    else:
        electrostatics_force.createExceptionsFromBonds(
            bonds=bonds,
            coulomb14Scale=electrostatics_handler.scale_14,
            lj14Scale=vdw_handler.scale_14,
        )

        for i in range(electrostatics_force.getNumExceptions()):
            (p1, p2, q, sig,
             eps) = electrostatics_force.getExceptionParameters(i)

            # If the interactions are both zero, assume this is a 1-2 or 1-3 interaction
            if q._value == 0 and eps._value == 0:
                pass
            else:
                # Assume this is a 1-4 interaction
                # Look up the vdW parameters for each particle
                sig1, eps1 = vdw_force.getParticleParameters(p1)
                sig2, eps2 = vdw_force.getParticleParameters(p2)
                q1, _, _ = electrostatics_force.getParticleParameters(p1)
                q2, _, _ = electrostatics_force.getParticleParameters(p2)

                # manually compute and set the 1-4 interactions
                sig_14 = (sig1 + sig2) * 0.5
                eps_14 = (eps1 * eps2)**0.5 * vdw_handler.scale_14
                qq = q1 * q2 * electrostatics_handler.scale_14

                vdw_14_force.addBond(p1, p2, [sig_14, eps_14])
                coul_14_force.addBond(p1, p2, [qq])
            vdw_force.addExclusion(p1, p2)
            # electrostatics_force.addExclusion(p1, p2)
            electrostatics_force.setExceptionParameters(
                i, p1, p2, 0.0, 0.0, 0.0)
Ejemplo n.º 25
0
# Add a Monte Carlo barostat to the system for pressure control
system.addForce(
    mm.MonteCarloBarostat(1 * unit.atmospheres, 300 * unit.kelvin, 25))

# Use the CPU platform
platform = mm.Platform.getPlatformByName('CPU')

### If you want to add any forces to your System or modify any
### of the existing forces, you should do it here - after the
### System has been created, but before the Simulation is created.

### these lines are for adding a force between atoms 1 and 273, the CA atoms
### of the first and last residues of Trp-cage, to unfold the protein

custom_bond = mm.CustomBondForce("0.5*k*(r-r0)^2")
#import IPython
#IPython.embed()
custom_bond.addGlobalParameter("k", 1000)
custom_bond.addGlobalParameter("r0", 5.0)
custom_bond.addBond(1, 273)
system.addForce(custom_bond)

# Create a Simulation object by putting together the objects above
simulation = app.Simulation(pdb.topology, system, integrator, platform)

# Set positions in the Simulation object
simulation.context.setPositions(pdb.positions)

# Minimize the energy of the system (intentionally doing a rough minimization)
print('Minimizing...')
Ejemplo n.º 26
0
def add_molecule_to_system(system,
                           molecule_system,
                           core_atoms,
                           variant,
                           atoms_to_exclude=[]):
    """
    Add the valence terms for the molecule from molecule_system.

    Parameters
    ----------
    system : simtk.openmm.System
       The system object to which the valence terms are to be added.
    molecule_system : simtk.openmm.System
       The system object from which core valence terms are to be taken.
    core_atoms : list of int
       The list of atom indices within molecule_system corresponding to core atoms.
    variant : int
       The variant index of this molecule if not a core fragment, or 0 if this is a core fragment and only core atoms are to be added.

    Returns
    -------
    mapping : dict of int
       mapping[index] is the atom index in `system` corresponding to atom `index` within `molecule_system`.

    """
    def _createCustomNonbondedForce(self,
                                    system,
                                    molecule_system,
                                    softcore_alpha=0.5,
                                    softcore_beta=12 * unit.angstrom**2):
        """
        Create alchemically-modified version of NonbondedForce.

        Parameters
        ----------
        system : simtk.openmm.System
            Alchemically-modified system being built.  This object will be modified.
        molecule_system : simtk.openmm.System
            Source molecule system to copy from.
        softcore_alpha : float, optional, default = 0.5
            Alchemical softcore parameter for Lennard-Jones.
        softcore_beta : simtk.unit.Quantity with units compatible with angstroms**2, optional, default = 12*angstrom**2
            Alchemical softcore parameter for electrostatics.

        TODO
        ----
        Try using a single, common "reff" effective softcore distance for both Lennard-Jones and Coulomb.

        """

        alchemical_atom_indices = self.ligand_atoms

        # Create a copy of the NonbondedForce to handle non-alchemical interactions.
        nonbonded_force = copy.deepcopy(reference_force)
        system.addForce(nonbonded_force)

        # Create CustomNonbondedForce objects to handle softcore interactions between alchemically-modified system and rest of system.

        # Create atom groups.
        natoms = system.getNumParticles()
        atomset1 = set(
            alchemical_atom_indices)  # only alchemically-modified atoms
        atomset2 = set(range(system.getNumParticles())
                       )  # all atoms, including alchemical region

        # CustomNonbondedForce energy expression.
        sterics_energy_expression = ""
        electrostatics_energy_expression = ""

        # Select functional form based on nonbonded method.
        method = reference_force.getNonbondedMethod()
        if method in [openmm.NonbondedForce.NoCutoff]:
            # soft-core Lennard-Jones
            sterics_energy_expression += "U_sterics = lambda_sterics*4*epsilon*x*(x-1.0); x = (sigma/reff_sterics)^6;"
            # soft-core Coulomb
            electrostatics_energy_expression += "U_electrostatics = ONE_4PI_EPS0*lambda_electrostatics*chargeprod/reff_electrostatics;"
        elif method in [
                openmm.NonbondedForce.CutoffPeriodic,
                openmm.NonbondedForce.CutoffNonPeriodic
        ]:
            # soft-core Lennard-Jones
            sterics_energy_expression += "U_sterics = lambda_sterics*4*epsilon*x*(x-1.0); x = (sigma/reff_sterics)^6;"
            # reaction-field electrostatics
            epsilon_solvent = reference_force.getReactionFieldDielectric()
            r_cutoff = reference_force.getCutoffDistance()
            electrostatics_energy_expression += "U_electrostatics = lambda_electrostatics*ONE_4PI_EPS0*chargeprod*(reff_electrostatics^(-1) + k_rf*reff_electrostatics^2 - c_rf);"
            k_rf = r_cutoff**(-3) * ((epsilon_solvent - 1) /
                                     (2 * epsilon_solvent + 1))
            c_rf = r_cutoff**(-1) * ((3 * epsilon_solvent) /
                                     (2 * epsilon_solvent + 1))
            electrostatics_energy_expression += "k_rf = %f;" % (
                k_rf / k_rf.in_unit_system(unit.md_unit_system).unit)
            electrostatics_energy_expression += "c_rf = %f;" % (
                c_rf / c_rf.in_unit_system(unit.md_unit_system).unit)
        elif method in [
                openmm.NonbondedForce.PME, openmm.NonbondedForce.Ewald
        ]:
            # soft-core Lennard-Jones
            sterics_energy_expression += "U_sterics = lambda_sterics*4*epsilon*x*(x-1.0); x = (sigma/reff_sterics)^6;"
            # Ewald direct-space electrostatics
            [alpha_ewald, nx, ny, nz] = reference_force.getPMEParameters()
            if alpha_ewald == 0.0:
                # If alpha is 0.0, alpha_ewald is computed by OpenMM from from the error tolerance.
                delta = reference_force.getEwaldErrorTolerance()
                r_cutoff = reference_force.getCutoffDistance()
                alpha_ewald = np.sqrt(-np.log(2 * delta)) / r_cutoff
            electrostatics_energy_expression += "U_electrostatics = lambda_electrostatics*ONE_4PI_EPS0*chargeprod*erfc(alpha_ewald*reff_electrostatics)/reff_electrostatics;"
            electrostatics_energy_expression += "alpha_ewald = %f;" % (
                alpha_ewald /
                alpha_ewald.in_unit_system(unit.md_unit_system).unit)
            # TODO: Handle reciprocal-space electrostatics
        else:
            raise Exception("Nonbonded method %s not supported yet." %
                            str(method))

        # Add additional definitions common to all methods.
        sterics_energy_expression += "reff_sterics = sigma*((softcore_alpha*(1.-lambda_sterics) + (r/sigma)^6))^(1/6);"  # effective softcore distance for sterics
        sterics_energy_expression += "softcore_alpha = %f;" % softcore_alpha
        electrostatics_energy_expression += "reff_electrostatics = sqrt(softcore_beta*(1.-lambda_electrostatics) + r^2);"  # effective softcore distance for electrostatics
        electrostatics_energy_expression += "softcore_beta = %f;" % (
            softcore_beta /
            softcore_beta.in_unit_system(unit.md_unit_system).unit)
        electrostatics_energy_expression += "ONE_4PI_EPS0 = %f;" % ONE_4PI_EPS0  # already in OpenMM units

        # Define mixing rules.
        sterics_mixing_rules = ""
        sterics_mixing_rules += "epsilon = sqrt(epsilon1*epsilon2);"  # mixing rule for epsilon
        sterics_mixing_rules += "sigma = 0.5*(sigma1 + sigma2);"  # mixing rule for sigma
        electrostatics_mixing_rules = ""
        electrostatics_mixing_rules += "chargeprod = charge1*charge2;"  # mixing rule for charges

        # Create CustomNonbondedForce to handle interactions between alchemically-modified atoms and rest of system.
        electrostatics_custom_nonbonded_force = openmm.CustomNonbondedForce(
            "U_electrostatics;" + electrostatics_energy_expression +
            electrostatics_mixing_rules)
        electrostatics_custom_nonbonded_force.addGlobalParameter(
            "lambda_electrostatics", 1.0)
        electrostatics_custom_nonbonded_force.addPerParticleParameter(
            "charge")  # partial charge
        sterics_custom_nonbonded_force = openmm.CustomNonbondedForce(
            "U_sterics;" + sterics_energy_expression + sterics_mixing_rules)
        sterics_custom_nonbonded_force.addGlobalParameter(
            "lambda_sterics", 1.0)
        sterics_custom_nonbonded_force.addPerParticleParameter(
            "sigma")  # Lennard-Jones sigma
        sterics_custom_nonbonded_force.addPerParticleParameter(
            "epsilon")  # Lennard-Jones epsilon

        # Set parameters to match reference force.
        sterics_custom_nonbonded_force.setUseSwitchingFunction(
            nonbonded_force.getUseSwitchingFunction())
        electrostatics_custom_nonbonded_force.setUseSwitchingFunction(False)
        sterics_custom_nonbonded_force.setCutoffDistance(
            nonbonded_force.getCutoffDistance())
        electrostatics_custom_nonbonded_force.setCutoffDistance(
            nonbonded_force.getCutoffDistance())
        sterics_custom_nonbonded_force.setSwitchingDistance(
            nonbonded_force.getSwitchingDistance())
        sterics_custom_nonbonded_force.setUseLongRangeCorrection(
            nonbonded_force.getUseDispersionCorrection())
        electrostatics_custom_nonbonded_force.setUseLongRangeCorrection(False)

        # Set periodicity and cutoff parameters corresponding to reference Force.
        if nonbonded_force.getNonbondedMethod() in [
                openmm.NonbondedForce.Ewald, openmm.NonbondedForce.PME,
                openmm.NonbondedForce.CutoffPeriodic
        ]:
            sterics_custom_nonbonded_force.setNonbondedMethod(
                openmm.CustomNonbondedForce.CutoffPeriodic)
            electrostatics_custom_nonbonded_force.setNonbondedMethod(
                openmm.CustomNonbondedForce.CutoffPeriodic)
        else:
            sterics_custom_nonbonded_force.setNonbondedMethod(
                nonbonded_force.getNonbondedMethod())
            electrostatics_custom_nonbonded_force.setNonbondedMethod(
                nonbonded_force.getNonbondedMethod())

        # Restrict interaction evaluation to be between alchemical atoms and rest of environment.
        # TODO: Exclude intra-alchemical region if we are separately handling that through a separate CustomNonbondedForce for decoupling.
        sterics_custom_nonbonded_force.addInteractionGroup(atomset1, atomset2)
        electrostatics_custom_nonbonded_force.addInteractionGroup(
            atomset1, atomset2)

        # Add custom forces.
        system.addForce(sterics_custom_nonbonded_force)
        system.addForce(electrostatics_custom_nonbonded_force)

        # Create CustomBondForce to handle exceptions for both kinds of interactions.
        custom_bond_force = openmm.CustomBondForce(
            "U_sterics + U_electrostatics;" + sterics_energy_expression +
            electrostatics_energy_expression)
        custom_bond_force.addGlobalParameter("lambda_electrostatics", 1.0)
        custom_bond_force.addGlobalParameter("lambda_sterics", 1.0)
        custom_bond_force.addPerBondParameter("chargeprod")  # charge product
        custom_bond_force.addPerBondParameter(
            "sigma")  # Lennard-Jones effective sigma
        custom_bond_force.addPerBondParameter(
            "epsilon")  # Lennard-Jones effective epsilon
        system.addForce(custom_bond_force)

        # Move NonbondedForce particle terms for alchemically-modified particles to CustomNonbondedForce.
        for particle_index in range(nonbonded_force.getNumParticles()):
            # Retrieve parameters.
            [charge, sigma,
             epsilon] = nonbonded_force.getParticleParameters(particle_index)
            # Add parameters to custom force handling interactions between alchemically-modified atoms and rest of system.
            sterics_custom_nonbonded_force.addParticle([sigma, epsilon])
            electrostatics_custom_nonbonded_force.addParticle([charge])
            # Turn off Lennard-Jones contribution from alchemically-modified particles.
            if particle_index in alchemical_atom_indices:
                nonbonded_force.setParticleParameters(particle_index,
                                                      0 * charge, sigma,
                                                      0 * epsilon)

        # Move NonbondedForce exception terms for alchemically-modified particles to CustomNonbondedForce/CustomBondForce.
        for exception_index in range(nonbonded_force.getNumExceptions()):
            # Retrieve parameters.
            [iatom, jatom, chargeprod, sigma,
             epsilon] = nonbonded_force.getExceptionParameters(exception_index)
            # Exclude this atom pair in CustomNonbondedForce.
            sterics_custom_nonbonded_force.addExclusion(iatom, jatom)
            electrostatics_custom_nonbonded_force.addExclusion(iatom, jatom)
            # Move exceptions involving alchemically-modified atoms to CustomBondForce.
            if self.annihilate_sterics and (iatom in alchemical_atom_indices
                                            ) and (jatom
                                                   in alchemical_atom_indices):
                # Add special CustomBondForce term to handle alchemically-modified Lennard-Jones exception.
                custom_bond_force.addBond(iatom, jatom,
                                          [chargeprod, sigma, epsilon])
                # Zero terms in NonbondedForce.
                nonbonded_force.setExceptionParameters(exception_index, iatom,
                                                       jatom, 0 * chargeprod,
                                                       sigma, 0 * epsilon)

        # TODO: Add back NonbondedForce terms for alchemical system needed in case of decoupling electrostatics or sterics via second CustomBondForce.
        # TODO: Also need to change current CustomBondForce to not alchemically disappear system.

        return

    # Build dict of forces.
    def create_force_dict(system):
        return {
            system.getForce(index).__class__.__name__: system.getForce(index)
            for index in range(system.getNumForces())
        }

    molecule_forces = create_force_dict(molecule_system)
    forces = create_force_dict(system)

    # Create Custom*Force classes if necessary.
    if 'CustomBondForce' not in forces:
        energy_expression = 'lambda*(K/2)*(r-length)^2;'
        energy_expression += 'lambda = (1-alchemical_lambda)*delta(variant) + alchemical_lambda*delta(variant-alchemical_variant);'
        custom_force = mm.CustomBondForce(energy_expression)
        custom_force.addGlobalParameter('alchemical_lambda', 0.0)
        custom_force.addGlobalParameter('alchemical_variant', 0.0)
        custom_force.addPerBondParameter('variant')
        custom_force.addPerBondParameter('length')
        custom_force.addPerBondParameter('K')
        system.addForce(custom_force)
        forces['CustomBondForce'] = custom_force
    if 'CustomAngleForce' not in forces:
        energy_expression = 'lambda*(K/2)*(theta-theta0)^2;'
        energy_expression += 'lambda = (1-alchemical_lambda)*delta(variant) + alchemical_lambda*delta(variant-alchemical_variant);'
        custom_force = mm.CustomAngleForce(energy_expression)
        custom_force.addGlobalParameter('alchemical_lambda', 0.0)
        custom_force.addGlobalParameter('alchemical_variant', 0.0)
        custom_force.addPerAngleParameter('variant')
        custom_force.addPerAngleParameter('theta0')
        custom_force.addPerAngleParameter('K')
        system.addForce(custom_force)
        forces['CustomAngleForce'] = custom_force
    if 'CustomTorsionForce' not in forces:
        energy_expression = 'lambda*K*(1+cos(periodicity*theta-phase));'
        energy_expression += 'lambda = (1-alchemical_lambda)*delta(variant) + alchemical_lambda*delta(variant-alchemical_variant);'
        custom_force = mm.CustomTorsionForce(energy_expression)
        custom_force.addGlobalParameter('alchemical_lambda', 0.0)
        custom_force.addGlobalParameter('alchemical_variant', 0.0)
        custom_force.addPerTorsionParameter('variant')
        custom_force.addPerTorsionParameter('periodicity')
        custom_force.addPerTorsionParameter('phase')
        custom_force.addPerTorsionParameter('K')
        system.addForce(custom_force)
        forces['CustomTorsionForce'] = custom_force
    if 'CustomNonbondedForce' not in forces:
        # DEBUG
        # TODO: Create proper CustomNonbondedForce here.
        energy_expression = "0.0;"
        custom_force = mm.CustomNonbondedForce(energy_expression)
        custom_force.addGlobalParameter('alchemical_lambda', 0.0)
        custom_force.addGlobalParameter('alchemical_variant', 0.0)
        custom_force.addPerParticleParameter('variant')
        custom_force.addPerParticleParameter('charge')
        custom_force.addPerParticleParameter('sigma')
        custom_force.addPerParticleParameter('epsilon')
        system.addForce(custom_force)
        forces['CustomNonbondedForce'] = custom_force

        # Add parameters for existing particles.
        for index in range(system.getNumParticles()):
            [charge, sigma,
             epsilon] = forces['NonbondedForce'].getParticleParameters(index)
            custom_force.addParticle([0, charge, sigma, epsilon])

    # Add particles to system.
    mapping = dict()  # mapping[index_in_molecule] = index_in_system
    for index_in_molecule in range(molecule_system.getNumParticles()):
        # Add all atoms, unless we're adding the core, in which case we just add core atoms.
        if (variant) or (index_in_molecule in core_atoms):
            # TODO: We may want to make masses lighter.
            index_in_system = system.addParticle(
                system.getParticleMass(index_in_molecule))
            mapping[index_in_molecule] = index_in_system

    # Constraints are not supported.
    # TODO: Later, consider supporting some constraints, such as those within core and those within variants.
    if (molecule_system.getNumConstraints() > 0):
        raise Exception(
            "Constraints are not supported for alchemically modified molecule."
        )

    # Process forces.
    # Valence terms involving only core atoms are created as Custom*Force classes where lambda=0 activates the "core" image and lambda=1 activates the "variant" image.
    for (force_name, force) in molecule_forces.iteritems():
        print force_name
        if force_name == 'HarmonicBondForce':
            for index in range(force.getNumBonds()):
                [atom_i, atom_j, length, K] = force.getBondParameters(index)
                if set([atom_i, atom_j]).issubset(core_atoms):
                    forces['CustomBondForce'].addBond(mapping[atom_i],
                                                      mapping[atom_j],
                                                      [variant, length, K])
                elif (variant):
                    forces[force_name].addBond(mapping[atom_i],
                                               mapping[atom_j], length, K)

        elif force_name == 'HarmonicAngleForce':
            for index in range(force.getNumAngles()):
                [atom_i, atom_j, atom_k, theta0,
                 K] = force.getAngleParameters(index)
                if set([atom_i, atom_j, atom_k]).issubset(core_atoms):
                    forces['CustomAngleForce'].addAngle(
                        mapping[atom_i], mapping[atom_j], mapping[atom_k],
                        [variant, theta0, K])
                elif (variant):
                    forces[force_name].addAngle(mapping[atom_i],
                                                mapping[atom_j],
                                                mapping[atom_k], theta0, K)

        elif force_name == 'PeriodicTorsionForce':
            for index in range(force.getNumTorsions()):
                [atom_i, atom_j, atom_k, atom_l, periodicity, phase,
                 K] = force.getTorsionParameters(index)
                if set([atom_i, atom_j, atom_k, atom_l]).issubset(core_atoms):
                    forces['CustomTorsionForce'].addTorsion(
                        mapping[atom_i], mapping[atom_j], mapping[atom_k],
                        mapping[atom_l], [variant, periodicity, phase, K])
                elif (variant):
                    forces[force_name].addTorsion(mapping[atom_i],
                                                  mapping[atom_j],
                                                  mapping[atom_k],
                                                  mapping[atom_l], periodicity,
                                                  phase, K)

        elif force_name == 'NonbondedForce':
            for index in range(force.getNumParticles()):
                # TODO: Nonbonded terms will have to be handled as CustomNonbondedForce terms.
                [charge, sigma, epsilon] = force.getParticleParameters(index)
                if set([index]).issubset(core_atoms):
                    forces[force_name].addParticle(0.0 * charge, sigma,
                                                   0.0 * epsilon)
                    forces['CustomNonbondedForce'].addParticle(
                        [variant, charge, sigma, epsilon])
                elif (variant):
                    forces[force_name].addParticle(0.0 * charge, sigma,
                                                   0.0 * epsilon)
                    forces['CustomNonbondedForce'].addParticle(
                        [variant, charge, sigma, epsilon])

            for index in range(force.getNumExceptions()):
                [atom_i, atom_j, chargeProd, sigma,
                 epsilon] = force.getExceptionParameters(index)
                if set([atom_i, atom_j]).issubset(core_atoms):
                    # TODO: Nonbonded exceptions will have to be handled as CustomBondForce terms.
                    forces[force_name].addException(
                        mapping[atom_i], mapping[atom_j],
                        0.0 * unit.elementary_charge**2, 1.0 * unit.angstrom,
                        0.0 * unit.kilocalories_per_mole)
                elif (variant):
                    forces[force_name].addException(mapping[atom_i],
                                                    mapping[atom_j],
                                                    chargeProd, sigma, epsilon)

        # TODO: Add GB force processing.

    # Add exclusions to previous variants and core.
    for atom_i in mapping.values():
        for atom_j in atoms_to_exclude:
            forces['NonbondedForce'].addException(
                atom_i, atom_j, 0.0 * unit.elementary_charge**2,
                1.0 * unit.angstrom, 0.0 * unit.kilocalories_per_mole)
            forces['CustomNonbondedForce'].addExclusion(atom_i, atom_j)

    print system.getNumParticles(), forces['NonbondedForce'].getNumParticles()

    return mapping
Ejemplo n.º 27
0
def WCADimer(N=natoms,
             density=density,
             mm=None,
             mass=mass,
             epsilon=epsilon,
             sigma=sigma,
             h=h,
             r0=r0,
             w=w):
    """
    Create a bistable bonded pair of particles (indices 0 and 1) optionally surrounded by a Weeks-Chandler-Andersen fluid.

    The bistable potential has form

    U(r) = h*(1-((r-r0-w)/w)^2)^2

    where r0 is the compact state separation, r0+2w is the extended state separation, and h is the barrier height.

    The WCA potential has form

    U(r) = 4 epsilon [ (sigma/r)^12 - (sigma/r)^6 ] + epsilon      (r < r*)
         = 0                                                       (r >= r*)

    where r* = 2^(1/6) sigma.

    OPTIONAL ARGUMENTS

    N (int) - total number of atoms (default: 2)
    density (float) - number density of particles (default: 0.96 / sigma**3)
    mass (simtk.unit.Quantity of mass) - particle mass (default: 39.948 amu)
    sigma (simtk.unit.Quantity of length) - Lennard-Jones sigma parameter (default: 0.3405 nm)
    epsilon (simtk.unit.Quantity of energy) - Lennard-Jones well depth (default: (119.8 Kelvin)*kB)
    h (simtk.unit.Quantity of energy) - bistable potential barrier height (default: ???)
    r0 (simtk.unit.Quantity of length) - bistable potential compact state separation (default: ???)
    w (simtk.unit.Quantity of length) - bistable potential extended state separation is r0+2*w (default: ???)

    """

    # Choose OpenMM package.
    if mm is None:
        mm = openmm

    # Compute cutoff for WCA fluid.
    r_WCA = 2.**(1. / 6.) * sigma  # cutoff at minimum of potential

    # Create system
    system = mm.System()

    # Compute total system volume.
    volume = N / density

    # Make system cubic in dimension.
    length = volume**(1.0 / 3.0)
    a = units.Quantity(numpy.array([1.0, 0.0, 0.0], numpy.float32),
                       units.nanometer) * length / units.nanometer
    b = units.Quantity(numpy.array([0.0, 1.0, 0.0], numpy.float32),
                       units.nanometer) * length / units.nanometer
    c = units.Quantity(numpy.array([0.0, 0.0, 1.0], numpy.float32),
                       units.nanometer) * length / units.nanometer
    print("box edge length = %s" % str(length))
    system.setDefaultPeriodicBoxVectors(a, b, c)

    # Add particles to system.
    for n in range(N):
        system.addParticle(mass)

    # WCA: Lennard-Jones truncated at minim and shifted so potential is zero at cutoff.
    energy_expression = '4.0*epsilon*((sigma/r)^12 - (sigma/r)^6) + epsilon'

    # Create force.
    force = mm.CustomNonbondedForce(energy_expression)

    # Set epsilon and sigma global parameters.
    force.addGlobalParameter('epsilon', epsilon)
    force.addGlobalParameter('sigma', sigma)

    # Add particles
    for n in range(N):
        force.addParticle([])

    # Add exclusion between bonded particles.
    force.addExclusion(0, 1)

    # Set periodic boundary conditions with cutoff.
    if (N > 2):
        force.setNonbondedMethod(mm.CustomNonbondedForce.CutoffPeriodic)
    else:
        force.setNonbondedMethod(mm.CustomNonbondedForce.CutoffNonPeriodic)
    print("setting cutoff distance to %s" % str(r_WCA))
    force.setCutoffDistance(r_WCA)

    # Add nonbonded force term to the system.
    system.addForce(force)

    # Add dimer potential to first two particles.
    dimer_force = openmm.CustomBondForce('h*(1-((r-r0-w)/w)^2)^2;')
    dimer_force.addGlobalParameter('h', h)  # barrier height
    dimer_force.addGlobalParameter('r0', r0)  # compact state separation
    dimer_force.addGlobalParameter('w', w)  # second minimum is at r0 + 2*w
    dimer_force.addBond(0, 1, [])
    system.addForce(dimer_force)

    # Create initial coordinates using random positions.
    coordinates = units.Quantity(numpy.random.rand(N, 3),
                                 units.nanometer) * (length / units.nanometer)

    # Reposition dimer particles at compact minimum.
    coordinates[0, :] *= 0.0
    coordinates[1, :] *= 0.0
    coordinates[1, 0] = r0

    # Return system and coordinates.
    return [system, coordinates]
Ejemplo n.º 28
0
    def createPerturbedSystem(self, alchemical_state, mm=None, verbose=False):
        """
        Create a perturbed copy of the system given the specified alchemical state.

        ARGUMENTS

        alchemical_state (AlchemicalState) - the alchemical state to create from the reference system

        TODO

        * Start from a deep copy of the system, rather than building copy through Python interface.
        * isinstance(mm.NonbondedForce) and related expressions won't work if reference system was created with a different OpenMM implemnetation.

        EXAMPLES

        Create alchemical intermediates for 'denihilating' one water in a water box.
        
        >>> # Create a reference system.
        >>> import testsystems
        >>> [reference_system, coordinates] = testsystems.WaterBox()
        >>> # Create a factory to produce alchemical intermediates.
        >>> factory = AbsoluteAlchemicalFactory(reference_system, ligand_atoms=[0, 1, 2])
        >>> # Create an alchemically-perturbed state corresponding to fully-interacting.
        >>> alchemical_state = AlchemicalState(0.00, 1.00, 1.00, 1.)
        >>> # Create the perturbed system.
        >>> alchemical_system = factory.createPerturbedSystem(alchemical_state)
        >>> # Compare energies.
        >>> import simtk.openmm as openmm
        >>> import simtk.unit as units
        >>> timestep = 1.0 * units.femtosecond
        >>> reference_integrator = openmm.VerletIntegrator(timestep)
        >>> reference_context = openmm.Context(reference_system, reference_integrator)
        >>> reference_state = reference_context.getState(getEnergy=True)
        >>> reference_potential = reference_state.getPotentialEnergy()
        >>> alchemical_integrator = openmm.VerletIntegrator(timestep)
        >>> alchemical_context = openmm.Context(alchemical_system, alchemical_integrator)
        >>> alchemical_state = alchemical_context.getState(getEnergy=True)
        >>> alchemical_potential = alchemical_state.getPotentialEnergy()
        >>> delta = alchemical_potential - reference_potential 
        >>> print delta
        0.0 kJ/mol
        
        Create alchemical intermediates for 'denihilating' p-xylene in T4 lysozyme L99A in GBSA.
        
        >>> # Create a reference system.
        >>> import testsystems
        >>> [reference_system, coordinates] = testsystems.LysozymeImplicit()
        >>> # Compute reference potential.
        >>> timestep = 1.0 * units.femtosecond
        >>> reference_integrator = openmm.VerletIntegrator(timestep)
        >>> reference_context = openmm.Context(reference_system, reference_integrator)
        >>> reference_context.setPositions(coordinates)
        >>> reference_state = reference_context.getState(getEnergy=True)
        >>> reference_potential = reference_state.getPotentialEnergy()
        >>> # Create a factory to produce alchemical intermediates.
        >>> receptor_atoms = range(0,2603) # T4 lysozyme L99A
        >>> ligand_atoms = range(2603,2621) # p-xylene
        >>> factory = AbsoluteAlchemicalFactory(reference_system, ligand_atoms=ligand_atoms)
        >>> # Create an alchemically-perturbed state corresponding to fully-interacting.
        >>> alchemical_state = AlchemicalState(0.00, 1.00, 1.00, 1.)
        >>> # Create the perturbed systems using this protocol.
        >>> alchemical_system = factory.createPerturbedSystem(alchemical_state)
        >>> # Compare energies.        
        >>> alchemical_integrator = openmm.VerletIntegrator(timestep)
        >>> alchemical_context = openmm.Context(alchemical_system, alchemical_integrator)
        >>> alchemical_context.setPositions(coordinates)
        >>> alchemical_state = alchemical_context.getState(getEnergy=True)
        >>> alchemical_potential = alchemical_state.getPotentialEnergy()
        >>> delta = alchemical_potential - reference_potential 
        >>> print delta
        0.0 kJ/mol

        NOTES

        If lambda = 1.0 is specified for some force terms, they will not be replaced with modified forms.        

        """

        # Record timing statistics.
        initial_time = time.time()
        if verbose: print "Creating alchemically modified intermediate..."

        reference_system = self.reference_system

        # Create new deep copy reference system to modify.
        system = openmm.System()

        # Set periodic box vectors.
        [a, b, c] = reference_system.getDefaultPeriodicBoxVectors()
        system.setDefaultPeriodicBoxVectors(a, b, c)

        # Add atoms.
        for atom_index in range(reference_system.getNumParticles()):
            mass = reference_system.getParticleMass(atom_index)
            system.addParticle(mass)

        # Add constraints
        for constraint_index in range(reference_system.getNumConstraints()):
            [iatom, jatom,
             r0] = reference_system.getConstraintParameters(constraint_index)
            system.addConstraint(iatom, jatom, r0)

        # Modify forces as appropriate, copying other forces without modification.
        nforces = reference_system.getNumForces()
        for force_index in range(nforces):
            reference_force = reference_system.getForce(force_index)

            if isinstance(reference_force, openmm.PeriodicTorsionForce):
                # PeriodicTorsionForce
                force = openmm.PeriodicTorsionForce()
                for torsion_index in range(reference_force.getNumTorsions()):
                    # Retrieve parmaeters.
                    [
                        particle1, particle2, particle3, particle4,
                        periodicity, phase, k
                    ] = reference_force.getTorsionParameters(torsion_index)
                    # Scale torsion barrier of alchemically-modified system.
                    if set([particle1, particle2, particle3,
                            particle4]).issubset(self.ligand_atomset):
                        k *= alchemical_state.ligandTorsions
                    force.addTorsion(particle1, particle2, particle3,
                                     particle4, periodicity, phase, k)
                system.addForce(force)

            elif isinstance(reference_force, openmm.NonbondedForce):

                # Copy NonbondedForce.
                force = copy.deepcopy(reference_force)
                system.addForce(force)

                # Modify electrostatics.
                if alchemical_state.ligandElectrostatics != 1.0:
                    for particle_index in range(force.getNumParticles()):
                        # Retrieve parameters.
                        [charge, sigma,
                         epsilon] = force.getParticleParameters(particle_index)
                        # Alchemically modify charges.
                        if particle_index in self.ligand_atomset:
                            charge *= alchemical_state.ligandElectrostatics
                        # Set modified particle parameters.
                        force.setParticleParameters(particle_index, charge,
                                                    sigma, epsilon)
                    for exception_index in range(force.getNumExceptions()):
                        # Retrieve parameters.
                        [iatom, jatom, chargeprod, sigma, epsilon
                         ] = force.getExceptionParameters(exception_index)
                        # Alchemically modify chargeprod.
                        if (iatom in self.ligand_atomset) and (
                                jatom in self.ligand_atomset):
                            if alchemical_state.annihilateElectrostatics:
                                chargeprod *= alchemical_state.ligandElectrostatics**2
                        # Set modified exception parameters.
                        force.setExceptionParameters(exception_index, iatom,
                                                     jatom, chargeprod, sigma,
                                                     epsilon)

                # Modify Lennard-Jones if required.
                if alchemical_state.ligandLennardJones != 1.0:
                    # Create softcore Lennard-Jones interactions by modifying NonbondedForce and adding CustomNonbondedForce.
                    #self._alchemicallyModifyLennardJones(system, force, self.ligand_atoms, alchemical_state)
                    self._alchemicallyModifyLennardJonesGroup(
                        system, force, self.ligand_atoms, alchemical_state)

            elif isinstance(reference_force, openmm.GBSAOBCForce) and (
                    alchemical_state.ligandElectrostatics != 1.0):

                # Create a CustomNonbondedForce to implement softcore interactions.
                particle_lambdas = numpy.ones([system.getNumParticles()],
                                              numpy.float32)
                particle_lambdas[
                    self.ligand_atoms] = alchemical_state.ligandElectrostatics
                custom_force = AbsoluteAlchemicalFactory._createCustomSoftcoreGBOBC(
                    reference_force, particle_lambdas)
                system.addForce(custom_force)

            elif isinstance(reference_force, openmm.CustomExternalForce):

                force = openmm.CustomExternalForce(
                    reference_force.getEnergyFunction())
                for parameter_index in range(
                        reference_force.getNumGlobalParameters()):
                    name = reference_force.getGlobalParameterName(
                        parameter_index)
                    default_value = reference_force.getGlobalParameterDefaultValue(
                        parameter_index)
                    force.addGlobalParameter(name, default_value)
                for parameter_index in range(
                        reference_force.getNumPerParticleParameters()):
                    name = reference_force.getPerParticleParameterName(
                        parameter_index)
                    force.addPerParticleParameter(name)
                for index in range(reference_force.getNumParticles()):
                    [particle_index,
                     parameters] = reference_force.getParticleParameters(index)
                    force.addParticle(particle_index, parameters)
                system.addForce(force)

            elif isinstance(reference_force, openmm.CustomBondForce):

                force = openmm.CustomBondForce(
                    reference_force.getEnergyFunction())
                for parameter_index in range(
                        reference_force.getNumGlobalParameters()):
                    name = reference_force.getGlobalParameterName(
                        parameter_index)
                    default_value = reference_force.getGlobalParameterDefaultValue(
                        parameter_index)
                    force.addGlobalParameter(name, default_value)
                for parameter_index in range(
                        reference_force.getNumPerBondParameters()):
                    name = reference_force.getPerBondParameterName(
                        parameter_index)
                    force.addPerBondParameter(name)
                for index in range(reference_force.getNumBonds()):
                    [particle1, particle2,
                     parameters] = reference_force.getBondParameters(index)
                    force.addBond(particle1, particle2, parameters)
                system.addForce(force)

            else:

                # Copy force without modification.
                force = copy.deepcopy(reference_force)
                system.addForce(force)

        # Record timing statistics.
        final_time = time.time()
        elapsed_time = final_time - initial_time
        if verbose: print "Elapsed time %.3f s." % (elapsed_time)

        return system
Ejemplo n.º 29
0
# Add umbrella restraint with global variable to control umbrella position
print('umbrella schedule for dihedral defined by atoms %s : %s' % (str(Roux_atoms), str(torsion_umbrella_values)))
print('umbrella schedule for distance defined by atoms %s : %s' % (str(DFG_atoms), str(distance_umbrella_values)))

from numpy import pi
energy_function = '- (torsion_K/2) * cos(min(dtheta, 2*pi-dtheta)); dtheta = abs(theta-torsion_theta0);'
energy_function += 'pi = %f;' % pi
umbrella_force = openmm.CustomTorsionForce(energy_function)
umbrella_force.addGlobalParameter('torsion_K', 0.0)
umbrella_force.addGlobalParameter('torsion_theta0', 0.0)
umbrella_force.addTorsion(*Roux_atoms, [])
torsion_K = kT/angle_sigma**2
system.addForce(umbrella_force)

energy_function = '(distance_K/2) * (r-distance_r0)^2;'
umbrella_force = openmm.CustomBondForce(energy_function)
umbrella_force.addGlobalParameter('distance_K', 0.0)
umbrella_force.addGlobalParameter('distance_r0', 0.0)
umbrella_force.addBond(*DFG_atoms, [])
distance_K = kT/distance_sigma**2
system.addForce(umbrella_force)

# Create thermodynamic states
thermodynamic_states = list()

# Umbrella off state
parameters = {
    'torsion_K' : 0.0, 'torsion_theta0' : 0.0, # umbrella parameters
    'distance_K' : 0.0, 'distance_r0' : 0.0, # umbrella parameters
}
thermodynamic_states.append( ThermodynamicState(system=system, temperature=temperature, pressure=pressure, parameters=parameters) )
Ejemplo n.º 30
0
def setup_fah_run(destination_path,
                  protein_pdb_filename,
                  oemol=None,
                  cache=None,
                  restrain_rmsd=False):
    """
    Prepare simulation

    Parameters
    ----------
    destination_path : str
        The path to the RUN to be created
    protein_pdb_filename : str
        Path to protein PDB file
    oemol : openeye.oechem.OEMol, optional, default=None
        The molecule to parameterize, with SDData attached
        If None, don't include the small molecule
    restrain_rmsd : bool, optional, default=False
        If True, restrain RMSD during first equilibration phase
    """
    # Parameters
    from simtk import unit, openmm
    protein_forcefield = 'amber14/protein.ff14SB.xml'
    solvent_forcefield = 'amber14/tip3p.xml'
    small_molecule_forcefield = 'openff-1.2.0'
    water_model = 'tip3p'
    solvent_padding = 10.0 * unit.angstrom
    ionic_strength = 70 * unit.millimolar  # assay buffer: 20 mM HEPES pH 7.3, 1 mM TCEP, 50 mM NaCl, 0.01% Tween-20, 10% glycerol
    pressure = 1.0 * unit.atmospheres
    collision_rate = 1.0 / unit.picoseconds
    temperature = 300.0 * unit.kelvin
    timestep = 4.0 * unit.femtoseconds
    iterations = 1000  # 1 ns equilibration
    nsteps_per_iteration = 250

    # Prepare phases
    import os
    system_xml_filename = os.path.join(destination_path, 'system.xml.bz2')
    integrator_xml_filename = os.path.join(destination_path,
                                           'integrator.xml.bz2')
    state_xml_filename = os.path.join(destination_path, 'state.xml.bz2')

    # Check if we can skip setup
    openmm_files_exist = os.path.exists(
        system_xml_filename) and os.path.exists(
            state_xml_filename) and os.path.exists(integrator_xml_filename)
    if openmm_files_exist:
        return

    # Create barostat
    barostat = openmm.MonteCarloBarostat(pressure, temperature)

    # Create RUN directory if it does not yet exist
    os.makedirs(destination_path, exist_ok=True)

    # Load any molecule(s)
    molecule = None
    if oemol is not None:
        from openforcefield.topology import Molecule
        molecule = Molecule.from_openeye(oemol, allow_undefined_stereo=True)
        molecule.name = 'MOL'  # Ensure residue is MOL
        print([res for res in molecule.to_topology().to_openmm().residues()])

    # Create SystemGenerator
    import os
    from simtk.openmm import app
    forcefield_kwargs = {
        'removeCMMotion': False,
        'hydrogenMass': 3.0 * unit.amu,
        'constraints': app.HBonds,
        'rigidWater': True
    }
    periodic_kwargs = {
        'nonbondedMethod': app.PME,
        'ewaldErrorTolerance': 2.5e-04
    }
    forcefields = [protein_forcefield, solvent_forcefield]
    from openmmforcefields.generators import SystemGenerator
    openmm_system_generator = SystemGenerator(
        forcefields=forcefields,
        molecules=molecule,
        small_molecule_forcefield=small_molecule_forcefield,
        cache=cache,
        barostat=barostat,
        forcefield_kwargs=forcefield_kwargs,
        periodic_forcefield_kwargs=periodic_kwargs)

    # Read protein
    print(f'Reading protein from {protein_pdb_filename}...')
    pdbfile = app.PDBFile(protein_pdb_filename)
    modeller = app.Modeller(pdbfile.topology, pdbfile.positions)

    if oemol is not None:
        # Add small molecule to the system
        modeller.add(molecule.to_topology().to_openmm(),
                     molecule.conformers[0])
        # DEBUG : Check residue name
        with open(os.path.join(destination_path, 'initial-complex.pdb'),
                  'wt') as outfile:
            app.PDBFile.writeFile(modeller.topology, modeller.positions,
                                  outfile)

    # Add solvent
    print('Adding solvent...')
    kwargs = {'padding': solvent_padding}
    modeller.addSolvent(openmm_system_generator.forcefield,
                        model='tip3p',
                        ionicStrength=ionic_strength,
                        **kwargs)

    # Create an OpenMM system
    print('Creating OpenMM system...')
    system = openmm_system_generator.create_system(modeller.topology)

    # Add a virtual bond between protein and ligand to make sure they are not imaged separately
    if oemol is not None:
        import mdtraj as md
        mdtop = md.Topology.from_openmm(
            modeller.topology)  # excludes solvent and ions
        for res in mdtop.residues:
            print(res)
        protein_atom_indices = mdtop.select(
            '(protein and name CA)')  # protein CA atoms
        ligand_atom_indices = mdtop.select(
            '((resname MOL) and (mass > 1))')  # ligand heavy atoms
        protein_atom_index = int(protein_atom_indices[0])
        ligand_atom_index = int(ligand_atom_indices[0])
        force = openmm.CustomBondForce('0')
        force.addBond(protein_atom_index, ligand_atom_index, [])
        system.addForce(force)

    # Add RMSD restraints if requested
    if restrain_rmsd:
        print('Adding RMSD restraint...')
        kB = unit.AVOGADRO_CONSTANT_NA * unit.BOLTZMANN_CONSTANT_kB
        kT = kB * temperature
        import mdtraj as md
        mdtop = md.Topology.from_openmm(
            pdbfile.topology)  # excludes solvent and ions
        #heavy_atom_indices = mdtop.select('mass > 1') # heavy solute atoms
        rmsd_atom_indices = mdtop.select(
            '(protein and (name CA)) or ((resname MOL) and (mass > 1))'
        )  # CA atoms and ligand heavy atoms
        rmsd_atom_indices = [int(index) for index in rmsd_atom_indices]
        custom_cv_force = openmm.CustomCVForce('(K_RMSD/2)*RMSD^2')
        custom_cv_force.addGlobalParameter('K_RMSD', kT / unit.angstrom**2)
        rmsd_force = openmm.RMSDForce(modeller.positions, rmsd_atom_indices)
        custom_cv_force.addCollectiveVariable('RMSD', rmsd_force)
        force_index = system.addForce(custom_cv_force)

    # Create OpenM Context
    platform = openmm.Platform.getPlatformByName('OpenCL')
    platform.setPropertyDefaultValue('Precision', 'mixed')
    from openmmtools import integrators
    integrator = integrators.LangevinIntegrator(temperature, collision_rate,
                                                timestep)
    context = openmm.Context(system, integrator, platform)
    context.setPositions(modeller.positions)

    # Report initial potential energy
    state = context.getState(getEnergy=True)
    print(
        f'Initial potential energy is {state.getPotentialEnergy()/unit.kilocalories_per_mole:.3f} kcal/mol'
    )

    # Store snapshots in MDTraj trajectory to examine RMSD
    import mdtraj as md
    import numpy as np
    mdtop = md.Topology.from_openmm(pdbfile.topology)
    atom_indices = mdtop.select('all')  # all solute atoms
    protein_atom_indices = mdtop.select(
        'protein and (mass > 1)')  # heavy solute atoms
    if oemol is not None:
        ligand_atom_indices = mdtop.select(
            '(resname MOL) and (mass > 1)')  # ligand heavy atoms
    trajectory = md.Trajectory(
        np.zeros([iterations + 1, len(atom_indices), 3], np.float32), mdtop)
    trajectory.xyz[0, :, :] = context.getState(getPositions=True).getPositions(
        asNumpy=True)[atom_indices] / unit.nanometers

    # Minimize
    print('Minimizing...')
    openmm.LocalEnergyMinimizer.minimize(context)

    # Equilibrate (with RMSD restraint if needed)
    import numpy as np
    from rich.progress import track
    import time
    initial_time = time.time()
    for iteration in track(range(iterations), 'Equilibrating...'):
        integrator.step(nsteps_per_iteration)
        trajectory.xyz[iteration + 1, :, :] = context.getState(
            getPositions=True).getPositions(
                asNumpy=True)[atom_indices] / unit.nanometers
    elapsed_time = (time.time() - initial_time) * unit.seconds
    ns_per_day = (context.getState().getTime() /
                  elapsed_time) / (unit.nanoseconds / unit.day)
    print(f'Performance: {ns_per_day:8.3f} ns/day')

    if restrain_rmsd:
        # Disable RMSD restraint
        context.setParameter('K_RMSD', 0.0)

        print('Minimizing...')
        openmm.LocalEnergyMinimizer.minimize(context)

        for iteration in track(range(iterations),
                               'Equilibrating without RMSD restraint...'):
            integrator.step(nsteps_per_iteration)

    # Retrieve state
    state = context.getState(getPositions=True,
                             getVelocities=True,
                             getEnergy=True,
                             getForces=True)
    system.setDefaultPeriodicBoxVectors(*state.getPeriodicBoxVectors())
    modeller.topology.setPeriodicBoxVectors(state.getPeriodicBoxVectors())
    print(
        f'Final potential energy is {state.getPotentialEnergy()/unit.kilocalories_per_mole:.3f} kcal/mol'
    )

    # Equilibrate again if we restrained the RMSD
    if restrain_rmsd:
        print('Removing RMSD restraint from system...')
        system.removeForce(force_index)

    #if oemol is not None:
    #    # Check final RMSD
    #    print('checking RMSD...')
    #    trajectory.superpose(trajectory, atom_indices=protein_atom_indices)
    #    protein_rmsd = md.rmsd(trajectory, trajectory[-1], atom_indices=protein_atom_indices)[-1] * 10 # Angstroms
    #    oechem.OESetSDData(oemol, 'equil_protein_rmsd', f'{protein_rmsd:.2f} A')
    #    ligand_rmsd = md.rmsd(trajectory, trajectory[-1], atom_indices=ligand_atom_indices)[-1] * 10 # Angstroms
    #    oechem.OESetSDData(oemol, 'equil_ligand_rmsd', f'{ligand_rmsd:.2f} A')
    #    print('RMSD after equilibration: protein {protein_rmsd:8.2f} A | ligand {ligand_rmsd:8.3f} A')

    # Save as OpenMM
    print('Exporting for OpenMM FAH simulation...')
    import bz2
    with bz2.open(integrator_xml_filename, 'wt') as f:
        f.write(openmm.XmlSerializer.serialize(integrator))
    with bz2.open(state_xml_filename, 'wt') as f:
        f.write(openmm.XmlSerializer.serialize(state))
    with bz2.open(system_xml_filename, 'wt') as f:
        f.write(openmm.XmlSerializer.serialize(system))
    with bz2.open(os.path.join(destination_path, 'equilibrated-all.pdb.gz'),
                  'wt') as f:
        app.PDBFile.writeFile(modeller.topology, state.getPositions(), f)
    with open(os.path.join(destination_path, 'equilibrated-solute.pdb'),
              'wt') as f:
        import mdtraj
        mdtraj_topology = mdtraj.Topology.from_openmm(modeller.topology)
        mdtraj_trajectory = mdtraj.Trajectory(
            [state.getPositions(asNumpy=True) / unit.nanometers],
            mdtraj_topology)
        selection = mdtraj_topology.select('not water')
        mdtraj_trajectory = mdtraj_trajectory.atom_slice(selection)
        app.PDBFile.writeFile(mdtraj_trajectory.topology.to_openmm(),
                              mdtraj_trajectory.openmm_positions(0), f)
    if oemol is not None:
        # Write molecule as SDF, SMILES, and mol2
        for extension in ['sdf', 'mol2', 'smi', 'csv']:
            filename = os.path.join(destination_path, f'molecule.{extension}')
            with oechem.oemolostream(filename) as ofs:
                oechem.OEWriteMolecule(ofs, oemol)

    # Clean up
    del context, integrator