Source code for openff.evaluator.substances.amounts
"""
An API for defining and creating substances.
"""
import abc
import math
import typing
import numpy as np
from openff.evaluator.attributes import UNDEFINED, Attribute, AttributeClass
[docs]class Amount(AttributeClass, abc.ABC):
"""A representation of the amount of a given component
in a `Substance`.
"""
value = Attribute(
docstring="The value of this amount.",
type_hint=typing.Union[float, int],
read_only=True,
)
[docs] def __init__(self, value=UNDEFINED):
"""
Parameters
----------
value: float or int
The value of this amount.
"""
self._set_value("value", value)
@property
def identifier(self):
"""A string identifier for this amount."""
raise NotImplementedError()
[docs] @abc.abstractmethod
def to_number_of_molecules(self, total_substance_molecules, tolerance=None):
"""Converts this amount to an exact number of molecules
Parameters
----------
total_substance_molecules: int
The total number of molecules in the whole substance. This amount
will contribute to a portion of this total number.
tolerance: float, optional
The tolerance with which this amount should be in. As an example,
when converting a mole fraction into a number of molecules, the
total number of molecules may not be sufficiently large enough to
reproduce this amount.
Returns
-------
int
The number of molecules which this amount represents,
given the `total_substance_molecules`.
"""
raise NotImplementedError()
def __str__(self):
return self.identifier
def __repr__(self):
return f"<{self.__class__.__name__} {str(self)}>"
def __eq__(self, other):
return type(self) is type(other) and np.isclose(self.value, other.value)
def __ne__(self, other):
return not (self == other)
def __hash__(self):
return hash(self.identifier)
[docs]class MoleFraction(Amount):
"""The mole fraction of a `Component` in a `Substance`."""
value = Attribute(docstring="The value of this amount.", type_hint=float)
@property
def identifier(self):
return f"x={self.value:.6f}"
[docs] def to_number_of_molecules(self, total_substance_molecules, tolerance=None):
# Determine how many molecules of each type will be present in the system.
number_of_molecules = self.value * total_substance_molecules
fractional_number_of_molecules = number_of_molecules % 1
if np.isclose(fractional_number_of_molecules, 0.5):
number_of_molecules = int(number_of_molecules)
else:
number_of_molecules = int(round(number_of_molecules))
if number_of_molecules == 0:
raise ValueError(
"The total number of substance molecules was not large enough, "
"such that this non-zero amount translates into zero molecules "
"of this component in the substance."
)
if tolerance is not None:
mole_fraction = number_of_molecules / total_substance_molecules
if abs(mole_fraction - self.value) > tolerance:
raise ValueError(
f"The mole fraction ({mole_fraction}) given a total number of molecules "
f"({total_substance_molecules}) is outside of the tolerance {tolerance} "
f"of the target mole fraction {self.value}"
)
return number_of_molecules
[docs] def validate(self, attribute_type=None):
super(MoleFraction, self).validate(attribute_type)
if self.value <= 0.0 or self.value > 1.0:
raise ValueError(
"A mole fraction must be greater than zero, and less than or "
"equal to one."
)
if math.floor(self.value * 1e6) < 1:
raise ValueError(
"Mole fractions are only precise to the sixth "
"decimal place within this class representation."
)
[docs]class ExactAmount(Amount):
"""The exact number of instances of a `Component` in a `Substance`.
An assumption is made that this amount is for a component which is
infinitely dilute (such as ligands in binding calculations), and hence
do not contribute to the total mole fraction of a `Substance`.
"""
value = Attribute(docstring="The value of this amount.", type_hint=int)
@property
def identifier(self):
return f"n={int(round(self.value)):d}"
[docs] def to_number_of_molecules(self, total_substance_molecules, tolerance=None):
return self.value