1# coding: utf-8
2# Copyright (c) Pymatgen Development Team.
3# Distributed under the terms of the MIT License.
4
5"""
6This module implements representations of slabs and surfaces, as well as
7algorithms for generating them. If you use this module, please consider
8citing the following work::
9
10    R. Tran, Z. Xu, B. Radhakrishnan, D. Winston, W. Sun, K. A. Persson,
11    S. P. Ong, "Surface Energies of Elemental Crystals", Scientific Data,
12    2016, 3:160080, doi: 10.1038/sdata.2016.80.
13
14as well as::
15
16    Sun, W.; Ceder, G. Efficient creation and convergence of surface slabs,
17    Surface Science, 2013, 617, 53–59, doi:10.1016/j.susc.2013.05.016.
18"""
19
20import copy
21import itertools
22import json
23import logging
24import math
25import os
26import warnings
27from functools import reduce
28from math import gcd
29
30import numpy as np
31from monty.fractions import lcm
32from scipy.cluster.hierarchy import fcluster, linkage
33from scipy.spatial.distance import squareform
34
35from pymatgen.analysis.structure_matcher import StructureMatcher
36from pymatgen.core.lattice import Lattice
37from pymatgen.core.periodic_table import get_el_sp
38from pymatgen.core.sites import PeriodicSite
39from pymatgen.core.structure import Structure
40from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
41from pymatgen.util.coord import in_coord_list
42
43__author__ = "Richard Tran, Wenhao Sun, Zihan Xu, Shyue Ping Ong"
44
45
46logger = logging.getLogger(__name__)
47
48
49class Slab(Structure):
50    """
51    Subclass of Structure representing a Slab. Implements additional
52    attributes pertaining to slabs, but the init method does not
53    actually implement any algorithm that creates a slab. This is a
54    DUMMY class who's init method only holds information about the
55    slab. Also has additional methods that returns other information
56    about a slab such as the surface area, normal, and atom adsorption.
57
58    Note that all Slabs have the surface normal oriented perpendicular to the a
59    and b lattice vectors. This means the lattice vectors a and b are in the
60    surface plane and the c vector is out of the surface plane (though not
61    necessarily perpendicular to the surface).
62
63    .. attribute:: miller_index
64
65        Miller index of plane parallel to surface.
66
67    .. attribute:: scale_factor
68
69        Final computed scale factor that brings the parent cell to the
70        surface cell.
71
72    .. attribute:: shift
73
74        The shift value in Angstrom that indicates how much this
75        slab has been shifted.
76    """
77
78    def __init__(
79        self,
80        lattice,
81        species,
82        coords,
83        miller_index,
84        oriented_unit_cell,
85        shift,
86        scale_factor,
87        reorient_lattice=True,
88        validate_proximity=False,
89        to_unit_cell=False,
90        reconstruction=None,
91        coords_are_cartesian=False,
92        site_properties=None,
93        energy=None,
94    ):
95        """
96        Makes a Slab structure, a structure object with additional information
97        and methods pertaining to slabs.
98
99        Args:
100            lattice (Lattice/3x3 array): The lattice, either as a
101                :class:`pymatgen.core.lattice.Lattice` or
102                simply as any 2D array. Each row should correspond to a lattice
103                vector. E.g., [[10,0,0], [20,10,0], [0,0,30]] specifies a
104                lattice with lattice vectors [10,0,0], [20,10,0] and [0,0,30].
105            species ([Species]): Sequence of species on each site. Can take in
106                flexible input, including:
107
108                i.  A sequence of element / species specified either as string
109                    symbols, e.g. ["Li", "Fe2+", "P", ...] or atomic numbers,
110                    e.g., (3, 56, ...) or actual Element or Species objects.
111
112                ii. List of dict of elements/species and occupancies, e.g.,
113                    [{"Fe" : 0.5, "Mn":0.5}, ...]. This allows the setup of
114                    disordered structures.
115            coords (Nx3 array): list of fractional/cartesian coordinates of
116                each species.
117            miller_index ([h, k, l]): Miller index of plane parallel to
118                surface. Note that this is referenced to the input structure. If
119                you need this to be based on the conventional cell,
120                you should supply the conventional structure.
121            oriented_unit_cell (Structure): The oriented_unit_cell from which
122                this Slab is created (by scaling in the c-direction).
123            shift (float): The shift in the c-direction applied to get the
124                termination.
125            scale_factor (np.ndarray): scale_factor Final computed scale factor
126                that brings the parent cell to the surface cell.
127            reorient_lattice (bool): reorients the lattice parameters such that
128                the c direction is along the z axis.
129            validate_proximity (bool): Whether to check if there are sites
130                that are less than 0.01 Ang apart. Defaults to False.
131            reconstruction (str): Type of reconstruction. Defaults to None if
132                the slab is not reconstructed.
133            coords_are_cartesian (bool): Set to True if you are providing
134                coordinates in cartesian coordinates. Defaults to False.
135            site_properties (dict): Properties associated with the sites as a
136                dict of sequences, e.g., {"magmom":[5,5,5,5]}. The sequences
137                have to be the same length as the atomic species and
138                fractional_coords. Defaults to None for no properties.
139            energy (float): A value for the energy.
140        """
141        self.oriented_unit_cell = oriented_unit_cell
142        self.miller_index = tuple(miller_index)
143        self.shift = shift
144        self.reconstruction = reconstruction
145        self.scale_factor = np.array(scale_factor)
146        self.energy = energy
147        self.reorient_lattice = reorient_lattice
148        if self.reorient_lattice:
149            if coords_are_cartesian:
150                coords = lattice.get_fractional_coords(coords)
151                coords_are_cartesian = False
152            lattice = Lattice.from_parameters(
153                lattice.a,
154                lattice.b,
155                lattice.c,
156                lattice.alpha,
157                lattice.beta,
158                lattice.gamma,
159            )
160
161        super().__init__(
162            lattice,
163            species,
164            coords,
165            validate_proximity=validate_proximity,
166            to_unit_cell=to_unit_cell,
167            coords_are_cartesian=coords_are_cartesian,
168            site_properties=site_properties,
169        )
170
171    def get_orthogonal_c_slab(self):
172        """
173        This method returns a Slab where the normal (c lattice vector) is
174        "forced" to be exactly orthogonal to the surface a and b lattice
175        vectors. **Note that this breaks inherent symmetries in the slab.**
176        It should be pointed out that orthogonality is not required to get good
177        surface energies, but it can be useful in cases where the slabs are
178        subsequently used for postprocessing of some kind, e.g. generating
179        GBs or interfaces.
180        """
181        a, b, c = self.lattice.matrix
182        new_c = np.cross(a, b)
183        new_c /= np.linalg.norm(new_c)
184        new_c = np.dot(c, new_c) * new_c
185        new_latt = Lattice([a, b, new_c])
186        return Slab(
187            lattice=new_latt,
188            species=self.species_and_occu,
189            coords=self.cart_coords,
190            miller_index=self.miller_index,
191            oriented_unit_cell=self.oriented_unit_cell,
192            shift=self.shift,
193            scale_factor=self.scale_factor,
194            coords_are_cartesian=True,
195            energy=self.energy,
196            reorient_lattice=self.reorient_lattice,
197            site_properties=self.site_properties,
198        )
199
200    def get_tasker2_slabs(self, tol=0.01, same_species_only=True):
201        """
202        Get a list of slabs that have been Tasker 2 corrected.
203
204        Args:
205            tol (float): Tolerance to determine if atoms are within same plane.
206                This is a fractional tolerance, not an absolute one.
207            same_species_only (bool): If True, only that are of the exact same
208                species as the atom at the outermost surface are considered for
209                moving. Otherwise, all atoms regardless of species that is
210                within tol are considered for moving. Default is True (usually
211                the desired behavior).
212
213        Returns:
214            ([Slab]) List of tasker 2 corrected slabs.
215        """
216        sites = list(self.sites)
217        slabs = []
218
219        sortedcsites = sorted(sites, key=lambda site: site.c)
220
221        # Determine what fraction the slab is of the total cell size in the
222        # c direction. Round to nearest rational number.
223        nlayers_total = int(round(self.lattice.c / self.oriented_unit_cell.lattice.c))
224        nlayers_slab = int(round((sortedcsites[-1].c - sortedcsites[0].c) * nlayers_total))
225        slab_ratio = nlayers_slab / nlayers_total
226
227        a = SpacegroupAnalyzer(self)
228        symm_structure = a.get_symmetrized_structure()
229
230        def equi_index(site):
231            for i, equi_sites in enumerate(symm_structure.equivalent_sites):
232                if site in equi_sites:
233                    return i
234            raise ValueError("Cannot determine equi index!")
235
236        for surface_site, shift in [
237            (sortedcsites[0], slab_ratio),
238            (sortedcsites[-1], -slab_ratio),
239        ]:
240            tomove = []
241            fixed = []
242            for site in sites:
243                if abs(site.c - surface_site.c) < tol and (
244                    (not same_species_only) or site.species == surface_site.species
245                ):
246                    tomove.append(site)
247                else:
248                    fixed.append(site)
249
250            # Sort and group the sites by the species and symmetry equivalence
251            tomove = sorted(tomove, key=lambda s: equi_index(s))
252
253            grouped = [list(sites) for k, sites in itertools.groupby(tomove, key=lambda s: equi_index(s))]
254
255            if len(tomove) == 0 or any(len(g) % 2 != 0 for g in grouped):
256                warnings.warn(
257                    "Odd number of sites to divide! Try changing "
258                    "the tolerance to ensure even division of "
259                    "sites or create supercells in a or b directions "
260                    "to allow for atoms to be moved!"
261                )
262                continue
263            combinations = []
264            for g in grouped:
265                combinations.append(list(itertools.combinations(g, int(len(g) / 2))))
266
267            for selection in itertools.product(*combinations):
268                species = [site.species for site in fixed]
269                fcoords = [site.frac_coords for site in fixed]
270
271                for s in tomove:
272                    species.append(s.species)
273                    for group in selection:
274                        if s in group:
275                            fcoords.append(s.frac_coords)
276                            break
277                    else:
278                        # Move unselected atom to the opposite surface.
279                        fcoords.append(s.frac_coords + [0, 0, shift])
280
281                # sort by species to put all similar species together.
282                sp_fcoord = sorted(zip(species, fcoords), key=lambda x: x[0])
283                species = [x[0] for x in sp_fcoord]
284                fcoords = [x[1] for x in sp_fcoord]
285                slab = Slab(
286                    self.lattice,
287                    species,
288                    fcoords,
289                    self.miller_index,
290                    self.oriented_unit_cell,
291                    self.shift,
292                    self.scale_factor,
293                    energy=self.energy,
294                    reorient_lattice=self.reorient_lattice,
295                )
296                slabs.append(slab)
297        s = StructureMatcher()
298        unique = [ss[0] for ss in s.group_structures(slabs)]
299        return unique
300
301    def is_symmetric(self, symprec=0.1):
302        """
303        Checks if surfaces are symmetric, i.e., contains inversion, mirror on (hkl) plane,
304            or screw axis (rotation and translation) about [hkl].
305
306        Args:
307            symprec (float): Symmetry precision used for SpaceGroup analyzer.
308
309        Returns:
310            (bool) Whether surfaces are symmetric.
311        """
312
313        sg = SpacegroupAnalyzer(self, symprec=symprec)
314        symmops = sg.get_point_group_operations()
315
316        if (
317            sg.is_laue()
318            or any(op.translation_vector[2] != 0 for op in symmops)
319            or any(np.alltrue(op.rotation_matrix[2] == np.array([0, 0, -1])) for op in symmops)
320        ):
321            # Check for inversion symmetry. Or if sites from surface (a) can be translated
322            # to surface (b) along the [hkl]-axis, surfaces are symmetric. Or because the
323            # two surfaces of our slabs are always parallel to the (hkl) plane,
324            # any operation where theres an (hkl) mirror plane has surface symmetry
325            return True
326        return False
327
328    def get_sorted_structure(self, key=None, reverse=False):
329        """
330        Get a sorted copy of the structure. The parameters have the same
331        meaning as in list.sort. By default, sites are sorted by the
332        electronegativity of the species. Note that Slab has to override this
333        because of the different __init__ args.
334
335        Args:
336            key: Specifies a function of one argument that is used to extract
337                a comparison key from each list element: key=str.lower. The
338                default value is None (compare the elements directly).
339            reverse (bool): If set to True, then the list elements are sorted
340                as if each comparison were reversed.
341        """
342        sites = sorted(self, key=key, reverse=reverse)
343        s = Structure.from_sites(sites)
344        return Slab(
345            s.lattice,
346            s.species_and_occu,
347            s.frac_coords,
348            self.miller_index,
349            self.oriented_unit_cell,
350            self.shift,
351            self.scale_factor,
352            site_properties=s.site_properties,
353            reorient_lattice=self.reorient_lattice,
354        )
355
356    def copy(self, site_properties=None, sanitize=False):
357        """
358        Convenience method to get a copy of the structure, with options to add
359        site properties.
360
361        Args:
362            site_properties (dict): Properties to add or override. The
363                properties are specified in the same way as the constructor,
364                i.e., as a dict of the form {property: [values]}. The
365                properties should be in the order of the *original* structure
366                if you are performing sanitization.
367            sanitize (bool): If True, this method will return a sanitized
368                structure. Sanitization performs a few things: (i) The sites are
369                sorted by electronegativity, (ii) a LLL lattice reduction is
370                carried out to obtain a relatively orthogonalized cell,
371                (iii) all fractional coords for sites are mapped into the
372                unit cell.
373
374        Returns:
375            A copy of the Structure, with optionally new site_properties and
376            optionally sanitized.
377        """
378        props = self.site_properties
379        if site_properties:
380            props.update(site_properties)
381        return Slab(
382            self.lattice,
383            self.species_and_occu,
384            self.frac_coords,
385            self.miller_index,
386            self.oriented_unit_cell,
387            self.shift,
388            self.scale_factor,
389            site_properties=props,
390            reorient_lattice=self.reorient_lattice,
391        )
392
393    @property
394    def dipole(self):
395        """
396        Calculates the dipole of the Slab in the direction of the surface
397        normal. Note that the Slab must be oxidation state-decorated for this
398        to work properly. Otherwise, the Slab will always have a dipole of 0.
399        """
400        dipole = np.zeros(3)
401        mid_pt = np.sum(self.cart_coords, axis=0) / len(self)
402        normal = self.normal
403        for site in self:
404            charge = sum([getattr(sp, "oxi_state", 0) * amt for sp, amt in site.species.items()])
405            dipole += charge * np.dot(site.coords - mid_pt, normal) * normal
406        return dipole
407
408    def is_polar(self, tol_dipole_per_unit_area=1e-3):
409        """
410        Checks whether the surface is polar by computing the dipole per unit
411        area. Note that the Slab must be oxidation state-decorated for this
412        to work properly. Otherwise, the Slab will always be non-polar.
413
414        Args:
415            tol_dipole_per_unit_area (float): A tolerance. If the dipole
416                magnitude per unit area is less than this value, the Slab is
417                considered non-polar. Defaults to 1e-3, which is usually
418                pretty good. Normalized dipole per unit area is used as it is
419                more reliable than using the total, which tends to be larger for
420                slabs with larger surface areas.
421        """
422        dip_per_unit_area = self.dipole / self.surface_area
423        return np.linalg.norm(dip_per_unit_area) > tol_dipole_per_unit_area
424
425    @property
426    def normal(self):
427        """
428        Calculates the surface normal vector of the slab
429        """
430        normal = np.cross(self.lattice.matrix[0], self.lattice.matrix[1])
431        normal /= np.linalg.norm(normal)
432        return normal
433
434    @property
435    def surface_area(self):
436        """
437        Calculates the surface area of the slab
438        """
439        m = self.lattice.matrix
440        return np.linalg.norm(np.cross(m[0], m[1]))
441
442    @property
443    def center_of_mass(self):
444        """
445        Calculates the center of mass of the slab
446        """
447        weights = [s.species.weight for s in self]
448        center_of_mass = np.average(self.frac_coords, weights=weights, axis=0)
449        return center_of_mass
450
451    def add_adsorbate_atom(self, indices, specie, distance):
452        """
453        Gets the structure of single atom adsorption.
454        slab structure from the Slab class(in [0, 0, 1])
455
456        Args:
457            indices ([int]): Indices of sites on which to put the absorbate.
458                Absorbed atom will be displaced relative to the center of
459                these sites.
460            specie (Species/Element/str): adsorbed atom species
461            distance (float): between centers of the adsorbed atom and the
462                given site in Angstroms.
463        """
464        # Let's do the work in cartesian coords
465        center = np.sum([self[i].coords for i in indices], axis=0) / len(indices)
466
467        coords = center + self.normal * distance / np.linalg.norm(self.normal)
468
469        self.append(specie, coords, coords_are_cartesian=True)
470
471    def __str__(self):
472        comp = self.composition
473        outs = [
474            "Slab Summary (%s)" % comp.formula,
475            "Reduced Formula: %s" % comp.reduced_formula,
476            "Miller index: %s" % (self.miller_index,),
477            "Shift: %.4f, Scale Factor: %s" % (self.shift, self.scale_factor.__str__()),
478        ]
479
480        def to_s(x):
481            return "%0.6f" % x
482
483        outs.append("abc   : " + " ".join([to_s(i).rjust(10) for i in self.lattice.abc]))
484        outs.append("angles: " + " ".join([to_s(i).rjust(10) for i in self.lattice.angles]))
485        outs.append("Sites ({i})".format(i=len(self)))
486        for i, site in enumerate(self):
487            outs.append(
488                " ".join(
489                    [
490                        str(i + 1),
491                        site.species_string,
492                        " ".join([to_s(j).rjust(12) for j in site.frac_coords]),
493                    ]
494                )
495            )
496        return "\n".join(outs)
497
498    def as_dict(self):
499        """
500        :return: MSONAble dict
501        """
502        d = super().as_dict()
503        d["@module"] = self.__class__.__module__
504        d["@class"] = self.__class__.__name__
505        d["oriented_unit_cell"] = self.oriented_unit_cell.as_dict()
506        d["miller_index"] = self.miller_index
507        d["shift"] = self.shift
508        d["scale_factor"] = self.scale_factor.tolist()
509        d["reconstruction"] = self.reconstruction
510        d["energy"] = self.energy
511        return d
512
513    @classmethod
514    def from_dict(cls, d):
515        """
516        :param d: dict
517        :return: Creates slab from dict.
518        """
519        lattice = Lattice.from_dict(d["lattice"])
520        sites = [PeriodicSite.from_dict(sd, lattice) for sd in d["sites"]]
521        s = Structure.from_sites(sites)
522
523        return Slab(
524            lattice=lattice,
525            species=s.species_and_occu,
526            coords=s.frac_coords,
527            miller_index=d["miller_index"],
528            oriented_unit_cell=Structure.from_dict(d["oriented_unit_cell"]),
529            shift=d["shift"],
530            scale_factor=d["scale_factor"],
531            site_properties=s.site_properties,
532            energy=d["energy"],
533        )
534
535    def get_surface_sites(self, tag=False):
536        """
537        Returns the surface sites and their indices in a dictionary. The
538        oriented unit cell of the slab will determine the coordination number
539        of a typical site. We use VoronoiNN to determine the
540        coordination number of bulk sites and slab sites. Due to the
541        pathological error resulting from some surface sites in the
542        VoronoiNN, we assume any site that has this error is a surface
543        site as well. This will work for elemental systems only for now. Useful
544        for analysis involving broken bonds and for finding adsorption sites.
545
546            Args:
547                tag (bool): Option to adds site attribute "is_surfsite" (bool)
548                    to all sites of slab. Defaults to False
549
550            Returns:
551                A dictionary grouping sites on top and bottom of the slab
552                together.
553                {"top": [sites with indices], "bottom": [sites with indices}
554
555        TODO:
556            Is there a way to determine site equivalence between sites in a slab
557            and bulk system? This would allow us get the coordination number of
558            a specific site for multi-elemental systems or systems with more
559            than one unequivalent site. This will allow us to use this for
560            compound systems.
561        """
562
563        from pymatgen.analysis.local_env import VoronoiNN
564
565        # Get a dictionary of coordination numbers
566        # for each distinct site in the structure
567        a = SpacegroupAnalyzer(self.oriented_unit_cell)
568        ucell = a.get_symmetrized_structure()
569        cn_dict = {}
570        v = VoronoiNN()
571        unique_indices = [equ[0] for equ in ucell.equivalent_indices]
572
573        for i in unique_indices:
574            el = ucell[i].species_string
575            if el not in cn_dict.keys():
576                cn_dict[el] = []
577            # Since this will get the cn as a result of the weighted polyhedra, the
578            # slightest difference in cn will indicate a different environment for a
579            # species, eg. bond distance of each neighbor or neighbor species. The
580            # decimal place to get some cn to be equal.
581            cn = v.get_cn(ucell, i, use_weights=True)
582            cn = float("%.5f" % (round(cn, 5)))
583            if cn not in cn_dict[el]:
584                cn_dict[el].append(cn)
585
586        v = VoronoiNN()
587
588        surf_sites_dict, properties = {"top": [], "bottom": []}, []
589        for i, site in enumerate(self):
590            # Determine if site is closer to the top or bottom of the slab
591            top = site.frac_coords[2] > self.center_of_mass[2]
592
593            try:
594                # A site is a surface site, if its environment does
595                # not fit the environment of other sites
596                cn = float("%.5f" % (round(v.get_cn(self, i, use_weights=True), 5)))
597                if cn < min(cn_dict[site.species_string]):
598                    properties.append(True)
599                    key = "top" if top else "bottom"
600                    surf_sites_dict[key].append([site, i])
601                else:
602                    properties.append(False)
603            except RuntimeError:
604                # or if pathological error is returned, indicating a surface site
605                properties.append(True)
606                key = "top" if top else "bottom"
607                surf_sites_dict[key].append([site, i])
608
609        if tag:
610            self.add_site_property("is_surf_site", properties)
611        return surf_sites_dict
612
613    def get_symmetric_site(self, point, cartesian=False):
614        """
615        This method uses symmetry operations to find equivalent sites on
616            both sides of the slab. Works mainly for slabs with Laue
617            symmetry. This is useful for retaining the non-polar and
618            symmetric properties of a slab when creating adsorbed
619            structures or symmetric reconstructions.
620
621        Arg:
622            point: Fractional coordinate.
623
624        Returns:
625            point: Fractional coordinate. A point equivalent to the
626                parameter point, but on the other side of the slab
627        """
628
629        sg = SpacegroupAnalyzer(self)
630        ops = sg.get_symmetry_operations(cartesian=cartesian)
631
632        # Each operation on a point will return an equivalent point.
633        # We want to find the point on the other side of the slab.
634        for op in ops:
635            slab = self.copy()
636            site2 = op.operate(point)
637            if "%.6f" % (site2[2]) == "%.6f" % (point[2]):
638                continue
639
640            # Add dummy site to check the overall structure is symmetric
641            slab.append("O", point, coords_are_cartesian=cartesian)
642            slab.append("O", site2, coords_are_cartesian=cartesian)
643            sg = SpacegroupAnalyzer(slab)
644            if sg.is_laue():
645                break
646
647            # If not symmetric, remove the two added
648            # sites and try another symmetry operator
649            slab.remove_sites([len(slab) - 1])
650            slab.remove_sites([len(slab) - 1])
651
652        return site2
653
654    def symmetrically_add_atom(self, specie, point, coords_are_cartesian=False):
655        """
656        Class method for adding a site at a specified point in a slab.
657            Will add the corresponding site on the other side of the
658            slab to maintain equivalent surfaces.
659
660        Arg:
661            specie (str): The specie to add
662            point (coords): The coordinate of the site in the slab to add.
663            coords_are_cartesian (bool): Is the point in cartesian coordinates
664
665        Returns:
666            (Slab): The modified slab
667        """
668
669        # For now just use the species of the
670        # surface atom as the element to add
671
672        # Get the index of the corresponding site at the bottom
673        point2 = self.get_symmetric_site(point, cartesian=coords_are_cartesian)
674
675        self.append(specie, point, coords_are_cartesian=coords_are_cartesian)
676        self.append(specie, point2, coords_are_cartesian=coords_are_cartesian)
677
678    def symmetrically_remove_atoms(self, indices):
679        """
680        Class method for removing sites corresponding to a list of indices.
681            Will remove the corresponding site on the other side of the
682            slab to maintain equivalent surfaces.
683
684        Arg:
685            indices ([indices]): The indices of the sites
686                in the slab to remove.
687        """
688
689        slabcopy = SpacegroupAnalyzer(self.copy()).get_symmetrized_structure()
690        points = [slabcopy[i].frac_coords for i in indices]
691        removal_list = []
692
693        for pt in points:
694            # Get the index of the original site on top
695            cart_point = slabcopy.lattice.get_cartesian_coords(pt)
696            dist = [site.distance_from_point(cart_point) for site in slabcopy]
697            site1 = dist.index(min(dist))
698
699            # Get the index of the corresponding site at the bottom
700            for i, eq_sites in enumerate(slabcopy.equivalent_sites):
701                if slabcopy[site1] in eq_sites:
702                    eq_indices = slabcopy.equivalent_indices[i]
703                    break
704            i1 = eq_indices[eq_sites.index(slabcopy[site1])]
705
706            for i2 in eq_indices:
707                if i2 == i1:
708                    continue
709                if slabcopy[i2].frac_coords[2] == slabcopy[i1].frac_coords[2]:
710                    continue
711                # Test site remove to see if it results in symmetric slab
712                s = self.copy()
713                s.remove_sites([i1, i2])
714                if s.is_symmetric():
715                    removal_list.extend([i1, i2])
716                    break
717
718        # If expected, 2 atoms are removed per index
719        if len(removal_list) == 2 * len(indices):
720            self.remove_sites(removal_list)
721        else:
722            warnings.warn("Equivalent sites could not be found for removal for all indices. Surface unchanged.")
723
724
725class SlabGenerator:
726    """
727    This class generates different slabs using shift values determined by where
728    a unique termination can be found along with other criterias such as where a
729    termination doesn't break a polyhedral bond. The shift value then indicates
730    where the slab layer will begin and terminate in the slab-vacuum system.
731
732    .. attribute:: oriented_unit_cell
733
734        A unit cell of the parent structure with the miller
735        index of plane parallel to surface
736
737    .. attribute:: parent
738
739        Parent structure from which Slab was derived.
740
741    .. attribute:: lll_reduce
742
743        Whether or not the slabs will be orthogonalized
744
745    .. attribute:: center_slab
746
747        Whether or not the slabs will be centered between
748        the vacuum layer
749
750    .. attribute:: slab_scale_factor
751
752        Final computed scale factor that brings the parent cell to the
753        surface cell.
754
755    .. attribute:: miller_index
756
757        Miller index of plane parallel to surface.
758
759    .. attribute:: min_slab_size
760
761        Minimum size in angstroms of layers containing atoms
762
763    .. attribute:: min_vac_size
764
765        Minimize size in angstroms of layers containing vacuum
766
767    """
768
769    def __init__(
770        self,
771        initial_structure,
772        miller_index,
773        min_slab_size,
774        min_vacuum_size,
775        lll_reduce=False,
776        center_slab=False,
777        in_unit_planes=False,
778        primitive=True,
779        max_normal_search=None,
780        reorient_lattice=True,
781    ):
782        """
783        Calculates the slab scale factor and uses it to generate a unit cell
784        of the initial structure that has been oriented by its miller index.
785        Also stores the initial information needed later on to generate a slab.
786
787        Args:
788            initial_structure (Structure): Initial input structure. Note that to
789                ensure that the miller indices correspond to usual
790                crystallographic definitions, you should supply a conventional
791                unit cell structure.
792            miller_index ([h, k, l]): Miller index of plane parallel to
793                surface. Note that this is referenced to the input structure. If
794                you need this to be based on the conventional cell,
795                you should supply the conventional structure.
796            min_slab_size (float): In Angstroms or number of hkl planes
797            min_vacuum_size (float): In Angstroms or number of hkl planes
798            lll_reduce (bool): Whether to perform an LLL reduction on the
799                eventual structure.
800            center_slab (bool): Whether to center the slab in the cell with
801                equal vacuum spacing from the top and bottom.
802            in_unit_planes (bool): Whether to set min_slab_size and min_vac_size
803                in units of hkl planes (True) or Angstrom (False/default).
804                Setting in units of planes is useful for ensuring some slabs
805                have a certain nlayer of atoms. e.g. for Cs (100), a 10 Ang
806                slab will result in a slab with only 2 layer of atoms, whereas
807                Fe (100) will have more layer of atoms. By using units of hkl
808                planes instead, we ensure both slabs
809                have the same number of atoms. The slab thickness will be in
810                min_slab_size/math.ceil(self._proj_height/dhkl)
811                multiples of oriented unit cells.
812            primitive (bool): Whether to reduce any generated slabs to a
813                primitive cell (this does **not** mean the slab is generated
814                from a primitive cell, it simply means that after slab
815                generation, we attempt to find shorter lattice vectors,
816                which lead to less surface area and smaller cells).
817            max_normal_search (int): If set to a positive integer, the code will
818                conduct a search for a normal lattice vector that is as
819                perpendicular to the surface as possible by considering
820                multiples linear combinations of lattice vectors up to
821                max_normal_search. This has no bearing on surface energies,
822                but may be useful as a preliminary step to generating slabs
823                for absorption and other sizes. It is typical that this will
824                not be the smallest possible cell for simulation. Normality
825                is not guaranteed, but the oriented cell will have the c
826                vector as normal as possible (within the search range) to the
827                surface. A value of up to the max absolute Miller index is
828                usually sufficient.
829            reorient_lattice (bool): reorients the lattice parameters such that
830                the c direction is the third vector of the lattice matrix
831
832        """
833        # pylint: disable=E1130
834        # Add Wyckoff symbols of the bulk, will help with
835        # identfying types of sites in the slab system
836        sg = SpacegroupAnalyzer(initial_structure)
837        initial_structure.add_site_property("bulk_wyckoff", sg.get_symmetry_dataset()["wyckoffs"])
838        initial_structure.add_site_property("bulk_equivalent", sg.get_symmetry_dataset()["equivalent_atoms"].tolist())
839        latt = initial_structure.lattice
840        miller_index = _reduce_vector(miller_index)
841        # Calculate the surface normal using the reciprocal lattice vector.
842        recp = latt.reciprocal_lattice_crystallographic
843        normal = recp.get_cartesian_coords(miller_index)
844        normal /= np.linalg.norm(normal)
845
846        slab_scale_factor = []
847        non_orth_ind = []
848        eye = np.eye(3, dtype=np.int_)
849        for i, j in enumerate(miller_index):
850            if j == 0:
851                # Lattice vector is perpendicular to surface normal, i.e.,
852                # in plane of surface. We will simply choose this lattice
853                # vector as one of the basis vectors.
854                slab_scale_factor.append(eye[i])
855            else:
856                # Calculate projection of lattice vector onto surface normal.
857                d = abs(np.dot(normal, latt.matrix[i])) / latt.abc[i]
858                non_orth_ind.append((i, d))
859
860        # We want the vector that has maximum magnitude in the
861        # direction of the surface normal as the c-direction.
862        # Results in a more "orthogonal" unit cell.
863        c_index, dist = max(non_orth_ind, key=lambda t: t[1])
864
865        if len(non_orth_ind) > 1:
866            lcm_miller = lcm(*[miller_index[i] for i, d in non_orth_ind])
867            for (i, di), (j, dj) in itertools.combinations(non_orth_ind, 2):
868                l = [0, 0, 0]
869                l[i] = -int(round(lcm_miller / miller_index[i]))
870                l[j] = int(round(lcm_miller / miller_index[j]))
871                slab_scale_factor.append(l)
872                if len(slab_scale_factor) == 2:
873                    break
874
875        if max_normal_search is None:
876            slab_scale_factor.append(eye[c_index])
877        else:
878
879            index_range = sorted(
880                reversed(range(-max_normal_search, max_normal_search + 1)),
881                key=lambda x: abs(x),
882            )
883            candidates = []
884            for uvw in itertools.product(index_range, index_range, index_range):
885                if (not any(uvw)) or abs(np.linalg.det(slab_scale_factor + [uvw])) < 1e-8:
886                    continue
887                vec = latt.get_cartesian_coords(uvw)
888                l = np.linalg.norm(vec)
889                cosine = abs(np.dot(vec, normal) / l)
890                candidates.append((uvw, cosine, l))
891                if abs(abs(cosine) - 1) < 1e-8:
892                    # If cosine of 1 is found, no need to search further.
893                    break
894            # We want the indices with the maximum absolute cosine,
895            # but smallest possible length.
896            uvw, cosine, l = max(candidates, key=lambda x: (x[1], -x[2]))
897            slab_scale_factor.append(uvw)
898
899        slab_scale_factor = np.array(slab_scale_factor)
900
901        # Let's make sure we have a left-handed crystallographic system
902        if np.linalg.det(slab_scale_factor) < 0:
903            slab_scale_factor *= -1
904
905        # Make sure the slab_scale_factor is reduced to avoid
906        # unnecessarily large slabs
907
908        reduced_scale_factor = [_reduce_vector(v) for v in slab_scale_factor]
909        slab_scale_factor = np.array(reduced_scale_factor)
910
911        single = initial_structure.copy()
912        single.make_supercell(slab_scale_factor)
913
914        # When getting the OUC, lets return the most reduced
915        # structure as possible to reduce calculations
916        self.oriented_unit_cell = Structure.from_sites(single, to_unit_cell=True)
917        self.max_normal_search = max_normal_search
918        self.parent = initial_structure
919        self.lll_reduce = lll_reduce
920        self.center_slab = center_slab
921        self.slab_scale_factor = slab_scale_factor
922        self.miller_index = miller_index
923        self.min_vac_size = min_vacuum_size
924        self.min_slab_size = min_slab_size
925        self.in_unit_planes = in_unit_planes
926        self.primitive = primitive
927        self._normal = normal
928        a, b, c = self.oriented_unit_cell.lattice.matrix
929        self._proj_height = abs(np.dot(normal, c))
930        self.reorient_lattice = reorient_lattice
931
932    def get_slab(self, shift=0, tol=0.1, energy=None):
933        """
934        This method takes in shift value for the c lattice direction and
935        generates a slab based on the given shift. You should rarely use this
936        method. Instead, it is used by other generation algorithms to obtain
937        all slabs.
938
939        Arg:
940            shift (float): A shift value in Angstrom that determines how much a
941                slab should be shifted.
942            tol (float): Tolerance to determine primitive cell.
943            energy (float): An energy to assign to the slab.
944
945        Returns:
946            (Slab) A Slab object with a particular shifted oriented unit cell.
947        """
948
949        h = self._proj_height
950        p = round(h / self.parent.lattice.d_hkl(self.miller_index), 8)
951        if self.in_unit_planes:
952            nlayers_slab = int(math.ceil(self.min_slab_size / p))
953            nlayers_vac = int(math.ceil(self.min_vac_size / p))
954        else:
955            nlayers_slab = int(math.ceil(self.min_slab_size / h))
956            nlayers_vac = int(math.ceil(self.min_vac_size / h))
957        nlayers = nlayers_slab + nlayers_vac
958
959        species = self.oriented_unit_cell.species_and_occu
960        props = self.oriented_unit_cell.site_properties
961        props = {k: v * nlayers_slab for k, v in props.items()}
962        frac_coords = self.oriented_unit_cell.frac_coords
963        frac_coords = np.array(frac_coords) + np.array([0, 0, -shift])[None, :]
964        frac_coords -= np.floor(frac_coords)
965        a, b, c = self.oriented_unit_cell.lattice.matrix
966        new_lattice = [a, b, nlayers * c]
967        frac_coords[:, 2] = frac_coords[:, 2] / nlayers
968        all_coords = []
969        for i in range(nlayers_slab):
970            fcoords = frac_coords.copy()
971            fcoords[:, 2] += i / nlayers
972            all_coords.extend(fcoords)
973
974        slab = Structure(new_lattice, species * nlayers_slab, all_coords, site_properties=props)
975
976        scale_factor = self.slab_scale_factor
977        # Whether or not to orthogonalize the structure
978        if self.lll_reduce:
979            lll_slab = slab.copy(sanitize=True)
980            mapping = lll_slab.lattice.find_mapping(slab.lattice)
981            scale_factor = np.dot(mapping[2], scale_factor)
982            slab = lll_slab
983
984        # Whether or not to center the slab layer around the vacuum
985        if self.center_slab:
986            avg_c = np.average([c[2] for c in slab.frac_coords])
987            slab.translate_sites(list(range(len(slab))), [0, 0, 0.5 - avg_c])
988
989        if self.primitive:
990            prim = slab.get_primitive_structure(tolerance=tol)
991            if energy is not None:
992                energy = prim.volume / slab.volume * energy
993            slab = prim
994
995        # Reorient the lattice to get the correct reduced cell
996        ouc = self.oriented_unit_cell.copy()
997        if self.primitive:
998            # find a reduced ouc
999            slab_l = slab.lattice
1000            ouc = ouc.get_primitive_structure(
1001                constrain_latt={
1002                    "a": slab_l.a,
1003                    "b": slab_l.b,
1004                    "alpha": slab_l.alpha,
1005                    "beta": slab_l.beta,
1006                    "gamma": slab_l.gamma,
1007                }
1008            )
1009            # Check this is the correct oriented unit cell
1010            ouc = self.oriented_unit_cell if slab_l.a != ouc.lattice.a or slab_l.b != ouc.lattice.b else ouc
1011
1012        return Slab(
1013            slab.lattice,
1014            slab.species_and_occu,
1015            slab.frac_coords,
1016            self.miller_index,
1017            ouc,
1018            shift,
1019            scale_factor,
1020            energy=energy,
1021            site_properties=slab.site_properties,
1022            reorient_lattice=self.reorient_lattice,
1023        )
1024
1025    def _calculate_possible_shifts(self, tol=0.1):
1026        frac_coords = self.oriented_unit_cell.frac_coords
1027        n = len(frac_coords)
1028
1029        if n == 1:
1030            # Clustering does not work when there is only one data point.
1031            shift = frac_coords[0][2] + 0.5
1032            return [shift - math.floor(shift)]
1033
1034        # We cluster the sites according to the c coordinates. But we need to
1035        # take into account PBC. Let's compute a fractional c-coordinate
1036        # distance matrix that accounts for PBC.
1037        dist_matrix = np.zeros((n, n))
1038        h = self._proj_height
1039        # Projection of c lattice vector in
1040        # direction of surface normal.
1041        for i, j in itertools.combinations(list(range(n)), 2):
1042            if i != j:
1043                cdist = frac_coords[i][2] - frac_coords[j][2]
1044                cdist = abs(cdist - round(cdist)) * h
1045                dist_matrix[i, j] = cdist
1046                dist_matrix[j, i] = cdist
1047
1048        condensed_m = squareform(dist_matrix)
1049        z = linkage(condensed_m)
1050        clusters = fcluster(z, tol, criterion="distance")
1051
1052        # Generate dict of cluster# to c val - doesn't matter what the c is.
1053        c_loc = {c: frac_coords[i][2] for i, c in enumerate(clusters)}
1054
1055        # Put all c into the unit cell.
1056        possible_c = [c - math.floor(c) for c in sorted(c_loc.values())]
1057
1058        # Calculate the shifts
1059        nshifts = len(possible_c)
1060        shifts = []
1061        for i in range(nshifts):
1062            if i == nshifts - 1:
1063                # There is an additional shift between the first and last c
1064                # coordinate. But this needs special handling because of PBC.
1065                shift = (possible_c[0] + 1 + possible_c[i]) * 0.5
1066                if shift > 1:
1067                    shift -= 1
1068            else:
1069                shift = (possible_c[i] + possible_c[i + 1]) * 0.5
1070            shifts.append(shift - math.floor(shift))
1071        shifts = sorted(shifts)
1072        return shifts
1073
1074    def _get_c_ranges(self, bonds):
1075        c_ranges = []
1076        bonds = {(get_el_sp(s1), get_el_sp(s2)): dist for (s1, s2), dist in bonds.items()}
1077        for (sp1, sp2), bond_dist in bonds.items():
1078            for site in self.oriented_unit_cell:
1079                if sp1 in site.species:
1080                    for nn in self.oriented_unit_cell.get_neighbors(site, bond_dist):
1081                        if sp2 in nn.species:
1082                            c_range = tuple(sorted([site.frac_coords[2], nn.frac_coords[2]]))
1083                            if c_range[1] > 1:
1084                                # Takes care of PBC when c coordinate of site
1085                                # goes beyond the upper boundary of the cell
1086                                c_ranges.append((c_range[0], 1))
1087                                c_ranges.append((0, c_range[1] - 1))
1088                            elif c_range[0] < 0:
1089                                # Takes care of PBC when c coordinate of site
1090                                # is below the lower boundary of the unit cell
1091                                c_ranges.append((0, c_range[1]))
1092                                c_ranges.append((c_range[0] + 1, 1))
1093                            elif c_range[0] != c_range[1]:
1094                                c_ranges.append((c_range[0], c_range[1]))
1095        return c_ranges
1096
1097    def get_slabs(
1098        self,
1099        bonds=None,
1100        ftol=0.1,
1101        tol=0.1,
1102        max_broken_bonds=0,
1103        symmetrize=False,
1104        repair=False,
1105    ):
1106        """
1107        This method returns a list of slabs that are generated using the list of
1108        shift values from the method, _calculate_possible_shifts(). Before the
1109        shifts are used to create the slabs however, if the user decides to take
1110        into account whether or not a termination will break any polyhedral
1111        structure (bonds is not None), this method will filter out any shift
1112        values that do so.
1113
1114        Args:
1115            bonds ({(specie1, specie2): max_bond_dist}: bonds are
1116                specified as a dict of tuples: float of specie1, specie2
1117                and the max bonding distance. For example, PO4 groups may be
1118                defined as {("P", "O"): 3}.
1119            tol (float): General tolerance paramter for getting primitive
1120                cells and matching structures
1121            ftol (float): Threshold parameter in fcluster in order to check
1122                if two atoms are lying on the same plane. Default thresh set
1123                to 0.1 Angstrom in the direction of the surface normal.
1124            max_broken_bonds (int): Maximum number of allowable broken bonds
1125                for the slab. Use this to limit # of slabs (some structures
1126                may have a lot of slabs). Defaults to zero, which means no
1127                defined bonds must be broken.
1128            symmetrize (bool): Whether or not to ensure the surfaces of the
1129                slabs are equivalent.
1130            repair (bool): Whether to repair terminations with broken bonds
1131                or just omit them. Set to False as repairing terminations can
1132                lead to many possible slabs as oppose to just omitting them.
1133
1134        Returns:
1135            ([Slab]) List of all possible terminations of a particular surface.
1136            Slabs are sorted by the # of bonds broken.
1137        """
1138        c_ranges = [] if bonds is None else self._get_c_ranges(bonds)
1139
1140        slabs = []
1141        for shift in self._calculate_possible_shifts(tol=ftol):
1142            bonds_broken = 0
1143            for r in c_ranges:
1144                if r[0] <= shift <= r[1]:
1145                    bonds_broken += 1
1146            slab = self.get_slab(shift, tol=tol, energy=bonds_broken)
1147            if bonds_broken <= max_broken_bonds:
1148                slabs.append(slab)
1149            elif repair:
1150                # If the number of broken bonds is exceeded,
1151                # we repair the broken bonds on the slab
1152                slabs.append(self.repair_broken_bonds(slab, bonds))
1153
1154        # Further filters out any surfaces made that might be the same
1155        m = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False)
1156
1157        new_slabs = []
1158        for g in m.group_structures(slabs):
1159            # For each unique termination, symmetrize the
1160            # surfaces by removing sites from the bottom.
1161            if symmetrize:
1162                slabs = self.nonstoichiometric_symmetrized_slab(g[0])
1163                new_slabs.extend(slabs)
1164            else:
1165                new_slabs.append(g[0])
1166
1167        match = StructureMatcher(ltol=tol, stol=tol, primitive_cell=False, scale=False)
1168        new_slabs = [g[0] for g in match.group_structures(new_slabs)]
1169
1170        return sorted(new_slabs, key=lambda s: s.energy)
1171
1172    def repair_broken_bonds(self, slab, bonds):
1173        """
1174        This method will find undercoordinated atoms due to slab
1175        cleaving specified by the bonds parameter and move them
1176        to the other surface to make sure the bond is kept intact.
1177        In a future release of surface.py, the ghost_sites will be
1178        used to tell us how the repair bonds should look like.
1179
1180        Arg:
1181            slab (structure): A structure object representing a slab.
1182            bonds ({(specie1, specie2): max_bond_dist}: bonds are
1183                specified as a dict of tuples: float of specie1, specie2
1184                and the max bonding distance. For example, PO4 groups may be
1185                defined as {("P", "O"): 3}.
1186
1187        Returns:
1188            (Slab) A Slab object with a particular shifted oriented unit cell.
1189        """
1190
1191        for pair in bonds.keys():
1192            blength = bonds[pair]
1193
1194            # First lets determine which element should be the
1195            # reference (center element) to determine broken bonds.
1196            # e.g. P for a PO4 bond. Find integer coordination
1197            # numbers of the pair of elements wrt to each other
1198            cn_dict = {}
1199            for i, el in enumerate(pair):
1200                cnlist = []
1201                for site in self.oriented_unit_cell:
1202                    poly_coord = 0
1203                    if site.species_string == el:
1204
1205                        for nn in self.oriented_unit_cell.get_neighbors(site, blength):
1206                            if nn[0].species_string == pair[i - 1]:
1207                                poly_coord += 1
1208                    cnlist.append(poly_coord)
1209                cn_dict[el] = cnlist
1210
1211            # We make the element with the higher coordination our reference
1212            if max(cn_dict[pair[0]]) > max(cn_dict[pair[1]]):
1213                element1, element2 = pair
1214            else:
1215                element2, element1 = pair
1216
1217            for i, site in enumerate(slab):
1218                # Determine the coordination of our reference
1219                if site.species_string == element1:
1220                    poly_coord = 0
1221                    for neighbor in slab.get_neighbors(site, blength):
1222                        poly_coord += 1 if neighbor.species_string == element2 else 0
1223
1224                    # suppose we find an undercoordinated reference atom
1225                    if poly_coord not in cn_dict[element1]:
1226                        # We get the reference atom of the broken bonds
1227                        # (undercoordinated), move it to the other surface
1228                        slab = self.move_to_other_side(slab, [i])
1229
1230                        # find its NNs with the corresponding
1231                        # species it should be coordinated with
1232                        neighbors = slab.get_neighbors(slab[i], blength, include_index=True)
1233                        tomove = [nn[2] for nn in neighbors if nn[0].species_string == element2]
1234                        tomove.append(i)
1235                        # and then move those NNs along with the central
1236                        # atom back to the other side of the slab again
1237                        slab = self.move_to_other_side(slab, tomove)
1238
1239        return slab
1240
1241    def move_to_other_side(self, init_slab, index_of_sites):
1242        """
1243        This method will Move a set of sites to the
1244        other side of the slab (opposite surface).
1245
1246        Arg:
1247            init_slab (structure): A structure object representing a slab.
1248            index_of_sites (list of ints): The list of indices representing
1249                the sites we want to move to the other side.
1250
1251        Returns:
1252            (Slab) A Slab object with a particular shifted oriented unit cell.
1253        """
1254
1255        slab = init_slab.copy()
1256
1257        # Determine what fraction the slab is of the total cell size
1258        # in the c direction. Round to nearest rational number.
1259        h = self._proj_height
1260        p = h / self.parent.lattice.d_hkl(self.miller_index)
1261        if self.in_unit_planes:
1262            nlayers_slab = int(math.ceil(self.min_slab_size / p))
1263            nlayers_vac = int(math.ceil(self.min_vac_size / p))
1264        else:
1265            nlayers_slab = int(math.ceil(self.min_slab_size / h))
1266            nlayers_vac = int(math.ceil(self.min_vac_size / h))
1267        nlayers = nlayers_slab + nlayers_vac
1268        slab_ratio = nlayers_slab / nlayers
1269
1270        # Sort the index of sites based on which side they are on
1271        top_site_index = [i for i in index_of_sites if slab[i].frac_coords[2] > slab.center_of_mass[2]]
1272        bottom_site_index = [i for i in index_of_sites if slab[i].frac_coords[2] < slab.center_of_mass[2]]
1273
1274        # Translate sites to the opposite surfaces
1275        slab.translate_sites(top_site_index, [0, 0, slab_ratio])
1276        slab.translate_sites(bottom_site_index, [0, 0, -slab_ratio])
1277
1278        return Slab(
1279            init_slab.lattice,
1280            slab.species,
1281            slab.frac_coords,
1282            init_slab.miller_index,
1283            init_slab.oriented_unit_cell,
1284            init_slab.shift,
1285            init_slab.scale_factor,
1286            energy=init_slab.energy,
1287        )
1288
1289    def nonstoichiometric_symmetrized_slab(self, init_slab):
1290
1291        """
1292        This method checks whether or not the two surfaces of the slab are
1293        equivalent. If the point group of the slab has an inversion symmetry (
1294        ie. belong to one of the Laue groups), then it is assumed that the
1295        surfaces should be equivalent. Otherwise, sites at the bottom of the
1296        slab will be removed until the slab is symmetric. Note the removal of sites
1297        can destroy the stoichiometry of the slab. For non-elemental
1298        structures, the chemical potential will be needed to calculate surface energy.
1299
1300        Arg:
1301            init_slab (Structure): A single slab structure
1302
1303        Returns:
1304            Slab (structure): A symmetrized Slab object.
1305        """
1306
1307        if init_slab.is_symmetric():
1308            return [init_slab]
1309
1310        nonstoich_slabs = []
1311        # Build an equivalent surface slab for each of the different surfaces
1312        for top in [True, False]:
1313            asym = True
1314            slab = init_slab.copy()
1315            slab.energy = init_slab.energy
1316
1317            while asym:
1318                # Keep removing sites from the bottom one by one until both
1319                # surfaces are symmetric or the number of sites removed has
1320                # exceeded 10 percent of the original slab
1321
1322                c_dir = [site[2] for i, site in enumerate(slab.frac_coords)]
1323
1324                if top:
1325                    slab.remove_sites([c_dir.index(max(c_dir))])
1326                else:
1327                    slab.remove_sites([c_dir.index(min(c_dir))])
1328                if len(slab) <= len(self.parent):
1329                    break
1330
1331                # Check if the altered surface is symmetric
1332                if slab.is_symmetric():
1333                    asym = False
1334                    nonstoich_slabs.append(slab)
1335
1336        if len(slab) <= len(self.parent):
1337            warnings.warn("Too many sites removed, please use a larger slab size.")
1338
1339        return nonstoich_slabs
1340
1341
1342module_dir = os.path.dirname(os.path.abspath(__file__))
1343with open(os.path.join(module_dir, "reconstructions_archive.json")) as data_file:
1344    reconstructions_archive = json.load(data_file)
1345
1346
1347class ReconstructionGenerator:
1348    """
1349    This class takes in a pre-defined dictionary specifying the parameters
1350    need to build a reconstructed slab such as the SlabGenerator parameters,
1351    transformation matrix, sites to remove/add and slab/vacuum size. It will
1352    then use the formatted instructions provided by the dictionary to build
1353    the desired reconstructed slab from the initial structure.
1354
1355    .. attribute:: slabgen_params
1356
1357        Parameters for the SlabGenerator
1358
1359    .. trans_matrix::
1360
1361        A 3x3 transformation matrix to generate the reconstructed
1362            slab. Only the a and b lattice vectors are actually
1363            changed while the c vector remains the same. This
1364            matrix is what the Wood's notation is based on.
1365
1366    .. reconstruction_json::
1367
1368        The full json or dictionary containing the instructions
1369            for building the reconstructed slab
1370
1371    .. termination::
1372
1373        The index of the termination of the slab
1374
1375    TODO:
1376    - Right now there is no way to specify what atom is being
1377        added. In the future, use basis sets?
1378    """
1379
1380    def __init__(self, initial_structure, min_slab_size, min_vacuum_size, reconstruction_name):
1381        """
1382        Generates reconstructed slabs from a set of instructions
1383            specified by a dictionary or json file.
1384
1385        Args:
1386            initial_structure (Structure): Initial input structure. Note
1387                that to ensure that the miller indices correspond to usual
1388                crystallographic definitions, you should supply a conventional
1389                unit cell structure.
1390            min_slab_size (float): In Angstroms
1391            min_vacuum_size (float): In Angstroms
1392
1393            reconstruction (str): Name of the dict containing the instructions
1394                for building a reconstructed slab. The dictionary can contain
1395                any item the creator deems relevant, however any instructions
1396                archived in pymatgen for public use needs to contain the
1397                following keys and items to ensure compatibility with the
1398                ReconstructionGenerator:
1399
1400                    "name" (str): A descriptive name for the type of
1401                        reconstruction. Typically the name will have the type
1402                        of structure the reconstruction is for, the Miller
1403                        index, and Wood's notation along with anything to
1404                        describe the reconstruction: e.g.:
1405                        "fcc_110_missing_row_1x2"
1406                    "description" (str): A longer description of your
1407                        reconstruction. This is to help future contributors who
1408                        want to add other types of reconstructions to the
1409                        archive on pymatgen to check if the reconstruction
1410                        already exists. Please read the descriptions carefully
1411                        before adding a new type of reconstruction to ensure it
1412                        is not in the archive yet.
1413                    "reference" (str): Optional reference to where the
1414                        reconstruction was taken from or first observed.
1415                    "spacegroup" (dict): e.g. {"symbol": "Fm-3m", "number": 225}
1416                        Indicates what kind of structure is this reconstruction.
1417                    "miller_index" ([h,k,l]): Miller index of your reconstruction
1418                    "Woods_notation" (str): For a reconstruction, the a and b
1419                        lattice may change to accomodate the symmetry of the
1420                        reconstruction. This notation indicates the change in
1421                        the vectors relative to the primitive (p) or
1422                        conventional (c) slab cell. E.g. p(2x1):
1423
1424                        Wood, E. A. (1964). Vocabulary of surface
1425                        crystallography. Journal of Applied Physics, 35(4),
1426                        1306–1312.
1427
1428                    "transformation_matrix" (numpy array): A 3x3 matrix to
1429                        transform the slab. Only the a and b lattice vectors
1430                        should change while the c vector remains the same.
1431                    "SlabGenerator_parameters" (dict): A dictionary containing
1432                        the parameters for the SlabGenerator class excluding the
1433                        miller_index, min_slab_size and min_vac_size as the
1434                        Miller index is already specified and the min_slab_size
1435                        and min_vac_size can be changed regardless of what type
1436                        of reconstruction is used. Having a consistent set of
1437                        SlabGenerator parameters allows for the instructions to
1438                        be reused to consistently build a reconstructed slab.
1439                    "points_to_remove" (list of coords): A list of sites to
1440                        remove where the first two indices are fraction (in a
1441                        and b) and the third index is in units of 1/d (in c).
1442                    "points_to_add" (list of frac_coords): A list of sites to add
1443                        where the first two indices are fraction (in a an b) and
1444                        the third index is in units of 1/d (in c).
1445
1446                    "base_reconstruction" (dict): Option to base a reconstruction on
1447                        an existing reconstruction model also exists to easily build
1448                        the instructions without repeating previous work. E.g. the
1449                        alpha reconstruction of halites is based on the octopolar
1450                        reconstruction but with the topmost atom removed. The dictionary
1451                        for the alpha reconstruction would therefore contain the item
1452                        "reconstruction_base": "halite_111_octopolar_2x2", and
1453                        additional sites for "points_to_remove" and "points_to_add"
1454                        can be added to modify this reconstruction.
1455
1456                    For "points_to_remove" and "points_to_add", the third index for
1457                        the c vector is in units of 1/d where d is the spacing
1458                        between atoms along hkl (the c vector) and is relative to
1459                        the topmost site in the unreconstructed slab. e.g. a point
1460                        of [0.5, 0.25, 1] corresponds to the 0.5 frac_coord of a,
1461                        0.25 frac_coord of b and a distance of 1 atomic layer above
1462                        the topmost site. [0.5, 0.25, -0.5] where the third index
1463                        corresponds to a point half a atomic layer below the topmost
1464                        site. [0.5, 0.25, 0] corresponds to a point in the same
1465                        position along c as the topmost site. This is done because
1466                        while the primitive units of a and b will remain constant,
1467                        the user can vary the length of the c direction by changing
1468                        the slab layer or the vacuum layer.
1469
1470            NOTE: THE DICTIONARY SHOULD ONLY CONTAIN "points_to_remove" AND
1471            "points_to_add" FOR THE TOP SURFACE. THE ReconstructionGenerator
1472            WILL MODIFY THE BOTTOM SURFACE ACCORDINGLY TO RETURN A SLAB WITH
1473            EQUIVALENT SURFACES.
1474        """
1475
1476        if reconstruction_name not in reconstructions_archive.keys():
1477            raise KeyError(
1478                "The reconstruction_name entered (%s) does not exist in the "
1479                "archive. Please select from one of the following reconstructions: %s "
1480                "or add the appropriate dictionary to the archive file "
1481                "reconstructions_archive.json." % (reconstruction_name, list(reconstructions_archive.keys()))
1482            )
1483
1484        # Get the instructions to build the reconstruction
1485        # from the reconstruction_archive
1486        recon_json = copy.deepcopy(reconstructions_archive[reconstruction_name])
1487        new_points_to_add, new_points_to_remove = [], []
1488        if "base_reconstruction" in recon_json.keys():
1489            if "points_to_add" in recon_json.keys():
1490                new_points_to_add = recon_json["points_to_add"]
1491            if "points_to_remove" in recon_json.keys():
1492                new_points_to_remove = recon_json["points_to_remove"]
1493
1494            # Build new instructions from a base reconstruction
1495            recon_json = copy.deepcopy(reconstructions_archive[recon_json["base_reconstruction"]])
1496            if "points_to_add" in recon_json.keys():
1497                del recon_json["points_to_add"]
1498            if "points_to_remove" in recon_json.keys():
1499                del recon_json["points_to_remove"]
1500            if new_points_to_add:
1501                recon_json["points_to_add"] = new_points_to_add
1502            if new_points_to_remove:
1503                recon_json["points_to_remove"] = new_points_to_remove
1504
1505        slabgen_params = copy.deepcopy(recon_json["SlabGenerator_parameters"])
1506        slabgen_params["initial_structure"] = initial_structure.copy()
1507        slabgen_params["miller_index"] = recon_json["miller_index"]
1508        slabgen_params["min_slab_size"] = min_slab_size
1509        slabgen_params["min_vacuum_size"] = min_vacuum_size
1510
1511        self.slabgen_params = slabgen_params
1512        self.trans_matrix = recon_json["transformation_matrix"]
1513        self.reconstruction_json = recon_json
1514        self.name = reconstruction_name
1515
1516    def build_slabs(self):
1517        """
1518        Builds the reconstructed slab by:
1519            (1) Obtaining the unreconstructed slab using the specified
1520                parameters for the SlabGenerator.
1521            (2) Applying the appropriate lattice transformation in the
1522                a and b lattice vectors.
1523            (3) Remove any specified sites from both surfaces.
1524            (4) Add any specified sites to both surfaces.
1525
1526        Returns:
1527            (Slab): The reconstructed slab.
1528        """
1529
1530        slabs = self.get_unreconstructed_slabs()
1531        recon_slabs = []
1532
1533        for slab in slabs:
1534            d = get_d(slab)
1535            top_site = sorted(slab, key=lambda site: site.frac_coords[2])[-1].coords
1536
1537            # Remove any specified sites
1538            if "points_to_remove" in self.reconstruction_json.keys():
1539                pts_to_rm = copy.deepcopy(self.reconstruction_json["points_to_remove"])
1540                for p in pts_to_rm:
1541                    p[2] = slab.lattice.get_fractional_coords([top_site[0], top_site[1], top_site[2] + p[2] * d])[2]
1542                    cart_point = slab.lattice.get_cartesian_coords(p)
1543                    dist = [site.distance_from_point(cart_point) for site in slab]
1544                    site1 = dist.index(min(dist))
1545                    slab.symmetrically_remove_atoms([site1])
1546
1547            # Add any specified sites
1548            if "points_to_add" in self.reconstruction_json.keys():
1549                pts_to_add = copy.deepcopy(self.reconstruction_json["points_to_add"])
1550                for p in pts_to_add:
1551                    p[2] = slab.lattice.get_fractional_coords([top_site[0], top_site[1], top_site[2] + p[2] * d])[2]
1552                    slab.symmetrically_add_atom(slab[0].specie, p)
1553
1554            slab.reconstruction = self.name
1555            setattr(slab, "recon_trans_matrix", self.trans_matrix)
1556
1557            # Get the oriented_unit_cell with the same axb area.
1558            ouc = slab.oriented_unit_cell.copy()
1559            ouc.make_supercell(self.trans_matrix)
1560            slab.oriented_unit_cell = ouc
1561            recon_slabs.append(slab)
1562
1563        return recon_slabs
1564
1565    def get_unreconstructed_slabs(self):
1566        """
1567        Generates the unreconstructed or pristine super slab.
1568        """
1569        slabs = []
1570        for slab in SlabGenerator(**self.slabgen_params).get_slabs():
1571            slab.make_supercell(self.trans_matrix)
1572            slabs.append(slab)
1573        return slabs
1574
1575
1576def get_d(slab):
1577    """
1578    Determine the distance of space between
1579    each layer of atoms along c
1580    """
1581    sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2])
1582    for i, site in enumerate(sorted_sites):
1583        if not "%.6f" % (site.frac_coords[2]) == "%.6f" % (sorted_sites[i + 1].frac_coords[2]):
1584            d = abs(site.frac_coords[2] - sorted_sites[i + 1].frac_coords[2])
1585            break
1586    return slab.lattice.get_cartesian_coords([0, 0, d])[2]
1587
1588
1589def is_already_analyzed(miller_index: tuple, miller_list: list, symm_ops: list) -> bool:
1590    """
1591    Helper function to check if a given Miller index is
1592    part of the family of indices of any index in a list
1593
1594    Args:
1595        miller_index (tuple): The Miller index to analyze
1596        miller_list (list): List of Miller indices. If the given
1597            Miller index belongs in the same family as any of the
1598            indices in this list, return True, else return False
1599        symm_ops (list): Symmetry operations of a
1600            lattice, used to define family of indices
1601    """
1602    for op in symm_ops:
1603        if in_coord_list(miller_list, op.operate(miller_index)):
1604            return True
1605    return False
1606
1607
1608def get_symmetrically_equivalent_miller_indices(structure, miller_index, return_hkil=True):
1609    """
1610    Returns all symmetrically equivalent indices for a given structure. Analysis
1611    is based on the symmetry of the reciprocal lattice of the structure.
1612
1613    Args:
1614        miller_index (tuple): Designates the family of Miller indices
1615            to find. Can be hkl or hkil for hexagonal systems
1616        return_hkil (bool): If true, return hkil form of Miller
1617            index for hexagonal systems, otherwise return hkl
1618    """
1619
1620    # Change to hkl if hkil because in_coord_list only handles tuples of 3
1621    miller_index = (miller_index[0], miller_index[1], miller_index[3]) if len(miller_index) == 4 else miller_index
1622    mmi = max(np.abs(miller_index))
1623    r = list(range(-mmi, mmi + 1))
1624    r.reverse()
1625
1626    sg = SpacegroupAnalyzer(structure)
1627    # Get distinct hkl planes from the rhombohedral setting if trigonal
1628    if sg.get_crystal_system() == "trigonal":
1629        prim_structure = SpacegroupAnalyzer(structure).get_primitive_standard_structure()
1630        symm_ops = prim_structure.lattice.get_recp_symmetry_operation()
1631    else:
1632        symm_ops = structure.lattice.get_recp_symmetry_operation()
1633
1634    equivalent_millers = [miller_index]
1635    for miller in itertools.product(r, r, r):
1636        if miller == miller_index:
1637            continue
1638        if any(i != 0 for i in miller):
1639            if is_already_analyzed(miller, equivalent_millers, symm_ops):
1640                equivalent_millers.append(miller)
1641
1642            # include larger Miller indices in the family of planes
1643            if all(mmi > i for i in np.abs(miller)) and not in_coord_list(equivalent_millers, miller):
1644                if is_already_analyzed(mmi * np.array(miller), equivalent_millers, symm_ops):
1645                    equivalent_millers.append(miller)
1646
1647    if return_hkil and sg.get_crystal_system() in ["trigonal", "hexagonal"]:
1648        return [(hkl[0], hkl[1], -1 * hkl[0] - hkl[1], hkl[2]) for hkl in equivalent_millers]
1649    return equivalent_millers
1650
1651
1652def get_symmetrically_distinct_miller_indices(structure, max_index, return_hkil=False):
1653    """
1654    Returns all symmetrically distinct indices below a certain max-index for
1655    a given structure. Analysis is based on the symmetry of the reciprocal
1656    lattice of the structure.
1657    Args:
1658        structure (Structure): input structure.
1659        max_index (int): The maximum index. For example, a max_index of 1
1660            means that (100), (110), and (111) are returned for the cubic
1661            structure. All other indices are equivalent to one of these.
1662        return_hkil (bool): If true, return hkil form of Miller
1663            index for hexagonal systems, otherwise return hkl
1664    """
1665
1666    r = list(range(-max_index, max_index + 1))
1667    r.reverse()
1668
1669    # First we get a list of all hkls for conventional (including equivalent)
1670    conv_hkl_list = [miller for miller in itertools.product(r, r, r) if any(i != 0 for i in miller)]
1671
1672    sg = SpacegroupAnalyzer(structure)
1673    # Get distinct hkl planes from the rhombohedral setting if trigonal
1674    if sg.get_crystal_system() == "trigonal":
1675        transf = sg.get_conventional_to_primitive_transformation_matrix()
1676        miller_list = [hkl_transformation(transf, hkl) for hkl in conv_hkl_list]
1677        prim_structure = SpacegroupAnalyzer(structure).get_primitive_standard_structure()
1678        symm_ops = prim_structure.lattice.get_recp_symmetry_operation()
1679    else:
1680        miller_list = conv_hkl_list
1681        symm_ops = structure.lattice.get_recp_symmetry_operation()
1682
1683    unique_millers, unique_millers_conv = [], []
1684
1685    for i, miller in enumerate(miller_list):
1686        d = abs(reduce(gcd, miller))
1687        miller = tuple(int(i / d) for i in miller)
1688        if not is_already_analyzed(miller, unique_millers, symm_ops):
1689            if sg.get_crystal_system() == "trigonal":
1690                # Now we find the distinct primitive hkls using
1691                # the primitive symmetry operations and their
1692                # corresponding hkls in the conventional setting
1693                unique_millers.append(miller)
1694                d = abs(reduce(gcd, conv_hkl_list[i]))
1695                cmiller = tuple(int(i / d) for i in conv_hkl_list[i])
1696                unique_millers_conv.append(cmiller)
1697            else:
1698                unique_millers.append(miller)
1699                unique_millers_conv.append(miller)
1700
1701    if return_hkil and sg.get_crystal_system() in ["trigonal", "hexagonal"]:
1702        return [(hkl[0], hkl[1], -1 * hkl[0] - hkl[1], hkl[2]) for hkl in unique_millers_conv]
1703    return unique_millers_conv
1704
1705
1706def hkl_transformation(transf, miller_index):
1707    """
1708    Returns the Miller index from setting
1709    A to B using a transformation matrix
1710    Args:
1711        transf (3x3 array): The transformation matrix
1712            that transforms a lattice of A to B
1713        miller_index ([h, k, l]): Miller index to transform to setting B
1714    """
1715    # Get a matrix of whole numbers (ints)
1716
1717    def lcm(a, b):
1718        return a * b // math.gcd(a, b)
1719
1720    reduced_transf = reduce(lcm, [int(1 / i) for i in itertools.chain(*transf) if i != 0]) * transf
1721    reduced_transf = reduced_transf.astype(int)
1722
1723    # perform the transformation
1724    t_hkl = np.dot(reduced_transf, miller_index)
1725    d = abs(reduce(gcd, t_hkl))
1726    t_hkl = np.array([int(i / d) for i in t_hkl])
1727
1728    # get mostly positive oriented Miller index
1729    if len([i for i in t_hkl if i < 0]) > 1:
1730        t_hkl *= -1
1731
1732    return tuple(t_hkl)
1733
1734
1735def generate_all_slabs(
1736    structure,
1737    max_index,
1738    min_slab_size,
1739    min_vacuum_size,
1740    bonds=None,
1741    tol=0.1,
1742    ftol=0.1,
1743    max_broken_bonds=0,
1744    lll_reduce=False,
1745    center_slab=False,
1746    primitive=True,
1747    max_normal_search=None,
1748    symmetrize=False,
1749    repair=False,
1750    include_reconstructions=False,
1751    in_unit_planes=False,
1752):
1753    """
1754    A function that finds all different slabs up to a certain miller index.
1755    Slabs oriented under certain Miller indices that are equivalent to other
1756    slabs in other Miller indices are filtered out using symmetry operations
1757    to get rid of any repetitive slabs. For example, under symmetry operations,
1758    CsCl has equivalent slabs in the (0,0,1), (0,1,0), and (1,0,0) direction.
1759
1760    Args:
1761        structure (Structure): Initial input structure. Note that to
1762                ensure that the miller indices correspond to usual
1763                crystallographic definitions, you should supply a conventional
1764                unit cell structure.
1765        max_index (int): The maximum Miller index to go up to.
1766        min_slab_size (float): In Angstroms
1767        min_vacuum_size (float): In Angstroms
1768        bonds ({(specie1, specie2): max_bond_dist}: bonds are
1769            specified as a dict of tuples: float of specie1, specie2
1770            and the max bonding distance. For example, PO4 groups may be
1771            defined as {("P", "O"): 3}.
1772        tol (float): Threshold parameter in fcluster in order to check
1773            if two atoms are lying on the same plane. Default thresh set
1774            to 0.1 Angstrom in the direction of the surface normal.
1775        max_broken_bonds (int): Maximum number of allowable broken bonds
1776            for the slab. Use this to limit # of slabs (some structures
1777            may have a lot of slabs). Defaults to zero, which means no
1778            defined bonds must be broken.
1779        lll_reduce (bool): Whether to perform an LLL reduction on the
1780            eventual structure.
1781        center_slab (bool): Whether to center the slab in the cell with
1782            equal vacuum spacing from the top and bottom.
1783        primitive (bool): Whether to reduce any generated slabs to a
1784            primitive cell (this does **not** mean the slab is generated
1785            from a primitive cell, it simply means that after slab
1786            generation, we attempt to find shorter lattice vectors,
1787            which lead to less surface area and smaller cells).
1788        max_normal_search (int): If set to a positive integer, the code will
1789            conduct a search for a normal lattice vector that is as
1790            perpendicular to the surface as possible by considering
1791            multiples linear combinations of lattice vectors up to
1792            max_normal_search. This has no bearing on surface energies,
1793            but may be useful as a preliminary step to generating slabs
1794            for absorption and other sizes. It is typical that this will
1795            not be the smallest possible cell for simulation. Normality
1796            is not guaranteed, but the oriented cell will have the c
1797            vector as normal as possible (within the search range) to the
1798            surface. A value of up to the max absolute Miller index is
1799            usually sufficient.
1800        symmetrize (bool): Whether or not to ensure the surfaces of the
1801            slabs are equivalent.
1802        repair (bool): Whether to repair terminations with broken bonds
1803            or just omit them
1804        include_reconstructions (bool): Whether to include reconstructed
1805            slabs available in the reconstructions_archive.json file.
1806    """
1807    all_slabs = []
1808
1809    for miller in get_symmetrically_distinct_miller_indices(structure, max_index):
1810        gen = SlabGenerator(
1811            structure,
1812            miller,
1813            min_slab_size,
1814            min_vacuum_size,
1815            lll_reduce=lll_reduce,
1816            center_slab=center_slab,
1817            primitive=primitive,
1818            max_normal_search=max_normal_search,
1819            in_unit_planes=in_unit_planes,
1820        )
1821        slabs = gen.get_slabs(
1822            bonds=bonds,
1823            tol=tol,
1824            ftol=ftol,
1825            symmetrize=symmetrize,
1826            max_broken_bonds=max_broken_bonds,
1827            repair=repair,
1828        )
1829
1830        if len(slabs) > 0:
1831            logger.debug("%s has %d slabs... " % (miller, len(slabs)))
1832            all_slabs.extend(slabs)
1833
1834    if include_reconstructions:
1835        sg = SpacegroupAnalyzer(structure)
1836        symbol = sg.get_space_group_symbol()
1837        # enumerate through all posisble reconstructions in the
1838        # archive available for this particular structure (spacegroup)
1839        for name, instructions in reconstructions_archive.items():
1840            if "base_reconstruction" in instructions.keys():
1841                instructions = reconstructions_archive[instructions["base_reconstruction"]]
1842            if instructions["spacegroup"]["symbol"] == symbol:
1843                # check if this reconstruction has a max index
1844                # equal or less than the given max index
1845                if max(instructions["miller_index"]) > max_index:
1846                    continue
1847                recon = ReconstructionGenerator(structure, min_slab_size, min_vacuum_size, name)
1848                all_slabs.extend(recon.build_slabs())
1849
1850    return all_slabs
1851
1852
1853def get_slab_regions(slab, blength=3.5):
1854    """
1855    Function to get the ranges of the slab regions. Useful for discerning where
1856    the slab ends and vacuum begins if the slab is not fully within the cell
1857    Args:
1858        slab (Structure): Structure object modelling the surface
1859        blength (float, Ang): The bondlength between atoms. You generally
1860            want this value to be larger than the actual bondlengths in
1861            order to find atoms that are part of the slab
1862    """
1863
1864    fcoords, indices, all_indices = [], [], []
1865    for site in slab:
1866        # find sites with c < 0 (noncontiguous)
1867        neighbors = slab.get_neighbors(site, blength, include_index=True, include_image=True)
1868        for nn in neighbors:
1869            if nn[0].frac_coords[2] < 0:
1870                # sites are noncontiguous within cell
1871                fcoords.append(nn[0].frac_coords[2])
1872                indices.append(nn[-2])
1873                if nn[-2] not in all_indices:
1874                    all_indices.append(nn[-2])
1875
1876    if fcoords:
1877        # If slab is noncontiguous, locate the lowest
1878        # site within the upper region of the slab
1879        while fcoords:
1880            last_fcoords = copy.copy(fcoords)
1881            last_indices = copy.copy(indices)
1882            site = slab[indices[fcoords.index(min(fcoords))]]
1883            neighbors = slab.get_neighbors(site, blength, include_index=True, include_image=True)
1884            fcoords, indices = [], []
1885            for nn in neighbors:
1886                if 1 > nn[0].frac_coords[2] > 0 and nn[0].frac_coords[2] < site.frac_coords[2]:
1887                    # sites are noncontiguous within cell
1888                    fcoords.append(nn[0].frac_coords[2])
1889                    indices.append(nn[-2])
1890                    if nn[-2] not in all_indices:
1891                        all_indices.append(nn[-2])
1892
1893        # Now locate the highest site within the lower region of the slab
1894        upper_fcoords = []
1895        for site in slab:
1896            if all(nn.index not in all_indices for nn in slab.get_neighbors(site, blength)):
1897                upper_fcoords.append(site.frac_coords[2])
1898        coords = copy.copy(last_fcoords) if not fcoords else copy.copy(fcoords)
1899        min_top = slab[last_indices[coords.index(min(coords))]].frac_coords[2]
1900        ranges = [[0, max(upper_fcoords)], [min_top, 1]]
1901    else:
1902        # If the entire slab region is within the slab cell, just
1903        # set the range as the highest and lowest site in the slab
1904        sorted_sites = sorted(slab, key=lambda site: site.frac_coords[2])
1905        ranges = [[sorted_sites[0].frac_coords[2], sorted_sites[-1].frac_coords[2]]]
1906
1907    return ranges
1908
1909
1910def miller_index_from_sites(lattice, coords, coords_are_cartesian=True, round_dp=4, verbose=True):
1911    """
1912    Get the Miller index of a plane from a list of site coordinates.
1913
1914    A minimum of 3 sets of coordinates are required. If more than 3 sets of
1915    coordinates are given, the best plane that minimises the distance to all
1916    points will be calculated.
1917
1918    Args:
1919        lattice (list or Lattice): A 3x3 lattice matrix or `Lattice` object (for
1920            example obtained from Structure.lattice).
1921        coords (iterable): A list or numpy array of coordinates. Can be
1922            cartesian or fractional coordinates. If more than three sets of
1923            coordinates are provided, the best plane that minimises the
1924            distance to all sites will be calculated.
1925        coords_are_cartesian (bool, optional): Whether the coordinates are
1926            in cartesian space. If using fractional coordinates set to False.
1927        round_dp (int, optional): The number of decimal places to round the
1928            miller index to.
1929        verbose (bool, optional): Whether to print warnings.
1930
1931    Returns:
1932        (tuple): The Miller index.
1933    """
1934    if not isinstance(lattice, Lattice):
1935        lattice = Lattice(lattice)
1936
1937    return lattice.get_miller_index_from_coords(
1938        coords,
1939        coords_are_cartesian=coords_are_cartesian,
1940        round_dp=round_dp,
1941        verbose=verbose,
1942    )
1943
1944
1945def center_slab(slab):
1946    """
1947    The goal here is to ensure the center of the slab region
1948        is centered close to c=0.5. This makes it easier to
1949        find the surface sites and apply operations like doping.
1950
1951    There are three cases where the slab in not centered:
1952
1953    1. The slab region is completely between two vacuums in the
1954    box but not necessarily centered. We simply shift the
1955    slab by the difference in its center of mass and 0.5
1956    along the c direction.
1957
1958    2. The slab completely spills outside the box from the bottom
1959    and into the top. This makes it incredibly difficult to
1960    locate surface sites. We iterate through all sites that
1961    spill over (z>c) and shift all sites such that this specific
1962    site is now on the other side. Repeat for all sites with z>c.
1963
1964    3. This is a simpler case of scenario 2. Either the top or bottom
1965    slab sites are at c=0 or c=1. Treat as scenario 2.
1966
1967    Args:
1968        slab (Slab): Slab structure to center
1969    Returns:
1970        Returns a centered slab structure
1971    """
1972
1973    # get a reasonable r cutoff to sample neighbors
1974    bdists = sorted([nn[1] for nn in slab.get_neighbors(slab[0], 10) if nn[1] > 0])
1975    r = bdists[0] * 3
1976
1977    all_indices = [i for i, site in enumerate(slab)]
1978
1979    # check if structure is case 2 or 3, shift all the
1980    # sites up to the other side until it is case 1
1981    for site in slab:
1982        if any(nn[1] > slab.lattice.c for nn in slab.get_neighbors(site, r)):
1983            shift = 1 - site.frac_coords[2] + 0.05
1984            slab.translate_sites(all_indices, [0, 0, shift])
1985
1986    # now the slab is case 1, shift the center of mass of the slab to 0.5
1987    weights = [s.species.weight for s in slab]
1988    center_of_mass = np.average(slab.frac_coords, weights=weights, axis=0)
1989    shift = 0.5 - center_of_mass[2]
1990    slab.translate_sites(all_indices, [0, 0, shift])
1991
1992    return slab
1993
1994
1995def _reduce_vector(vector):
1996    # small function to reduce vectors
1997
1998    d = abs(reduce(gcd, vector))
1999    vector = tuple(int(i / d) for i in vector)
2000
2001    return vector
2002