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