"""
Properties base API.
"""
import uuid
from enum import IntFlag, unique
import numpy as np
from propertyestimator import unit
from propertyestimator.utils.serialization import TypedBaseModel
[docs]@unique
class PropertyPhase(IntFlag):
"""An enum describing the phase a property was collected in.
"""
Undefined = 0x00
Solid = 0x01
Liquid = 0x02
Gas = 0x04
def __str__(self):
"""
Returns
---
str
A string representation of the PropertyPhase enum
"""
phases = '|'.join([phase.name for phase in PropertyPhase if self & phase])
return phases
def __repr__(self):
"""
Returns
---
str
A string representation of the PropertyPhase enum
"""
return str(self)
[docs]class Source(TypedBaseModel):
"""Container class for information about how a property was measured / calculated.
.. todo:: Swap this out with a more general provenance class.
"""
def __getstate__(self): return {}
def __setstate__(self, state): pass
[docs]class MeasurementSource(Source):
"""Contains any metadata about how a physical property was measured by experiment.
This class contains either the DOI and/or the reference, but must contain at
least one as the observable must have a source, even if it was measured in lab.
Attributes
----------
doi : str or None, default None
The DOI for the source, preferred way to identify for source
reference : str
The long form description of the source if no DOI is available, or more
information is needed or wanted.
"""
[docs] def __init__(self, doi='', reference=''):
"""Constructs a new MeasurementSource object.
Parameters
----------
doi : str or None, default None
The DOI for the source, preferred way to identify for source
reference : str
The long form description of the source if no DOI is available, or more
information is needed or wanted.
"""
self.doi = doi
self.reference = reference
def __getstate__(self):
return {
'doi': self.doi,
'reference': self.reference,
}
def __setstate__(self, state):
self.doi = state['doi']
self.reference = state['reference']
[docs]class CalculationSource(Source):
"""Contains any metadata about how a physical property was calculated.
This includes at which fidelity the property was calculated at (e.g Direct
simulation, reweighting, ...) in addition to the parameters which were
used as part of the calculations.
Attributes
----------
fidelity : str
The fidelity at which the property was calculated
provenance : dict of str and Any
A dictionary containing information about how the property was calculated.
"""
[docs] def __init__(self, fidelity=None, provenance=None):
"""Constructs a new CalculationSource object.
Parameters
----------
fidelity : str
The fidelity at which the property was calculated
provenance : dict of str and Any
A dictionary containing information about how the property was calculated.
"""
self.fidelity = fidelity
self.provenance = provenance
def __getstate__(self):
return {
'fidelity': self.fidelity,
'provenance': self.provenance,
}
def __setstate__(self, state):
self.fidelity = state['fidelity']
self.provenance = state['provenance']
[docs]class ParameterGradientKey:
@property
def tag(self):
return self._tag
@property
def smirks(self):
return self._smirks
@property
def attribute(self):
return self._attribute
[docs] def __init__(self, tag=None, smirks=None, attribute=None):
self._tag = tag
self._smirks = smirks
self._attribute = attribute
def __getstate__(self):
return {
'tag': self._tag,
'smirks': self._smirks,
'attribute': self._attribute
}
def __setstate__(self, state):
self._tag = state['tag']
self._smirks = state['smirks']
self._attribute = state['attribute']
def __str__(self):
return f'tag={self._tag} smirks={self._smirks} attribute={self._attribute}'
def __repr__(self):
return f'<ParameterGradientKey {str(self)}>'
def __hash__(self):
return hash((self._tag, self._smirks, self._attribute))
def __eq__(self, other):
return (isinstance(other, ParameterGradientKey) and
self._tag == other._tag and
self._smirks == other._smirks and
self._attribute == other._attribute)
def __ne__(self, other):
return not self.__eq__(other)
[docs]class ParameterGradient:
@property
def key(self):
return self._key
@property
def value(self):
return self._value
[docs] def __init__(self, key=None, value=None):
self._key = key
self._value = value
def __getstate__(self):
return {
'key': self._key,
'value': self._value,
}
def __setstate__(self, state):
self._key = state['key']
self._value = state['value']
def __str__(self):
return f'key=({self._key}) value={self._value}'
def __repr__(self):
return f'<ParameterGradient key={self._key} value={self._value}>'
def __add__(self, other):
"""
Parameters
----------
other: ParameterGradient
"""
if not isinstance(other, ParameterGradient):
raise ValueError('Only ParameterGradient objects can be added together.')
elif other.key != self.key:
raise ValueError('Only ParameterGradient objects with the same key can be added together.')
return ParameterGradient(self.key, self.value + other.value)
def __sub__(self, other):
"""
Parameters
----------
other: ParameterGradient
"""
if not isinstance(other, ParameterGradient):
raise ValueError('Only ParameterGradient objects can be subtracted.')
elif other.key != self.key:
raise ValueError('Only ParameterGradient objects with the same key can be subtracted.')
return ParameterGradient(self.key, self.value - other.value)
def __mul__(self, other):
"""
Parameters
----------
other: float, int, Quantity
"""
if (not isinstance(other, float) and
not isinstance(other, int) and
not isinstance(other, unit.Quantity)):
raise ValueError('ParameterGradient objects can only be multiplied by int\'s, '
'float\'s or Quantity objects.')
return ParameterGradient(self.key, self.value * other)
def __rmul__(self, other):
return self.__mul__(other)
def __truediv__(self, other):
"""
Parameters
----------
other: float, int, Quantity
"""
if (not isinstance(other, float) and
not isinstance(other, int) and
not isinstance(other, unit.Quantity)):
raise ValueError('ParameterGradient objects can only be divided by int\'s, '
'float\'s or Quantity objects.')
return ParameterGradient(self.key, self.value / other)
def __eq__(self, other):
return (isinstance(other, ParameterGradient) and
self.key == other.key and
np.isclose(self.value, other.value))
[docs]class PhysicalProperty(TypedBaseModel):
"""Represents the value of any physical property and it's uncertainty.
It additionally stores the thermodynamic state at which the property
was collected, the phase it was collected in, information about
the composition of the observed system, and metadata about how the
property was collected.
"""
[docs] def __init__(self, thermodynamic_state=None, phase=PropertyPhase.Undefined,
substance=None, value=None, uncertainty=None, gradients=None, source=None):
"""Constructs a new PhysicalProperty object.
Parameters
----------
thermodynamic_state : ThermodynamicState
The thermodynamic state that the property was measured in.
phase : PropertyPhase
The phase that the property was measured in.
substance : Substance
The composition of the substance that was measured.
value: unit.Quantity
The value of the measured physical property.
uncertainty: unit.Quantity
The uncertainty in the measured value.
source: Source
The source of this property.
"""
self.id = str(uuid.uuid4())
self.thermodynamic_state = thermodynamic_state
self.phase = phase
self.substance = substance
self.value = value
self.uncertainty = uncertainty
self.gradients = []
self.source = source
self._metadata = {}
def __getstate__(self):
return {
'id': self.id,
'thermodynamic_state': self.thermodynamic_state,
'phase': self.phase,
'substance': self.substance,
'value': self.value,
'uncertainty': self.uncertainty,
'gradients': self.gradients,
'source': self.source,
'metadata': self._metadata
}
def __setstate__(self, state):
self.id = state['id']
self.thermodynamic_state = state['thermodynamic_state']
self.phase = state['phase']
self.substance = state['substance']
self.value = state['value']
self.uncertainty = state['uncertainty']
self.gradients = state['gradients']
self.source = state['source']
self._metadata = state['metadata']
@property
def temperature(self):
"""propertyestimator.unit.Quantity or None: The temperature at which the property was collected."""
return None if self.thermodynamic_state is None else self.thermodynamic_state.temperature
@property
def pressure(self):
"""propertyestimator.unit.Quantity or None: The pressure at which the property was collected."""
return None if self.thermodynamic_state is None else self.thermodynamic_state.pressure
@property
def metadata(self):
"""dict of str and Any: Additional metadata associated with this property, such as
file paths to coordinate files or ...
All property metadata will be made accessible to property estimation workflows.
"""
return self._metadata
@metadata.setter
def metadata(self, value):
self._metadata = value
[docs] def set_value(self, value, uncertainty):
"""Set the value and uncertainty of this property.
Parameters
----------
value : propertyestimator.unit.Quantity
The value of the property.
uncertainty : propertyestimator.unit.Quantity
The uncertainty in the properties value.
"""
self.value = value
self.uncertainty = uncertainty
[docs] @staticmethod
def get_default_workflow_schema(calculation_layer, options=None):
"""Returns the default workflow schema to use for
a specific calculation layer.
Parameters
----------
calculation_layer: str
The calculation layer which will attempt to execute the workflow
defined by this schema.
options: WorkflowOptions
The options to use when setting up the default workflows.
Returns
-------
WorkflowSchema
The default workflow schema.
"""
raise NotImplementedError()