Source code for hiphive.cutoffs

import pickle
from typing import Union, BinaryIO, TextIO
import numpy as np
from ase import Atoms
from ase.neighborlist import NeighborList
from hiphive.input_output.pretty_table_prints import table_array_to_string


[docs] class Cutoffs: """ This class maintains information about the cutoff configuration, i.e. which clusters will be included ("inside cutoff"). It also encapsulates functionality that is used e.g., during cluster space construction. Here, `n-body` refers to number of atoms in a cluster. For example the cluster (0011) is a two-body cluster of fourth order and the cluster (123) is a three-body cluster of third order. Parameters ---------- cutoff_matrix : numpy.ndarray The matrix element `ij` provides to the cutoff for order `j+2` and nbody `i+2`; elements with `i>j` will be ignored. """ def __init__(self, cutoff_matrix): self._cutoff_matrix = np.array(cutoff_matrix, dtype=float) if len(self._cutoff_matrix.shape) != 2: raise ValueError('Please specify cutoff matrix as a 2D array') for i, row in enumerate(self._cutoff_matrix): if np.any(row[i:] < 0): raise ValueError('Negative number as cutoff') row[:i] = np.nan self._cutoff_matrix = self._cutoff_matrix[:(self.max_nbody-1), :(self.max_order-1)] @property def cutoff_matrix(self) -> np.ndarray: """ Copy of cutoff matrix. """ return self._cutoff_matrix.copy() @property def orders(self) -> list[int]: """ Allowed orders. """ return list(range(2, self.max_order + 1)) @property def nbodies(self) -> list[int]: """ Allowed bodies. """ return list(range(2, self.max_nbody + 1))
[docs] def get_cutoff(self, order: int, nbody: int) -> float: """ Returns the cutoff for a given body and order. Parameters ---------- order nbody Raises ------ ValueError If `order` is not in orders. ValueError If `nbody` is not in nbodies. ValueError If `nbody` is larger than order. """ if order not in self.orders: raise ValueError('order not in orders') if nbody not in self.nbodies: raise ValueError('nbody not in nbodies') if nbody > order: raise ValueError('nbody can not be larger than order') return self._cutoff_matrix[nbody - 2, order - 2]
@property def max_cutoff(self) -> float: """ Maximum cutoff. """ max_cutoff = 0 for i, row in enumerate(self._cutoff_matrix): max_cutoff = max(max_cutoff, np.max(row[i:])) return max_cutoff @property def max_nbody(self) -> int: """ Maximum body. """ nbody = 1 for i, row in enumerate(self._cutoff_matrix): if np.any(row[i:]): nbody = i + 2 return nbody @property def max_order(self) -> int: """ Maximum order. """ order = None for col in range(self._cutoff_matrix.shape[1]): if np.any(self._cutoff_matrix[:col + 1, col]): order = col + 2 return order
[docs] def max_nbody_cutoff(self, nbody: int): """ Return maximum cutoff for a given body. Parameters ---------- nbody """ if nbody not in self.nbodies: raise ValueError('nbody not in nbodies.') return np.max(self._cutoff_matrix[nbody - 2, max(0, nbody - 2):])
[docs] def max_nbody_order(self, nbody: int): """ Returns maximum order for a given body. Parameters ---------- nbody """ if nbody not in self.nbodies: raise ValueError('nbody not in nbodies.') row = self._cutoff_matrix[nbody - 2] max_order = None for order, cutoff in enumerate(row[nbody-2:], start=nbody): if cutoff: max_order = order return max_order
[docs] def write(self, fileobj: Union[BinaryIO, TextIO]): """ Writes instance to file. Parameters ---------- fileobj File-like object to which the cutoffs will be written. """ pickle.dump(self._cutoff_matrix, fileobj)
[docs] def read(fileobj: Union[BinaryIO, TextIO]): """ Reads a :class:`Cutoffs` instance from file. Parameters ---------- fileobj Input file to read from. """ data = pickle.load(fileobj) return Cutoffs(data)
[docs] def to_filename_tag(self) -> str: """ Simple function turning cutoffs into a string to be used in, e.g., file names. """ s = [] for i, c in enumerate(self._cutoff_matrix.tolist(), start=2): s.append('{}body-{}'.format(i, '_'.join(map(str, c)))) return '_'.join(s)
def __str__(self): cutoff_matrix = self._cutoff_matrix.copy() cutoff_matrix = np.vstack(([[None] * len(self.orders)], cutoff_matrix)) s = table_array_to_string(cutoff_matrix) width = max(len(c) for c in s.split('\n')) header = ' Cutoffs '.center(width, '=') + '\n' bottom = '\n' + ''.center(width, '=') s = header + s + bottom return s def __repr__(self): return 'Cutoffs({!r})'.format(self._cutoff_matrix)
[docs] class CutoffMaximumBody(Cutoffs): """ Class for specifying cutoff-list plus maximum body. Usefull when creating, e.g., sixth order expansions but with only 3-body interactions. Parameters ---------- cutoff_list : list[float] List of cutoffs for order 2, 3, etc. Must be in decresing order. max_nbody : int No clusters containing more than `max_nbody` atoms will be generated. """ def __init__(self, cutoff_list, max_nbody): cutoff_matrix = np.zeros((max_nbody - 1, len(cutoff_list))) for order, cutoff in enumerate(cutoff_list, start=2): cutoff_matrix[:, order - 2] = cutoff super().__init__(cutoff_matrix)
[docs] def is_cutoff_allowed(atoms: Atoms, cutoff: float) -> bool: """ Checks if atoms is compatible with cutoff. Parameters ---------- atoms Structure used for checking compatibility with cutoff. cutoff Cutoff to be tested. Returns ------- True if `cutoff` is compatible with `atoms`, else False. """ nbrlist = NeighborList(cutoffs=[cutoff / 2] * len(atoms), skin=0, self_interaction=False, bothways=True) nbrlist.update(atoms) for i in range(len(atoms)): neighbors, _ = nbrlist.get_neighbors(i) if i in neighbors: return False if len(neighbors) != len(set(neighbors)): return False return True
[docs] def estimate_maximum_cutoff(atoms: Atoms, max_iter: int = 11) -> float: """ Estimates the maximum possible cutoff given the atoms object. Parameters ---------- atoms Structure used for checking compatibility with cutoff. max_iter Number of iterations in binary search. """ # First upper boundary of cutoff upper_cutoff = min(np.linalg.norm(atoms.cell, axis=1)) # generate all possible offsets given upper_cutoff nbrlist = NeighborList(cutoffs=[upper_cutoff / 2] * len(atoms), skin=0, self_interaction=False, bothways=True) nbrlist.update(atoms) all_offsets = [] for i in range(len(atoms)): _, offsets = nbrlist.get_neighbors(i) all_offsets.extend([tuple(offset) for offset in offsets]) # find lower boundary and new upper boundary unique_offsets = set(all_offsets) unique_offsets.discard((0, 0, 0)) upper = min(np.linalg.norm(np.dot(offset, atoms.cell)) for offset in unique_offsets) lower = upper / 2.0 # run binary search between the upper and lower bounds for _ in range(max_iter): cutoff = (upper + lower) / 2 if is_cutoff_allowed(atoms, cutoff): lower = cutoff else: upper = cutoff return lower
[docs] class BaseClusterFilter: """Base cluster filter class. This filter simply accepts all proposed clusters. A proper subclass must implement the same methods. """
[docs] def setup(self, atoms: Atoms): """ The filter is passed the environment of the primitive cell. Parameters ---------- atoms Non-pbc primitive cell plus neighboring atoms. """ self._atoms = atoms
def __call__(self, cluster: tuple[int]): """ Returns True or False when a cluster is proposed. Parameters ---------- cluster Indices of proposed cluster referenced to the internal :class:`Atoms` object. """ return True