Source code for cinnabar.measurements

"""
Measurements
============

Contains the :class:`Measurement` class which is used to define a single free energy difference,
as well as the :class:`ReferenceState` class which denotes the end point for absolute measurements.

"""
from openff.models.models import DefaultModel
from openff.models.types import FloatQuantity
from openff.units import unit
from typing import Hashable, Union
import math


[docs] class ReferenceState: """A label indicating a reference point to which absolute measurements are relative A ``ReferenceState`` optionally has a label, which is used to differentiate it to other absolute measurements that might be relative to a different reference point. E.g. MLE estimations are against an arbitrary reference state that can be linked to the reference point of experiments. """ label: str def __init__(self, label: str = ""): """ Parameters ---------- label: str, optional label for this reference point. If no label is given, an empty string is used, signifying the "true zero" reference point. """ self.label = label
[docs] def is_true_ground(self) -> bool: """If this ReferenceState is the zero point of all other measurements""" return not self.label
def __repr__(self): if self.is_true_ground(): return "<ReferenceState Zero>" else: return f"<ReferenceState ({self.label})>" def __eq__(self, other): return other.__class__ == self.__class__ and other.label == self.label def __hash__(self): return hash(self.label)
[docs] class Measurement(DefaultModel): """The free energy difference of moving from A to B All quantities are accompanied by units, to prevent mix-ups associated with kcal and kJ. This is done via the `openff.units` package:: >>> m = Measurement(labelA='LigandA', labelB='LigandB', ... DG=2.4 * unit.kilocalorie_per_mol, ... uncertainty=0.2 * unit.kilocalorie_per_mol, ... computational=True, ... source='gromacs') Alternatively strings are automatically coerced into quantities, making this equivalent to above:: >>> m = Measurement(labelA='LigandA', labelB='LigandB', ... DG='2.4 kcal/mol', ... uncertainty='0.2 kcal/mol', ... computational=True, ... source='gromacs') Where a measurement is "absolute" then a `ReferenceState` can be used as the label at one end of the measurement. I.e. it is relative to a reference ground state. E.g. an absolute measurement for "LigandA" is defined as:: >>> m = Measurement(labelA=ReferenceState(), labelB='LigandA', ... DG=-11.2 * unit.kilocalorie_per_mol, ... uncertainty=0.3 * unit.kilocalorie_per_mol, ... computational=False) """
[docs] class Config: frozen = True
labelA: Hashable """Label of state A, e.g. a ligand name or any hashable Python object""" labelB: Hashable """Label of state B""" DG: FloatQuantity['kilocalorie_per_mole'] """The free energy difference of moving from A to B""" uncertainty: FloatQuantity['kilocalorie_per_mole'] """The uncertainty of the DG measurement""" temperature: FloatQuantity['kelvin'] = 298.15 * unit.kelvin """Temperature that the measurement was taken as""" computational: bool """If this measurement is computationally based (or experimental)""" source: str = "" """An arbitrary label to group measurements from a common source"""
[docs] @classmethod def from_experiment(cls, label: Union[str, Hashable], Ki: unit.Quantity, uncertainty: unit.Quantity = 0 * unit.nanomolar, *, source: str = '', temperature: unit.Quantity = 298.15 * unit.kelvin, ): """Shortcut to create a Measurement from experimental data Can perform conversion from Ki values to kcal/mol values. Parameters ---------- label: str | Hashable label for this data point. Ki: unit.Quantity experimental Ki value ex.: 500 * unit.nanomolar OR 0.5 * unit.micromolar uncertainty: unit.Quantity uncertainty of the experimental value default is zero if no uncertainty is provided (0 * unit.nanomolar) source: str, optional source of experimental measurement temperature: unit.Quantity, optional temperature in K at which the experimental measurement was carried out. By default: 298 K (298.15 * unit.kelvin) """ if Ki > 0 * unit.molar: DG = (unit.molar_gas_constant * temperature.to(unit.kelvin) * math.log( Ki / unit.molar)).to(unit.kilocalorie_per_mole) else: raise ValueError( "Ki value cannot be zero or negative. " "Check if dG value was provided instead of Ki." ) # Convert Ki uncertainty into dG uncertainty: RT * uncertainty/Ki # https://physics.stackexchange.com/questions/95254/the-error-of-the-natural-logarithm if uncertainty >= 0 * unit.molar: uncertainty_DG = (unit.molar_gas_constant * temperature.to(unit.kelvin) * uncertainty / Ki).to(unit.kilocalorie_per_mole) else: raise ValueError( "Uncertainty cannot be negative. Check input." ) return cls(labelA=ReferenceState(), labelB=label, DG=DG, uncertainty=uncertainty_DG, computational=False, source=source, temperature=temperature, )