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