Loading and modifying a SMIRNOFF-format forcefield

This notebook illustrates how to load a SMIRNOFF-format forcefield, apply it to an example molecule, get the energy, then manipulate the parameters in the forcefield and update the energy.

Prep some utility functions/import stuff


In [1]:
from openforcefield.topology import Molecule, Topology
from openforcefield.typing.engines.smirnoff.forcefield import ForceField
from openforcefield.utils import get_data_file_path
from simtk import openmm, unit
import numpy as np

Define utility function we'll use to get energy of an OpenMM system


In [2]:
def get_energy(system, positions):
    """
    Return the potential energy.

    Parameters
    ----------
    system : simtk.openmm.System
        The system to check
    positions : simtk.unit.Quantity of dimension (natoms,3) with units of length
        The positions to use
    Returns
    ---------
    energy
    """

    integrator = openmm.VerletIntegrator(1.0 * unit.femtoseconds)
    context = openmm.Context(system, integrator)
    context.setPositions(positions)
    state = context.getState(getEnergy=True)
    energy = state.getPotentialEnergy().in_units_of(unit.kilocalories_per_mole)
    return energy

Example 1: Load a molecule and evaluate its energy before and after a parameter modification

In this example, we load a single ethanol molecule with geometry information, parameterize it using the smirnoff99Frosst forcefield, and evaluate its energy. We then modify the parameter that is applied to the C-O-H angle and re-evaluate the energy.

Load a molecule


In [3]:
molecule = Molecule.from_file(get_data_file_path('molecules/ethanol.sdf'))

Get positions for use below


In [4]:
positions = molecule.conformers[0]

Load the smirnoff99Frosst forcefield file


In [5]:
ff = ForceField('openff-1.0.0.offxml')

Generate an openforcefield Topology containing only this molecule


In [6]:
topology = molecule.to_topology()

Parameterize the molecule, creating an OpenMM system


In [7]:
orig_system = ff.create_openmm_system(topology)

Calculate energy before parameter modification


In [8]:
orig_energy = get_energy(orig_system, positions)
print(f"Original energy: {orig_energy}")


Original energy: -2.4072882322234816 kcal/mol

Get parameters for the C-O-H angle


In [9]:
smirks = '[*:1]-[#8:2]-[*:3]' # SMIRKS for the parameter to retrieve
parameter = ff.get_parameter_handler('Angles').parameters[smirks]

Modify the parameters


In [10]:
parameter.k *= 0.9
parameter.angle *= 1.1

Evaluate energy after parameter modification


In [11]:
new_system = ff.create_openmm_system(topology)
new_energy = get_energy(new_system, positions)

Print out energy


In [12]:
print(f"Original energy: {orig_energy}. New energy: {new_energy}")


Original energy: -2.4072882322234816 kcal/mol. New energy: 0.5977504795869495 kcal/mol

Example 2: Inspect and manipulate nonbonded treatment

The SMIRNOFF spec aims to specify all aspects of a system that contribute to the energy within the ForceField object. This includes the treatment of long-range electrostatics and van der Waals interactions. This may be different for some users, as other packages set these parameters at runtime, for example in an AMBER mdin file or GROMACS MDP file.

This example evaluates the energy of a periodic box of solvent molecules using the "standard" smirnoff99Frosst settings (PME for electrostatics, 9 Angstrom cutoff for vdW interactions). It then changes the ForceField's vdW treatment method to "PME" and re-evaluates the energy.

Note: The Open Force Field toolkit ensures that its `create_openmm_system` function produces a system that employs the `ForceField`-specified nonbonded treatment. However, operations which convert this system to AMBER or GROMACS-format topologies/structures are likely to lose these details, as there is no equivalent data field in those objects. In the future we will work on developing robust ways to create other system formats which preserve all details of a `ForceField` object.

In [13]:
# Create a new ForceField containing the smirnoff99Frosst parameter set:
forcefield = ForceField('openff-1.0.0.offxml')
# Inspect the long-range van der Waals method:
vdw_handler = forcefield.get_parameter_handler('vdW')
print(f"The vdW method is currently set to: {vdw_handler.method}")


The vdW method is currently set to: cutoff

Select a solvent box to parameterize.

Note: This process will parameterize water using smirnoff99Frosst parameters. We do not recommend this, and instead suggest parameterizing water externally with a model like TIP3P and merging systems using ParmEd.

In [14]:
from simtk.openmm import app

# A 239-molecule mixture of cyclohexane and ethanol
pdbfile = app.PDBFile(get_data_file_path('systems/packmol_boxes/cyclohexane_ethanol_0.4_0.6.pdb'))

# A 340-molecule mixture of propane, methane, and butanol.
#pdbfile = app.PDBFile(get_data_file_path('systems/packmol_boxes/propane_methane_butanol_0.2_0.3_0.5.pdb'))

# One cyclohexane in a box of roughly 1400 waters
#pdbfile = app.PDBFile(get_data_file_path('systems/packmol_boxes/cyclohexane_water.pdb'))

# One ethanol in a box of roughly 1300 waters
#pdbfile = app.PDBFile(get_data_file_path('systems/packmol_boxes/ethanol_water.pdb'))

Provide a "complete" (including bond orders, charges, and stereochemistry) representation of each molecule that might be in the PDB. This is necessary because a PDB representation of a molecule does not contain sufficient information for parameterization.


In [15]:
molecules = [Molecule.from_smiles('C'), # methane
             Molecule.from_smiles('CCC'),# propane
             Molecule.from_smiles('CCCCO'), # butanol
             Molecule.from_smiles('O'), # water
             Molecule.from_smiles('CCO'), #ethanol
             Molecule.from_smiles('C1CCCCC1'), #cyclohexane
            ]

Create an Open Force Field Topology object by matching the Open Force Field molecules defined above to those in the PDB

Note: This function is currently unoptimized and may take a minute to run.

In [16]:
top = Topology.from_openmm(pdbfile.topology, unique_molecules=molecules)
orig_system = forcefield.create_openmm_system(top)
orig_energy = get_energy(orig_system, pdbfile.getPositions())
print(f"Original energy: {orig_energy}")


Original energy: 1094.735978367715 kcal/mol

Change the long-range van der Waals method to be PME


In [17]:
vdw_handler.method = 'PME'
print(f"The vdW method is currently set to: {vdw_handler.method}")


The vdW method is currently set to: PME

The Open Force Field toolkit applies vdW parameters using a SMIRKS-based typing scheme. Inspect the first few vdW parameters. These can be changed programmatically, as shown in example 1.


In [18]:
for vdw_param in forcefield.get_parameter_handler('vdW').parameters[0:3]:
    print(vdw_param)


<vdWType with smirks: [#1:1]  epsilon: 0.0157 kcal/mol  id: n1  rmin_half: 0.6 A  >
<vdWType with smirks: [#1:1]-[#6X4]  epsilon: 0.0157 kcal/mol  id: n2  rmin_half: 1.487 A  >
<vdWType with smirks: [#1:1]-[#6X4]-[#7,#8,#9,#16,#17,#35]  epsilon: 0.0157 kcal/mol  id: n3  rmin_half: 1.387 A  >

Now recompute the energy of the system using PME for long-range vdW interactions

Note: This function is currently unoptimized and may take a minute to run.

In [19]:
new_system = forcefield.create_openmm_system(top)
new_energy = get_energy(new_system, pdbfile.getPositions())

print(f"Original energy (with LJ cutoff): {orig_energy}")
print(f"New energy (using LJ PME): {new_energy}")


Original energy (with LJ cutoff): 1094.735978367715 kcal/mol
New energy (using LJ PME): 1096.758351182301 kcal/mol