def test_init(self): zno_slab = Slab(self.zno55.lattice, self.zno55.species, self.zno55.frac_coords, self.zno55.miller_index, self.zno55.oriented_unit_cell, 0, self.zno55.scale_factor) m = self.zno55.lattice.matrix area = np.linalg.norm(np.cross(m[0], m[1])) self.assertAlmostEqual(zno_slab.surface_area, area) self.assertEqual(zno_slab.lattice.parameters, self.zno55.lattice.parameters) self.assertEqual(zno_slab.oriented_unit_cell.composition, self.zno1.composition) self.assertEqual(len(zno_slab), 8) # check reorient_lattice. get a slab not oriented and check that orientation # works even with cartesian coordinates. zno_not_or = SlabGenerator(self.zno1, [1, 0, 0], 5, 5, lll_reduce=False, center_slab=False, reorient_lattice=False).get_slab() zno_slab_cart = Slab(zno_not_or.lattice, zno_not_or.species, zno_not_or.cart_coords, zno_not_or.miller_index, zno_not_or.oriented_unit_cell, 0, zno_not_or.scale_factor, coords_are_cartesian=True, reorient_lattice=True) self.assertArrayAlmostEqual(zno_slab.frac_coords, zno_slab_cart.frac_coords) c = zno_slab_cart.lattice.matrix[2] self.assertArrayAlmostEqual([0, 0, np.linalg.norm(c)], c)
def test_as_dict(self): slabs = generate_all_slabs( self.ti, 1, 10, 10, bonds=None, tol=1e-3, max_broken_bonds=0, lll_reduce=False, center_slab=False, primitive=True, ) slab = slabs[0] s = json.dumps(slab.as_dict()) d = json.loads(s) self.assertEqual(slab, Slab.from_dict(d)) # test initialising with a list scale_factor slab = Slab( self.zno55.lattice, self.zno55.species, self.zno55.frac_coords, self.zno55.miller_index, self.zno55.oriented_unit_cell, 0, self.zno55.scale_factor.tolist(), ) s = json.dumps(slab.as_dict()) d = json.loads(s) self.assertEqual(slab, Slab.from_dict(d))
def __init__(self, strt, hkl=[1,1,1], min_thick=10, min_vac=10, supercell=[1,1,1], name=None, adsorb_on_species=None, adatom_on_lig=None, ligand=None, displacement=1.0, surface_coverage=None, scell_nmax=10, coverage_tol=0.25, solvent=None, start_from_slab=False, validate_proximity=False, to_unit_cell=False, coords_are_cartesian=False, primitive=True, from_ase=False, x_shift=0, y_shift=0, rot=[0,0,0], center_slab=True): self.from_ase = from_ase vac_extension = 0 if ligand is not None: vac_extension = ligand.max_dist if isinstance(strt, Structure) and not isinstance(strt, Slab): self.min_vac = min_vac + vac_extension if self.from_ase: strt = get_ase_slab(strt, hkl=hkl, min_thick=min_thick, min_vac=min_vac + vac_extension, center_slab=center_slab) else: strt = SlabGenerator(strt, hkl, min_thick, min_vac + vac_extension, center_slab=center_slab, primitive = primitive).get_slab() strt.make_supercell(supercell) else: self.min_vac = min_vac Slab.__init__(self, strt.lattice, strt.species_and_occu, strt.frac_coords, miller_index=strt.miller_index, oriented_unit_cell=strt.oriented_unit_cell, shift=strt.shift, scale_factor=strt.scale_factor, validate_proximity=validate_proximity, to_unit_cell=to_unit_cell, coords_are_cartesian=coords_are_cartesian, site_properties=strt.site_properties, energy=strt.energy ) self.strt= strt self.name = name self.hkl = hkl self.min_thick = min_thick self.supercell = supercell self.ligand = ligand self.slab = strt self.displacement = displacement self.solvent = solvent self.surface_coverage = surface_coverage self.adsorb_on_species = adsorb_on_species self.adatom_on_lig = adatom_on_lig self.scell_nmax = scell_nmax self.coverage_tol = coverage_tol self.x_shift = x_shift self.y_shift = y_shift self.rot = rot
def slab_from_file(structure, hkl): """ Reads in structure from the file and returns slab object. Args: structure (str): Structure file in any format supported by pymatgen. Will accept a pymatgen.Structure object directly. hkl (tuple): Miller index of the slab in the input file. Returns: Slab object """ if type(structure) == str: slab_input = Structure.from_file(structure) else: slab_input = structure return Slab( slab_input.lattice, slab_input.species_and_occu, slab_input.frac_coords, hkl, Structure.from_sites(slab_input, to_unit_cell=True), # this OUC is not correct, need to get it from slabgenerator shift=0, scale_factor=np.eye(3, dtype=np.int), site_properties=slab_input.site_properties)
def get_ase_slab(pmg_struct, hkl=(1, 1, 1), min_thick=10, min_vac=10): """ takes in the intial structure as pymatgen Structure object uses ase to generate the slab returns pymatgen Slab object Args: pmg_struct: pymatgen structure object hkl: hkl index of surface of slab to be created min_thick: minimum thickness of slab in Angstroms min_vac: minimum vacuum spacing """ ase_atoms = AseAtomsAdaptor().get_atoms(pmg_struct) pmg_slab_gen = SlabGenerator(pmg_struct, hkl, min_thick, min_vac) h = pmg_slab_gen._proj_height nlayers = int(math.ceil(pmg_slab_gen.min_slab_size / h)) ase_slab = surface(ase_atoms, hkl, nlayers) ase_slab.center(vacuum=min_vac / 2, axis=2) pmg_slab_structure = AseAtomsAdaptor().get_structure(ase_slab) return Slab(lattice=pmg_slab_structure.lattice, species=pmg_slab_structure.species_and_occu, coords=pmg_slab_structure.frac_coords, site_properties=pmg_slab_structure.site_properties, miller_index=hkl, oriented_unit_cell=pmg_slab_structure, shift=0., scale_factor=None, energy=None)
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 get_shear_reduced_slab(slab): """ Reduce the vectors of the slab plane according to the algorithm in substrate_analyzer, then make a new Slab with a Lattice with those reduced vectors. Args: slab (Slab): Slab to reduce Returns: Slab object of identical structure to the input slab but rduced in-plane lattice vectors """ original_vectors = [slab.lattice.matrix[0], slab.lattice.matrix[1]] reduced_vectors = reduce_vectors(slab.lattice.matrix[0], slab.lattice.matrix[1]) new_lattice = Lattice( [reduced_vectors[0], reduced_vectors[1], slab.lattice.matrix[2]]) return Slab(lattice=new_lattice, species=slab.species, coords=slab.cart_coords, miller_index=slab.miller_index, oriented_unit_cell=slab.oriented_unit_cell, shift=slab.shift, scale_factor=slab.scale_factor, coords_are_cartesian=True, energy=slab.energy, reorient_lattice=slab.reorient_lattice, to_unit_cell=True)
def test_add_adsorbate_atom(self): zno_slab = Slab(self.zno55.lattice, self.zno55.species, self.zno55.frac_coords, self.zno55.miller_index, self.zno55.oriented_unit_cell, 0, self.zno55.scale_factor) zno_slab.add_adsorbate_atom([1], 'H', 1) self.assertEqual(len(zno_slab), 9) self.assertEqual(str(zno_slab[8].specie), 'H') self.assertAlmostEqual(zno_slab.get_distance(1, 8), 1.0) self.assertTrue(zno_slab[8].c > zno_slab[0].c) m = self.zno55.lattice.matrix area = np.linalg.norm(np.cross(m[0], m[1])) self.assertAlmostEqual(zno_slab.surface_area, area) self.assertEqual(zno_slab.lattice.lengths_and_angles, self.zno55.lattice.lengths_and_angles)
def test_as_dict(self): slabs = generate_all_slabs(self.ti, 1, 10, 10, bonds=None, tol=1e-3, max_broken_bonds=0, lll_reduce=False, center_slab=False, primitive=True) slab = slabs[0] s = json.dumps(slab.as_dict()) d = json.loads(s) self.assertEqual(slab, Slab.from_dict(d))
def test_add_adsorbate_atom(self): zno_slab = Slab(self.zno55.lattice, self.zno55.species, self.zno55.frac_coords, self.zno55.miller_index, self.zno55.oriented_unit_cell, 0, self.zno55.scale_factor) zno_slab.add_adsorbate_atom([1], 'H', 1) self.assertEqual(len(zno_slab), 9) self.assertEqual(str(zno_slab[8].specie), 'H') self.assertAlmostEqual(zno_slab.get_distance(1, 8), 1.0) self.assertTrue(zno_slab[8].c > zno_slab[0].c) m = self.zno55.lattice.matrix area = np.linalg.norm(np.cross(m[0], m[1])) self.assertAlmostEqual(zno_slab.surface_area, area) self.assertEqual(zno_slab.lattice.lengths_and_angles, self.zno55.lattice.lengths_and_angles)
def test_init(self): zno_slab = Slab(self.zno55.lattice, self.zno55.species, self.zno55.frac_coords, self.zno55.miller_index, self.zno55.oriented_unit_cell, 0, self.zno55.scale_factor) m = self.zno55.lattice.matrix area = np.linalg.norm(np.cross(m[0], m[1])) self.assertAlmostEqual(zno_slab.surface_area, area) self.assertEqual(zno_slab.lattice.parameters, self.zno55.lattice.parameters) self.assertEqual(zno_slab.oriented_unit_cell.composition, self.zno1.composition) self.assertEqual(len(zno_slab), 8)
def trajToMG(self): from pymatgen.io.ase import AseAtomsAdaptor from pymatgen.core.surface import Slab atoms = self.trajInput() struct = AseAtomsAdaptor.get_structure(atoms) return Slab(struct.lattice, struct.species, struct.cart_coords, (1, 1, 0), struct, 3, (1, 1, 1), coords_are_cartesian=True, to_unit_cell=True)
def merge_slabs(substrate, film, slab_offset, x_offset, y_offset, vacuum=20, **kwargs): """ Given substrate and film supercells (oriented to match as closely as possible), strain the film to match the substrate lattice and combine the slabs. Args: slab_offset: spacing between the substrate and film x_offset y_offset: in-plane displacement of the film in Cartesian coordinates vacuum: vacuum buffer above the film Returns: combined_structure (Slab): A structure with the strained film and substrate combined into one structure """ # strain film to match substrate new_latt = film.lattice.matrix.copy() new_latt[:2, :2] = substrate.lattice.matrix[:2, :2] film.lattice = Lattice(new_latt) combined_species = [*substrate.species, *film.species] if kwargs.get('cell_height'): height = kwargs.get('cell_height') else: added_height = vacuum + slab_offset + film.lattice.c height = added_height + substrate.lattice.matrix[2, 2] combined_lattice = substrate.lattice.matrix.copy() combined_lattice[2, :] *= height / substrate.lattice.matrix[2, 2] max_substrate = np.max(substrate.cart_coords[:, 2]) min_substrate = np.min(film.cart_coords[:, 2]) offset = max_substrate - min_substrate + slab_offset offset_film_coords = [np.add(coord, [x_offset, y_offset, offset]) for coord in film.cart_coords] combined_coords = [*substrate.cart_coords, *offset_film_coords] combined_site_properties = {} for key, item in substrate.site_properties.items(): combined_site_properties[key] = [*substrate.site_properties[key], *film.site_properties[key]] labels = ['substrate'] * len(substrate) + ['film'] * len(film) combined_site_properties['interface_label'] = labels combined_structure = Slab(lattice=Lattice(combined_lattice), species=combined_species, coords=combined_coords, miller_index=substrate.miller_index, oriented_unit_cell=substrate, shift=substrate.shift, scale_factor=substrate.scale_factor, coords_are_cartesian=True, energy=substrate.energy, reorient_lattice=False, to_unit_cell=True, site_properties=combined_site_properties) return combined_structure
def makePMGSlabFromASE(a, facet): lattice = a.get_cell() species = a.get_chemical_symbols() coords = a.get_positions() miller_index = facet oriented_unit_cell = AseAtomsAdaptor.get_structure(a) shift = 0 scale_factor = None #??????? return Slab(lattice, species, coords, miller_index, oriented_unit_cell, shift, scale_factor, coords_are_cartesian=True)
def slab_from_file(structure, hkl): """ Reads in structure from the file and returns slab object. Args: structure (str): structure file in any format supported by pymatgen hkl (tuple): Miller index of the slab in the input file. Returns: Slab object """ slab_input = Structure.from_file(structure) return Slab(slab_input.lattice, slab_input.species_and_occu, slab_input.frac_coords, hkl, Structure.from_sites(slab_input, to_unit_cell=True), shift=0, scale_factor=np.eye(3, dtype=np.int), site_properties=slab_input.site_properties)
def slab_from_file(hkl, filename): """ reads in structure from the file and returns slab object. useful for reading in 2d/substrate structures from file. Args: hkl: miller index of the slab in the input file. filename: structure file in any format supported by pymatgen Returns: Slab object """ slab_input = Structure.from_file(filename) return Slab(slab_input.lattice, slab_input.species_and_occu, slab_input.frac_coords, hkl, Structure.from_sites(slab_input, to_unit_cell=True), shift=0, scale_factor=np.eye(3, dtype=np.int), site_properties=slab_input.site_properties)
def add_vacuum_padding(slab, vacuum, hkl=[0, 0, 1]): """ add vacuum spacing to the given structure Args: slab: sructure/slab object to be padded vacuum: in angstroms hkl: miller index Returns: Structure object """ min_z = np.min([fcoord[2] for fcoord in slab.frac_coords]) slab.translate_sites(list(range(len(slab))), [0, 0, -min_z]) a, b, c = slab.lattice.matrix z = [coord[2] for coord in slab.cart_coords] zmax = np.max(z) zmin = np.min(z) thickness = zmax - zmin new_c = c / np.linalg.norm(c) * (thickness + vacuum) new_lattice = Lattice(np.array([a, b, new_c])) new_sites = [] for site in slab: new_sites.append( PeriodicSite(site.species_and_occu, site.coords, new_lattice, properties=site.properties, coords_are_cartesian=True)) new_struct = Structure.from_sites(new_sites) # center the slab avg_z = np.average([fcoord[2] for fcoord in new_struct.frac_coords]) new_struct.translate_sites(list(range(len(new_struct))), [0, 0, 0.5 - avg_z]) return Slab(new_struct.lattice, new_struct.species_and_occu, new_struct.frac_coords, hkl, Structure.from_sites(new_struct, to_unit_cell=True), shift=0, scale_factor=np.eye(3, dtype=np.int), site_properties=new_struct.site_properties)
def adsorb_both_surfaces(slab, molecule, selective_dynamics=False, height=0.9, mi_vec=None, repeat=None, min_lw=5.0, reorient=True, find_args={}): """ Function that generates all adsorption structures for a given molecular adsorbate on both surfaces of a slab. Args: slab (Slab): slab object for which to find adsorbate sites selective_dynamics (bool): flag for whether to assign non-surface sites as fixed for selective dynamics molecule (Molecule): molecule corresponding to adsorbate selective_dynamics (bool): flag for whether to assign non-surface sites as fixed for selective dynamics height (float): height criteria for selection of surface sites mi_vec (3-D array-like): vector corresponding to the vector concurrent with the miller index, this enables use with slabs that have been reoriented, but the miller vector must be supplied manually repeat (3-tuple or list): repeat argument for supercell generation min_lw (float): minimum length and width of the slab, only used if repeat is None reorient (bool): flag on whether or not to reorient adsorbate along the miller index find_args (dict): dictionary of arguments to be passed to the call to self.find_adsorption_sites, e.g. {"distance":2.0} """ matcher = StructureMatcher() # Get adsorption on top adsgen_top = AdsorbateSiteFinder(slab, selective_dynamics=selective_dynamics, height=height, mi_vec=mi_vec, top_surface=True) structs = adsgen_top.generate_adsorption_structures(molecule, repeat=repeat, min_lw=min_lw, reorient=reorient, find_args=find_args) adslabs = [g[0] for g in matcher.group_structures(structs)] # Get adsorption on bottom adsgen_bottom = AdsorbateSiteFinder(slab, selective_dynamics=selective_dynamics, height=height, mi_vec=mi_vec, top_surface=False) structs = adsgen_bottom.generate_adsorption_structures(molecule, repeat=repeat, min_lw=min_lw, reorient=reorient, find_args=find_args) adslabs.extend([g[0] for g in matcher.group_structures(structs)]) # Group symmetrically similar slabs adsorbed_slabs = [] for group in matcher.group_structures(adslabs): # Further group each group by which surface adsorbed top_ads, bottom_ads = [], [] for s in group: sites = sorted(s, key=lambda site: site.frac_coords[2]) if sites[0].surface_properties == "adsorbate": bottom_ads.append(s) else: top_ads.append(s) if not top_ads or not bottom_ads: warnings.warn("There are not enough sites at the bottom or " "top to generate a symmetric adsorbed slab") continue # Combine the adsorbates of both top and bottom slabs # into one slab with one adsorbate on each side coords = list(top_ads[0].frac_coords) species = top_ads[0].species lattice = top_ads[0].lattice coords.extend([ site.frac_coords for site in bottom_ads[0] if site.surface_properties == "adsorbate" ]) species.extend([ site.specie for site in bottom_ads[0] if site.surface_properties == "adsorbate" ]) slab = Slab(lattice, species, coords, slab.miller_index, slab.oriented_unit_cell, slab.shift, slab.scale_factor) adsorbed_slabs.append(slab) return adsorbed_slabs
def test_as_from_dict(self): d = self.zno55.as_dict() obj = Slab.from_dict(d) self.assertEqual(obj.miller_index, (1, 0, 0))
with open(orgdir + 'structure_collection_bulk.json') as f: st_bulk_json = json.load(f) f.close() bulkkeys = list(st_bulk_json.keys()) surface_sites_dict = {} for matkey in st_json.keys(): tmp_st = st_json[matkey] st = Structure.from_dict(tmp_st['st_after']) bulkkey = ext_bulk_soap(matkey, bulkkeys) st_bulk = Structure.from_dict(st_bulk_json[bulkkey]) slab = Slab(lattice=st.lattice, species=st.species, coords=st.frac_coords, miller_index=[1, 1, 1], oriented_unit_cell=st_bulk, shift=0, scale_factor=1, reorient_lattice=False) print(slab.is_symmetric(), slab.have_equivalent_surfaces()) surface_sites = [] surface_sites_prop = [] for tol in [0.0, 0.25, 0.5]: tmp_surface_sites = get_surface_sites(slab, tol=tol) surface_sites_prop.append(slab.site_properties['is_surf_site']) surface_sites_prop = np.asarray(surface_sites_prop) surface_sites_prop = np.any(surface_sites_prop, axis=0) surface_sites = np.arange(0, slab.num_sites, 1)[surface_sites_prop] surface_sites = [
def __init__(self, strt, hkl=[1, 1, 1], min_thick=10, min_vac=10, supercell=[1, 1, 1], name=None, adsorb_on_species=None, adatom_on_lig=None, ligand=None, displacement=1.0, surface_coverage=None, scell_nmax=10, coverage_tol=0.25, solvent=None, start_from_slab=False, validate_proximity=False, to_unit_cell=False, coords_are_cartesian=False, primitive=True, from_ase=False, lll_reduce=False, center_slab=True, max_normal_search=None, force_normalize=False, x_shift=0, y_shift=0, rot=[0, 0, 0]): self.from_ase = from_ase vac_extension = 0 if ligand is not None: vac_extension = ligand.max_dist if isinstance(strt, Structure) and not isinstance(strt, Slab): self.min_vac = min_vac + vac_extension if self.from_ase: strt = get_ase_slab(strt, hkl=hkl, min_thick=min_thick, min_vac=min_vac + vac_extension) else: slab = SlabGenerator(strt, hkl, min_thick, min_vac + vac_extension, center_slab=center_slab, lll_reduce=lll_reduce, max_normal_search=max_normal_search, primitive=primitive).get_slab() if force_normalize: strt = slab.get_orthogonal_c_slab() else: strt = slab strt.make_supercell(supercell) else: self.min_vac = min_vac Slab.__init__(self, strt.lattice, strt.species_and_occu, strt.frac_coords, miller_index=strt.miller_index, oriented_unit_cell=strt.oriented_unit_cell, shift=strt.shift, scale_factor=strt.scale_factor, validate_proximity=validate_proximity, to_unit_cell=to_unit_cell, coords_are_cartesian=coords_are_cartesian, site_properties=strt.site_properties, energy=strt.energy) self.strt = strt self.name = name self.hkl = hkl self.min_thick = min_thick self.supercell = supercell self.ligand = ligand self.slab = strt self.displacement = displacement self.solvent = solvent self.surface_coverage = surface_coverage self.adsorb_on_species = adsorb_on_species self.adatom_on_lig = adatom_on_lig self.scell_nmax = scell_nmax self.coverage_tol = coverage_tol self.x_shift = x_shift self.y_shift = y_shift self.rot = rot
def set_slab(self): """ set the slab on to which the ligand is adsorbed""" self.slab = Slab.from_dict(super(Interface, self).as_dict())
def set_slab(self): """ set the slab on to which the ligand is adsorbed""" self.slab = Slab.from_dict(super(Interface, self).as_dict())
def test_as_from_dict(self): d = self.zno55.as_dict() obj = Slab.from_dict(d) self.assertEqual(obj.miller_index, (1, 0, 0))
def from_slabs( cls, substrate_slab: Slab, film_slab: Slab, in_plane_offset: Tuple[float, float] = (0, 0), gap: float = 1.6, vacuum_over_film: float = 0.0, interface_properties: Optional[Dict] = None, center_slab: bool = True, ) -> "Interface": """ Makes an interface structure by merging a substrate and film slabs The film a- and b-vectors will be forced to be the substrate slab's a- and b-vectors. For now, it's suggested to use a factory method that will ensure the appropriate interface structure is already met. Args: sub_slab: slab for the substrate film_slab: slab for the film in_plane_offset: fractional shift in plane for the film with respect to the substrate gap: gap between substrate and film in Angstroms vacuum_over_film: vacuum space above the film in Angstroms structure_properties: dictionary of misc properties for this structure center_slab: center the slab """ interface_properties = interface_properties or {} # Ensure c-axis is orthogonal to a/b plane if isinstance(substrate_slab, Slab): substrate_slab = substrate_slab.get_orthogonal_c_slab() if isinstance(film_slab, Slab): film_slab = film_slab.get_orthogonal_c_slab() assert np.allclose(film_slab.lattice.alpha, 90, 0.1) assert np.allclose(film_slab.lattice.beta, 90, 0.1) assert np.allclose(substrate_slab.lattice.alpha, 90, 0.1) assert np.allclose(substrate_slab.lattice.beta, 90, 0.1) # Ensure sub is right-handed # IE sub has surface facing "up" sub_vecs = substrate_slab.lattice.matrix.copy() if np.dot(np.cross(*sub_vecs[:2]), sub_vecs[2]) < 0: sub_vecs[2] *= -1.0 substrate_slab.lattice = Lattice(sub_vecs) # Find the limits of C-coords sub_coords = substrate_slab.frac_coords film_coords = film_slab.frac_coords sub_min_c = np.min(sub_coords[:, 2]) * substrate_slab.lattice.c sub_max_c = np.max(sub_coords[:, 2]) * substrate_slab.lattice.c film_min_c = np.min(film_coords[:, 2]) * film_slab.lattice.c film_max_c = np.max(film_coords[:, 2]) * film_slab.lattice.c min_height = np.abs(film_max_c - film_min_c) + np.abs(sub_max_c - sub_min_c) # construct new lattice abc = substrate_slab.lattice.abc[:2] + (min_height + gap + vacuum_over_film, ) angles = substrate_slab.lattice.angles lattice = Lattice.from_parameters(*abc, *angles) # Get the species species = substrate_slab.species + film_slab.species # Get the coords # Shift substrate to bottom in new lattice sub_coords = np.subtract(sub_coords, [0, 0, np.min(sub_coords[:, 2])]) sub_coords[:, 2] *= substrate_slab.lattice.c / lattice.c # Flip the film over film_coords[:, 2] *= -1.0 film_coords[:, 2] *= film_slab.lattice.c / lattice.c # Shift the film coords to right over the substrate + gap film_coords = np.subtract(film_coords, [0, 0, np.min(film_coords[:, 2])]) film_coords = np.add( film_coords, [0, 0, gap / lattice.c + np.max(sub_coords[:, 2])]) # Build coords coords = np.concatenate([sub_coords, film_coords]) # Shift coords to center if center_slab: coords = np.add(coords, [0, 0, 0.5 - np.average(coords[:, 2])]) # Only merge site properties in both slabs site_properties = {} site_props_in_both = set(substrate_slab.site_properties.keys()) & set( film_slab.site_properties.keys()) for key in site_props_in_both: site_properties[key] = [ *substrate_slab.site_properties[key], *film_slab.site_properties[key], ] site_properties["interface_label"] = ["substrate"] * len( substrate_slab) + ["film"] * len(film_slab) iface = cls( lattice=lattice, species=species, coords=coords, to_unit_cell=False, coords_are_cartesian=False, site_properties=site_properties, validate_proximity=False, in_plane_offset=in_plane_offset, gap=gap, vacuum_over_film=vacuum_over_film, interface_properties=interface_properties, ) iface.sort() return iface