Plotting OpenFE results against experiment using Cinnabar v0.6.0#

This notebook shows how one would go about creating a cinnabar style plot of OpenFE results against known experimental values. For more information on the cinnabar API and the available plot styles, see the tutorials.

Setup for Google Colab#

If you are running this example in Google Colab, run the following cells to setup the environment. If you are running this notebook locally, skip down to Loading experimental data

[1]:
# NBVAL_SKIP
# Only run this cell if on Google Colab
import os
if "COLAB_RELEASE_TAG" in os.environ:
    # fix for colab's torchvision causing issues
    !rm -r /usr/local/lib/python3.12/dist-packages/torchvision

    !pip install -q condacolab
    import condacolab
    condacolab.install_from_url("https://github.com/OpenFreeEnergy/openfe/releases/download/v1.7.0/OpenFEforge-1.7.0-Linux-x86_64.sh")
[2]:
# NBVAL_SKIP
# Only run this cell if on google colab
import os
if "COLAB_RELEASE_TAG" in os.environ:
    import condacolab
    import locale
    locale.getpreferredencoding = lambda: "UTF-8"
    !mkdir inputs && cd inputs && openfe fetch rbfe-tutorial
    # quick fix for https://github.com/conda-incubator/condacolab/issues/75
    # if deprecation warnings persist, rerun this cell
    import warnings
    warnings.filterwarnings(action="ignore", message=r"datetime.datetime.utcnow")
    for _ in range(3):
      # Sometimes we have to re-run the check
      try:
        condacolab.check()
      except:
        pass
      else:
        break

Loading experimental data#

First, we load the experimental data from a tab-separated values (TSV) file.

The format of the TSV file is as follows:

ligand  estimate (kcal/mol)     uncertainty (kcal/mol)

Important note: Please note that for now you must have an experimental entry for every ligand involved in your free energy network. In future versions of cinnabar this will no longer be needed.

[3]:
# First we do a set of imports
%matplotlib inline
%config InlineBackend.figure_formats = ['svg']
import pandas as pd
from cinnabar import FEMap, plotting
from openff.units import unit
[4]:
# read in the experimental data
experimental_data = pd.read_csv('experimental.tsv', sep="\t")
experimental_data
[4]:
ligand estimate (kcal/mol) uncertainty (kcal/mol)
0 lig_ejm_31 -9.57 0.18
1 lig_ejm_42 -9.81 0.18
2 lig_ejm_43 -8.29 0.18
3 lig_ejm_45 -9.59 0.18
4 lig_ejm_46 -11.35 0.17
5 lig_ejm_47 -9.73 0.18
6 lig_ejm_48 -9.03 0.18
7 lig_ejm_50 -9.01 0.18
8 lig_ejm_54 -10.57 0.18
9 lig_ejm_55 -9.24 0.18
10 lig_jmc_23 -11.74 0.18
11 lig_jmc_27 -11.31 0.17
12 lig_jmc_28 -11.01 0.18

Loading free energy results#

Next we load in results from the TSV file created by running openfe gather --report ddg on a network of results from relative binding free energy calculations.

Please see the full RBFE gather command tutorial for more information.

[5]:
# Read in calculated relative free energy results
calculated_data = pd.read_csv('final_results.tsv', sep="\t")
calculated_data
[5]:
ligand_i ligand_j DDG(i->j) (kcal/mol) uncertainty (kcal/mol)
0 lig_ejm_31 lig_ejm_42 0.49 0.09
1 lig_ejm_31 lig_ejm_46 -0.73 0.10
2 lig_ejm_31 lig_ejm_47 0.00 0.20
3 lig_ejm_31 lig_ejm_48 0.50 0.20
4 lig_ejm_31 lig_ejm_50 0.94 0.07
5 lig_ejm_42 lig_ejm_43 1.20 0.10
6 lig_ejm_46 lig_jmc_23 -0.39 0.05
7 lig_ejm_46 lig_jmc_27 -0.60 0.10
8 lig_ejm_46 lig_jmc_28 -0.12 0.04

Creating a Cinnabar FEMap#

Next we will use the cinnabar API to gather the experimental and calculated results into a single object which we can use to plot and analyse the network of results.

Note: The FEMap object requires estimates and uncertainties to have units attached to remove ambiguity in the data. When using OpenFE results, the units are always kcal/mol, so we can attach these units using the OpenFF Units API.

[6]:
fe_map = FEMap()

# add the calculated data
for _, row in calculated_data.iterrows():
    fe_map.add_relative_calculation(
        # the ligand we are transforming from
        labelA=row['ligand_i'],
        # the ligand we are transforming to
        labelB=row['ligand_j'],
        # the computed relative free energy change for the transformation with units
        value=row["DDG(i->j) (kcal/mol)"] * unit.kilocalories_per_mole,
        # the uncertainty in the computed relative free energy change with units
        uncertainty=row["uncertainty (kcal/mol)"] * unit.kilocalories_per_mole,
        # the source of the data, can be any string but should be consistent across all data points from the same source
        source="openfe",
    )

# add the experimental data
for _, row in experimental_data.iterrows():
    fe_map.add_experimental_measurement(
        # the ligand for which we have an experimental measurement
        label=row['ligand'],
        # the experimental free energy for that ligand with units
        value=row['estimate (kcal/mol)'] * unit.kilocalories_per_mole,
        # the uncertainty in the experimental free energy with units
        uncertainty=row['uncertainty (kcal/mol)'] * unit.kilocalories_per_mole,
        # the source of the data, can be any string but should be consistent across all data points from the same source
        source="Experimental",
    )

Plotting the results using Cinnabar#

Once we’ve created the FEMap we can inspect the alchemical network:

[7]:
fe_map.draw_graph()
../_images/tutorials_plotting_with_cinnabar_12_0.svg

Plotting out the relative free energy results#

Next we can go ahead and plot out the relative free energy results.

[8]:
# note you can pass the filename argument to write things out to file
plotting.plot_DDGs(
    fe_map,
    source="openfe", # the name of the computational source to plot
    figsize=5
    )
../_images/tutorials_plotting_with_cinnabar_14_0.svg

Plotting out the absolute free energy results#

Finally let’s generate an estimate of the absolute binding free energy for each ligand using the MLE estimator and plot out the derived values:

[9]:
# note you can pass the filename argument to write to file
fe_map.generate_absolute_values()  # Get MLE generated estimates of the absolute values
plotting.plot_DGs(
    fe_map,
    source="MLE", # the name of the computational source to plot
    figsize=5
    )
../_images/tutorials_plotting_with_cinnabar_16_0.svg

We can also shift our free energies by the average experimental value to have DGs on the same scale as experiment.

[10]:
shift = experimental_data["estimate (kcal/mol)"].mean()  # shift the computed values by the mean of the experimental values to put them on a similar scale

plotting.plot_DGs(
    fe_map,
    source="MLE",
    figsize=5,
    shift=shift
)
../_images/tutorials_plotting_with_cinnabar_18_0.svg

Writing out the MLE derived absolute free energies#

Finally, we can also extract the MLE derived absolute free energies along with the experimental results into a pandas DataFrame.

[11]:
abs_df = fe_map.get_absolute_dataframe()
abs_df
[11]:
label DG (kcal/mol) uncertainty (kcal/mol) source computational
0 lig_ejm_31 -9.570 0.180000 Experimental False
1 lig_ejm_42 -9.810 0.180000 Experimental False
2 lig_ejm_43 -8.290 0.180000 Experimental False
3 lig_ejm_45 -9.590 0.180000 Experimental False
4 lig_ejm_46 -11.350 0.170000 Experimental False
5 lig_ejm_47 -9.730 0.180000 Experimental False
6 lig_ejm_48 -9.030 0.180000 Experimental False
7 lig_ejm_50 -9.010 0.180000 Experimental False
8 lig_ejm_54 -10.570 0.180000 Experimental False
9 lig_ejm_55 -9.240 0.180000 Experimental False
10 lig_jmc_23 -11.740 0.180000 Experimental False
11 lig_jmc_27 -11.310 0.170000 Experimental False
12 lig_jmc_28 -11.010 0.180000 Experimental False
13 lig_ejm_31 0.041 0.054900 MLE True
14 lig_ejm_42 0.531 0.088736 MLE True
15 lig_ejm_43 1.731 0.125992 MLE True
16 lig_ejm_46 -0.689 0.070810 MLE True
17 lig_ejm_47 0.041 0.187120 MLE True
18 lig_ejm_48 0.541 0.187120 MLE True
19 lig_ejm_50 0.981 0.083271 MLE True
20 lig_jmc_23 -1.079 0.083750 MLE True
21 lig_jmc_27 -1.289 0.114079 MLE True
22 lig_jmc_28 -0.809 0.079335 MLE True

We can then extract the MLE derived values.

Note: you can obtain this directly from your results by calling ``openfe gather –report dg``

[12]:
mle_df = abs_df[abs_df["source"] == "MLE"].reset_index(drop=True)
mle_df
[12]:
label DG (kcal/mol) uncertainty (kcal/mol) source computational
0 lig_ejm_31 0.041 0.054900 MLE True
1 lig_ejm_42 0.531 0.088736 MLE True
2 lig_ejm_43 1.731 0.125992 MLE True
3 lig_ejm_46 -0.689 0.070810 MLE True
4 lig_ejm_47 0.041 0.187120 MLE True
5 lig_ejm_48 0.541 0.187120 MLE True
6 lig_ejm_50 0.981 0.083271 MLE True
7 lig_jmc_23 -1.079 0.083750 MLE True
8 lig_jmc_27 -1.289 0.114079 MLE True
9 lig_jmc_28 -0.809 0.079335 MLE True