示例#1
0
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)