Source code for shakenbreak.distortions

"""Module containing functions for applying distortions to defect structures."""

import os
import sys
import warnings
from typing import Optional

import numpy as np
from ase.neighborlist import NeighborList
from hiphive.structure_generation.rattle import _probability_mc_rattle, generate_mc_rattled_structures
from pymatgen.analysis.local_env import CrystalNN, MinimumDistanceNN
from pymatgen.core.structure import Structure
from pymatgen.io.ase import AseAtomsAdaptor  # could be removed to use the Structure.to/from_ase_atoms()

# methods added in pymatgen 2024.5.31, but then not backwards compatible. Will refactor to this if/when
# pymatgen>=2024.5.31 is a necessary requirement.


def _warning_on_one_line(message, category, filename, lineno, file=None, line=None):
    """Format warnings output."""
    return f"{os.path.split(filename)[-1]}:{lineno}: {category.__name__}: {message}\n"


warnings.formatwarning = _warning_on_one_line


[docs] def distort( structure: Structure, num_nearest_neighbours: int, distortion_factor: float, site_index: Optional[int] = None, # starting from 1 frac_coords: Optional[np.array] = None, # use frac coords for vacancies distorted_element: Optional[str] = None, distorted_atoms: Optional[list] = None, verbose: Optional[bool] = False, ) -> dict: """ Applies bond distortions to `num_nearest_neighbours` of the defect (specified by `site_index` (for substitutions or interstitials) or `frac_coords` (for vacancies)). Args: structure (:obj:`~pymatgen.core.structure.Structure`): Defect structure as a pymatgen object num_nearest_neighbours (:obj:`int`): Number of defect nearest neighbours to apply bond distortions to distortion_factor (:obj:`float`): The distortion factor to apply to the bond distance between the defect and nearest neighbours. Typical choice is between 0.4 (-60%) and 1.6 (+60%). site_index (:obj:`int`, optional): Index of defect site in structure (for substitutions or interstitials), counting from 1. frac_coords (:obj:`numpy.ndarray`, optional): Fractional coordinates of the defect site in the structure (for vacancies). distorted_element (:obj:`str`, optional): Neighbouring element to distort. If None, the closest neighbours to the defect will be chosen. (Default: None) distorted_atoms (:obj:`list`, optional): List of atom indices to distort. If None, the closest neighbours to the defect will be chosen. (Default: None) verbose (:obj:`bool`, optional): Whether to print distortion information. (Default: False) Returns: :obj:`dict`: Dictionary with distorted defect structure and the distortion parameters. """ aaa = AseAtomsAdaptor() input_structure_ase = aaa.get_atoms(structure) if site_index is not None: # site_index can be 0 atom_number = site_index - 1 # Align atom number with python 0-indexing elif isinstance(frac_coords, np.ndarray): # Only for vacancies! input_structure_ase.append("V") # fake "V" at vacancy input_structure_ase.positions[-1] = np.dot(frac_coords, input_structure_ase.cell) atom_number = len(input_structure_ase) - 1 else: raise ValueError( "Insufficient information to apply bond distortions, no `site_index`" " or `frac_coords` provided." ) neighbours = num_nearest_neighbours + 1 # Prevent self-counting of the defect atom itself if distorted_atoms and len(distorted_atoms) >= num_nearest_neighbours: nearest = [ ( round(input_structure_ase.get_distance(atom_number, index, mic=True), 4), index + 1, input_structure_ase.get_chemical_symbols()[index], ) for index in distorted_atoms ] nearest = sorted(nearest, key=lambda tup: tup[0])[0:num_nearest_neighbours] else: if distorted_atoms: warnings.warn( f"Only {len(distorted_atoms)} atoms were specified to distort in `distorted_atoms`, " f"but `num_nearest_neighbours` was set to {num_nearest_neighbours}. " f"Will overide the indices specified in `distorted_atoms` and distort the " f"{num_nearest_neighbours} closest neighbours to the defect site." ) distances = [ # Get all distances between the selected atom and all other atoms ( round(input_structure_ase.get_distance(atom_number, index, mic=True), 4), index + 1, # Indices start from 1 symbol, ) for index, symbol in zip( list(range(len(input_structure_ase))), input_structure_ase.get_chemical_symbols(), ) ] distances = sorted(distances, key=lambda tup: tup[0]) # Sort the distances shortest->longest if distorted_element: # filter the neighbours that match the element criteria and are # closer than 4.5 Angstroms nearest = [] # list of nearest neighbours for dist, index, element in distances[1:]: # starting from 1 to exclude defect atom if element == distorted_element and dist < 4.5 and len(nearest) < num_nearest_neighbours: nearest.append((dist, index, element)) # if the number of nearest neighbours not reached, add other neighbouring # elements if len(nearest) < num_nearest_neighbours: for i in distances[1:]: if len(nearest) < num_nearest_neighbours and i not in nearest and i[0] < 4.5: nearest.append(i) warnings.warn( f"{distorted_element} was specified as the nearest neighbour " f"element to distort, with `distortion_factor` {distortion_factor} " f"but did not find `num_nearest_neighbours` " f"({num_nearest_neighbours}) of these elements within 4.5 \u212B " f"of the defect site. For the remaining neighbours to distort, " f"we ignore the elemental identity. The final distortion information is:" ) sys.stderr.flush() # ensure warning message printed before distortion info verbose = True else: nearest = distances[1:neighbours] # Extract the nearest neighbours according to distance distorted = [ (i[0] * distortion_factor, i[1], i[2]) for i in nearest ] # Distort the nearest neighbour distances according to the distortion factor for i in distorted: input_structure_ase.set_distance( atom_number, i[1] - 1, i[0], fix=0, mic=True ) # Set the distorted distances in the ASE Atoms object if isinstance(frac_coords, np.ndarray): input_structure_ase.pop(-1) # remove fake V from vacancy structure distorted_structure = aaa.get_structure(input_structure_ase) distorted_atoms = [[i[1], i[2]] for i in nearest] # element and site number # Create dictionary with distortion info & distorted structure bond_distorted_defect = { "distorted_structure": distorted_structure, "num_distorted_neighbours": num_nearest_neighbours, "distorted_atoms": distorted_atoms, "undistorted_structure": structure, } if site_index: bond_distorted_defect["defect_site_index"] = site_index elif isinstance(frac_coords, np.ndarray): bond_distorted_defect["defect_frac_coords"] = frac_coords if verbose: distorted = [(round(i[0], 2), i[1], i[2]) for i in distorted] nearest = [(round(i[0], 2), i[1], i[2]) for i in nearest] # round numbers print( f"""\tDefect Site Index / Frac Coords: { site_index or np.around(frac_coords, decimals=3)} Original Neighbour Distances: {nearest} Distorted Neighbour Distances:\n\t{distorted}""" ) return bond_distorted_defect
[docs] def apply_dimer_distortion( structure: Structure, site_index: Optional[int] = None, frac_coords: Optional[np.array] = None, # use frac coords for vacancies ) -> dict: """ Apply a dimer distortion to a defect structure. The defect nearest neighbours are determined, from them the two closest in distance are selected, which are pushed towards each other so that their distance is 2.0 A. Args: structure (Structure): Defect structure. site_index (Optional[int], optional): Index of defect site (for non vacancy defects). Defaults to None. frac_coords (Optional[np.array], optional): Fractional coordinates of the defect site in the structure (for vacancies). Defaults to None. Returns: obj:`Structure`: Distorted dimer structure """ # Get ase atoms object aaa = AseAtomsAdaptor() input_structure_ase = aaa.get_atoms(structure) if site_index is not None: # site_index can be 0 atom_number = site_index - 1 # Align atom number with python 0-indexing elif type(frac_coords) in [list, tuple, np.ndarray]: # Only for vacancies! input_structure_ase.append("V") # fake "V" at vacancy input_structure_ase.positions[-1] = np.dot(frac_coords, input_structure_ase.cell) atom_number = len(input_structure_ase) - 1 else: raise ValueError( "Insufficient information to apply bond distortions, no `site_index`" " or `frac_coords` provided." ) # Get defect nn struct = aaa.get_structure(input_structure_ase) cnn = CrystalNN() sites = [d["site"] for d in cnn.get_nn_info(struct, atom_number)] # Get distances between NN distances = {} for i, site in enumerate(sites): for other_site in sites[i + 1 :]: distances[(site.index, other_site.index)] = site.distance(other_site) # Get defect NN with smallest distance and lowest indices: site_indexes = tuple( sorted(min(distances, key=lambda k: (round(distances.get(k, 10), 3), k[0], k[1]))) ) # Set their distance to 2 A input_structure_ase.set_distance( a0=site_indexes[0], a1=site_indexes[1], distance=2.0, fix=0.5, mic=True ) if type(frac_coords) in [list, tuple, np.ndarray]: input_structure_ase.pop(-1) # remove fake V from vacancy structure distorted_structure = aaa.get_structure(input_structure_ase) # Create dictionary with distortion info & distorted structure # Get distorted atoms distorted_structure_wout_oxi = distorted_structure.copy() distorted_structure_wout_oxi.remove_oxidation_states() distorted_atoms = [ [site_indexes[0], distorted_structure_wout_oxi[site_indexes[0]].species_string], [site_indexes[1], distorted_structure_wout_oxi[site_indexes[1]].species_string], ] bond_distorted_defect = { "distorted_structure": distorted_structure, "num_distorted_neighbours": 2, "distorted_atoms": distorted_atoms, "undistorted_structure": structure, } if site_index: bond_distorted_defect["defect_site_index"] = site_index elif type(frac_coords) in [np.ndarray, list]: bond_distorted_defect["defect_frac_coords"] = frac_coords return bond_distorted_defect
[docs] def rattle( structure: Structure, stdev: Optional[float] = None, d_min: Optional[float] = None, verbose: bool = False, n_iter: int = 1, active_atoms: Optional[list] = None, nbr_cutoff: float = 5, width: float = 0.1, max_attempts: int = 5000, max_disp: float = 2.0, seed: int = 42, ) -> Structure: """ Given a pymatgen Structure object, apply random displacements to all atomic positions, with the displacement distances randomly drawn from a Gaussian distribution of standard deviation `stdev`. Args: structure (:obj:`~pymatgen.core.structure.Structure`): Structure as a pymatgen object stdev (:obj:`float`): Standard deviation (in Angstroms) of the Gaussian distribution from which random atomic displacement distances are drawn during rattling. Default is set to 10% of the bulk nearest neighbour distance. d_min (:obj:`float`): Minimum interatomic distance (in Angstroms) in the rattled structure. Monte Carlo rattle moves that put atoms at distances less than this will be heavily penalised. Default is to set this to 80% of the nearest neighbour distance in the defect supercell. verbose (:obj:`bool`): Whether to print information about the rattling process. n_iter (:obj:`int`): Number of Monte Carlo cycles to perform. (Default: 1) active_atoms (:obj:`list`, optional): List of which atomic indices should undergo Monte Carlo rattling. If not set, rattles all atoms in the structure. (Default: None) nbr_cutoff (:obj:`float`): The radial cutoff distance (in Angstroms) used to construct the list of atomic neighbours for checking interatomic distances. (Default: 5) width (:obj:`float`): Width of the Monte Carlo rattling error function, in Angstroms. (Default: 0.1) max_disp (:obj:`float`): Maximum atomic displacement (in Angstroms) during Monte Carlo rattling. Rarely occurs and is used primarily as a safety net. (Default: 2.0) max_attempts (:obj:`int`): Maximum Monte Carlo rattle move attempts allowed for a single atom; if this limit is reached an `Exception` is raised. (Default: 5000) seed (:obj:`int`): Seed for NumPy random state from which random rattle displacements are generated. (Default: 42) Returns: :obj:`Structure`: Rattled pymatgen Structure object """ aaa = AseAtomsAdaptor() ase_struct = aaa.get_atoms(structure) if active_atoms is not None: # select only the distances involving active_atoms distance_matrix = structure.distance_matrix[active_atoms, :][:, active_atoms] else: distance_matrix = structure.distance_matrix sorted_distances = np.sort(distance_matrix[distance_matrix > 0.8].flatten()) if stdev is None: stdev = 0.1 * sorted_distances[0] if stdev > 0.4 or stdev < 0.02: warnings.warn( f"Automatic bond-length detection gave a bulk bond length of {10 * stdev} " f"\u212B and thus a rattle `stdev` of {stdev} ( = 10% bond length), " f"which is unreasonable. Reverting to 0.25 \u212B. If this is too large, " f"set `stdev` manually" ) stdev = 0.25 if d_min is None: d_min = 0.8 * sorted_distances[0] if d_min < 1.0: warnings.warn( f"Automatic bond-length detection gave a bulk bond length of " f"{(1/0.8)*d_min} \u212B, which is almost certainly too small. " f"Reverting to 2.25 \u212B. If this is too large, set `d_min` manually" ) d_min = 2.25 try: rattled_ase_struct = generate_mc_rattled_structures( ase_struct, 1, # n_configs in hiphive <= 1.1, n_structures in hiphive >= 1.2 rattle_std=stdev, d_min=d_min, n_iter=n_iter, active_atoms=active_atoms, nbr_cutoff=nbr_cutoff, width=width, max_attempts=max_attempts, max_disp=max_disp, seed=seed, )[0] except Exception as ex: if "attempts" in str(ex): for i in range(1, 10): # reduce d_min in 10% increments reduced_d_min = d_min * float(1 - (i / 10)) try: rattled_ase_struct = generate_mc_rattled_structures( ase_struct, 1, # n_configs in hiphive <= 1.1, n_structures in hiphive >= 1.2 rattle_std=stdev, d_min=reduced_d_min, n_iter=n_iter, active_atoms=active_atoms, nbr_cutoff=nbr_cutoff, width=width, max_attempts=max(max_attempts, 7000), # default is 5000 max_disp=max_disp, seed=seed, )[0] break except Exception as ex: if "attempts" in str(ex): continue raise ex if verbose: warnings.warn( f"Initial rattle with d_min {d_min:.2f} \u212B failed (some bond lengths " f"significantly smaller than this present), setting d_min to" f" {reduced_d_min:.2f} \u212B for this defect." ) else: raise ex return aaa.get_structure(rattled_ase_struct)
def _local_mc_rattle_displacements( atoms, site_index, rattle_std, d_min, width=0.1, n_iter=10, max_attempts=5000, max_disp=2.0, active_atoms=None, nbr_cutoff=None, seed=42, ) -> np.ndarray: # This function has been adapted from https://gitlab.com/materials-modeling/hiphive """ Generate displacements using the Monte Carlo rattle method. The displacements tail off as we move away from the defect site. Args: atoms (:obj:`ase.Atoms`): prototype structure site_index (:obj:`int`): index of defect, starting from 0 rattle_std (:obj:`float`): rattle amplitude (standard deviation in normal distribution) d_min (:obj:`float`): interatomic distance used for computing the probability for each rattle move. Center position of the error function width (:obj:`float`): width of the error function n_iter (:obj:`int`): number of Monte Carlo cycle max_disp (:obj:`float`): rattle moves that yields a displacement larger than max_disp will always be rejected. This rarley occurs and is more used as a safety net for not generating structures where two or more have swapped positions. max_attempts (:obj:`int`): limit for how many attempted rattle moves are allowed a single atom; if this limit is reached an `Exception` is raised. active_atoms (:obj:`list`): list of which atomic indices should undergo Monte Carlo rattling nbr_cutoff (:obj:`float`): The cutoff used to construct the neighborlist used for checking interatomic distances, defaults to 2 * d_min seed (:obj:`int`): Seed for NumPy random state from which random rattle displacements are generated. (Default: 42) Returns: :obj:`numpy.ndarray`: atomic displacements (`Nx3`) """ def scale_stdev(disp, r_min, r): """ Linearly scale the rattle standard deviation used for a given site according to its distance to the defect (e.g. sites further away from defect will experience a smaller distortion). """ if r == 0: # avoid dividing by 0 for defect site r = r_min return disp * r_min / r # Transform to pymatgen structure aaa = AseAtomsAdaptor() structure = aaa.get_structure(atoms) dist_defect_to_nn = max( structure[site_index].distance(_["site"]) for _ in MinimumDistanceNN().get_nn_info(structure, site_index) ) # distance between defect and nearest neighbours # setup rs = np.random.RandomState(seed) if nbr_cutoff is None: nbr_cutoff = 2 * d_min if active_atoms is None: active_atoms = range(len(atoms)) atoms_rattle = atoms.copy() reference_positions = atoms_rattle.get_positions() nbr_list = NeighborList( [nbr_cutoff / 2] * len(atoms_rattle), skin=0.0, self_interaction=False, bothways=True, ) nbr_list.update(atoms_rattle) # run Monte Carlo for _ in range(n_iter): for i in active_atoms: i_nbrs = np.setdiff1d(nbr_list.get_neighbors(i)[0], [i]) # Distance between defect and site i dist_defect_to_i = atoms.get_distance(site_index, i, mic=True) for _ in range(max_attempts): # generate displacement delta_disp = rs.normal( 0.0, scale_stdev(rattle_std, dist_defect_to_nn, dist_defect_to_i), 3, ) # displacement tails off with distance from defect atoms_rattle.positions[i] += delta_disp disp_i = atoms_rattle.positions[i] - reference_positions[i] # if total displacement of atom is greater than max_disp, then reject delta_disp if np.linalg.norm(disp_i) > max_disp: # revert delta_disp atoms_rattle[i].position -= delta_disp continue # compute min distance if len(i_nbrs) == 0: min_distance = np.inf else: min_distance = np.min(atoms_rattle.get_distances(i, i_nbrs, mic=True)) # accept or reject delta_disp if _probability_mc_rattle(min_distance, d_min, width) > rs.rand(): # accept delta_disp break # revert delta_disp atoms_rattle[i].position -= delta_disp else: raise Exception(f"Maximum attempts ({max_attempts}) for atom {i}") return atoms_rattle.positions - reference_positions def _generate_local_mc_rattled_structures( atoms, site_index, n_configs, rattle_std, d_min, seed=42, **kwargs ) -> list: r""" Returns list of configurations after applying a Monte Carlo local rattle. Compared to the standard Monte Carlo rattle, here the displacements tail off as we move away from the defect site. Rattling atom `i` is carried out as a Monte Carlo move that is accepted with a probability determined from the minimum interatomic distance :math:`d_{ij}`. If :math:`\\min(d_{ij})` is smaller than :math:`d_{min}` the move is only accepted with a low probability. This process is repeated for each atom a number of times meaning the magnitude of the final displacements is not *directly* connected to `rattle_std`. This function has been adapted from https://gitlab.com/materials-modeling/hiphive Warning: Repeatedly calling this function *without* providing different seeds will yield identical or correlated results. To avoid this behavior it is recommended to specify a different seed for each call to this function. Notes: The procedure implemented here might not generate a symmetric distribution for the displacements `kwargs` will be forwarded to `mc_rattle` (see user guide for a detailed explanation) Args: atoms (:obj:`ase.Atoms`): Prototype structure site_index (:obj:`int`): Index of defect site in structure (for substitutions or interstitials), counting from 1. n_configs (:obj:`int`): Number of structures to generate rattle_std (:obj:`float`): Rattle amplitude (standard deviation in normal distribution); note this value is not connected to the final average displacement for the structures d_min (:obj:`float`): Interatomic distance used for computing the probability for each rattle move seed (:obj:`int`): Seed for NumPy random state from which random rattle displacements are generated. (Default: 42) n_iter (:obj:`int`): Number of Monte Carlo cycles **kwargs: Additional keyword arguments to be passed to `mc_rattle` Returns: :obj:`list`: list of ase.Atoms generated structures """ rs = np.random.RandomState(seed) atoms_list = [] for _ in range(n_configs): atoms_tmp = atoms.copy() seed = rs.randint(1, 1000000000) displacements = _local_mc_rattle_displacements( atoms_tmp, site_index, rattle_std, d_min, seed=seed, **kwargs ) atoms_tmp.positions += displacements atoms_list.append(atoms_tmp) return atoms_list
[docs] def local_mc_rattle( structure: Structure, site_index: Optional[int] = None, # starting from 1 frac_coords: Optional[np.array] = None, # use frac coords for vacancies stdev: Optional[float] = None, d_min: Optional[float] = None, verbose: Optional[bool] = False, n_iter: int = 1, active_atoms: Optional[list] = None, nbr_cutoff: float = 5, width: float = 0.1, max_attempts: int = 5000, max_disp: float = 2.0, seed: int = 42, ) -> Structure: """ Given a pymatgen Structure object, apply random displacements to all atomic positions, with the displacement distances randomly drawn from a Gaussian distribution of standard deviation `stdev`. The random displacements tail off as we move away from the defect site. Args: structure (:obj:`~pymatgen.core.structure.Structure`): Structure as a pymatgen object site_index (:obj:`int`, optional): Index of defect site in structure (for substitutions or interstitials), counting from 1. frac_coords (:obj:`numpy.ndarray`, optional): Fractional coordinates of the defect site in the structure (for vacancies). stdev (:obj:`float`): Standard deviation (in Angstroms) of the Gaussian distribution from which random atomic displacement distances are drawn during rattling. Default is set to 10% of the bulk nearest neighbour distance. d_min (:obj:`float`): Minimum interatomic distance (in Angstroms) in the rattled structure. Monte Carlo rattle moves that put atoms at distances less than this will be heavily penalised. Default is to set this to 80% of the nearest neighbour distance in the defect supercell (ignoring interstitials). verbose (:obj:`bool`): Whether to print out information about the rattling process. n_iter (:obj:`int`): Number of Monte Carlo cycles to perform. (Default: 1) active_atoms (:obj:`list`, optional): List of which atomic indices should undergo Monte Carlo rattling. (Default: None) nbr_cutoff (:obj:`float`): The radial cutoff distance (in Angstroms) used to construct the list of atomic neighbours for checking interatomic distances. (Default: 5) width (:obj:`float`): Width of the Monte Carlo rattling error function, in Angstroms. (Default: 0.1) max_disp (:obj:`float`): Maximum atomic displacement (in Angstroms) during Monte Carlo rattling. Rarely occurs and is used primarily as a safety net. (Default: 2.0) max_attempts (:obj:`int`): Maximum Monte Carlo rattle move attempts allowed for a single atom; if this limit is reached an `Exception` is raised. (Default: 5000) seed (:obj:`int`): Seed for NumPy random state from which random rattle displacements are generated. (Default: 42) Returns: :obj:`Structure`: Rattled pymatgen Structure object """ aaa = AseAtomsAdaptor() ase_struct = aaa.get_atoms(structure) if active_atoms is not None: # select only the distances involving active_atoms distance_matrix = structure.distance_matrix[active_atoms, :][:, active_atoms] else: distance_matrix = structure.distance_matrix sorted_distances = np.sort(distance_matrix[distance_matrix > 0.5].flatten()) if isinstance(site_index, int): atom_number = site_index - 1 # Align atom number with python 0-indexing elif isinstance(frac_coords, np.ndarray): # Only for vacancies! ase_struct.append("V") # fake "V" at vacancy ase_struct.positions[-1] = np.dot(frac_coords, ase_struct.cell) atom_number = len(ase_struct) - 1 else: raise ValueError( "Insufficient information to apply local rattle, no `site_index` or `frac_coords` provided." ) if stdev is None: stdev = 0.1 * sorted_distances[0] if stdev > 0.4 or stdev < 0.02: warnings.warn( f"Automatic bond-length detection gave a bulk bond length of {10 * stdev} " f"\u212B and thus a rattle `stdev` of {stdev} ( = 10% bond length), " f"which is unreasonable. Reverting to 0.25 \u212B. If this is too large, " f"set `stdev` manually" ) stdev = 0.25 if d_min is None: d_min = 0.8 * sorted_distances[0] if d_min < 1.0: warnings.warn( f"Automatic bond-length detection gave a bulk bond length of " f"{(1 / 0.8) * d_min} \u212B, which is almost certainly too small. " f"Reverting to 2.25 \u212B. If this is too large, set `d_min` manually" ) d_min = 2.25 try: local_rattled_ase_struct = _generate_local_mc_rattled_structures( ase_struct, site_index=atom_number, n_configs=1, rattle_std=stdev, d_min=d_min, n_iter=n_iter, active_atoms=active_atoms, nbr_cutoff=nbr_cutoff, width=width, max_attempts=max_attempts, max_disp=max_disp, seed=seed, )[0] except Exception as ex: if "attempts" in str(ex): reduced_d_min = sorted_distances[0] + stdev local_rattled_ase_struct = _generate_local_mc_rattled_structures( ase_struct, site_index=atom_number, n_configs=1, rattle_std=stdev, d_min=reduced_d_min, n_iter=n_iter, active_atoms=active_atoms, nbr_cutoff=nbr_cutoff, width=width, max_attempts=max(7000, max_attempts), # default is 5000 max_disp=max_disp, seed=seed, )[0] if verbose: warnings.warn( f"Initial rattle with d_min {d_min:.2f} \u212B failed (some bond lengths " f"significantly smaller than this present), setting d_min to" f" {reduced_d_min:.2f} \u212B for this defect." ) else: raise ex if isinstance(frac_coords, np.ndarray): local_rattled_ase_struct.pop(-1) # remove fake V from vacancy structure return aaa.get_structure(local_rattled_ase_struct)