Getting Started

This page details how to perform free energy calculations between a molecular mechanics force field (in this case the open-forcefield is used) and a neural network potential (ANI-2x).

Installation

We recommend setting up a new python conda environment with python=3.9 and installing the packages defined here using mamba. This package can be installed using: pip install git+https://github.com/wiederm/endstate_correction.git.

How to use this package

We have prepared two scripts that should help to use this package, both are located in endstate_correction/scripts. We will start by describing the use of the generate_endstate_samples.py script and then discuss the perform_correction.py script.

A typical Non-equilibrium (NEQ) switching workflow

Generate the equilibrium distribution \(\pi(x, \lambda=0)\)

In order to perform a NEQ work protocol, we need samples drawn from the equilibrium distribution from which we initialize our annealing simulations. If samples are not already available, the generate_endstate_samples.py script provides and easy way to obtain these samples.

In the following we will use a molecule from the HiPen dataset with the SMILES string “”Cn1cc(Cl)c(/C=N/O)n1” as a test system.

The scripts starts by defining some control parameters such as n_samples (number of samples that should be generated), n_steps_per_sample (integration steps between samples), base (where to store the samples).

We then start setting the simulation object

forcefield = ForceField("openff_unconstrained-2.0.0.offxml")
env = "vacuum"

potential = MLPotential("ani2x")
molecule = Molecule.from_smiles(smiles, hydrogens_are_explicit=False) # generate molecule from smile
molecule.generate_conformers(n_conformers=1) # generate a single conforamtion

topology = molecule.to_topology()
system = forcefield.create_openmm_system(topology)
# define region that should be treated with the nnp
ml_atoms = [atom.molecule_particle_index for atom in topology.atoms]
# set up the integrator and the platform (here we are assuming you have a CUDA GPU)
integrator = LangevinIntegrator(temperature, collision_rate, stepsize)
platform = Platform.getPlatformByName("CUDA")
# we are creating a mixed system using openmm-ml
topology = topology.to_openmm()
ml_system = potential.createMixedSystem(topology, system, ml_atoms, interpolate=True)
sim = Simulation(topology, ml_system, integrator, platform=platform)

Note that we explicitly define the atoms that should be perturbed from the reference to the target potential using

ml_atoms = [atom.molecule_particle_index for atom in topology.atoms]

If you want to perform bidirectional FEP or NEQ you need to sample at \(\pi(x, \lambda=0)\) and \(\pi(x, \lambda=1)\). This can be controlled by setting the number using the variable nr_lambda_states. The default value set in the generate_endstate_samples.py script is nr_lambda_states=2, generating samples from both endstate equilibrium distributions.

nr_lambda_states = 2  # samples equilibrium distribution at both endstates
lambs = np.linspace(0, 1, nr_lambda_states)

Perform unidirectional NEQ from \(\pi(x, \lambda=0)\)

The endstate correction can be performed using the script perform_correction.py. The script will calculate the free energy estimate using the samples generated with the generate_endstate_samples.py script. Subsequently, the relevant sections of the perform_correction.py script are explained — but they should work for for the testsystem without any modifications.

To perform a specific endstate correction we need to define a protocol (some standard protocols are shown here) with:

neq_protocol = NEQProtocol(
    sim=sim,
    reference_samples=mm_samples,
    nr_of_switches=100,
    neq_switching_length=1_000,
    save_endstates=False,
    save_trajs=False
)

This protocol is then passed to the actual function performing the protocol: perform_endstate_correction(neq_protocol). The particular code above defines unidirectional NEQ switching using 100 switches and a switching length of 1 ps. The direciton of the switching simulation is controlled by the sampels that are provided: if reference_samples are provided, switching is performed from the reference to the target level of theory, if target_samples are provided, switching is performed from the target level to the reference level. If both samples are provided, bidirectional NEQ switching is performed (for an example see below). There is the possibility to save the endstate of each switch if save_endstates is set to True. Additionally, the switching trajectory of each switch can be saved if save_trajs=True. Both options only make sense for a NEQ protocol.

Perform bidirectional NEQ from \(\pi(x, \lambda=0)\) and \(\pi(x, \lambda=1)\)

The endstate correction can be performed using the script perform_correction.py and the following protocol.

neq_protocol = NEQProtocol(
    sim=sim,
    reference_samples=mm_samples,
    target_samples=nnp_samples,
    nr_of_switches=100,
    neq_switching_length=1_000,
)

This protocol is then passed to the actual function performing the protocol: perform_endstate_correction(neq_protocol).

Perform unidirectional FEP from \(\pi(x, \lambda=0)\)

The protocol has to be adopted slightly:

fep_protocol = FEPProtocol(
    sim=sim,
    reference_samples=mm_samples,
    nr_of_switches=1_000,
)

This protocol is then passed to the actual function performing the protocol: perform_endstate_correction(fep_protocol).

Perform unidirectional SMC from \(\pi(x, \lambda=0)\)

To perform a unidirectional Sequential Monte-Carlo (SMC) protocol, the following adaptations have to be made:

smc_protocol = SMCProtocol(
    sim=sim,
    reference_samples=mm_samples,
    nr_of_walkers=100,
    protocol_length=1_000
    nr_of_resampling_steps=100,
    save_endstates=True
)

Here, nr_of_walkers indicates the number of walkers selected from the reference distribution, which are propagated for 1ps (protocol_length). nr_of nr_of_resampling_steps states that the walkers are resampled 100 times. With the option save_endstates samples at the end of the protocol can be saved.

Perform multiple protocols

It is possible to combine several protocols into one with AllProtocol.

protocol = AllProtocol(
    fep_protocol=fep_protocol,
    neq_protocol=neq_protocol,
    smc_protocol=smc_protocol
)

This protocol is then passed to perform_endstate_correction() to perform all endstate corrections.

Analyse results of an unidirection NEQ protocol

To analyse the results generated by r = perform_endstate_correction() pass the return value to plot_endstate_correction_results(system_name, r, "results_neq_unidirectional.png") and results will be plotted and printed. It should be noted, that perform_endstate_correction() returns an instance of AllResults(), which, depending on the passed protocol, contains results of an FEP, NEQ switching and SMC protocol (instances of the FEPResults(), NEQResults() and SMCResults() class, respectively). It is also possible to add equilibrium sampling results to the instance of AllResults() retrospectively (EquResults()).

Available protocols

unidirectional NEQ protocol (reference to target)

neq_protocol = NEQProtocol(
    sim=sim,
    reference_samples=mm_samples,
    nr_of_switches=100,
    neq_switching_length=1_000,
)

bidirectional NEQ protocol (save switching trajectory and endstate of each switch)

neq_protocol = NEQProtocol(
    sim=sim,
    reference_samples=mm_samples,
    target_samples=nnp_samples,
    nr_of_switches=100,
    neq_switching_length=1_000,
    save_endstates=True,
    save_trajs=True,
)

unidirectional FEP protocol (reference to target)

fep_protocol = FEPProtocol(
    sim=sim,
    reference_samples=mm_samples,
    nr_of_switches=1_000,
)

bidirectional FEP protocol

fep_protocol = FEPProtocol(
    sim=sim,
    reference_samples=mm_samples,
    target_samples=nnp_samples,
    nr_of_switches=1_000,
)

unidirectional SMC protocol (reference to target)

smc_protocol = SMCProtocol(
    sim=sim,
    reference_samples=mm_samples,
    nr_of_walkers=100,
    nr_of_resampling_steps=1_000,
)