Demo - Using alchemiscale
to evaluate a relative binding free energy network¶
This notebook details the process of running a relative binding free energy calculation using the openfe toolkit and the execution platform alchemiscale.
The notebook is broken down into the following section:
Creating an
AlchemicalNetwork
Submitting an
Alchemicalnetwork
toalchemiscale
Monitoring the progress of an
alchemiscale
submissionGathering results and analyzing them using
cinnabar
Credits:
Irfan Alibay (@ialibay) for initial structure and content
David Dotson (@dotsdl) for expansion and editing for use as a tutorial
Creating an AlchemicalNetwork
for the tyk2
benchmark system¶
We first illustrate how to build AlchemicalNetwork
objects,
from a starting point of chemical models stored in sdf and pdb files.
An AlchemicalNetwork
is used to represent an entire network of calculations,
and is composed of many smaller objects:
An
AlchemicalNetwork
composed ofeach node a
ChemicalSystem
each containing many components, such as
SmallMoleculeComponent
,ProteinComponent
internally each Component usually wraps an RDKit representation
each directed edge a
Transformation
, containingtwo
ChemicalSystem
s, the ‘A’ and ‘B’ sidezero or more
Mapping
objects relating these two sidesa
Protocol
defining the computational method to be applied to other items
import openfe
import gufe
from openff.units import unit
from rdkit import Chem
print("gufe version: ", gufe.__version__)
print("openfe version: ", openfe.__version__)
gufe version: 1.0.0
openfe version: 1.0.1+0.g48dcbb26.dirty
import logging
logger = logging.getLogger()
logger.setLevel(logging.WARN)
Define ChemicalSystem
s for network nodes¶
We’ll start by defining the nodes for our network.
A ChemicalSystem
is made of one or more Component
s. These can be one of ProteinComponent
, SmallMoleculeComponent
, or SolventComponent
, and potentially others as needed. This design allows for memory efficient representation of large networks with perhaps hundreds or thousands of nodes, but perhaps far fewer variants in proteins, ligands, etc.
Reading Ligands¶
The ligands are concatenated in a single sdf file, we’ll read these using RDKit.
Each of the ligands have been pre-docked into the protein and aligned to their common scaffold. It is important to recognize that any processing required to prepare ligand and protein structures for alchemical free energy calculations should be done before the steps we are taking here.
ligands = [
openfe.SmallMoleculeComponent(m) for m in Chem.SDMolSupplier('ligands.sdf', removeHs=False)
]
ligands
[SmallMoleculeComponent(name=lig_ejm_54),
SmallMoleculeComponent(name=lig_jmc_23),
SmallMoleculeComponent(name=lig_ejm_47),
SmallMoleculeComponent(name=lig_jmc_27),
SmallMoleculeComponent(name=lig_ejm_46),
SmallMoleculeComponent(name=lig_ejm_31),
SmallMoleculeComponent(name=lig_ejm_42),
SmallMoleculeComponent(name=lig_ejm_50),
SmallMoleculeComponent(name=lig_ejm_45),
SmallMoleculeComponent(name=lig_jmc_28),
SmallMoleculeComponent(name=lig_ejm_55),
SmallMoleculeComponent(name=lig_ejm_43),
SmallMoleculeComponent(name=lig_ejm_48)]
# The OpenFE RBFE protocol does not yet support charge changes (to be changed very soon!)
# Here we raise an error if a change in formal charge is identified
base_formalcharge = Chem.rdmolops.GetFormalCharge(ligands[0].to_rdkit())
for mol in ligands[1:]:
if Chem.rdmolops.GetFormalCharge(mol.to_rdkit()) != base_formalcharge:
errmsg = f"The molecule {mol} has a different formal charge than the first ligand molecule"
raise ValueError(errmsg)
Reading the protein¶
The protein is supplied as a PDB file, readable via the ProteinComponent.from_pdb_file
class method.
protein = openfe.ProteinComponent.from_pdb_file('protein.pdb', name='tyk2')
protein
ProteinComponent(name=tyk2)
Defining the solvent¶
We’ll also need at least one SolventComponent
to encode our choice of solvent and counterions, with concentration.
The concentration is defined as having units supplied by openff.units
, this package is used to avoid confusion.
The SolventComponent
doesn’t actually perform any actual solvation (packing water molecules, ions); that is performed just before simulation time during Protocol
execution.
solvent = openfe.SolventComponent(positive_ion='Na',
negative_ion='Cl',
neutralize=True,
ion_concentration=0.15*unit.molar)
solvent
SolventComponent(name=O, Na+, Cl-)
Build the ChemicalSystem
s¶
We can now construct the ChemicalSystem
s we want represented in our network. Since we are planning to perform relative binding free energy (RBFE) calculations, we’ll define both complex and solvent variants for each ligand.
This produces a dictionary mapping the ligand name to the ChemicalSystem
that contains that ligand.
There are two dictionaries, for complexed and solvated ligands respectively.
complexed = {l.name: openfe.ChemicalSystem(components={'ligand': l,
'solvent': solvent,
'protein': protein},
name=f"{l.name}_complex")
for l in ligands}
complexed
{'lig_ejm_54': ChemicalSystem(name=lig_ejm_54_complex, components={'ligand': SmallMoleculeComponent(name=lig_ejm_54), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)}),
'lig_jmc_23': ChemicalSystem(name=lig_jmc_23_complex, components={'ligand': SmallMoleculeComponent(name=lig_jmc_23), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)}),
'lig_ejm_47': ChemicalSystem(name=lig_ejm_47_complex, components={'ligand': SmallMoleculeComponent(name=lig_ejm_47), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)}),
'lig_jmc_27': ChemicalSystem(name=lig_jmc_27_complex, components={'ligand': SmallMoleculeComponent(name=lig_jmc_27), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)}),
'lig_ejm_46': ChemicalSystem(name=lig_ejm_46_complex, components={'ligand': SmallMoleculeComponent(name=lig_ejm_46), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)}),
'lig_ejm_31': ChemicalSystem(name=lig_ejm_31_complex, components={'ligand': SmallMoleculeComponent(name=lig_ejm_31), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)}),
'lig_ejm_42': ChemicalSystem(name=lig_ejm_42_complex, components={'ligand': SmallMoleculeComponent(name=lig_ejm_42), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)}),
'lig_ejm_50': ChemicalSystem(name=lig_ejm_50_complex, components={'ligand': SmallMoleculeComponent(name=lig_ejm_50), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)}),
'lig_ejm_45': ChemicalSystem(name=lig_ejm_45_complex, components={'ligand': SmallMoleculeComponent(name=lig_ejm_45), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)}),
'lig_jmc_28': ChemicalSystem(name=lig_jmc_28_complex, components={'ligand': SmallMoleculeComponent(name=lig_jmc_28), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)}),
'lig_ejm_55': ChemicalSystem(name=lig_ejm_55_complex, components={'ligand': SmallMoleculeComponent(name=lig_ejm_55), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)}),
'lig_ejm_43': ChemicalSystem(name=lig_ejm_43_complex, components={'ligand': SmallMoleculeComponent(name=lig_ejm_43), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)}),
'lig_ejm_48': ChemicalSystem(name=lig_ejm_48_complex, components={'ligand': SmallMoleculeComponent(name=lig_ejm_48), 'solvent': SolventComponent(name=O, Na+, Cl-), 'protein': ProteinComponent(name=tyk2)})}
solvated = {l.name: openfe.ChemicalSystem(components={'ligand': l,
'solvent': solvent},
name=f"{l.name}_solvent")
for l in ligands}
solvated
{'lig_ejm_54': ChemicalSystem(name=lig_ejm_54_solvent, components={'ligand': SmallMoleculeComponent(name=lig_ejm_54), 'solvent': SolventComponent(name=O, Na+, Cl-)}),
'lig_jmc_23': ChemicalSystem(name=lig_jmc_23_solvent, components={'ligand': SmallMoleculeComponent(name=lig_jmc_23), 'solvent': SolventComponent(name=O, Na+, Cl-)}),
'lig_ejm_47': ChemicalSystem(name=lig_ejm_47_solvent, components={'ligand': SmallMoleculeComponent(name=lig_ejm_47), 'solvent': SolventComponent(name=O, Na+, Cl-)}),
'lig_jmc_27': ChemicalSystem(name=lig_jmc_27_solvent, components={'ligand': SmallMoleculeComponent(name=lig_jmc_27), 'solvent': SolventComponent(name=O, Na+, Cl-)}),
'lig_ejm_46': ChemicalSystem(name=lig_ejm_46_solvent, components={'ligand': SmallMoleculeComponent(name=lig_ejm_46), 'solvent': SolventComponent(name=O, Na+, Cl-)}),
'lig_ejm_31': ChemicalSystem(name=lig_ejm_31_solvent, components={'ligand': SmallMoleculeComponent(name=lig_ejm_31), 'solvent': SolventComponent(name=O, Na+, Cl-)}),
'lig_ejm_42': ChemicalSystem(name=lig_ejm_42_solvent, components={'ligand': SmallMoleculeComponent(name=lig_ejm_42), 'solvent': SolventComponent(name=O, Na+, Cl-)}),
'lig_ejm_50': ChemicalSystem(name=lig_ejm_50_solvent, components={'ligand': SmallMoleculeComponent(name=lig_ejm_50), 'solvent': SolventComponent(name=O, Na+, Cl-)}),
'lig_ejm_45': ChemicalSystem(name=lig_ejm_45_solvent, components={'ligand': SmallMoleculeComponent(name=lig_ejm_45), 'solvent': SolventComponent(name=O, Na+, Cl-)}),
'lig_jmc_28': ChemicalSystem(name=lig_jmc_28_solvent, components={'ligand': SmallMoleculeComponent(name=lig_jmc_28), 'solvent': SolventComponent(name=O, Na+, Cl-)}),
'lig_ejm_55': ChemicalSystem(name=lig_ejm_55_solvent, components={'ligand': SmallMoleculeComponent(name=lig_ejm_55), 'solvent': SolventComponent(name=O, Na+, Cl-)}),
'lig_ejm_43': ChemicalSystem(name=lig_ejm_43_solvent, components={'ligand': SmallMoleculeComponent(name=lig_ejm_43), 'solvent': SolventComponent(name=O, Na+, Cl-)}),
'lig_ejm_48': ChemicalSystem(name=lig_ejm_48_solvent, components={'ligand': SmallMoleculeComponent(name=lig_ejm_48), 'solvent': SolventComponent(name=O, Na+, Cl-)})}
Define Transformation
s between ChemicalSystem
s as network edges¶
A Transformation
is a directed edge between two ChemicalSystem
s. It includes a Protocol
parameterized with Settings
, and optionally a ComponentMapping
.
The Protocol
defines the actual computational method used to evaluate the Transformation
to yield estimates for the free energy difference between the ChemicalSystem
s.
The ComponentMapping
defines the atom mapping(s) between corresponding Component
s in the two ChemicalSystem
s. This is often critical for relative binding free energy calculations, since the choice of mapping can heavily influence convergence of the resulting estimates.
Define the Protocol
used for Transformation
evaluation¶
For this example, we’ll use the same Protocol
for all our Transformation
s, with identical Settings
for each.
from openfe.protocols.openmm_rfe import RelativeHybridTopologyProtocol
Any given Protocol
has a default_settings()
method, which can be used to get the default settings that are specific to that Protocol
:
protocol_settings = RelativeHybridTopologyProtocol.default_settings()
protocol_settings.dict()
{'forcefield_settings': {'constraints': 'hbonds',
'rigid_water': True,
'hydrogen_mass': 3.0,
'forcefields': ['amber/ff14SB.xml',
'amber/tip3p_standard.xml',
'amber/tip3p_HFE_multivalent.xml',
'amber/phosaa10.xml'],
'small_molecule_forcefield': 'openff-2.1.1',
'nonbonded_cutoff': 1.0 <Unit('nanometer')>,
'nonbonded_method': 'PME'},
'thermo_settings': {'temperature': 298.15 <Unit('kelvin')>,
'pressure': 0.9869232667160129 <Unit('standard_atmosphere')>,
'ph': None,
'redox_potential': None},
'protocol_repeats': 3,
'solvation_settings': {'solvent_model': 'tip3p',
'solvent_padding': 1.2 <Unit('nanometer')>},
'partial_charge_settings': {'partial_charge_method': 'am1bcc',
'off_toolkit_backend': 'ambertools',
'number_of_conformers': None,
'nagl_model': None},
'lambda_settings': {'lambda_functions': 'default', 'lambda_windows': 11},
'alchemical_settings': {'softcore_LJ': 'gapsys',
'explicit_charge_correction_cutoff': 0.8 <Unit('nanometer')>,
'endstate_dispersion_correction': False,
'use_dispersion_correction': False,
'softcore_alpha': 0.85,
'turn_off_core_unique_exceptions': False,
'explicit_charge_correction': False},
'simulation_settings': {'equilibration_length': 1.0 <Unit('nanosecond')>,
'production_length': 5.0 <Unit('nanosecond')>,
'minimization_steps': 5000,
'time_per_iteration': 1 <Unit('picosecond')>,
'real_time_analysis_interval': 250 <Unit('picosecond')>,
'early_termination_target_error': 0.0 <Unit('kilocalorie_per_mole')>,
'real_time_analysis_minimum_time': 500 <Unit('picosecond')>,
'sampler_method': 'repex',
'sams_flatness_criteria': 'logZ-flatness',
'sams_gamma0': 1.0,
'n_replicas': 11},
'engine_settings': {'compute_platform': None},
'integrator_settings': {'timestep': 4 <Unit('femtosecond')>,
'langevin_collision_rate': 1.0 <Unit('1 / picosecond')>,
'barostat_frequency': 25 <Unit('timestep')>,
'remove_com': False,
'reassign_velocities': False,
'n_restart_attempts': 20,
'constraint_tolerance': 1e-06},
'output_settings': {'checkpoint_interval': 250 <Unit('picosecond')>,
'forcefield_cache': 'db.json',
'output_indices': 'not water',
'checkpoint_storage_filename': 'checkpoint.chk',
'output_filename': 'simulation.nc',
'output_structure': 'hybrid_system.pdb'}}
protocol_settings
{'alchemical_settings': {'endstate_dispersion_correction': False,
'explicit_charge_correction': False,
'explicit_charge_correction_cutoff': <Quantity(0.8, 'nanometer')>,
'softcore_LJ': 'gapsys',
'softcore_alpha': 0.85,
'turn_off_core_unique_exceptions': False,
'use_dispersion_correction': False},
'engine_settings': {'compute_platform': None},
'forcefield_settings': {'constraints': 'hbonds',
'forcefields': ['amber/ff14SB.xml',
'amber/tip3p_standard.xml',
'amber/tip3p_HFE_multivalent.xml',
'amber/phosaa10.xml'],
'hydrogen_mass': 3.0,
'nonbonded_cutoff': <Quantity(1.0, 'nanometer')>,
'nonbonded_method': 'PME',
'rigid_water': True,
'small_molecule_forcefield': 'openff-2.1.1'},
'integrator_settings': {'barostat_frequency': <Quantity(25, 'timestep')>,
'constraint_tolerance': 1e-06,
'langevin_collision_rate': <Quantity(1.0, '1 / picosecond')>,
'n_restart_attempts': 20,
'reassign_velocities': False,
'remove_com': False,
'timestep': <Quantity(4, 'femtosecond')>},
'lambda_settings': {'lambda_functions': 'default', 'lambda_windows': 11},
'output_settings': {'checkpoint_interval': <Quantity(250, 'picosecond')>,
'checkpoint_storage_filename': 'checkpoint.chk',
'forcefield_cache': 'db.json',
'output_filename': 'simulation.nc',
'output_indices': 'not water',
'output_structure': 'hybrid_system.pdb'},
'partial_charge_settings': {'nagl_model': None,
'number_of_conformers': None,
'off_toolkit_backend': 'ambertools',
'partial_charge_method': 'am1bcc'},
'protocol_repeats': 3,
'simulation_settings': {'early_termination_target_error': <Quantity(0.0, 'kilocalorie_per_mole')>,
'equilibration_length': <Quantity(1.0, 'nanosecond')>,
'minimization_steps': 5000,
'n_replicas': 11,
'production_length': <Quantity(5.0, 'nanosecond')>,
'real_time_analysis_interval': <Quantity(250, 'picosecond')>,
'real_time_analysis_minimum_time': <Quantity(500, 'picosecond')>,
'sampler_method': 'repex',
'sams_flatness_criteria': 'logZ-flatness',
'sams_gamma0': 1.0,
'time_per_iteration': <Quantity(1, 'picosecond')>},
'solvation_settings': {'solvent_model': 'tip3p',
'solvent_padding': <Quantity(1.2, 'nanometer')>},
'thermo_settings': {'ph': None,
'pressure': <Quantity(0.986923267, 'standard_atmosphere')>,
'redox_potential': None,
'temperature': <Quantity(298.15, 'kelvin')>}}
# Here we make it so that each simulation only features a single repeat
# We then do multiple repeats by running each simulation multiple times
protocol_settings.protocol_repeats = 1
We can now produce a parameterized RelativeHybridTopologyProtocol
instance:
protocol = RelativeHybridTopologyProtocol(protocol_settings)
Build the Transformation
s¶
We can now construct the Transformation
s we want represented in our network.
We’ll use openfe
’s implementation of the LomapAtomMapper
to produce mappings between ligands, the default LOMAP scorer for assessing the quality of the mappings produced, and the minimal spanning tree network planner. However, you could use any network planner of your choice to generate the network connections and mappings and use those instead.
mapper = openfe.LomapAtomMapper(max3d=1.0, element_change=True)
scorer = openfe.lomap_scorers.default_lomap_score
network_planner = openfe.ligand_network_planning.generate_minimal_spanning_network
# Note creating a minimum spanning tree requires an all-to-all mapping generation
# This can be quite time consuming...
ligand_network = network_planner(
ligands=ligands,
mappers=[mapper],
scorer=scorer
)
from openfe.utils.atommapping_network_plotting import plot_atommapping_network
plot_atommapping_network(ligand_network)
Since we are planning to perform relative binding free energy (RBFE) calculations, we’ll define both complex and solvent variants for each Transformation
:
for mapping in ligand_network.edges:
print(mapping.componentA.name, mapping.componentB.name)
lig_ejm_46 lig_ejm_31
lig_ejm_55 lig_ejm_43
lig_ejm_31 lig_ejm_50
lig_ejm_31 lig_ejm_48
lig_ejm_47 lig_ejm_31
lig_ejm_42 lig_ejm_50
lig_jmc_23 lig_ejm_46
lig_ejm_42 lig_ejm_43
lig_jmc_23 lig_jmc_28
lig_jmc_27 lig_jmc_28
lig_ejm_54 lig_ejm_55
lig_ejm_31 lig_ejm_45
complexed_transformations = []
solvated_transformations = []
for mapping in ligand_network.edges:
ligA_name = mapping.componentA.name
ligB_name = mapping.componentB.name
ligA = complexed[ligA_name]['ligand']
ligB = complexed[ligB_name]['ligand']
complexed_transformations.append(
openfe.Transformation(stateA=complexed[ligA_name],
stateB=complexed[ligB_name],
mapping={'ligand': mapping},
protocol=protocol,
name=f"{complexed[ligA_name].name}->{complexed[ligB_name].name}")
)
solvated_transformations.append(
openfe.Transformation(stateA=solvated[ligA_name],
stateB=solvated[ligB_name],
mapping={'ligand': mapping},
protocol=protocol,
name=f"{solvated[ligA_name].name}->{solvated[ligB_name].name}")
)
Create the AlchemicalNetwork
¶
An AlchemicalNetwork
is simply the combination of ChemicalSystem
s (nodes) and Transformation
s (directed edges) that we want to evaluate \(\Delta G\)s for. This data structure functions as a declaration of what you want to compute.
We’ll finish here by creating an AlchemicalNetwork
from the collection of objects we’ve built so far.
network = openfe.AlchemicalNetwork(edges=(solvated_transformations + complexed_transformations),
nodes=(list(solvated.values()) + list(complexed.values())),
name="tyk2_relative_benchmark")
network
<AlchemicalNetwork-901e26f212324c47939436bc9b64a964>
That’s it! We simply toss in all Transformation
s (edges) and ChemicalSystem
s (nodes) we want included in this AlchemicalNetwork
, and optionally give it a name that means something to us (it need not be unique, but can be used to query for network(s) later).
We could have chosen here to leave the nodes
argument off, since every ChemicalSystem
we included was already represented among the edges
, but we show it here for completeness. In this way, it’s possible to include ChemicalSystem
s in the network that aren’t connected via any Transformation
s to others, though in practice there isn’t much utility in this.
Submitting an AlchemicalNetwork
to alchemiscale
¶
import alchemiscale
print("alchemiscale version: ", alchemiscale.__version__)
alchemiscale version: 0.5.0
from alchemiscale import AlchemiscaleClient, Scope, ScopedKey
from getpass import getpass
identity_key = getpass()
asc = AlchemiscaleClient('https://api.alchemiscale.org', 'ddotson', identity_key)
asc
AlchemiscaleClient('https://api.alchemiscale.org')
asc.list_scopes()
[<Scope('*-*-*')>]
asc.query_networks(scope=Scope('ddotson'))
[]
AlchemicalNetwork
s and the objects associated with them live in a specific Scope
. Because my user identity is able to use any Scope
from the above ('*-*-*'
), I can choose whatever I want. AlchemicalNetwork
s submitted to the same Scope
will share Transformation
s they have in common, along with results for those Transformation
s; AlchemicalNetwork
s in different Scope
s will share nothing.
How you choose to use the Scope
s you have access to is entirely up to you. I’ll choose this is my Scope
for this network:
scope = Scope('ddotson', 'tyk2', 'demo')
And then we’ll submit the AlchemicalNetwork
to that Scope
with:
an_sk = asc.create_network(network, scope)
We get back a ScopedKey
for the `AlchemicalNetwork:
an_sk
<ScopedKey('AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo')>
This uniquely identifies the AlchemicalNetwork
in the database, and allows us to perform operations later on it. We can get the ScopedKey
s for all AlchemicalNetwork
s that exist in Scope
s accessible to us with:
asc.query_networks(scope=Scope('ddotson'))
[<ScopedKey('AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo')>]
We can likewise retrieve an AlchemicalNetwork
that we previously submitted with:
network_again = asc.get_network(an_sk)
Executing Transformation
s by creating and actioning Task
s¶
Submitting an AlchemicalNetwork
with create_network
only defines it on the server; it does not initiate any calculations. For this, we need to create and action Task
s.
To obtain a free energy estimate \(\Delta G\) for each Transformation
in the AlchemicalNetwork
, we’ll create a Task
for each one:
tf_sks = asc.get_network_transformations(an_sk)
tasks = asc.create_transformations_tasks(tf_sks)
tasks
[<ScopedKey('Task-ef9ccf8712d6458582e075a5be87d06e-ddotson-tyk2-demo')>,
<ScopedKey('Task-f6a91ccc6f9e4bcbace3331a0109c9da-ddotson-tyk2-demo')>,
<ScopedKey('Task-cccc389fc316494994cb6c8f834aa006-ddotson-tyk2-demo')>,
<ScopedKey('Task-6205c49da54647a491ae11bce9105288-ddotson-tyk2-demo')>,
<ScopedKey('Task-7003ead7ec9a4ff18965067add5e80fb-ddotson-tyk2-demo')>,
<ScopedKey('Task-de763d3ed6f045f596e96373ae231cfd-ddotson-tyk2-demo')>,
<ScopedKey('Task-a2505069149d47738f2b30a47f24d860-ddotson-tyk2-demo')>,
<ScopedKey('Task-eee82adb878f4701bd61ce765abc5da6-ddotson-tyk2-demo')>,
<ScopedKey('Task-4e9d22efa6364a96854fd2e45dfe1939-ddotson-tyk2-demo')>,
<ScopedKey('Task-3fade5ba936a4d1fb967c695137aa210-ddotson-tyk2-demo')>,
<ScopedKey('Task-320fc3cb408a4e5d9cea7304cf73d7f1-ddotson-tyk2-demo')>,
<ScopedKey('Task-191cb8e4a03c4595a249cf1b7b8771db-ddotson-tyk2-demo')>,
<ScopedKey('Task-112dcba14c0342fba611c9079c0c6004-ddotson-tyk2-demo')>,
<ScopedKey('Task-1200e42c2a8a4d43b1ae39528cb48e8d-ddotson-tyk2-demo')>,
<ScopedKey('Task-35577a6ccebc44f58a129f0ad061d7b7-ddotson-tyk2-demo')>,
<ScopedKey('Task-859860913fa54db68d28ed2e2ec7e494-ddotson-tyk2-demo')>,
<ScopedKey('Task-0d67a73e932f47a094e9c1111a0833bc-ddotson-tyk2-demo')>,
<ScopedKey('Task-5e85cac6182b48258d572d037b52bc71-ddotson-tyk2-demo')>,
<ScopedKey('Task-6ec4facb5ba54ebaa9905bacb0e39a5c-ddotson-tyk2-demo')>,
<ScopedKey('Task-91e70ed59c084d3388df9009ab1553e2-ddotson-tyk2-demo')>,
<ScopedKey('Task-21b0cba98a594da4a15f02c5ab20ed62-ddotson-tyk2-demo')>,
<ScopedKey('Task-ce83f523df324508a6e6cfbce1c34393-ddotson-tyk2-demo')>,
<ScopedKey('Task-acbad8588c5b4f0c962f347785277c94-ddotson-tyk2-demo')>,
<ScopedKey('Task-e8d59e999687420e94a9afc201a14861-ddotson-tyk2-demo')>]
Now checking the AlchemicalNetwork
status, we see 24 Task
s 'waiting'
, 1 for each Transformation
:
asc.get_network_status(an_sk)
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ status ┃ count ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ complete │ 0 │ │ running │ 0 │ │ waiting │ 24 │ │ error │ 0 │ │ invalid │ 0 │ │ deleted │ 0 │ └──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘
{'waiting': 24}
But there is one more step: Task
s must also be actioned on the AlchemicalNetwork
in order to be picked up by compute services:
asc.action_tasks(tasks, an_sk)
[<ScopedKey('Task-ef9ccf8712d6458582e075a5be87d06e-ddotson-tyk2-demo')>,
<ScopedKey('Task-f6a91ccc6f9e4bcbace3331a0109c9da-ddotson-tyk2-demo')>,
<ScopedKey('Task-cccc389fc316494994cb6c8f834aa006-ddotson-tyk2-demo')>,
<ScopedKey('Task-6205c49da54647a491ae11bce9105288-ddotson-tyk2-demo')>,
<ScopedKey('Task-7003ead7ec9a4ff18965067add5e80fb-ddotson-tyk2-demo')>,
<ScopedKey('Task-de763d3ed6f045f596e96373ae231cfd-ddotson-tyk2-demo')>,
<ScopedKey('Task-a2505069149d47738f2b30a47f24d860-ddotson-tyk2-demo')>,
<ScopedKey('Task-eee82adb878f4701bd61ce765abc5da6-ddotson-tyk2-demo')>,
<ScopedKey('Task-4e9d22efa6364a96854fd2e45dfe1939-ddotson-tyk2-demo')>,
<ScopedKey('Task-3fade5ba936a4d1fb967c695137aa210-ddotson-tyk2-demo')>,
<ScopedKey('Task-320fc3cb408a4e5d9cea7304cf73d7f1-ddotson-tyk2-demo')>,
<ScopedKey('Task-191cb8e4a03c4595a249cf1b7b8771db-ddotson-tyk2-demo')>,
<ScopedKey('Task-112dcba14c0342fba611c9079c0c6004-ddotson-tyk2-demo')>,
<ScopedKey('Task-1200e42c2a8a4d43b1ae39528cb48e8d-ddotson-tyk2-demo')>,
<ScopedKey('Task-35577a6ccebc44f58a129f0ad061d7b7-ddotson-tyk2-demo')>,
<ScopedKey('Task-859860913fa54db68d28ed2e2ec7e494-ddotson-tyk2-demo')>,
<ScopedKey('Task-0d67a73e932f47a094e9c1111a0833bc-ddotson-tyk2-demo')>,
<ScopedKey('Task-5e85cac6182b48258d572d037b52bc71-ddotson-tyk2-demo')>,
<ScopedKey('Task-6ec4facb5ba54ebaa9905bacb0e39a5c-ddotson-tyk2-demo')>,
<ScopedKey('Task-91e70ed59c084d3388df9009ab1553e2-ddotson-tyk2-demo')>,
<ScopedKey('Task-21b0cba98a594da4a15f02c5ab20ed62-ddotson-tyk2-demo')>,
<ScopedKey('Task-ce83f523df324508a6e6cfbce1c34393-ddotson-tyk2-demo')>,
<ScopedKey('Task-acbad8588c5b4f0c962f347785277c94-ddotson-tyk2-demo')>,
<ScopedKey('Task-e8d59e999687420e94a9afc201a14861-ddotson-tyk2-demo')>]
The reason for this extra step is that a given Transformation
may be a member of multiple AlchemicalNetwork
s in its Scope
. Computational effort is independently allocated on each AlchemicalNetwork
through its actioned Task
s, and these can differ between AlchemicalNetwork
s in the same Scope
. In the future, automated Strategy
s will operate on individual AlchemicalNetwork
s, and this ability to independently choose where to apply effort is critical for this.
After actioning our Task
s, we can see that some have been picked up and are now 'running'
:
asc.get_network_status(an_sk)
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ status ┃ count ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ complete │ 0 │ │ running │ 0 │ │ waiting │ 24 │ │ error │ 0 │ │ invalid │ 0 │ │ deleted │ 0 │ └──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘
{'waiting': 24}
Later on, some are now 'complete'
:
asc.get_network_status(an_sk)
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ status ┃ count ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ complete │ 4 │ │ running │ 12 │ │ waiting │ 8 │ │ error │ 0 │ │ invalid │ 0 │ │ deleted │ 0 │ └──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘
{'waiting': 8, 'complete': 4, 'running': 12}
…and some might have errored:
asc.get_network_status(an_sk)
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ status ┃ count ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ complete │ 14 │ │ running │ 9 │ │ waiting │ 0 │ │ error │ 1 │ │ invalid │ 0 │ │ deleted │ 0 │ └──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘
{'error': 1, 'running': 9, 'complete': 14}
…and at some point, none are waiting
or running
; they are either complete
or errored
:
asc.get_network_status(an_sk)
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ status ┃ count ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ complete │ 21 │ │ running │ 0 │ │ waiting │ 0 │ │ error │ 3 │ │ invalid │ 0 │ │ deleted │ 0 │ └──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘
{'error': 3, 'complete': 21}
Dealing with errors¶
Inevitably, some of your Task
s will encounter problems in execution, either random or systematic errors. When this happens, the Task
status will be set to 'error'
.
We’ll get all the Task
s in this AlchemicalNetwork
with status 'error'
:
failed_tasks = asc.get_network_tasks(an_sk, status='error')
failed_tasks
[<ScopedKey('Task-ef9ccf8712d6458582e075a5be87d06e-ddotson-tyk2-demo')>,
<ScopedKey('Task-f6a91ccc6f9e4bcbace3331a0109c9da-ddotson-tyk2-demo')>,
<ScopedKey('Task-ce83f523df324508a6e6cfbce1c34393-ddotson-tyk2-demo')>]
We’ll choose one, and get all the failed results for it:
failures = asc.get_task_failures(failed_tasks[0])
failures
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-15fdd9ff3f68cde67e706ffa87f6a9b7-ddotson-tyk2-demo/failures/ProtocolDAGResultRef-2bc722e46be751f9ec1a770028f393f7-ddotson-tyk2-demo "HTTP/1.1 200 OK"
[<ProtocolDAGResult-4dd969e50a25508075044c4b9a61b7fe>]
Each of these is a ProtocolDAGResult
, but with at least one ProtocolUnitFailure
:
failures[0]
<ProtocolDAGResult-4dd969e50a25508075044c4b9a61b7fe>
failures[0].protocol_unit_failures
[ProtocolUnitFailure(lig_jmc_27 to lig_jmc_28 repeat 0 generation 0),
ProtocolUnitFailure(lig_jmc_27 to lig_jmc_28 repeat 0 generation 0),
ProtocolUnitFailure(lig_jmc_27 to lig_jmc_28 repeat 0 generation 0),
ProtocolUnitFailure(lig_jmc_27 to lig_jmc_28 repeat 0 generation 0)]
We can introspect the traceback
on any of the ProtocolUnitFailure
s present in the ProtocolDAGResult
to understand what went wrong here:
print(failures[0].protocol_unit_failures[0].traceback)
Traceback (most recent call last):
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/gufe/protocols/protocolunit.py", line 320, in execute
outputs = self._execute(context, **inputs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/equil_rfe_methods.py", line 1129, in _execute
outputs = self.run(scratch_basepath=ctx.scratch,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/equil_rfe_methods.py", line 998, in run
sampler.setup(
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py", line 121, in setup
minimize(compound_thermostate_copy, sampler_state,
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py", line 295, in minimize
context, integrator = dummy_cache.get_context(
^^^^^^^^^^^^^^^^^^^^^^^^
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openmmtools/cache.py", line 770, in get_context
context = thermodynamic_state.create_context(integrator, self.platform)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openmmtools/states.py", line 1179, in create_context
return openmm.Context(system, integrator, platform)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openmm/openmm.py", line 8037, in __init__
_openmm.Context_swiginit(self, _openmm.new_Context(*args))
^^^^^^^^^^^^^^^^^^^^^^^^^^
openmm.OpenMMException: Error initializing CUDA: CUDA_ERROR_MPS_CONNECTION_FAILED (805) at /home/conda/feedstock_root/build_artifacts/openmm_1721257909416/work/platforms/cuda/src/CudaContext.cpp:91
In this case, it like it may be due to a configuration issue on a compute resource our Task
s are landing on.
If we wish to re-run an 'error'
ed Task
, we can do so by setting its status back to 'waiting'
:
asc.set_tasks_status(failed_tasks[:1], 'waiting')
INFO: HTTP Request: POST https://api.alchemiscale.org/bulk/tasks/status/set "HTTP/1.1 200 OK"
[<ScopedKey('Task-ef9ccf8712d6458582e075a5be87d06e-ddotson-tyk2-demo')>]
Gathering and analyzing results¶
We can gather results for each Transformation
with:
results = dict()
for tf_sk in asc.get_network_transformations(an_sk):
results[str(tf_sk)] = asc.get_transformation_results(tf_sk, visualize=False)
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-82e90d15b483a813a6501073e3964930-ddotson-tyk2-demo/results/ProtocolDAGResultRef-01a2e46b4c33a9c52aab71b8c01ba5cd-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-73164716e2ad3c2e410fce413fa41be4-ddotson-tyk2-demo/results/ProtocolDAGResultRef-caabc7fbbe8e9bae4ad0da4f5db7cb61-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-e3022bfd607e772136f5c262875a8e78-ddotson-tyk2-demo/results/ProtocolDAGResultRef-b6b00c75b4f66e546623a98d42b165c8-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-6064e55fed6e90b90b727b299462cbad-ddotson-tyk2-demo/results/ProtocolDAGResultRef-f87f2f38715e0e03c30b574fe3c0e01d-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-28453f83bb03486a08afc096a8dc5298-ddotson-tyk2-demo/results/ProtocolDAGResultRef-d206475f1ae25c4818964579eabca7b0-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-eebbe29feefc097086749081844d8adc-ddotson-tyk2-demo/results/ProtocolDAGResultRef-2a9f4953c83460388614a4cb872bc601-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-2a345646fa5b3900aba8b74935fc4ec6-ddotson-tyk2-demo/results/ProtocolDAGResultRef-821c1a7ccc7740a28f4dd3d553b2f035-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-f8fb4a09881bce886dcb55a88be49016-ddotson-tyk2-demo/results/ProtocolDAGResultRef-8295104d5c32f10bdd86b69675bb7494-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-874c39cca17041ceb7a91f142104e8eb-ddotson-tyk2-demo/results/ProtocolDAGResultRef-2d7d1eed2ee1b96e67c95385c14b220c-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-b4857c06e9564cf3c2451f436f7af410-ddotson-tyk2-demo/results/ProtocolDAGResultRef-fdea668674894a4ebc21cd51a9ca8e02-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-958a3d55d581ec276b72394435df4bd0-ddotson-tyk2-demo/results/ProtocolDAGResultRef-19825c22d09bf2a8d70e47e5cac34829-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-22a2847c2a28584956e90d549fc6bcc6-ddotson-tyk2-demo/results/ProtocolDAGResultRef-83f986401353591db65c9ed401af260b-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-d5cdd9f4f4edd380361129082a01360b-ddotson-tyk2-demo/results/ProtocolDAGResultRef-57a8c3097273d30ff5b0350943883824-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-20fd18a057e4affd5c78cad41a81d3b4-ddotson-tyk2-demo/results/ProtocolDAGResultRef-2798447525f7139b158919d08faadb95-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-44cf523e9d0e53674fadc59c9b9110b7-ddotson-tyk2-demo/results/ProtocolDAGResultRef-f56c970e363dbe075f2dda74386055a8-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-856f85d23d9b8b84916d5b83ebfa1773-ddotson-tyk2-demo/results/ProtocolDAGResultRef-f94dec1b5cc13aff45aba30eb1647c3c-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-ef57a778259ba88b9fb4c23cc2bd78f4-ddotson-tyk2-demo/results/ProtocolDAGResultRef-474df4f73b5bdb270a43361d8651dfa4-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-c33480c9f0dfd80852541205f46914a7-ddotson-tyk2-demo/results/ProtocolDAGResultRef-943e801e0c7774f3c962ca8127150ace-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-4bfeb3edd7f0d47ec52f533a5cf435a0-ddotson-tyk2-demo/results/ProtocolDAGResultRef-0d56400cc2c06fe557f170b872c9a465-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-06eac733376ab1ff566248040fe37e01-ddotson-tyk2-demo/results/ProtocolDAGResultRef-cc1df82df45efd8fba44f029a28094b6-ddotson-tyk2-demo "HTTP/1.1 200 OK"
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-76de739d87d53981abfff4ab9ce071e4-ddotson-tyk2-demo/results/ProtocolDAGResultRef-810f1f48475da9a8cbb7ed364848cdf2-ddotson-tyk2-demo "HTTP/1.1 200 OK"
results
{'Transformation-15fdd9ff3f68cde67e706ffa87f6a9b7-ddotson-tyk2-demo': None,
'Transformation-152928d4d69298dcedf2dd622073a95b-ddotson-tyk2-demo': None,
'Transformation-82e90d15b483a813a6501073e3964930-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-948bfdff0b72d5ef4f67adc85c2634c6>,
'Transformation-73164716e2ad3c2e410fce413fa41be4-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-df5750fe54c2c580cfd73fa84ce840f1>,
'Transformation-e3022bfd607e772136f5c262875a8e78-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-337e4d8e80363a149a0d04f2b1ac2446>,
'Transformation-6064e55fed6e90b90b727b299462cbad-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-cf56bc1ab11ffae35e0dd045d6fe29dc>,
'Transformation-28453f83bb03486a08afc096a8dc5298-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-8226e5d9a3fa772e8923f309174d4671>,
'Transformation-eebbe29feefc097086749081844d8adc-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-f91bea18368378e55ace96616d6ec6f1>,
'Transformation-2a345646fa5b3900aba8b74935fc4ec6-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-fa766a1fc145b6700102b9fdff3d0db3>,
'Transformation-f8fb4a09881bce886dcb55a88be49016-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-a7a764961d833469c7da71645ddcc7cd>,
'Transformation-874c39cca17041ceb7a91f142104e8eb-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-c7576997d867d44c08191252879de95b>,
'Transformation-b4857c06e9564cf3c2451f436f7af410-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-4c344a156e08ebb132223c1b86dd3e45>,
'Transformation-958a3d55d581ec276b72394435df4bd0-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-0dff0aaf2d05bb1245a49961851d462c>,
'Transformation-22a2847c2a28584956e90d549fc6bcc6-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-6e452d9c821c26e75d2dc8bae917088d>,
'Transformation-d5cdd9f4f4edd380361129082a01360b-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-aad5127a208dc1f5dc23a3eca56b63da>,
'Transformation-20fd18a057e4affd5c78cad41a81d3b4-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-d0d4d4749d8e0bab09409247b1f40d44>,
'Transformation-44cf523e9d0e53674fadc59c9b9110b7-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-11ede0f561d2d787f0ce7d49d825cea1>,
'Transformation-856f85d23d9b8b84916d5b83ebfa1773-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-be791dff8238a59df7d08e6973844fbb>,
'Transformation-ef57a778259ba88b9fb4c23cc2bd78f4-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-626117d35fc898d368179c703b79ae44>,
'Transformation-c33480c9f0dfd80852541205f46914a7-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-6948f63f1a437cfffa21869cc8c59205>,
'Transformation-4bfeb3edd7f0d47ec52f533a5cf435a0-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-8a44fd759abe0b2ee5e5e1b3354712b1>,
'Transformation-8e74110792d4c68bb5a772733d490401-ddotson-tyk2-demo': None,
'Transformation-06eac733376ab1ff566248040fe37e01-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-b8c7ed6500e0de9a622001aa69793dbc>,
'Transformation-76de739d87d53981abfff4ab9ce071e4-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-49834c8fefc70456a6bcf909bc86d2dd>}
results['Transformation-82e90d15b483a813a6501073e3964930-ddotson-tyk2-demo'].get_estimate()
results['Transformation-82e90d15b483a813a6501073e3964930-ddotson-tyk2-demo'].get_uncertainty()
In this case, we have only a single ProtocolDAGResult
for each Transformation
(since we created and actioned only 1 Task
for each), and so the uncertainty (standard deviation between replicate simulations) given for this ProtocolResult
is 0.0. The RelativeHybridTopologyProtocol
combines ProtocolDAGResult
values statistically, reducing the uncertainty but not increasing convergence with additional Task
s
By contrast other Protocol
s, such as the feflow
NonEquilibriumCyclingProtocol
, will improve convergence with more Task
s on a given Transformation
; in the case of the NonEquilibriumCyclingProtocol
, the non-equilibrium work values for each ProtocolDAGResult
are combined together and fed to BAR
to produce a single estimate with its own uncertainty.
Let’s create and action 2 additional Task
s for each Transformation
:
tasks = []
for tf_sk in asc.get_network_transformations(an_sk):
tasks.extend(asc.create_tasks(tf_sk, count=2))
asc.action_tasks(tasks, an_sk)
[<ScopedKey('Task-a3bb16b506354cc1920c60931fc3b6e7-ddotson-tyk2-demo')>,
<ScopedKey('Task-5519d7feced84701bcd4caa4edf280de-ddotson-tyk2-demo')>,
<ScopedKey('Task-111a3cff46504f3abe702a16b5b164c1-ddotson-tyk2-demo')>,
<ScopedKey('Task-32c8103f4d624fcca7985557caeae893-ddotson-tyk2-demo')>,
<ScopedKey('Task-0cff85cbbcef4ae49336957c4bbbb285-ddotson-tyk2-demo')>,
<ScopedKey('Task-0a749b1896db48bf80210690b0d9ac6f-ddotson-tyk2-demo')>,
<ScopedKey('Task-88a7d011602c4ce39c5ff94cbb7a98ae-ddotson-tyk2-demo')>,
<ScopedKey('Task-1555231f3cd24d8db03a8e8a327adbba-ddotson-tyk2-demo')>,
<ScopedKey('Task-974c0495a7624e1aa2996a76c80c32fb-ddotson-tyk2-demo')>,
<ScopedKey('Task-3c291b7e214a4dba8ee403f939af516c-ddotson-tyk2-demo')>,
<ScopedKey('Task-7ff6c66b32644095bd881b3b9c723e3a-ddotson-tyk2-demo')>,
<ScopedKey('Task-8872ddfbd2534af390543d769b8587a9-ddotson-tyk2-demo')>,
<ScopedKey('Task-c8db674e068241cabb0f95ad03dfbb68-ddotson-tyk2-demo')>,
<ScopedKey('Task-0d72db080df544068687f3c4cfe0c4b4-ddotson-tyk2-demo')>,
<ScopedKey('Task-77457ab4507d43b996eb004856af7a1b-ddotson-tyk2-demo')>,
<ScopedKey('Task-c1f28d1a77de45bfb0acbd287f343fac-ddotson-tyk2-demo')>,
<ScopedKey('Task-61bee1dabd264ef88d558acec688f5fc-ddotson-tyk2-demo')>,
<ScopedKey('Task-c0c2070639064571b205f56eb7a695ab-ddotson-tyk2-demo')>,
<ScopedKey('Task-4aeb583ab91d40388eee8e8f13b6cc1e-ddotson-tyk2-demo')>,
<ScopedKey('Task-a0537c24e96c4cc4950f3e37b807a879-ddotson-tyk2-demo')>,
<ScopedKey('Task-1213ffa058ec4395bc96ba779f46609f-ddotson-tyk2-demo')>,
<ScopedKey('Task-7ac8ed14ac0f427da03689a87c9cf44c-ddotson-tyk2-demo')>,
<ScopedKey('Task-4b02f8758a934223b71395eaa8b26db6-ddotson-tyk2-demo')>,
<ScopedKey('Task-c5fd174873f5433cb23da737fa4642aa-ddotson-tyk2-demo')>,
<ScopedKey('Task-c3e1dc4d32634258acda412544ae778e-ddotson-tyk2-demo')>,
<ScopedKey('Task-ed05fb435acd4fca8661684a92cbe246-ddotson-tyk2-demo')>,
<ScopedKey('Task-134d60f5433e4e0babb1a81e24bdd63a-ddotson-tyk2-demo')>,
<ScopedKey('Task-19154647d7264f729cef865c47c55122-ddotson-tyk2-demo')>,
<ScopedKey('Task-a318bc8e78dd4625a6028ae0801a998c-ddotson-tyk2-demo')>,
<ScopedKey('Task-5312ba1a78784f909f5f4a767fc6fc49-ddotson-tyk2-demo')>,
<ScopedKey('Task-bb556e64e1a24bf8998b23b3f78a7ece-ddotson-tyk2-demo')>,
<ScopedKey('Task-ad7a64014b704fa685254441bf16e5ef-ddotson-tyk2-demo')>,
<ScopedKey('Task-aeee5cf01ef0494e8cb51c2e86324cf6-ddotson-tyk2-demo')>,
<ScopedKey('Task-789fbed60ab342dea8d071e2d925279f-ddotson-tyk2-demo')>,
<ScopedKey('Task-50033fbe0ffe4ee185566164cb33af65-ddotson-tyk2-demo')>,
<ScopedKey('Task-87a6955b36be404293b9491f476f7045-ddotson-tyk2-demo')>,
<ScopedKey('Task-8dc6bc5c62f044258c5b129a8c4d570f-ddotson-tyk2-demo')>,
<ScopedKey('Task-3c387ea6205447d786f9c1431591bef9-ddotson-tyk2-demo')>,
<ScopedKey('Task-8e6815ffb28b447988722bca9ddc8039-ddotson-tyk2-demo')>,
<ScopedKey('Task-5779831c1fcd4bdc819aefc608890940-ddotson-tyk2-demo')>,
<ScopedKey('Task-1b17a99a7acf4fea92879158e87b82ff-ddotson-tyk2-demo')>,
<ScopedKey('Task-37b15e41fba948c286cf73d70eff627f-ddotson-tyk2-demo')>,
<ScopedKey('Task-dea84bf82e354bb39c9b2d70fc91709d-ddotson-tyk2-demo')>,
<ScopedKey('Task-f1a16bc51f934dd19736cbf34e76804f-ddotson-tyk2-demo')>,
<ScopedKey('Task-1c8808c0d25d40e3bb02ceaa71132402-ddotson-tyk2-demo')>,
<ScopedKey('Task-fa2d45744f214c09a53246456d7dd27e-ddotson-tyk2-demo')>,
<ScopedKey('Task-52a5e061bec9413e912eaafaa1967456-ddotson-tyk2-demo')>,
<ScopedKey('Task-7d1c27206cd44bf296bf46f8f5267a2b-ddotson-tyk2-demo')>]
asc.get_network_status(an_sk)
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ status ┃ count ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ complete │ 21 │ │ running │ 0 │ │ waiting │ 49 │ │ error │ 2 │ │ invalid │ 0 │ │ deleted │ 0 │ └──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘
{'waiting': 49, 'error': 2, 'complete': 21}
As we watch our Task
s 'complete'
or 'error'
, we can introspect and deal with errors as before:
asc.get_network_status(an_sk)
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ status ┃ count ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ complete │ 46 │ │ running │ 24 │ │ waiting │ 0 │ │ error │ 2 │ │ invalid │ 0 │ │ deleted │ 0 │ └──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘
{'complete': 46, 'running': 24, 'error': 2}
asc.get_network_tasks(an_sk, status='error')
[<ScopedKey('Task-f6a91ccc6f9e4bcbace3331a0109c9da-ddotson-tyk2-demo')>,
<ScopedKey('Task-ce83f523df324508a6e6cfbce1c34393-ddotson-tyk2-demo')>]
print(asc.get_task_failures(asc.get_network_tasks(an_sk, status='error')[0])[0].protocol_unit_failures[0].traceback)
INFO: HTTP Request: GET https://api.alchemiscale.org/transformations/Transformation-152928d4d69298dcedf2dd622073a95b-ddotson-tyk2-demo/failures/ProtocolDAGResultRef-08d10b9130b46d11e70cdf91343e686e-ddotson-tyk2-demo "HTTP/1.1 200 OK"
Traceback (most recent call last):
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/gufe/protocols/protocolunit.py", line 320, in execute
outputs = self._execute(context, **inputs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/equil_rfe_methods.py", line 1129, in _execute
outputs = self.run(scratch_basepath=ctx.scratch,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/equil_rfe_methods.py", line 998, in run
sampler.setup(
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py", line 121, in setup
minimize(compound_thermostate_copy, sampler_state,
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openfe/protocols/openmm_rfe/_rfe_utils/multistate.py", line 295, in minimize
context, integrator = dummy_cache.get_context(
^^^^^^^^^^^^^^^^^^^^^^^^
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openmmtools/cache.py", line 770, in get_context
context = thermodynamic_state.create_context(integrator, self.platform)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openmmtools/states.py", line 1179, in create_context
return openmm.Context(system, integrator, platform)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/lila/home/dotson/mambaforge/envs/alchemiscalemiscale-compute-ddotson-v0.5.0-2024.08.15/lib/python3.12/site-packages/openmm/openmm.py", line 8037, in __init__
_openmm.Context_swiginit(self, _openmm.new_Context(*args))
^^^^^^^^^^^^^^^^^^^^^^^^^^
openmm.OpenMMException: Error initializing CUDA: CUDA_ERROR_MPS_CONNECTION_FAILED (805) at /home/conda/feedstock_root/build_artifacts/openmm_1721257909416/work/platforms/cuda/src/CudaContext.cpp:91
asc.set_tasks_status(asc.get_network_tasks(an_sk, status='error'), 'waiting')
INFO: HTTP Request: POST https://api.alchemiscale.org/bulk/tasks/status/set "HTTP/1.1 200 OK"
[<ScopedKey('Task-f6a91ccc6f9e4bcbace3331a0109c9da-ddotson-tyk2-demo')>,
<ScopedKey('Task-ce83f523df324508a6e6cfbce1c34393-ddotson-tyk2-demo')>]
asc.get_network_status(an_sk)
AlchemicalNetwork-6f22642592f789c4e7f918412ca947c5-ddotson-tyk2-demo ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ status ┃ count ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ complete │ 72 │ │ running │ 0 │ │ waiting │ 0 │ │ error │ 0 │ │ invalid │ 0 │ │ deleted │ 0 │ └──────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘
{'complete': 72}
Finally, with more sampling, we can pull our results again, this time with more ProtocolDAGResult
s included in the ProtocolResult
for our Transformation
s:
results = dict()
for tf_sk in asc.get_network_transformations(an_sk):
results[str(tf_sk)] = asc.get_transformation_results(tf_sk, visualize=False)
results
{'Transformation-15fdd9ff3f68cde67e706ffa87f6a9b7-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-394e0443ec86ffc0c16c8e00c82e9b12>,
'Transformation-152928d4d69298dcedf2dd622073a95b-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-d116d14de4407acbeaac9f29587719e9>,
'Transformation-82e90d15b483a813a6501073e3964930-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-dcd71f35d39c66af70ed89aa7a279000>,
'Transformation-73164716e2ad3c2e410fce413fa41be4-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-ad94c858081e514585b43f65c5a68f18>,
'Transformation-e3022bfd607e772136f5c262875a8e78-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-18c2551684f6b806fe8b6a9ece095009>,
'Transformation-6064e55fed6e90b90b727b299462cbad-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-d55f3325367e5a7f0a211077df9ccc7e>,
'Transformation-28453f83bb03486a08afc096a8dc5298-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-09120f442dacd96143ee12c6d166ef79>,
'Transformation-eebbe29feefc097086749081844d8adc-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-fe22eaa7d7ebd7a5d23adae41e98e1f6>,
'Transformation-2a345646fa5b3900aba8b74935fc4ec6-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-ed6c30bbaeb8124335f6080b648f78da>,
'Transformation-f8fb4a09881bce886dcb55a88be49016-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-6365aa29ece7fbc051405ba330d52328>,
'Transformation-874c39cca17041ceb7a91f142104e8eb-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-1d47e63b207e04a4960741d14d80480c>,
'Transformation-b4857c06e9564cf3c2451f436f7af410-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-febae6aa0f84830b17f89913b20b3e31>,
'Transformation-958a3d55d581ec276b72394435df4bd0-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-501a8034121f24d44579bc6edd1ba167>,
'Transformation-22a2847c2a28584956e90d549fc6bcc6-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-ef58753a1b7c856a2831287b22a11fa2>,
'Transformation-d5cdd9f4f4edd380361129082a01360b-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-0172ce2b7010333b1c341e7d14eb3a81>,
'Transformation-20fd18a057e4affd5c78cad41a81d3b4-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-32536721cc9db99a4929e743344fb5ff>,
'Transformation-44cf523e9d0e53674fadc59c9b9110b7-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-761f181afdb778b924eaaf8f096aff42>,
'Transformation-856f85d23d9b8b84916d5b83ebfa1773-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-ed7a2dd2de1cb55ecd338eee10989819>,
'Transformation-ef57a778259ba88b9fb4c23cc2bd78f4-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-889ff92aa5ec0aa090eca47f055ecd0e>,
'Transformation-c33480c9f0dfd80852541205f46914a7-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-bb3df5d7f37c55d3f4eddd4fa553d0db>,
'Transformation-4bfeb3edd7f0d47ec52f533a5cf435a0-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-09615a191ec58d54947dd7c974c9da3d>,
'Transformation-8e74110792d4c68bb5a772733d490401-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-2e0e7ef5064dc92335ad8baf158a2052>,
'Transformation-06eac733376ab1ff566248040fe37e01-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-836ae5c50d7bdeaa4273fa9aa0835226>,
'Transformation-76de739d87d53981abfff4ab9ce071e4-ddotson-tyk2-demo': <RelativeHybridTopologyProtocolResult-2dbee9e919321cdfac39967b797f27de>}
Advanced example: comparing results to experimental values with cinnabar
¶
Source notebook: https://github.com/IAlibay/alchemiscale-utilities/blob/main/notebooks/TYK2_MST/Alchemiscale_submit_and_analyze.ipynb
We can gather up results for each Transformation
as above and put into a form suitable for use with cinnabar
. This will allow us to compare to experimental values for the binding free energies of these ligands. We use experimental values listed in ligands.yml
, originally sourced from openforcefield/protein-ligand-benchmark:
%cat ligands.yml
lig_ejm_31:
measurement:
comment: Table 4, entry 31
doi: 10.1016/j.ejmech.2013.03.070
error: 0.029
type: ki
unit: uM
value: 0.096
name: lig_ejm_31
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)C([H])([H])[H])[H])[H])Cl)[H]'
lig_ejm_42:
measurement:
comment: Table 4, entry 42
doi: 10.1016/j.ejmech.2013.03.070
error: 0.019
type: ki
unit: uM
value: 0.064
name: lig_ejm_42
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)C([H])([H])C([H])([H])[H])[H])[H])Cl)[H]'
lig_ejm_43:
measurement:
comment: Table 4, entry 43
doi: 10.1016/j.ejmech.2013.03.070
error: 0.25
type: ki
unit: uM
value: 0.84
name: lig_ejm_43
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)C([H])(C([H])([H])[H])C([H])([H])[H])[H])[H])Cl)[H]'
lig_ejm_45:
measurement:
comment: Table 4, entry 45
doi: 10.1016/j.ejmech.2013.03.070
error: 0.028
type: ki
unit: uM
value: 0.094
name: lig_ejm_45
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)C([H])([H])C3(C(C3([H])[H])([H])[H])[H])[H])[H])Cl)[H]'
lig_ejm_46:
measurement:
comment: Table 4, entry 46
doi: 10.1016/j.ejmech.2013.03.070
error: 0.0014
type: ki
unit: uM
value: 0.0048
name: lig_ejm_46
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)C3(C(C3([H])[H])([H])[H])[H])[H])[H])Cl)[H]'
lig_ejm_47:
measurement:
comment: Table 4, entry 47
doi: 10.1016/j.ejmech.2013.03.070
error: 0.022
type: ki
unit: uM
value: 0.074
name: lig_ejm_47
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)C3(C(C(C3([H])[H])([H])[H])([H])[H])[H])[H])[H])Cl)[H]'
lig_ejm_48:
measurement:
comment: Table 4, entry 48
doi: 10.1016/j.ejmech.2013.03.070
error: 0.072
type: ki
unit: uM
value: 0.24
name: lig_ejm_48
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)C3(C(C(C(C3([H])[H])([H])[H])([H])[H])([H])[H])[H])[H])[H])Cl)[H]'
lig_ejm_50:
measurement:
comment: Table 4, entry 50
doi: 10.1016/j.ejmech.2013.03.070
error: 0.075
type: ki
unit: uM
value: 0.25
name: lig_ejm_50
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)C([H])([H])O[H])[H])[H])Cl)[H]'
lig_ejm_54:
measurement:
comment: Table 4, entry 54
doi: 10.1016/j.ejmech.2013.03.070
error: 0.0054
type: ki
unit: uM
value: 0.018
name: lig_ejm_54
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)N([H])C([H])([H])C([H])([H])[H])[H])[H])Cl)[H]'
lig_ejm_55:
measurement:
comment: Table 4, entry 55
doi: 10.1016/j.ejmech.2013.03.070
error: 0.051
type: ki
unit: uM
value: 0.17
name: lig_ejm_55
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)OC([H])([H])[H])[H])[H])Cl)[H]'
lig_jmc_23:
measurement:
comment: Table 2, entry 23; values for four different enantionmers listed, this
one is for cis
doi: 10.1021/jm400266t
error: 0.75
type: ki
unit: nM
value: 2.5
name: lig_jmc_23
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)[C@@]3([C@](C3([H])[H])([H])F)[H])[H])[H])Cl)[H]'
lig_jmc_27:
measurement:
comment: Table 2, entry 27; cis enantiomer
doi: 10.1021/jm400266t
error: 1.5
type: ki
unit: nM
value: 5.1
name: lig_jmc_27
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)[C@@]3([C@](C3([H])[H])([H])Cl)[H])[H])[H])Cl)[H]'
lig_jmc_28:
measurement:
comment: Table 2, entry 28; cis enantiomer
doi: 10.1021/jm400266t
error: 2.6
type: ki
unit: nM
value: 8.5
name: lig_jmc_28
smiles: '[H]c1c(c(c(c(c1[H])Cl)C(=O)N([H])c2c(c(nc(c2[H])N([H])C(=O)[C@@]3([C@](C3([H])[H])([H])C([H])([H])[H])[H])[H])[H])Cl)[H]'
from pathlib import Path
# Next we define a convenience method to check what type of transformation we are looking at
def _scan_components(system):
comps = system.components.values()
if any([isinstance(comp, openfe.ProteinComponent) for comp in comps]):
return "complex"
elif any([isinstance(comp, openfe.SolventComponent) for comp in comps]):
return "solvent"
else:
return "vacuum"
# Next we create a results dictionary and scan our network edges to accumulate all
# the free energy results and their uncertainty
results_dir = Path('results')
results_dir.mkdir(parents=True, exist_ok=True)
results = dict()
for tf_sk in asc.get_network_transformations(an_sk):
transformation = asc.get_transformation(tf_sk)
result = asc.get_transformation_results(tf_sk)
if result is None:
continue
runtype = _scan_components(transformation.stateA)
mapping = transformation.mapping[0]
nameA = mapping.componentA.name
nameB = mapping.componentB.name
# store in accumulator
if f"{nameA}_{nameB}" in results.keys():
results[f"{nameA}_{nameB}"][runtype] = result
else:
results[f"{nameA}_{nameB}"] = {runtype: result, 'molA': nameA, 'molB': nameB}
# output individual results to a separate `.dat` file for future use
filename = results_dir / f"{nameA}_{nameB}.{runtype}.results.dat"
output = f"{result.get_estimate()},{result.get_uncertainty()}"
with open(filename, 'w') as f:
f.write(output)
Writing out a cinnabar
input CSV file¶
Since this is a known benchmark system, we have experimental values for each of our ligands. We can combine them with our results to create a cinnabar
input CSV file.
# Here we create a dictionary of experimental values from an input protein-ligand benchmark ligands.yml
# First load the yaml data
import yaml
with open('ligands.yml') as stream:
exp_data = yaml.safe_load(stream)
# Define a method for converting between Ki to estimated DG
from openff.units import unit
import math
def ki_to_dg(
ki: unit.Quantity, uncertainty: unit.Quantity,
temperature: unit.Quantity = 298.15 * unit.kelvin
) -> tuple[unit.Quantity, unit.Quantity]:
"""
Convenience method to convert a Ki w/ a given uncertainty to an
experimental estimate of the binding free energy.
Parameters
----------
ki : unit.Quantity
Experimental Ki value (e.g. 5 * unit.nanomolar)
uncertainty : unit.Quantity
Experimental error. Note: returns 0 if =< 0 * unit.nanomolar.
temperature : unit.Quantity
Experimental temperature. Default: 298.15 * unit.kelvin.
Returns
-------
DG : unit.Quantity
Gibbs binding free energy.
dDG : unit.Quantity
Error in binding free energy.
"""
if ki > 1e-15 * unit.nanomolar:
DG = (unit.molar_gas_constant * temperature.to(unit.kelvin)
* math.log(ki / unit.molar)).to(unit.kilocalorie_per_mole)
else:
raise ValueError("negative Ki values are not supported")
if uncertainty > 0 * unit.molar:
dDG = (unit.molar_gas_constant * temperature.to(unit.kelvin)
* uncertainty / ki).to(unit.kilocalorie_per_mole)
else:
dDG = 0 * unit.kilocalorie_per_mole
return DG, dDG
from openff.units import unit
exp_values = {}
for lig in exp_data:
exp_units = unit(exp_data[lig]['measurement']['unit'])
exp_values[lig] = {}
DG, dDG = ki_to_dg(exp_data[lig]['measurement']['value'] * exp_units,
exp_data[lig]['measurement']['error'] * exp_units)
exp_values[lig]['value'] = DG
exp_values[lig]['error'] = dDG
import numpy as np
# write out the cinnabar input file
with open('cinnabar_input.csv', 'w') as f:
f.write("# Experimental block\n")
f.write("# Ligand, expt_DDG, expt_dDDG\n")
for entry in exp_values:
f.write(f"{entry},{exp_values[entry]['value'].m:.2f},{exp_values[entry]['error'].m:.2f}\n")
f.write('\n')
f.write('# Calculated block\n')
f.write('# Ligand1,Ligand2,calc_DDG,calc_dDDG(MBAR),calc_dDDG(additional)\n')
for entry in results:
estimate = (results[entry]['complex'].get_estimate()
- results[entry]['solvent'].get_estimate())
err = np.sqrt(results[entry]['complex'].get_uncertainty()**2
+ results[entry]['solvent'].get_uncertainty()**2)
molA = results[entry]['molA']
molB = results[entry]['molB']
f.write(f"{molA},{molB},{estimate.m:.2f},0,{err.m:.2f}\n")
Plotting out results using cinnabar
components¶
Note: the cinnabar
API will change in its next release.
import cinnabar
from cinnabar import plotting as cinnabar_plotting
from cinnabar import FEMap, femap
%matplotlib inline
Generating a Cinnabar FEMap and plotting out the network¶
First let’s load the data into cinnabar and draw out the network of free energy results:
fe = FEMap.from_csv('cinnabar_input.csv')
fe.generate_absolute_values() # Get MLE generated estimates of the absolute values
fe.draw_graph()
Plotting out the relative free energy results¶
Next we can go ahead and plot out the relative free energy results:
# note you can pass the filename argument to write things out to file
cinnabar_plotting.plot_DDGs(fe.to_legacy_graph(), figsize=5, xy_lim=[5, -5])
Plotting out the absolute free energy results¶
Finally let’s go ahead and plot out the MLE derived absolute free energies:
# note you can pass the filename argument to write to file
cinnabar_plotting.plot_DGs(fe.to_legacy_graph(), figsize=5, xy_lim=[5, -5])
We can also shift our free energies by the average experimental value to have DGs on the same scale as experiment:
data = femap.read_csv('cinnabar_input.csv')
exp_DG_sum = sum([data['Experimental'][i].DG for i in data['Experimental'].keys()])
shift = exp_DG_sum / len(data['Experimental'].keys())
cinnabar_plotting.plot_DGs(fe.to_legacy_graph(), figsize=5, shift=shift.m)
/home/david/micromamba/envs/alchemiscale-client-user-guide-2024.09.26/lib/python3.12/site-packages/cinnabar/femap.py:35: UserWarning: Assuming kcal/mol units on measurements
warnings.warn("Assuming kcal/mol units on measurements")