class InterfaceBuilder: """ This class constructs the epitaxially matched interfaces between two crystalline slabs """ def __init__(self, substrate_structure, film_structure): """ Args: substrate_structure (Structure): structure of substrate film_structure (Structure): structure of film """ # Bulk structures self.original_substrate_structure = substrate_structure self.original_film_structure = film_structure self.matches = [] self.match_index = None # SlabGenerator objects for the substrate and film self.sub_sg = None self.substrate_layers = None self.film_sg = None self.film_layers = None # Structures with no vacuum self.substrate_structures = [] self.film_structures = [] # "slab" structure (with no vacuum) oriented with a direction along x-axis and ab plane normal aligned with z-axis self.oriented_substrate = None self.oriented_film = None # Strained structures with no vacuum self.strained_substrate = None self.strained_film = None # Substrate with transformation/matches applied self.modified_substrate_structures = [] self.modified_film_structures = [] # Non-stoichiometric slabs with symmetric surfaces, as generated by pymatgen. Please check, this is highly # unreliable from tests. self.sym_modified_substrate_structures = [] self.sym_modified_film_structures = [] # Interface structures self.interfaces = [] self.interface_labels = [] def get_summary_dict(self): """ Return dictionary with information about the InterfaceBuilder, with currently generated structures included. """ d = {'match': self.matches[0]} d['substrate_layers'] = self.substrate_layers d['film_layers'] = self.film_layers d['bulk_substrate'] = self.original_substrate_structure d['bulk_film'] = self.original_film_structure d['strained_substrate'] = self.strained_substrate d['strained_film'] = self.strained_film d['slab_substrates'] = self.modified_substrate_structures d['slab_films'] = self.modified_film_structures d['interfaces'] = self.interfaces d['interface_labels'] = self.interface_labels return d def write_all_structures(self): """ Write all of the structures relevant for the interface calculation to VASP POSCAR files. """ _poscar = Poscar(self.original_substrate_structure) _poscar.write_file('bulk_substrate_POSCAR') _poscar = Poscar(self.original_film_structure) _poscar.write_file('bulk_film_POSCAR') _poscar = Poscar(self.strained_substrate) _poscar.write_file('strained_substrate_POSCAR') _poscar = Poscar(self.strained_film) _poscar.write_file('strained_film_POSCAR') for i, interface in enumerate(self.modified_substrate_structures): _poscar = Poscar(interface) _poscar.write_file('slab_substrate_%d_POSCAR' % i) for i, interface in enumerate(self.modified_film_structures): _poscar = Poscar(interface) _poscar.write_file('slab_film_%d_POSCAR' % i) for i, interface in enumerate(self.film_structures): _poscar = Poscar(interface) _poscar.write_file('slab_unit_film_%d_POSCAR' % i) for label, interface in zip(self.interface_labels, self.interfaces): _poscar = Poscar(interface) _poscar.write_file('interface_%s_POSCAR' % label.replace("/", "-")) return def generate_interfaces(self, film_millers=None, substrate_millers=None, film_layers=3, substrate_layers=3, **kwargs): """ Generate a list of Interface (Structure) objects and store them to self.interfaces. Args: film_millers (list of [int]): list of film surfaces substrate_millers (list of [int]): list of substrate surfaces film_layers (int): number of layers of film to include in Interface structures. substrate_layers (int): number of layers of substrate to include in Interface structures. """ self.get_oriented_slabs(lowest=True, film_millers=film_millers, substrate_millers=substrate_millers, film_layers=film_layers, substrate_layers=substrate_layers) self.combine_slabs(**kwargs) return def get_oriented_slabs(self, film_layers=3, substrate_layers=3, match_index=0, **kwargs): """ Get a list of oriented slabs for constructing interfaces and put them in self.film_structures, self.substrate_structures, self.modified_film_structures, and self.modified_substrate_structures. Currently only uses first match (lowest SA) in the list of matches Args: film_layers (int): number of layers of film to include in Interface structures. substrate_layers (int): number of layers of substrate to include in Interface structures. match_index (int): ZSL match from which to construct slabs. """ self.match_index = match_index self.substrate_layers = substrate_layers self.film_layers = film_layers if 'zslgen' in kwargs.keys(): sa = SubstrateAnalyzer(zslgen=kwargs.get('zslgen')) del kwargs['zslgen'] else: sa = SubstrateAnalyzer() # Generate all possible interface matches self.matches = list( sa.calculate(self.original_film_structure, self.original_substrate_structure, **kwargs)) match = self.matches[match_index] # Generate substrate slab and align x axis to (100) and slab normal to (001) ## Get no-vacuum structure for strained bulk calculation self.sub_sg = SlabGenerator(self.original_substrate_structure, match['sub_miller'], substrate_layers, 0, in_unit_planes=True, reorient_lattice=False, primitive=False) no_vac_sub_slab = self.sub_sg.get_slab() no_vac_sub_slab = get_shear_reduced_slab(no_vac_sub_slab) self.oriented_substrate = align_x(no_vac_sub_slab) self.oriented_substrate.sort() ## Get slab with vacuum self.sub_sg = SlabGenerator(self.original_substrate_structure, match['sub_miller'], substrate_layers, 1, in_unit_planes=True, reorient_lattice=False, primitive=False) sub_slabs = self.sub_sg.get_slabs() for i, sub_slab in enumerate(sub_slabs): sub_slab = get_shear_reduced_slab(sub_slab) sub_slab = align_x(sub_slab) sub_slab.sort() sub_slabs[i] = sub_slab self.substrate_structures = sub_slabs # Generate film slab and align x axis to (100) and slab normal to (001) ## Get no-vacuum structure for strained bulk calculation self.film_sg = SlabGenerator(self.original_film_structure, match['film_miller'], film_layers, 0, in_unit_planes=True, reorient_lattice=False, primitive=False) no_vac_film_slab = self.film_sg.get_slab() no_vac_film_slab = get_shear_reduced_slab(no_vac_film_slab) self.oriented_film = align_x(no_vac_film_slab) self.oriented_film.sort() ## Get slab with vacuum self.film_sg = SlabGenerator(self.original_film_structure, match['film_miller'], film_layers, 1, in_unit_planes=True, reorient_lattice=False, primitive=False) film_slabs = self.film_sg.get_slabs() for i, film_slab in enumerate(film_slabs): film_slab = get_shear_reduced_slab(film_slab) film_slab = align_x(film_slab) film_slab.sort() film_slabs[i] = film_slab self.film_structures = film_slabs # Apply transformation to produce matched area and a & b vectors self.apply_transformations(match) # Get non-stoichioimetric substrate slabs sym_sub_slabs = [] for sub_slab in self.modified_substrate_structures: sym_sub_slab = self.sub_sg.nonstoichiometric_symmetrized_slab( sub_slab) for slab in sym_sub_slab: if not slab == sub_slab: sym_sub_slabs.append(slab) self.sym_modified_substrate_structures = sym_sub_slabs # Get non-stoichioimetric film slabs sym_film_slabs = [] for film_slab in self.modified_film_structures: sym_film_slab = self.film_sg.nonstoichiometric_symmetrized_slab( film_slab) for slab in sym_film_slab: if not slab == film_slab: sym_film_slabs.append(slab) self.sym_modified_film_structures = sym_film_slabs # Strained film structures (No Vacuum) self.strained_substrate, self.strained_film = strain_slabs( self.oriented_substrate, self.oriented_film) return def apply_transformation(self, structure, matrix): """ Make a supercell of structure using matrix Args: structure (Slab): Slab to make supercell of matrix (3x3 np.ndarray): supercell matrix Returns: (Slab) The supercell of structure """ modified_substrate_structure = structure.copy() # Apply scaling modified_substrate_structure.make_supercell(matrix) # Reduce vectors new_lattice = modified_substrate_structure.lattice.matrix.copy() new_lattice[:2, :] = reduce_vectors( *modified_substrate_structure.lattice.matrix[:2, :]) modified_substrate_structure = Slab( lattice=Lattice(new_lattice), species=modified_substrate_structure.species, coords=modified_substrate_structure.cart_coords, miller_index=modified_substrate_structure.miller_index, oriented_unit_cell=modified_substrate_structure.oriented_unit_cell, shift=modified_substrate_structure.shift, scale_factor=modified_substrate_structure.scale_factor, coords_are_cartesian=True, energy=modified_substrate_structure.energy, reorient_lattice=modified_substrate_structure.reorient_lattice, to_unit_cell=True) return modified_substrate_structure def apply_transformations(self, match): """ Using ZSL match, transform all of the film_structures by the ZSL supercell transformation. Args: match (dict): ZSL match returned by ZSLGenerator.__call__ """ film_transformation = match["film_transformation"] sub_transformation = match["substrate_transformation"] modified_substrate_structures = [ struct.copy() for struct in self.substrate_structures ] modified_film_structures = [ struct.copy() for struct in self.film_structures ] # Match angles in lattices with 𝛾=θ° and 𝛾=(180-θ)° if np.isclose(180 - modified_film_structures[0].lattice.gamma, modified_substrate_structures[0].lattice.gamma, atol=3): reflection = SymmOp.from_rotation_and_translation( ((-1, 0, 0), (0, 1, 0), (0, 0, 1)), (0, 0, 1)) for modified_film_structure in modified_film_structures: modified_film_structure.apply_operation(reflection, fractional=True) self.oriented_film.apply_operation(reflection, fractional=True) # ------------------------------------------------------------------------------------------------------------------------ sub_scaling = np.diag(np.diag(sub_transformation)) sub_shearing = np.dot(np.linalg.inv(sub_scaling), sub_transformation) # Turn into 3x3 Arrays sub_scaling = np.diag(np.append(np.diag(sub_scaling), 1)) temp_matrix = np.diag([1, 1, 1]) temp_matrix[:2, :2] = sub_transformation sub_shearing = temp_matrix for modified_substrate_structure in modified_substrate_structures: modified_substrate_structure = self.apply_transformation( modified_substrate_structure, temp_matrix) self.modified_substrate_structures.append( modified_substrate_structure) self.oriented_substrate = self.apply_transformation( self.oriented_substrate, temp_matrix) # ------------------------------------------------------------------------------------------------------------------------ film_scaling = np.diag(np.diag(film_transformation)) film_shearing = np.dot(np.linalg.inv(film_scaling), film_transformation) # Turn into 3x3 Arrays film_scaling = np.diag(np.append(np.diag(film_scaling), 1)) temp_matrix = np.diag([1, 1, 1]) temp_matrix[:2, :2] = film_transformation film_shearing = temp_matrix for modified_film_structure in modified_film_structures: modified_film_structure = self.apply_transformation( modified_film_structure, temp_matrix) self.modified_film_structures.append(modified_film_structure) self.oriented_film = self.apply_transformation(self.oriented_film, temp_matrix) return def combine_slabs(self): """ Combine the slabs generated by get_oriented_slabs into interfaces """ all_substrate_variants = [] sub_labels = [] for i, slab in enumerate(self.modified_substrate_structures): all_substrate_variants.append(slab) sub_labels.append(str(i)) sg = SpacegroupAnalyzer(slab, symprec=1e-3) if not sg.is_laue(): mirrored_slab = slab.copy() reflection_z = SymmOp.from_rotation_and_translation( ((1, 0, 0), (0, 1, 0), (0, 0, -1)), (0, 0, 0)) mirrored_slab.apply_operation(reflection_z, fractional=True) translation = [0, 0, -min(mirrored_slab.frac_coords[:, 2])] mirrored_slab.translate_sites(range(mirrored_slab.num_sites), translation) all_substrate_variants.append(mirrored_slab) sub_labels.append('%dm' % i) all_film_variants = [] film_labels = [] for i, slab in enumerate(self.modified_film_structures): all_film_variants.append(slab) film_labels.append(str(i)) sg = SpacegroupAnalyzer(slab, symprec=1e-3) if not sg.is_laue(): mirrored_slab = slab.copy() reflection_z = SymmOp.from_rotation_and_translation( ((1, 0, 0), (0, 1, 0), (0, 0, -1)), (0, 0, 0)) mirrored_slab.apply_operation(reflection_z, fractional=True) translation = [0, 0, -min(mirrored_slab.frac_coords[:, 2])] mirrored_slab.translate_sites(range(mirrored_slab.num_sites), translation) all_film_variants.append(mirrored_slab) film_labels.append('%dm' % i) # substrate first index, film second index self.interfaces = [] self.interface_labels = [] # self.interfaces = [[None for j in range(len(all_film_variants))] for i in range(len(all_substrate_variants))] for i, substrate in enumerate(all_substrate_variants): for j, film in enumerate(all_film_variants): self.interfaces.append(self.make_interface(substrate, film)) self.interface_labels.append('%s/%s' % (film_labels[j], sub_labels[i])) def make_interface(self, slab_substrate, slab_film, offset=None): """ Strain a film to fit a substrate and generate an interface. Args: slab_substrate (Slab): substrate structure supercell slab_film (Slab): film structure supercell offset ([int]): separation vector of film and substrate """ # Check if lattices are equal. If not, strain them to match # NOTE: CHANGED THIS TO MAKE COPY OF SUBSTRATE/FILM, self.modified_film_structures NO LONGER STRAINED unstrained_slab_substrate = slab_substrate.copy() slab_substrate = slab_substrate.copy() unstrained_slab_film = slab_film.copy() slab_film = slab_film.copy() latt_1 = slab_substrate.lattice.matrix.copy() latt_1[2, :] = [0, 0, 1] latt_2 = slab_film.lattice.matrix.copy() latt_2[2, :] = [0, 0, 1] if not Lattice(latt_1) == Lattice(latt_2): # Calculate lattice strained to match: matched_slab_substrate, matched_slab_film = strain_slabs( slab_substrate, slab_film) else: matched_slab_substrate = slab_substrate matched_slab_film = slab_film # Ensure substrate has positive c-direction: if matched_slab_substrate.lattice.matrix[2, 2] < 0: latt = matched_slab_substrate.lattice.matrix.copy() latt[2, 2] *= -1 new_struct = matched_slab_substrate.copy() new_struct.lattice = Lattice(latt) matched_slab_substrate = new_struct # Ensure film has positive c-direction: if matched_slab_film.lattice.matrix[2, 2] < 0: latt = matched_slab_film.lattice.matrix.copy() latt[2, 2] *= -1 new_struct = matched_slab_film.copy() new_struct.lattice = Lattice(latt) matched_slab_film = new_struct if offset is None: offset = (2.5, 0.0, 0.0) _structure = merge_slabs(matched_slab_substrate, matched_slab_film, *offset) orthogonal_structure = _structure.get_orthogonal_c_slab() orthogonal_structure.sort() if not orthogonal_structure.is_valid(tol=1): warnings.warn( "Check generated structure, it may contain atoms too closely placed" ) #offset_vector = (offset[1], offset[2], offset[0]) interface = Interface( orthogonal_structure.lattice.copy(), orthogonal_structure.species, orthogonal_structure.frac_coords, slab_substrate.miller_index, slab_film.miller_index, self.original_substrate_structure, self.original_film_structure, unstrained_slab_substrate, unstrained_slab_film, slab_substrate, slab_film, init_inplane_shift=offset[1:], site_properties=orthogonal_structure.site_properties) return interface def visualize_interface(self, interface_index=0, show_atoms=False, n_uc=2): """ Plot the film-substrate superlattice match, the film superlattice, and the substrate superlattice in three separate plots and show them. Args: interface_index (int, 0): Choice of interface to plot show_atoms (bool, False): Whether to plot atomic sites n_uc (int, 2): Number of 2D unit cells of the interface in each direction. (The unit cell of the interface is the supercell of th substrate that matches a supercel of the film.) """ film_index = int(self.interface_labels[interface_index][0]) sub_index = int(self.interface_labels[interface_index][2]) visualize_interface(self.interfaces[interface_index], show_atoms, n_uc) visualize_superlattice(self.film_structures[film_index], self.modified_film_structures[film_index], film=True, show_atoms=show_atoms, n_uc=n_uc) visualize_superlattice(self.substrate_structures[sub_index], self.modified_substrate_structures[sub_index], film=False, show_atoms=show_atoms, n_uc=n_uc)