Skip to content

Quickstart

A single particle on a two-dimensional Müller-Brown potential, biased along x and y, recovering the free energy surface.

import numpy as np
import openmm as mm
from openmm import app, unit

from openmm_opes import OPES

KB = unit.MOLAR_GAS_CONSTANT_R.value_in_unit(
    unit.kilojoules_per_mole / unit.kelvin
)

muller_brown = (
    "-200*exp(-(x-1)^2-10*y^2)"
    " -100*exp(-x^2-10*(y-0.5)^2)"
    " -170*exp(-6.5*(0.5+x)^2+11*(x+0.5)*(y-1.5)-6.5*(y-1.5)^2)"
    " +15*exp(0.7*(1+x)^2+0.6*(x+1)*(y-1)+0.7*(y-1)^2)"
)

system = mm.System()
system.addParticle(1.0)
potential = mm.CustomExternalForce(muller_brown)
potential.addParticle(0, [])
system.addForce(potential)

variables = []
for expression, lo, hi in (("x", -1.5, 1.2), ("y", -0.2, 2.0)):
    force = mm.CustomExternalForce(expression)
    force.addParticle(0, [])
    variables.append(app.BiasVariable(force, lo, hi, 0.1, False, 101))

temperature = 1.0 / KB * unit.kelvin
sampler = OPES(
    system,
    variables,
    temperature,
    barrier=20 * unit.kilojoules_per_mole,
    frequency=500,
    varianceFrequency=50,
)

topology = app.Topology()
topology.addAtom("P", None, topology.addResidue("MOL", topology.addChain()))
integrator = mm.LangevinMiddleIntegrator(
    temperature, 10.0 / unit.picosecond, 0.005 * unit.picoseconds
)
simulation = app.Simulation(
    topology, system, integrator, mm.Platform.getPlatformByName("Reference")
)
simulation.context.setPositions([mm.Vec3(-0.5, 1.4, 0.0)])
simulation.context.setVelocitiesToTemperature(temperature)

sampler.step(simulation, 2_000_000)

fes = sampler.getFreeEnergy().value_in_unit(unit.kilojoules_per_mole)
fes -= fes.min()
print(f"{sampler.getNumKernels()} kernels, barrier {fes.max():.1f} kJ/mol")

getFreeEnergy() returns an array whose shape is the CV grid with the last variable varying fastest, so for two CVs it has shape (ny, nx) and can be handed straight to matplotlib.pyplot.contourf.

Choosing the bandwidth

Both papers prescribe measuring the initial bandwidth from a short unbiased run. You can let the sampler do that for you:

sampler = OPES(..., varianceFrequency=50, warmupSteps=50_000)

Nothing is deposited during the first 50,000 steps; the CV variance measured there becomes the initial bandwidth and is then held fixed.

To fix the bandwidth yourself instead, pass varianceFrequency=None and set each variable's biasWidth. It is read as \(\sigma^{(0)}\), the standard deviation of the unbiased distribution, in both modes. OPES-explore kernels estimate the sampled distribution, which is \(\sqrt{\gamma}\) times wider, so they are deposited \(\sqrt{\gamma}\) times wider than biasWidth.