Source code for kliff.transforms.property_transforms
from typing import List, Union
import numpy as np
from kliff.dataset import Configuration, Dataset
[docs]
class PropertyTransform:
"""
A property transform is a function that maps a property of a configuration to a
transformed property. For example, current property transforms include normalization
of the energy and forces.
Attributes:
keep_original: If True, the original property values are kept in the configuration
object. Otherwise, the original property values are discarded.
original_values: A map that stores the original property values of
configurations. It stores list of property values for each configuration.
property_key: The key of the property to be transformed.
"""
def __init__(self, property_key: str = "energy", keep_original: bool = False):
self._keep_original = keep_original
self.original_values = None
self.property_key = property_key
@property
def keep_original(self):
return self._keep_original
[docs]
def transform(self, dataset: Union[List[Configuration], Dataset]):
"""
Transform the property of a configuration.
Args:
dataset: A list of configurations or a dataset.
"""
raise PropertyTransformError("This method is not implemented.")
[docs]
def inverse(self, dataset: Union[List[Configuration], Dataset]):
"""
Inverse transform the property of a configuration.
Args:
dataset: A list of configurations or a dataset.
"""
raise PropertyTransformError("This method is not implemented.")
def __call__(self, dataset: Union[List[Configuration], Dataset]):
self.transform(dataset)
[docs]
@staticmethod
def get_configuration_list(
dataset: Union[List[Configuration], Dataset],
) -> List[Configuration]:
"""
Get a list of configurations from a dataset. This method ensures constant API
for any arbitrary dataset. It is expected that this method will maintain the
order of the Configurations in the dataset. Tihs enqures the inverse property
mapping is done correctly.
Args:
dataset: A list of configurations or a dataset.
Returns:
A list of configurations.
"""
configuration_list = []
if isinstance(dataset, Dataset):
configuration_list = dataset.get_configs()
elif isinstance(dataset, List):
configuration_list = dataset
else:
PropertyTransformError("Unknown format of dataset.")
return configuration_list
def _get_property_values(
self, dataset: Union[List[Configuration], Dataset]
) -> np.ndarray:
"""
Get the property values from all the configurations in a dataset and return
them as a numpy array.
Args:
dataset: A list of configurations or a dataset
Returns:
A numpy array of property values.
"""
configuration_list = self.get_configuration_list(dataset)
property_values = list(
map(lambda config: getattr(config, self.property_key), configuration_list)
)
return np.asarray(property_values)
def _set_property_values(
self,
dataset: Union[List[Configuration], Dataset],
property_values: Union[np.ndarray, List[Union[float, int]]],
):
"""
Set the property values of all the configurations in a dataset. This method
assumes that the transformed values are in the same order as the configurations
and can be iterated over using sinle integer index.
Args:
dataset: A list of configurations or a dataset
property_values: A numpy array of property values.
"""
configuration_list = self.get_configuration_list(dataset)
for i, configuration in enumerate(configuration_list):
setattr(configuration, self.property_key, property_values[i])
[docs]
class NormalizedPropertyTransform(PropertyTransform):
"""
Normalize the property of a configuration to zero mean and unit variance.
.. math::
x' = \\frac{x - \\mu}{\\sigma}
"""
def __init__(self, property_key: str = "energy", keep_original: bool = False):
super().__init__(property_key, keep_original)
self.original_values = None
self.property_key = property_key
self._keep_original = keep_original
self.mean = 0.0
self.std = 1.0
[docs]
def transform(self, dataset: Union[List[Configuration], Dataset]):
original_property_values = self._get_property_values(dataset)
if self.keep_original:
self.original_values = original_property_values
self.mean = original_property_values.mean()
self.std = original_property_values.std()
transformed_values = (original_property_values - self.mean) / self.std
self._set_property_values(dataset, transformed_values)
[docs]
def inverse(self, dataset: Union[List[Configuration], Dataset]):
transformed_values = self._get_property_values(dataset)
transformed_values = transformed_values * self.std + self.mean
self._set_property_values(dataset, transformed_values)
[docs]
class RMSNormalizePropertyTransform(PropertyTransform):
"""
Normalize the property of a configuration to using the root mean square of the property.
It is useful for normalizing oscillators properties, usually because mean for
such properties is zero.
.. math::
x' = \\frac{x}{\\sqrt{\\frac{1}{N}\\sum_{i=1}^N x_i^2}}
"""
def __init__(self, property_key: str = "forces", keep_original: bool = False):
super().__init__(property_key, keep_original)
self.original_values = None
self.property_key = property_key
self._keep_original = keep_original
self.rms_mean = 0.0
[docs]
def transform(self, dataset: Union[List[Configuration], Dataset]):
original_property_values = self._get_property_values(dataset)
if self.keep_original:
self.original_values = original_property_values
self.rms_mean = np.sqrt(np.mean(np.square(original_property_values)))
transformed_values = original_property_values / self.rms_mean
self._set_property_values(dataset, transformed_values)
[docs]
def inverse(self, dataset: Union[List[Configuration], Dataset]):
transformed_values = self._get_property_values(dataset)
transformed_values = transformed_values * self.rms_mean
self._set_property_values(dataset, transformed_values)
[docs]
class RMSMagnitudeNormalizePropertyTransform(PropertyTransform):
"""
Normalize the property of a configuration using the root mean square of the magnitude
of the property. This method is useful for normalizing forces.
.. math::
x' = \\frac{x}{\\sqrt{\\frac{1}{N}\\sum_{i=1}^N \\|x_i\\|^2}}
"""
def __init__(self, property_key: str = "forces", keep_original: bool = False):
super().__init__(property_key, keep_original)
self.original_values = None
self.property_key = property_key
self._keep_original = keep_original
self.rms_mean_magnitude = 0.0
[docs]
def transform(self, dataset: Union[List[Configuration], Dataset]):
original_property_values = self._get_property_values(dataset)
if self.keep_original:
self.original_values = original_property_values
self.rms_mean_magnitude = np.sqrt(
np.mean(np.square(np.linalg.norm(original_property_values, axis=1)))
)
transformed_values = original_property_values / self.rms_mean_magnitude
self._set_property_values(dataset, transformed_values)
[docs]
def inverse(self, dataset: Union[List[Configuration], Dataset]):
transformed_values = self._get_property_values(dataset)
transformed_values = transformed_values * self.rms_mean_magnitude
self._set_property_values(dataset, transformed_values)
[docs]
class PropertyTransformError(Exception):
def __init__(self, msg):
super(PropertyTransformError, self).__init__(msg)
self.msg = msg