Source code for ase.build.supercells

# fmt: off

"""Helper functions for creating supercells."""

import numpy as np

from ase import Atoms
from ase.utils import deprecated


class SupercellError(Exception):
    """Use if construction of supercell fails"""


[docs] @deprecated('use `eval_length_deviation` instead.') def get_deviation_from_optimal_cell_shape(*args, **kwargs): return eval_length_deviation(*args, **kwargs)
def eval_shape_deviation(cell, target_shape="sc"): r""" Calculates the deviation of the given cell from the target cell metric. Parameters ---------- cell : (..., 3, 3) array_like Metric given as a 3x3 matrix of the input structure. Multiple cells can also be given as a higher-dimensional array. target_shape : {'sc', 'fcc'} Desired supercell shape. Returns ------- float or ndarray Cell metric(s) (0 is perfect score) """ cell = np.asarray(cell) eff_cubic_length = np.cbrt(np.abs(np.linalg.det(cell))) # 'a_0' if target_shape == 'sc': target_len = eff_cubic_length target_cos = 0.0 # cos(+-pi/2) = 0.0 target_metric = np.eye(3) elif target_shape == 'fcc': # FCC is characterised by 60 degree angles & lattice vectors = 2**(1/6) # times the eff cubic length: target_len = eff_cubic_length * 2 ** (1 / 6) target_cos = 0.5 # cos(+-pi/3) = 0.5 target_metric = np.eye(3) + target_cos * (np.ones((3, 3)) - np.eye(3)) else: raise ValueError(target_shape) # calculate cell @ cell.T for (... , 3, 3) # with cell -> C_mij # and metric -> M_mkl # M_mkl = (sum_j C_mkj * C_mlj) / leff**2 metric = cell @ np.swapaxes(cell, -2, -1) normed = metric / target_len[..., None, None] ** 2 # offdiagonal ~ cos angle -> score = np.abs(cos angle - cos target_angle) scores = np.add.reduce((normed - target_metric) ** 2, axis=(-2, -1)) return scores def eval_length_deviation(cell, target_shape="sc"): r"""Calculate the deviation from the target cell shape. Calculates the deviation of the given cell metric from the ideal cell metric defining a certain shape. Specifically, the function evaluates the expression `\Delta = || Q \mathbf{h} - \mathbf{h}_{target}||_2`, where `\mathbf{h}` is the input metric (*cell*) and `Q` is a normalization factor (*norm*) while the target metric `\mathbf{h}_{target}` (via *target_shape*) represent simple cubic ('sc') or face-centered cubic ('fcc') cell shapes. Replaced with code from the `doped` defect simulation package (https://doped.readthedocs.io) to be rotationally invariant, boosting performance. Parameters ---------- cell : (..., 3, 3) array_like Metric given as a 3x3 matrix of the input structure. Multiple cells can also be given as a higher-dimensional array. target_shape : {'sc', 'fcc'} Desired supercell shape. Can be 'sc' for simple cubic or 'fcc' for face-centered cubic. Returns ------- float or ndarray Cell metric(s) (0 is perfect score) .. deprecated:: 3.24.0 `norm` is unused in ASE 3.24.0 and removed in ASE 3.25.0. """ cell = np.asarray(cell) cell_lengths = np.sqrt(np.add.reduce(cell**2, axis=-1)) eff_cubic_length = np.cbrt(np.abs(np.linalg.det(cell))) # 'a_0' if target_shape == 'sc': target_len = eff_cubic_length elif target_shape == 'fcc': # FCC is characterised by 60 degree angles & lattice vectors = 2**(1/6) # times the eff cubic length: target_len = eff_cubic_length * 2 ** (1 / 6) else: raise ValueError(target_shape) inv_target_len = 1.0 / target_len # rms difference to eff cubic/FCC length: diffs = cell_lengths * inv_target_len[..., None] - 1.0 scores = np.sqrt(np.add.reduce(diffs**2, axis=-1)) return scores def _guess_initial_transformation(cell, target_shape, target_size, verbose=False): # Set up target metric if target_shape == 'sc': target_metric = np.eye(3) elif target_shape == 'fcc': target_metric = 0.5 * np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]], dtype=float) else: raise ValueError(target_shape) if verbose: print("target metric (h_target):") print(target_metric) # Normalize cell metric to reduce computation time during looping norm = (target_size * abs(np.linalg.det(cell)) / np.linalg.det(target_metric)) ** (-1.0 / 3) norm_cell = norm * cell if verbose: print(f"normalization factor (Q): {norm:g}") # Approximate initial P matrix ideal_P = np.dot(target_metric, np.linalg.inv(norm_cell)) if verbose: print("idealized transformation matrix:") print(ideal_P) starting_P = np.array(np.around(ideal_P, 0), dtype=int) if verbose: print("closest integer transformation matrix (P_0):") print(starting_P) return ideal_P, starting_P def _build_matrix_operations(starting_P, lower_limit, upper_limit): mat_dim = starting_P.shape[0] if not mat_dim == starting_P.shape[1]: raise ValueError('Cell matrix should be quadratic.') # Build a big matrix of all admissible integer matrix operations. # (If this takes too much memory we could do blocking but there are # too many for looping one by one.) dimensions = [(upper_limit + 1) - lower_limit] * mat_dim**2 operations = np.moveaxis(np.indices(dimensions), 0, -1) operations = operations.reshape(-1, mat_dim, mat_dim) operations += lower_limit # Each element runs from lower to upper limits. operations += starting_P return operations def _screen_supercell_size(operations, target_size): # screen supercells with the target size determinants = np.round(np.linalg.det(operations), 0).astype(int) good_indices = np.where(np.abs(determinants) == target_size)[0] if not good_indices.size: print("Failed to find a transformation matrix.") return None operations = operations[good_indices] return operations def _optimal_transformation(operations, scores, ideal_P): imin = np.argmin(scores) best_score = scores[imin] # screen candidates with the same best score operations = operations[np.abs(scores - best_score) < 1e-6] # select the one whose cell orientation is the closest to the target # https://gitlab.com/ase/ase/-/merge_requests/3522 imin = np.argmin(np.add.reduce((operations - ideal_P)**2, axis=(-2, -1))) optimal_P = operations[imin] if np.linalg.det(optimal_P) <= 0: optimal_P *= -1 # flip signs if negative determinant return optimal_P, best_score all_score_funcs = {"length": eval_length_deviation, "metric": eval_shape_deviation}
[docs] def find_optimal_cell_shape( cell, target_size, target_shape, lower_limit=-2, upper_limit=2, verbose=False, score_key='length' ): """Obtain the optimal transformation matrix for a supercell of target size and shape. Returns the transformation matrix that produces a supercell corresponding to *target_size* unit cells with metric *cell* that most closely approximates the shape defined by *target_shape*. Updated with code from the `doped` defect simulation package (https://doped.readthedocs.io) to be rotationally invariant and allow transformation matrices with negative determinants, boosting performance. Parameters: cell: 2D array of floats Metric given as a (3x3 matrix) of the input structure. target_size: integer Size of desired supercell in number of unit cells. target_shape: str Desired supercell shape. Can be 'sc' for simple cubic or 'fcc' for face-centered cubic. lower_limit: int Lower limit of search range. upper_limit: int Upper limit of search range. verbose: bool Set to True to obtain additional information regarding construction of transformation matrix. score_key: str key from all_score_funcs to select score function. Returns: 2D array of integers: Transformation matrix that produces the optimal supercell. """ # transform to np.array cell = np.asarray(cell) # get starting transformation # ideal_P ... transformation: target_cell = ideal_P @ cell # starting_P ... integer rounded (ideal_P) ideal_P, starting_P = _guess_initial_transformation(cell, target_shape, target_size, verbose=verbose) # build all admissible matrix operations 'centered' at starting_P operations = _build_matrix_operations(starting_P, lower_limit, upper_limit) # pre-screen operations based on target_size operations = _screen_supercell_size(operations, target_size) # evaluate derivations of the screened supercells if score_key in all_score_funcs: get_deviation_score = all_score_funcs[score_key] else: msg = (f'Score func key {score_key} not implemented.' + f'Please select from {all_score_funcs}.') raise SupercellError(msg) scores = get_deviation_score(operations @ cell, target_shape) # obtain optimal transformation from scores optimal_P, best_score = _optimal_transformation(operations, scores, ideal_P) # Finalize. if verbose: print(f"smallest score (|Q P h_p - h_target|_2): {best_score:f}") print("optimal transformation matrix (P_opt):") print(optimal_P) print("supercell metric:") print(np.round(np.dot(optimal_P, cell), 4)) det = np.linalg.det(optimal_P) print(f"determinant of optimal transformation matrix: {det:g}") return optimal_P
[docs] def make_supercell(prim, P, *, wrap=True, order="cell-major", tol=1e-5): r"""Generate a supercell by applying a general transformation (*P*) to the input configuration (*prim*). The transformation is described by a 3x3 integer matrix `\mathbf{P}`. Specifically, the new cell metric `\mathbf{h}` is given in terms of the metric of the input configuration `\mathbf{h}_p` by `\mathbf{P h}_p = \mathbf{h}`. Parameters: prim: ASE Atoms object Input configuration. P: 3x3 integer matrix Transformation matrix `\mathbf{P}`. wrap: bool wrap in the end order: str (default: "cell-major") how to order the atoms in the supercell "cell-major": [atom1_shift1, atom2_shift1, ..., atom1_shift2, atom2_shift2, ...] i.e. run first over all the atoms in cell1 and then move to cell2. "atom-major": [atom1_shift1, atom1_shift2, ..., atom2_shift1, atom2_shift2, ...] i.e. run first over atom1 in all the cells and then move to atom2. This may be the order preferred by most VASP users. tol: float tolerance for wrapping """ supercell_matrix = P supercell = clean_matrix(supercell_matrix @ prim.cell) # cartesian lattice points lattice_points_frac = lattice_points_in_supercell(supercell_matrix) lattice_points = np.dot(lattice_points_frac, supercell) N = len(lattice_points) if order == "cell-major": shifted = prim.positions[None, :, :] + lattice_points[:, None, :] elif order == "atom-major": shifted = prim.positions[:, None, :] + lattice_points[None, :, :] else: raise ValueError(f"invalid order: {order}") shifted_reshaped = shifted.reshape(-1, 3) superatoms = Atoms(positions=shifted_reshaped, cell=supercell, pbc=prim.pbc) # Copy over any other possible arrays, inspired by atoms.__imul__ for name, arr in prim.arrays.items(): if name == "positions": # This was added during construction of the super cell continue shape = (N * arr.shape[0], *arr.shape[1:]) if order == "cell-major": new_arr = np.repeat(arr[None, :], N, axis=0).reshape(shape) elif order == "atom-major": new_arr = np.repeat(arr[:, None], N, axis=1).reshape(shape) superatoms.set_array(name, new_arr) # check number of atoms is correct n_target = abs(int(np.round(np.linalg.det(supercell_matrix) * len(prim)))) if n_target != len(superatoms): msg = "Number of atoms in supercell: {}, expected: {}".format( n_target, len(superatoms)) raise SupercellError(msg) if wrap: superatoms.wrap(eps=tol) return superatoms
def lattice_points_in_supercell(supercell_matrix): """Find all lattice points contained in a supercell. Adapted from pymatgen, which is available under MIT license: The MIT License (MIT) Copyright (c) 2011-2012 MIT & The Regents of the University of California, through Lawrence Berkeley National Laboratory """ diagonals = np.array([ [0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1], ]) d_points = np.dot(diagonals, supercell_matrix) mins = np.min(d_points, axis=0) maxes = np.max(d_points, axis=0) + 1 ar = np.arange(mins[0], maxes[0])[:, None] * np.array([1, 0, 0])[None, :] br = np.arange(mins[1], maxes[1])[:, None] * np.array([0, 1, 0])[None, :] cr = np.arange(mins[2], maxes[2])[:, None] * np.array([0, 0, 1])[None, :] all_points = ar[:, None, None] + br[None, :, None] + cr[None, None, :] all_points = all_points.reshape((-1, 3)) frac_points = np.dot(all_points, np.linalg.inv(supercell_matrix)) tvects = frac_points[np.all(frac_points < 1 - 1e-10, axis=1) & np.all(frac_points >= -1e-10, axis=1)] assert len(tvects) == round(abs(np.linalg.det(supercell_matrix))) return tvects def clean_matrix(matrix, eps=1e-12): """ clean from small values""" matrix = np.array(matrix) for ij in np.ndindex(matrix.shape): if abs(matrix[ij]) < eps: matrix[ij] = 0 return matrix