Source code for mchammer.calculators.target_vector_calculator
from collections import OrderedDict
from typing import Any
import numpy as np
from ase import Atoms
from icet import ClusterSpace
from icet.core.sublattices import Sublattices
from mchammer.calculators.base_calculator import BaseCalculator
[docs]
class TargetVectorCalculator(BaseCalculator):
r"""
A :class:`TargetVectorCalculator` evaluates the similarity between a
structure and a target cluster vector.
It is used by :class:`TargetClusterVectorAnnealing
<mchammer.ensembles.TargetClusterVectorAnnealing>` to generate special
quasirandom structures and other structures with a prescribed cluster
vector, see the section on :ref:`special quasirandom structures
<advanced_topics_sqs_structures>`.
Such a comparison can be carried out in many ways, and this implementation
follows the measure proposed by van de Walle *et al.* in Calphad **42**, 13
(2013) [WalTiwJon13]_.
Specifically, the objective function :math:`Q` is calculated as
.. math::
Q = - \omega L + \sum_{\alpha}
\left||\Gamma_{\alpha} - \Gamma^{\text{target}}_{\alpha}\right||.
Here, :math:`\Gamma_{\alpha}` are the components of the cluster vector
and :math:`\Gamma^\text{target}_{\alpha}` the corresponding target values.
The factor :math:`\omega` is the radius of the largest pair cluster such
that all clusters with the same or smaller radii have
:math:`\Gamma_{\alpha} - \Gamma^\text{target}_{\alpha} = 0`.
The objective function depends on the whole cluster vector, so the
calculator evaluates full configurations only and does not implement
:func:`calculate_change`.
Parameters
----------
structure
Structure for which to set up the calculator.
cluster_space
Cluster space from which to build the calculator.
target_vector
Cluster vector that the cluster vector of a configuration is compared
to.
weights
Weight of each component in the comparison of the cluster vectors.
By default 1.0 for all components.
optimality_weight
Factor :math:`L`.
A high value favors a complete series of optimal cluster correlations
for the smallest pairs.
optimality_tol
Tolerance for determining whether a component matches the target
exactly, used in conjunction with :math:`L`.
name
Human-readable identifier for this calculator.
"""
def __init__(self, structure: Atoms, cluster_space: ClusterSpace,
target_vector: list[float],
weights: list[float] | None = None,
optimality_weight: float = 1.0,
optimality_tol: float = 1e-5,
name: str = 'Target vector calculator') -> None:
super().__init__(name=name)
if len(target_vector) != len(cluster_space):
raise ValueError('Cluster space and target vector '
'must have the same length')
self.cluster_space = cluster_space
self.target_vector = target_vector
if weights is None:
weights = np.array([1.0] * len(cluster_space))
else:
if len(weights) != len(cluster_space):
raise ValueError('Cluster space and weights '
'must have the same length')
self.weights = np.array(weights)
if optimality_weight is not None:
self.optimality_weight = optimality_weight
self.optimality_tol = optimality_tol
self.as_list = self.cluster_space.as_list
else:
self.optimality_weight = None
self.optimality_tol = None
self.as_list = None
self._cluster_space = cluster_space
self._structure = structure
self._sublattices = self._cluster_space.get_sublattices(structure)
def _get_rows(self) -> list[tuple[str, Any]]:
""" Label and value pairs describing this calculator. """
rows = super()._get_rows()
rows += [('number of sites', len(self._structure)),
('length of target vector', len(self.target_vector)),
('optimality_weight', self.optimality_weight),
('optimality_tol', self.optimality_tol)]
return rows
[docs]
def calculate_total(self, *, occupations: list[int]) -> float:
"""
Returns the objective function :math:`Q` of a configuration.
Parameters
----------
occupations
The entire occupation vector by atomic number.
"""
self._structure.set_atomic_numbers(occupations)
cv = self.cluster_space.get_cluster_vector(self._structure)
return compare_cluster_vectors(cv, self.target_vector,
self.as_list,
weights=self.weights,
optimality_weight=self.optimality_weight,
tol=self.optimality_tol)
[docs]
def calculate_change(self, *, sites: list[int],
current_occupations: list[int],
new_site_occupations: list[int]) -> float:
"""
Not implemented, since the objective function is not a sum of local
contributions.
Evaluate the changed configuration with :func:`calculate_total`
instead.
Raises
------
NotImplementedError
Always.
"""
raise NotImplementedError
@property
def sublattices(self) -> Sublattices:
""" Sublattices of the structure the calculator describes. """
return self._sublattices
[docs]
def compare_cluster_vectors(cv_1: np.ndarray, cv_2: np.ndarray,
as_list: list[OrderedDict],
weights: list[float] | None = None,
optimality_weight: float = 1.0,
tol: float = 1e-5) -> float:
"""
Returns a measure of the similarity between two cluster vectors.
The measure is the objective function :math:`Q` defined in the docstring
of :class:`TargetVectorCalculator
<mchammer.calculators.TargetVectorCalculator>`.
A smaller value means a closer match.
Parameters
----------
cv_1
First cluster vector.
cv_2
Second cluster vector.
as_list
Orbit data as obtained from :attr:`ClusterSpace.as_list
<icet.ClusterSpace.as_list>`.
weights
Weight assigned to each cluster vector element.
By default 1.0 for all elements.
optimality_weight
Quantity :math:`L` in [WalTiwJon13]_.
By default 1.0.
tol
Numerical tolerance for determining whether two elements are equal.
By default 1e-5.
"""
if weights is None:
weights = np.ones(len(cv_1))
diff = abs(cv_1 - cv_2)
score = np.dot(diff, weights)
if optimality_weight:
longest_optimal_radius = 0
for orbit_index, d in enumerate(diff):
orbit = as_list[orbit_index]
if orbit['order'] != 2:
continue
if d < tol:
longest_optimal_radius = orbit['radius']
else:
break
score -= optimality_weight * longest_optimal_radius
return score