Source code for atomrdf.build.defect

import logging
from platform import system
from atomrdf.build.bulk import bulk, _generate_atomic_sample_data
from atomrdf.datamodels.basemodels import DataProperty
import numpy as np
from atomrdf.datamodels.defects.dislocation import Dislocation
from atomrdf.datamodels.defects.stackingfault import StackingFault
from atomrdf.datamodels.defects.pointdefects import (
    Vacancy,
    Interstitial,
    Substitutional,
)
from atomrdf.datamodels.structure import AtomicScaleSample
from atomrdf.build.buildutils import _declass
from pyscal3.grain_boundary import GrainBoundary
from ase import Atoms
import atomrdf.datamodels.workflow.operations as ops

logger = logging.getLogger(__name__)

[docs] def stacking_fault( element, slip_plane, displacement_a, displacement_b=0, slip_direction_a=None, slip_direction_b=None, vacuum=0, minwidth=15, even=True, minimum_r=None, relative_fault_position=0.5, crystalstructure=None, a=None, b=None, c=None, alpha=None, covera=None, repeat=1, graph=None, ): """ Generate a stacking fault structure. Parameters ---------- slip_system : list of lists, shape (2 x 3) or (2 x 4) the slip system for the given system. The input should of type [[u, v, w], [h, k, l]]. [u, v, w] is the slip direction and [h, k, l] is the slip plane. For HCP systems, the input should be [[u, v, w, z], [h, k, l, m]]. distance : float Distance for translating one half of the cell along the [h k l] direction. Default is 1. """ try: import atomman as am import atomman.unitconvert as uc except ImportError: raise ImportError("This function requires the atomman package to be installed") a = _declass(a) b = _declass(b) c = _declass(c) alpha = _declass(alpha) covera = _declass(covera) input_structure, sdict = bulk( element, crystalstructure=crystalstructure, a=a, b=b, c=c, alpha=alpha, covera=covera, repeat=repeat, get_metadata=True, ) ucell = am.load("ase_Atoms", input_structure) sf = am.defect.StackingFault(slip_plane, ucell) if slip_direction_a is not None: sf.a1vect_uvw = slip_direction_a if slip_direction_b is not None: sf.a2vect_uvw = slip_direction_b surfacesystem = sf.surface( shift=sf.shifts[0], minwidth=minwidth, even=even, vacuumwidth=vacuum ) if relative_fault_position != 0.5: sf.faultpos_rel = relative_fault_position faultsystem = sf.fault(a1=displacement_a, a2=displacement_b) # get displacements displ = am.displacement(surfacesystem, faultsystem) aseatoms = faultsystem.dump("ase_Atoms", return_prop=False) aseatoms.set_positions(aseatoms.get_positions() + displ) if graph is not None: data = _generate_atomic_sample_data(aseatoms, sdict, repeat) sample = AtomicScaleSample(**data) datadict = StackingFault.template() datadict["plane"] = slip_plane # Convert numpy array to list for pydantic validation datadict["displacement"] = displ.flatten().tolist() setattr(sample, "stacking_fault", StackingFault(**datadict)) sample.to_graph(graph) aseatoms.info["id"] = sample.id return aseatoms
[docs] def dislocation( element, slip_system, dislocation_line, elastic_constant_dict, burgers_vector=None, dislocation_type="monopole", crystalstructure=None, a=None, b=None, c=None, alpha=None, covera=None, repeat=1, graph=None, label=None, return_atomman_dislocation=False, ): """ Notes ----- This function requires the atomman Python package to be installed. The elastic_constant_dict parameter should be a dictionary of elastic constants with keys corresponding to the following Voigt notation: "C11", "C12", "C13", "C14", "C15", "C16", "C22", "C23", "C24", "C25", "C26", "C33", "C34", "C35", "C36", "C44", "C45", "C46", "C55", "C56", "C66". The values should be given in GPa. The dislocation_type parameter can be set to "monopole" or "periodicarray". If set to "monopole", a single dislocation will be generated. If set to "periodicarray", a periodic array of dislocations will be generated. Needs atomman. """ a = _declass(a) b = _declass(b) c = _declass(c) alpha = _declass(alpha) covera = _declass(covera) try: from atomman.defect.Dislocation import Dislocation import atomman as am import atomman.unitconvert as uc except ImportError: raise ImportError("This function requires the atomman package to be installed") slip_direction = slip_system[0] slip_plane = slip_system[1] if burgers_vector is None: burgers_vector = slip_direction elif np.isscalar(burgers_vector): burgers_vector = burgers_vector * np.array(slip_direction) elif len(burgers_vector) != 3: raise ValueError("burgers vector should be None, scalar, or of length 3") input_structure, sdict = bulk( element, crystalstructure=crystalstructure, a=a, b=b, c=c, alpha=alpha, covera=covera, repeat=repeat, get_metadata=True, ) for key, val in elastic_constant_dict.items(): elastic_constant_dict[key] = uc.set_in_units(val, "GPa") C = am.ElasticConstants(**elastic_constant_dict) box = am.Box( avect=input_structure.cell[0], bvect=input_structure.cell[1], cvect=input_structure.cell[2], ) types = [1 for x in range(len(input_structure))] atoms = am.Atoms(atype=types, pos=input_structure.get_positions()) system = am.System( atoms=atoms, box=box, pbc=[True, True, True], symbols=element, scale=False ) disc = Dislocation( system, C, burgers_vector, dislocation_line, slip_plane, ) if dislocation_type == "monopole": disl_system = disc.monopole() elif dislocation_type == "periodicarray": disl_system = disc.periodicarray() aseatoms = disl_system.dump("ase_Atoms", return_prop=False) if graph is not None: data = _generate_atomic_sample_data(aseatoms, sdict, repeat) sample = AtomicScaleSample(**data) # now we need to add the dislocation info # find dislocation character angle = np.dot(dislocation_line, burgers_vector) / ( np.linalg.norm(dislocation_line) * np.linalg.norm(burgers_vector) ) angle_rad = np.arccos(angle) angle_deg = np.degrees(angle_rad) if ( (np.abs(angle_deg - 0) < 1e-3) or (np.abs(angle_deg - 180) < 1e-3) or (np.abs(angle_deg - 360) < 1e-3) ): from atomrdf.datamodels.defects.dislocation import ( ScrewDislocation as DislocationObject, ) disl_name = "screw_dislocation" elif (np.abs(angle_deg - 90) < 1e-3) or (np.abs(angle_deg - 270) < 1e-3): from atomrdf.datamodels.defects.dislocation import ( EdgeDislocation as DislocationObject, ) disl_name = "edge_dislocation" else: from atomrdf.datamodels.defects.dislocation import ( MixedDislocation as DislocationObject, ) disl_name = "mixed_dislocation" disl_dict = DislocationObject.template() disl_dict["line_direction"] = dislocation_line disl_dict["burgers_vector"] = burgers_vector disl_dict["slip_system"]["slip_direction"] = slip_direction disl_dict["slip_system"]["slip_plane"]["normal"] = slip_plane if disl_name == "mixed_dislocation": disl_dict["character_angle"] = angle_deg setattr(sample, disl_name, DislocationObject(**disl_dict)) sample.to_graph(graph) aseatoms.info["id"] = sample.id if return_atomman_dislocation: return aseatoms, disc return aseatoms
[docs] def grain_boundary( element, axis, sigma, gb_plane, crystalstructure=None, a=None, b=None, c=None, alpha=None, covera=None, overlap=0.0, gap=0.0, vacuum=0.0, delete_layer="0b0t0b0t", tolerance=0.25, uc_a=1, uc_b=1, repeat=None, graph=None, primitive=False, ): """ Create a grain boundary system. GB can be created either with AIMSGB or GBCode. Parameters: ----------- axis : tuple or list The rotation axis of the grain boundary. Used with backend 'aimsgb' and 'gbcode'. sigma : int The sigma value of the grain boundary. Used with backend 'aimsgb' and 'gbcode'. gb_plane : tuple or list The Miller indices of the grain boundary plane. Used with backend 'aimsgb' and 'gbcode'. backend : str, optional The backend to use to create the grain boundary. Default is 'aimsgb'. Some keyword arguments are only suitable for some backend. structure : the lattice structure to be used to create the GB, optional The lattice structure to populate the grain boundary with. Used with backend 'aimsgb' and 'gbcode'. element : str, optional The element symbol to populate the grain boundary with. Used with backend 'aimsgb' and 'gbcode'. lattice_constant : float, optional The lattice constant of the structure. Used with backend 'aimsgb' and 'gbcode'. repetitions : tuple or list, optional The number of repetitions of the structure that will be used to create the GB. Used only with 'gbcode'. For example, if (2,3,4) is provided, each grain will have these repetitions in (x,y,z) directions. For similar functionality in 'aimsgb', use 'uc_a' and 'uc_b'. overlap : float, optional The overlap between adjacent grain boundaries. Used only with 'gbcode'. vaccum : float, optional Adds space between the grains at one of the two interfaces that must exist due to periodic boundary conditions. Used only with 'aimsgb'. gap: float, optional Adds space between the grains at both of the two interfaces that must exist due to periodic boundary conditions. Used only with 'aimsgb'. delete_layer: str, optional To delete layers of the GB. Used only with 'aimsgb'. tolerance: float, optional Tolerance factor (in distance units) to determine whether two atoms are in the same plane. Used only with 'aimsgb'. primitive: bool, optional To generate primitive or non-primitive GB structure. Used only with 'aimsgb'. uc_a: int, optional Number of unit cells of left grain. Used only with 'aimsgb'. uc_b: int, optional Number of unit cells of right grain. Used only with 'aimsgb'. graph : atomrdf.KnowledgeGraph, optional The graph object to store the system. The system is only added to the KnowledgeGraph if this option is provided. names : bool, optional If True human readable names will be assigned to each property. If False random ids will be used. Default is False. label: str, optional Add a label to the structure add_extras: bool, optional returns internal objects of the GB creation process. Returns: -------- atomrdf.System The grain boundary system. Notes ----- This function requires the aimsgb and pymatgen packages to be installed to use the 'aimsgb' backend. `repetitions` is used only with the 'gbcode' backend. For similar functionality in 'aimsgb', use `uc_a` and `uc_b`. However, repetition in the third direction is not supported in 'aimsgb'. For a similar effect, after reaching the GB, `system.modify.repeat` function could be used with (1, 1, u_c). If 'gbcode' is used as backend, the specific type of GB is determined using the `find_gb_character` function When backend 'aimsgb' is used, this is attempted. If the type could not be found, a normal GB will be added in the annotation. """ try: from pymatgen.io.ase import AseAtomsAdaptor from aimsgb import GrainBoundary as AIMSGrainBoundary from aimsgb import Grain as AIMSGrain except ImportError: raise ImportError( "This function requires the aimsgb and pymatgen packages to be installed" ) a = _declass(a) b = _declass(b) c = _declass(c) alpha = _declass(alpha) covera = _declass(covera) input_structure, sdict = bulk( element, crystalstructure=crystalstructure, a=a, b=b, c=c, alpha=alpha, covera=covera, repeat=repeat, get_metadata=True, ) pmsys = AseAtomsAdaptor().get_structure(atoms=input_structure) grain = AIMSGrain(pmsys.lattice, pmsys.species, pmsys.frac_coords) gb = AIMSGrainBoundary( axis=axis, sigma=sigma, plane=gb_plane, initial_struct=grain, uc_a=uc_a, uc_b=uc_b, ) gb_struct = AIMSGrain.stack_grains( grain_a=gb.grain_a, grain_b=gb.grain_b, vacuum=vacuum, gap=gap, direction=gb.direction, delete_layer=delete_layer, tol=tolerance, to_primitive=primitive, ) asestruct = AseAtomsAdaptor().get_atoms(structure=gb_struct) if graph is not None: data = _generate_atomic_sample_data(asestruct, sdict, repeat) sample = AtomicScaleSample(**data) try: gb_inb = GrainBoundary() gb_inb.create_grain_boundary(axis=axis, sigma=sigma, gb_plane=gb_plane) gb_type = gb_inb.find_gb_character() except Exception as e: # If GB character detection fails, default to generic GB gb_type = None if gb_type is None: from atomrdf.datamodels.defects.grainboundary import ( GrainBoundary as GBObject, ) gb_name = "grain_boundary" elif gb_type == "Tilt": from atomrdf.datamodels.defects.grainboundary import ( TiltGrainBoundary as GBObject, ) gb_name = "tilt_grain_boundary" elif gb_type == "Twist": from atomrdf.datamodels.defects.grainboundary import ( TwistGrainBoundary as GBObject, ) gb_name = "twist_grain_boundary" elif gb_type == "Symmetric Tilt": from atomrdf.datamodels.defects.grainboundary import ( SymmetricalTiltGrainBoundary as GBObject, ) gb_name = "symmetric_tilt_grain_boundary" elif gb_type == "Mixed": from atomrdf.datamodels.defects.grainboundary import ( MixedGrainBoundary as GBObject, ) gb_name = "mixed_grain_boundary" datadict = GBObject.template() datadict["sigma"] = sigma # Use the input parameter datadict["rotation_axis"] = axis datadict["plane"] = gb_plane datadict["misorientation_angle"] = gb.theta[0] # gb is the AIMSGB object setattr(sample, gb_name, GBObject(**datadict)) sample.to_graph(graph) asestruct.info["id"] = sample.id return asestruct
def vacancy( name, indices=None, no_of_vacancies=None, crystalstructure: str = None, a: float = None, b: float = None, c: float = None, *, alpha: float = None, covera: float = None, u: float = None, orthorhombic: bool = False, cubic: bool = False, basis=None, repeat: int = 1, graph=None, ): if indices is None and no_of_vacancies is None: raise ValueError("Either indices or no_of_vacancies must be provided") if isinstance(name, Atoms): # just delet an atom if indices is None: indices = np.random.choice(range(len(name)), no_of_vacancies, replace=False) atoms = name.copy() indices = np.sort(indices) for index in indices[::-1]: del atoms[index] # atoms object is provided if graph is not None: # this means that old system was already linked to a graph if "id" in system.info.keys(): initial_sample_id = system.info["id"] # we recreate the sample sample = AtomicScaleSample.from_graph(graph, system.info["id"]) # ok but we have deleted an atom, so we need to update the sample sample.update_attributes(atoms) # now update the atom attributes sample.vacancy = Vacancy( **{ "concentration": no_of_vacancies / len(atoms), "number": no_of_vacancies, } ) sample.to_graph(graph) final_sample_id = sample.id # now we can add the activity data = { "initial_sample": initial_sample_id, "final_sample": final_sample_id, } activity = ops.DeleteAtom(**data) activity.to_graph(graph) # this means that old system was not linked to a graph # the user gave us a graph, so we create a new sample else: data = _generate_atomic_sample_data( atoms, ) sample = AtomicScaleSample(**data) sample.vacancy = Vacancy( **{ "concentration": no_of_vacancies / len(atoms), "number": no_of_vacancies, } ) sample.to_graph(graph) return atoms else: atoms, sdict = bulk( name, crystalstructure=crystalstructure, a=a, b=b, c=c, alpha=alpha, covera=covera, u=u, orthorhombic=orthorhombic, cubic=cubic, basis=basis, repeat=repeat, get_metadata=True, ) if indices is None: indices = np.random.choice( range(len(atoms)), no_of_vacancies, replace=False ) indices = np.sort(indices) remaining_atoms = len(atoms) - len(indices) if remaining_atoms <= 0: raise ValueError( f"Number of vacancies {len(indices)} is greater than or equal to the number of atoms {len(atoms)}" ) for index in indices[::-1]: del atoms[index] if graph is not None: data = _generate_atomic_sample_data(atoms, sdict, repeat) sample = AtomicScaleSample(**data) vacancy_data = Vacancy( **{ "concentration": no_of_vacancies / len(atoms), "number": no_of_vacancies, } ) sample.vacancy = vacancy_data sample.to_graph(graph) atoms.info["id"] = sample.id return atoms
[docs] def interstitial( atoms, element, void_type="tetrahedral", number=1, a=None, threshold=0.01, graph=None, ): """ Create interstitial defects by adding atoms at void positions. Parameters ---------- atoms : ase.Atoms or str Either an ASE Atoms object or element symbol for creating bulk structure element : str or list Element symbol(s) for interstitial atom(s) void_type : str, optional Type of void position: 'tetrahedral' or 'octahedral'. Default is 'tetrahedral' number : int, optional Number of interstitial atoms to add. Default is 1 a : float, optional Lattice constant. Required for octahedral voids threshold : float, optional Threshold for finding octahedral positions. Default is 0.01 graph : KnowledgeGraph, optional KnowledgeGraph to add the structure to Returns ------- ase.Atoms Structure with interstitial defects """ try: from pyscal3 import System except ImportError: raise ImportError("This function requires the pyscal3 package to be installed") if not isinstance(atoms, Atoms): raise ValueError("atoms must be an ASE Atoms object") atoms_copy = atoms.copy() sys = System(atoms_copy, format="ase") element = np.atleast_1d(element) # Ensure we have the right number of elements if len(element) < number: element = np.repeat(element, number)[:number] elif len(element) > number: element = element[:number] if void_type == "tetrahedral": sys.find.neighbors(method="voronoi", cutoff=0.1) verts = sys.unique_vertices if len(verts) == 0: raise ValueError("No tetrahedral void positions found") if len(verts) < number: raise ValueError( f"Only {len(verts)} tetrahedral positions found, but {number} requested" ) randindex = np.random.choice(len(verts), size=number, replace=False) randpos = np.array(verts)[randindex] elif void_type == "octahedral": # Try to get lattice constant from graph if available if a is None and graph is not None and "id" in atoms_copy.info: try: sample_id = atoms_copy.info["id"] sample = AtomicScaleSample.from_graph(graph, sample_id) if ( sample.material is not None and sample.material.crystal_structure is not None and sample.material.crystal_structure.unit_cell is not None and sample.material.crystal_structure.unit_cell.lattice_parameter is not None ): a = sample.material.crystal_structure.unit_cell.lattice_parameter[0] except (AttributeError, TypeError, IndexError) as e: logger.debug("Could not infer lattice constant from sample: %s", e) if a is None: raise ValueError("Please provide lattice constant a for octahedral voids") cutoff = a + threshold * 2 sys.find.neighbors(method="cutoff", cutoff=cutoff) octa_pos = [] for count, dist in enumerate(sys.atoms.neighbors.distance): diffs = np.abs(np.array(dist) - a) indices = np.where(diffs < threshold)[0] vector = np.array(sys.atoms["diff"][count])[indices] vector = sys.atoms.positions[count] + vector / 2 for vect in vector: vect = sys.modify.remap_position_to_box(vect) octa_pos.append(vect) if len(octa_pos) == 0: raise ValueError("No octahedral void positions found") octa_pos = np.unique(octa_pos, axis=0) if len(octa_pos) < number: raise ValueError( f"Only {len(octa_pos)} octahedral positions found, but {number} requested" ) randindex = np.random.choice(len(octa_pos), size=number, replace=False) randpos = octa_pos[randindex] else: raise ValueError( f'Unknown void type: {void_type}. Use "tetrahedral" or "octahedral"' ) # Add interstitial atoms sys = sys.add_atoms({"positions": randpos, "species": element}) new_atoms = sys.write.ase() # Calculate defect properties no_of_interstitials = len(randpos) original_natoms = len(atoms_copy) conc_of_interstitials = no_of_interstitials / original_natoms # Update graph if provided if graph is not None: # Generate new sample data data = _generate_atomic_sample_data(new_atoms) sample = AtomicScaleSample(**data) # Add interstitial defect metadata sample.interstitial = Interstitial( concentration=conc_of_interstitials, number=no_of_interstitials, ) sample.to_graph(graph) new_atoms.info["id"] = sample.id # If there was a previous sample, add activity to track the operation if "id" in atoms_copy.info: initial_sample_id = atoms_copy.info["id"] final_sample_id = sample.id data = { "input_sample": initial_sample_id, "output_sample": final_sample_id, } activity = ops.AddAtom(**data) activity.to_graph(graph) return new_atoms
[docs] def substitutional( atoms, element, number=1, indices=None, graph=None, ): """ Create substitutional defects by replacing atoms with different element(s). Parameters ---------- atoms : ase.Atoms ASE Atoms object element : str Element symbol for substitutional atom number : int, optional Number of substitutions to make. Default is 1 indices : list or array, optional Specific atom indices to substitute. If None, random atoms are chosen graph : KnowledgeGraph, optional KnowledgeGraph to add the structure to Returns ------- ase.Atoms Structure with substitutional defects """ if not isinstance(atoms, Atoms): raise ValueError("atoms must be an ASE Atoms object") atoms_copy = atoms.copy() species = atoms_copy.get_chemical_symbols() # Select atoms to substitute if indices is None: if number > len(species): raise ValueError( f"Number of substitutions {number} exceeds number of atoms {len(species)}" ) indices = np.random.choice(len(species), size=number, replace=False) else: indices = np.atleast_1d(indices) number = len(indices) if number > len(species): raise ValueError( f"Number of indices {number} exceeds number of atoms {len(species)}" ) # Perform substitution for idx in indices: species[idx] = element atoms_copy.set_chemical_symbols(species) # Calculate defect properties no_of_substitutions = number conc_of_substitutions = no_of_substitutions / len(atoms_copy) # Update graph if provided if graph is not None: # Generate new sample data data = _generate_atomic_sample_data(atoms_copy) sample = AtomicScaleSample(**data) # Add substitutional defect metadata sample.substitutional = Substitutional( concentration=conc_of_substitutions, number=no_of_substitutions, ) sample.to_graph(graph) atoms_copy.info["id"] = sample.id # If there was a previous sample, add activity to track the operation if "id" in atoms.info: initial_sample_id = atoms.info["id"] final_sample_id = sample.id data = { "input_sample": initial_sample_id, "output_sample": final_sample_id, } activity = ops.SubstituteAtom(**data) activity.to_graph(graph) return atoms_copy