def setUp(self): zno1 = Structure.from_file(get_path("ZnO-wz.cif"), primitive=False) zno55 = SlabGenerator(zno1, [1, 0, 0], 5, 5, lll_reduce=False, center_slab=False).get_slab() Ti = Structure( Lattice.hexagonal(4.6, 2.82), ["Ti", "Ti", "Ti"], [ [0.000000, 0.000000, 0.000000], [0.333333, 0.666667, 0.500000], [0.666667, 0.333333, 0.500000], ], ) Ag_fcc = Structure( Lattice.cubic(4.06), ["Ag", "Ag", "Ag", "Ag"], [ [0.000000, 0.000000, 0.000000], [0.000000, 0.500000, 0.500000], [0.500000, 0.000000, 0.500000], [0.500000, 0.500000, 0.000000], ], ) m = [[3.913449, 0, 0], [0, 3.913449, 0], [0, 0, 5.842644]] latt = Lattice(m) fcoords = [[0.5, 0, 0.222518], [0, 0.5, 0.777482], [0, 0, 0], [0, 0, 0.5], [0.5, 0.5, 0]] non_laue = Structure(latt, ["Nb", "Nb", "N", "N", "N"], fcoords) self.ti = Ti self.agfcc = Ag_fcc self.zno1 = zno1 self.zno55 = zno55 self.nonlaue = non_laue self.h = Structure(Lattice.cubic(3), ["H"], [[0, 0, 0]]) self.libcc = Structure(Lattice.cubic(3.51004), ["Li", "Li"], [[0, 0, 0], [0.5, 0.5, 0.5]])
def test_bonds_broken(self): # Querying the Materials Project database for Si s = self.get_structure("Si") # Conventional unit cell is supplied to ensure miller indices # correspond to usual crystallographic definitions conv_bulk = SpacegroupAnalyzer(s).get_conventional_standard_structure() slabgen = SlabGenerator(conv_bulk, [1, 1, 1], 10, 10, center_slab=True) # Setting a generous estimate for max_broken_bonds # so that all terminations are generated. These slabs # are ordered by ascending number of bonds broken # which is assigned to Slab.energy slabs = slabgen.get_slabs(bonds={("Si", "Si"): 2.40}, max_broken_bonds=30) # Looking at the two slabs generated in VESTA, we # expect 2 and 6 bonds broken so we check for this. # Number of broken bonds are floats due to primitive # flag check and subsequent transformation of slabs. self.assertTrue(slabs[0].energy, 2.0) self.assertTrue(slabs[1].energy, 6.0)
def test_get_slab_regions(self): # If a slab layer in the slab cell is not completely inside # the cell (noncontiguous), check that get_slab_regions will # be able to identify where the slab layers are located s = self.get_structure("LiFePO4") slabgen = SlabGenerator(s, (0, 0, 1), 15, 15) slab = slabgen.get_slabs()[0] slab.translate_sites([i for i, site in enumerate(slab)], [0, 0, -0.25]) bottom_c, top_c = [], [] for site in slab: if site.frac_coords[2] < 0.5: bottom_c.append(site.frac_coords[2]) else: top_c.append(site.frac_coords[2]) ranges = get_slab_regions(slab) self.assertEqual(tuple(ranges[0]), (0, max(bottom_c))) self.assertEqual(tuple(ranges[1]), (min(top_c), 1))
def make_slabs_from_bulk_atoms(atoms, miller_indices, slab_generator_settings, get_slab_settings): ''' Use pymatgen to enumerate the slabs from a bulk. Args: atoms The `ase.Atoms` object of the bulk that you want to make slabs out of miller_indices A 3-tuple of integers containing the three Miller indices of the slab[s] you want to make. slab_generator_settings A dictionary containing the settings to be passed to pymatgen's `SpaceGroupAnalyzer` class. get_slab_settings A dictionary containing the settings to be ppassed to the `get_slab` method of pymatgen's `SpaceGroupAnalyzer` class. Returns: slabs A list of the slabs in the form of pymatgen.Structure objects. Note that there may be multiple slabs because of different shifts/terminations. ''' # Get rid of the `miller_index` argument, which is superceded by the # `miller_indices` argument. try: slab_generator_settings = unfreeze_dict(slab_generator_settings) slab_generator_settings.pop('miller_index') warnings.warn( 'You passed a `miller_index` object into the ' '`slab_generator_settings` argument for the ' '`make_slabs_from_bulk_atoms` function. By design, ' 'this function will instead use the explicit ' 'argument, `miller_indices`.', SyntaxWarning) except KeyError: pass struct = AseAtomsAdaptor.get_structure(atoms) sga = SpacegroupAnalyzer(struct, symprec=0.1) struct_stdrd = sga.get_conventional_standard_structure() slab_gen = SlabGenerator(initial_structure=struct_stdrd, miller_index=miller_indices, **slab_generator_settings) slabs = slab_gen.get_slabs(**get_slab_settings) return slabs
def findHeight(unit, facet, layers, sym): """ Finds the slab thickness input necessary for SlabGenerator to produce slab with correct number of layers """ x0, dx, n = 1, 1, None # Angstrom getN = lambda x: len( SlabGenerator(unit, facet, x, 4, primitive=True).get_slabs(symmetrize= sym)[0]) while n != layers: x0 += dx n = getN(x0) if n > 20: raise ValueError, ( 'over ten atoms in primitive surface column' + ' ... primitive cell may contain multiple atoms (' + str(getN(0.1)) + ') not compatible with requested # of layers (' + str(layers) + ')') return x0
def findHeight(self): """ Finds the slab thickness input necessary for SlabGenerator to produce slab with correct number of layers """ from pymatgen.core.surface import SlabGenerator x0, dx, n = 1, 1, None # Angstrom getN = lambda x: len( SlabGenerator(self.bulkUnit(), self.facet, x, 4, primitive=True). get_slabs(symmetrize=self.symmetric)[0]) while n != self.scale[2]: #print x0,n x0 += dx n = getN(x0) if n > 20: raise ValueError, 'over ten atoms in primitive surface column ... primitive cell may contain multiple atoms (' + str( getN(0.1) ) + ') not compatible with requested # of layers (' + str( self.scale[2]) + ')' return x0
def from_zsl( cls, match: ZSLMatch, film: Structure, film_miller, substrate_miller, elasticity_tensor=None, ground_state_energy=0, ): """Generate a substrate match from a ZSL match plus metadata""" # Get the appropriate surface structure struc = SlabGenerator(film, film_miller, 20, 15, primitive=False).get_slab().oriented_unit_cell dfm = Deformation(match.match_transformation) strain = dfm.green_lagrange_strain.convert_to_ieee(struc, initial_fit=False) von_mises_strain = strain.von_mises_strain if elasticity_tensor is not None: energy_density = elasticity_tensor.energy_density(strain) elastic_energy = film.volume * energy_density / len(film.sites) else: elastic_energy = 0 match_dict = match.as_dict() cls( film_miller=film_miller, substrate_miller=substrate_miller, strain=strain, von_mises_strain=von_mises_strain, elastic_energy=elastic_energy, ground_state_energy=ground_state_energy, **{ k: match_dict[k] for k in match_dict.keys() if not k.startswith("@") }, )
def test_init(self): zno_slab = Slab( self.zno55.lattice, self.zno55.species, self.zno55.frac_coords, self.zno55.miller_index, self.zno55.oriented_unit_cell, 0, self.zno55.scale_factor, ) m = self.zno55.lattice.matrix area = np.linalg.norm(np.cross(m[0], m[1])) self.assertAlmostEqual(zno_slab.surface_area, area) self.assertEqual(zno_slab.lattice.parameters, self.zno55.lattice.parameters) self.assertEqual(zno_slab.oriented_unit_cell.composition, self.zno1.composition) self.assertEqual(len(zno_slab), 8) # check reorient_lattice. get a slab not oriented and check that orientation # works even with cartesian coordinates. zno_not_or = SlabGenerator( self.zno1, [1, 0, 0], 5, 5, lll_reduce=False, center_slab=False, reorient_lattice=False, ).get_slab() zno_slab_cart = Slab( zno_not_or.lattice, zno_not_or.species, zno_not_or.cart_coords, zno_not_or.miller_index, zno_not_or.oriented_unit_cell, 0, zno_not_or.scale_factor, coords_are_cartesian=True, reorient_lattice=True, ) self.assertArrayAlmostEqual(zno_slab.frac_coords, zno_slab_cart.frac_coords) c = zno_slab_cart.lattice.matrix[2] self.assertArrayAlmostEqual([0, 0, np.linalg.norm(c)], c)
def test_get_tasker2_slabs(self): # The uneven distribution of ions on the (111) facets of Halite # type slabs are typical examples of Tasker 3 structures. We # will test this algo to generate a Tasker 2 structure instead slabgen = SlabGenerator(self.MgO, (1, 1, 1), 10, 10, max_normal_search=1) # We generate the Tasker 3 structure first slab = slabgen.get_slabs()[0] self.assertFalse(slab.is_symmetric()) self.assertTrue(slab.is_polar()) # Now to generate the Tasker 2 structure, we must # ensure there are enough ions on top to move around slab.make_supercell([2, 1, 1]) slabs = slab.get_tasker2_slabs() # Check if our Tasker 2 slab is nonpolar and symmetric for slab in slabs: self.assertTrue(slab.is_symmetric()) self.assertFalse(slab.is_polar())
def test_nonstoichiometric_symmetrized_slab(self): # For the (111) halite slab, sometimes a nonstoichiometric # system is preferred over the stoichiometric Tasker 2. slabgen = SlabGenerator(self.MgO, (1, 1, 1), 10, 10, max_normal_search=1) slabs = slabgen.get_slabs(symmetrize=True) # We should end up with two terminations, one with # an Mg rich surface and another O rich surface self.assertEqual(len(slabs), 2) for slab in slabs: self.assertTrue(slab.is_symmetric()) # For a low symmetry primitive_elemental system such as # R-3m, there should be some nonsymmetric slabs # without using nonstoichiometric_symmetrized_slab slabs = generate_all_slabs(self.Dy, 1, 30, 30, center_slab=True, symmetrize=True) for s in slabs: self.assertTrue(s.is_symmetric()) self.assertGreater(len(s), len(self.Dy))
def test_move_to_other_side(self): # Tests to see if sites are added to opposite side s = self.get_structure("LiFePO4") slabgen = SlabGenerator(s, (0, 0, 1), 10, 10, center_slab=True) slab = slabgen.get_slab() surface_sites = slab.get_surface_sites() # check if top sites are moved to the bottom top_index = [ss[1] for ss in surface_sites["top"]] slab = slabgen.move_to_other_side(slab, top_index) all_bottom = [slab[i].frac_coords[2] < slab.center_of_mass[2] for i in top_index] self.assertTrue(all(all_bottom)) # check if bottom sites are moved to the top bottom_index = [ss[1] for ss in surface_sites["bottom"]] slab = slabgen.move_to_other_side(slab, bottom_index) all_top = [slab[i].frac_coords[2] > slab.center_of_mass[2] for i in bottom_index] self.assertTrue(all(all_top))
def calculate_unit_slab_height(atoms, miller_indices, slab_generator_settings=None): ''' Calculates the height of the smallest unit slab from a given bulk and Miller cut Args: atoms An `ase.Atoms` object of the bulk you want to make a surface out of miller_indices A 3-tuple of integers representing the Miller indices of the surface you want to make slab_generator_settings A dictionary that can be passed as kwargs to instantiate the `pymatgen.core.surface.SlabGenerator` class. Defaults to the settings in `gaspy.defaults.slab_settings`. Returns: height A float corresponding the height (in Angstroms) of the smallest unit slab ''' if slab_generator_settings is None: slab_generator_settings = slab_settings()['slab_generator_settings'] # We don't care about these things del slab_generator_settings['min_vacuum_size'] del slab_generator_settings['min_slab_size'] # Instantiate a pymatgen `SlabGenerator` structure = AseAtomsAdaptor.get_structure(atoms) sga = SpacegroupAnalyzer(structure, symprec=0.1) structure = sga.get_conventional_standard_structure() gen = SlabGenerator(initial_structure=structure, miller_index=miller_indices, min_vacuum_size=0., min_slab_size=0., **slab_generator_settings) # Get and return the height height = gen._proj_height return height
def makeSlab(bulkASE, facList, lay, sym, xy, vac): a = asedb.get_atoms(id=bulkASE) pmg_a = AseAtomsAdaptor.get_structure(a) unit = SpacegroupAnalyzer( pmg_a, symprec=0.1, angle_tolerance=5).get_conventional_standard_structure() gen = SlabGenerator(unit, facList, findHeight(unit, facList, lay, sym), vac, center_slab=sym, primitive=True, lll_reduce=True) slabs = gen.get_slabs() if len(slabs) > 1: print "Warning, multiple slabs generated..." slab = slabs[0] slab.make_supercell([str(xy)[0], str(xy)[1], 1]) return reorient_z(slab)
def create_slab(self, vacuum=12, thickness=10): """ set the vacuum spacing, slab thickness and call sd_flags for top 2 layers returns the poscar corresponding to the modified structure """ strt_structure = self.input_structure.copy() if self.from_ase: slab_struct = get_ase_slab(strt_structure, hkl=self.system['hkl'], min_thick=thickness, min_vac=vacuum) else: slab_struct = SlabGenerator(initial_structure=strt_structure, miller_index=self.system['hkl'], min_slab_size=thickness, min_vacuum_size=vacuum, lll_reduce=False, center_slab=True, primitive=False).get_slab() slab_struct.sort() sd = self.set_sd_flags(slab_struct) comment = 'VAC' + str(vacuum) + 'THICK' + str(thickness) return Poscar(slab_struct, comment=comment, selective_dynamics=sd)
def make_super_cell_pymatgen(jdata): make_unit_cell(jdata) out_dir = jdata['out_dir'] path_uc = os.path.join(out_dir, global_dirname_02) from_path = path_uc from_file = os.path.join(from_path, 'POSCAR.unit') ss = Structure.from_file(from_file) all_millers = jdata['millers'] path_sc = os.path.join(out_dir, global_dirname_02) z_min = jdata['z_min'] super_cell = jdata['super_cell'] cwd = os.getcwd() path_work = (path_sc) path_work = os.path.abspath(path_work) pcpy_cmd = os.path.join(cwd, 'tools') pcpy_cmd = os.path.join(pcpy_cmd, 'poscar_copy.py') os.chdir(path_work) for miller in all_millers: miller_str = "" for ii in miller: miller_str += str(ii) path_cur_surf = create_path('surf-' + miller_str) os.chdir(path_cur_surf) slabgen = SlabGenerator(ss, miller, z_min, 1e-3) all_slabs = slabgen.get_slabs() print("Miller %s: The slab has %s termination, use the first one" % (str(miller), len(all_slabs))) all_slabs[0].to('POSCAR', 'POSCAR') if super_cell[0] > 1 or super_cell[1] > 1: sp.check_call(pcpy_cmd + ' -n %d %d %d POSCAR POSCAR ' % (super_cell[0], super_cell[1], 1), shell=True) os.chdir(path_work) os.chdir(cwd)
def setUp(self): zno1 = Structure.from_file(get_path("ZnO-wz.cif"), primitive=False) zno55 = SlabGenerator(zno1, [1, 0, 0], 5, 5, lll_reduce=False, center_slab=False).get_slab() Ti = Structure(Lattice.hexagonal(4.6, 2.82), ["Ti", "Ti", "Ti"], [[0.000000, 0.000000, 0.000000], [0.333333, 0.666667, 0.500000], [0.666667, 0.333333, 0.500000]]) Ag_fcc = Structure(Lattice.cubic(4.06), ["Ag", "Ag", "Ag", "Ag"], [[0.000000, 0.000000, 0.000000], [0.000000, 0.500000, 0.500000], [0.500000, 0.000000, 0.500000], [0.500000, 0.500000, 0.000000]]) self.ti = Ti self.agfcc = Ag_fcc self.zno1 = zno1 self.zno55 = zno55 self.h = Structure(Lattice.cubic(3), ["H"], [[0, 0, 0]]) self.libcc = Structure(Lattice.cubic(3.51004), ["Li", "Li"], [[0, 0, 0], [0.5, 0.5, 0.5]])
def make_super_cell_pymatgen(jdata): make_unit_cell(jdata) out_dir = jdata['out_dir'] path_uc = os.path.join(out_dir, global_dirname_02) from_path = path_uc from_file = os.path.join(from_path, 'POSCAR.unit') ss = Structure.from_file(from_file) all_millers = jdata['millers'] path_sc = os.path.join(out_dir, global_dirname_02) z_min = jdata['z_min'] super_cell = jdata['super_cell'] cwd = os.getcwd() path_work = (path_sc) path_work = os.path.abspath(path_work) os.chdir(path_work) for miller in all_millers: miller_str = "" for ii in miller: miller_str += str(ii) path_cur_surf = create_path('surf-' + miller_str) os.chdir(path_cur_surf) slabgen = SlabGenerator(ss, miller, z_min, 1e-3) all_slabs = slabgen.get_slabs() dlog.info(os.getcwd()) dlog.info("Miller %s: The slab has %s termination, use the first one" % (str(miller), len(all_slabs))) all_slabs[0].to('POSCAR', 'POSCAR') if super_cell[0] > 1 or super_cell[1] > 1: st = Structure.from_file('POSCAR') st.make_supercell([super_cell[0], super_cell[1], 1]) st.to('POSCAR', 'POSCAR') os.chdir(path_work) os.chdir(cwd)
def get_slabs(self, atoms=None, index=None): """ """ from math import ceil if not atoms: atoms = self.atoms struct = AseAtomsAdaptor.get_structure(self.atoms) struct = SpacegroupAnalyzer( struct).get_conventional_standard_structure() slabgen = SlabGenerator(struct, miller_index=index, min_slab_size=self.min_slab_size, min_vacuum_size=self.min_vacuum_size, center_slab=False) slabs = {} for n, slab in enumerate(slabgen.get_slabs()): slab = AseAtomsAdaptor.get_atoms(slab) self.set_ase_cell(slab) a, b, c, alpha, beta, gamma = slab.get_cell_lengths_and_angles() supercell = [ceil(5 / a), ceil(5 / b), ceil(5 / c)] # print(a, b, c, supercell) slab = slab * supercell slabs[n] = slab.copy() return slabs
def __init__(self, strt, hkl=[1, 1, 1], min_thick=10, min_vac=10, supercell=[1, 1, 1], name=None, adsorb_on_species=None, adatom_on_lig=None, ligand=None, displacement=1.0, surface_coverage=None, scell_nmax=10, coverage_tol=0.25, solvent=None, start_from_slab=False, validate_proximity=False, to_unit_cell=False, coords_are_cartesian=False, primitive=True, from_ase=False, lll_reduce=False, center_slab=True, max_normal_search=None, force_normalize=False, x_shift=0, y_shift=0, rot=[0, 0, 0]): self.from_ase = from_ase vac_extension = 0 if ligand is not None: vac_extension = ligand.max_dist if isinstance(strt, Structure) and not isinstance(strt, Slab): self.min_vac = min_vac + vac_extension if self.from_ase: strt = get_ase_slab(strt, hkl=hkl, min_thick=min_thick, min_vac=min_vac + vac_extension) else: slab = SlabGenerator(strt, hkl, min_thick, min_vac + vac_extension, center_slab=center_slab, lll_reduce=lll_reduce, max_normal_search=max_normal_search, primitive=primitive).get_slab() if force_normalize: strt = slab.get_orthogonal_c_slab() else: strt = slab strt.make_supercell(supercell) else: self.min_vac = min_vac Slab.__init__(self, strt.lattice, strt.species_and_occu, strt.frac_coords, miller_index=strt.miller_index, oriented_unit_cell=strt.oriented_unit_cell, shift=strt.shift, scale_factor=strt.scale_factor, validate_proximity=validate_proximity, to_unit_cell=to_unit_cell, coords_are_cartesian=coords_are_cartesian, site_properties=strt.site_properties, energy=strt.energy) self.strt = strt self.name = name self.hkl = hkl self.min_thick = min_thick self.supercell = supercell self.ligand = ligand self.slab = strt self.displacement = displacement self.solvent = solvent self.surface_coverage = surface_coverage self.adsorb_on_species = adsorb_on_species self.adatom_on_lig = adatom_on_lig self.scell_nmax = scell_nmax self.coverage_tol = coverage_tol self.x_shift = x_shift self.y_shift = y_shift self.rot = rot
ax.set_xlim(x_lim) ax.set_ylim(y_lim) return ax ff = Structure(Lattice.from_parameters(2.45, 2.45, 2.45, 60, 60, 60), ['Cu'], [[0, 0, 0]]) CO = Molecule(["C", "O"], [[0, 0, 0], [0, 0, 1.5]]) H = Molecule(["H"], [[0, 0, 0]]) placement = [(CO, 'ontop', 0), (CO, 'hollow', 2), (H, 'hollow', 2)] unit = SpacegroupAnalyzer(ff).get_conventional_standard_structure() surfU = SlabGenerator(unit, [1, 1, 1], 6, 10) slab = (surfU.get_slabs()[0]) slab = reorient_z(slab) slab.make_supercell([2, 2, 1]) for p in placement: osites = len( AdsorbateSiteFinder(slab).find_adsorption_sites( symm_reduce=0)['ontop']) hsites = len( AdsorbateSiteFinder(slab).find_adsorption_sites( symm_reduce=0)['hollow']) bsites = len( AdsorbateSiteFinder(slab).find_adsorption_sites(
def test_get_slabs(self): gen = SlabGenerator(self.get_structure("CsCl"), [0, 0, 1], 10, 10) #Test orthogonality of some internal variables. a, b, c = gen.oriented_unit_cell.lattice.matrix self.assertAlmostEqual(np.dot(a, gen._normal), 0) self.assertAlmostEqual(np.dot(b, gen._normal), 0) self.assertEqual(len(gen.get_slabs()), 1) s = self.get_structure("LiFePO4") gen = SlabGenerator(s, [0, 0, 1], 10, 10) self.assertEqual(len(gen.get_slabs()), 5) self.assertEqual(len(gen.get_slabs(bonds={("P", "O"): 3})), 2) # There are no slabs in LFP that does not break either P-O or Fe-O # bonds for a miller index of [0, 0, 1]. self.assertEqual( len(gen.get_slabs(bonds={ ("P", "O"): 3, ("Fe", "O"): 3 })), 0) #If we allow some broken bonds, there are a few slabs. self.assertEqual( len( gen.get_slabs(bonds={ ("P", "O"): 3, ("Fe", "O"): 3 }, max_broken_bonds=2)), 2) # At this threshold, only the origin and center Li results in # clustering. All other sites are non-clustered. So the of # slabs is of sites in LiFePO4 unit cell - 2 + 1. self.assertEqual(len(gen.get_slabs(tol=1e-4)), 15) LiCoO2 = Structure.from_file(get_path("icsd_LiCoO2.cif"), primitive=False) gen = SlabGenerator(LiCoO2, [0, 0, 1], 10, 10) lco = gen.get_slabs(bonds={("Co", "O"): 3}) self.assertEqual(len(lco), 1) a, b, c = gen.oriented_unit_cell.lattice.matrix self.assertAlmostEqual(np.dot(a, gen._normal), 0) self.assertAlmostEqual(np.dot(b, gen._normal), 0) scc = Structure.from_spacegroup("Pm-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]]) gen = SlabGenerator(scc, [0, 0, 1], 10, 10) slabs = gen.get_slabs() self.assertEqual(len(slabs), 1) gen = SlabGenerator(scc, [1, 1, 1], 10, 10, max_normal_search=1) slabs = gen.get_slabs() self.assertEqual(len(slabs), 1) # Test whether using units of hkl planes instead of Angstroms for # min_slab_size and min_vac_size will give us the same number of atoms natoms = [] for a in [1, 1.4, 2.5, 3.6]: s = Structure.from_spacegroup("Im-3m", Lattice.cubic(a), ["Fe"], [[0, 0, 0]]) slabgen = SlabGenerator(s, (1, 1, 1), 10, 10, in_unit_planes=True, max_normal_search=2) natoms.append(len(slabgen.get_slab())) n = natoms[0] for i in natoms: self.assertEqual(n, i)
def _mp_generate_slabs(struc, hkl, thickness, vacuum, is_symmetric=True, center_slab=True, **mp_kwargs): """ Helper function for multiprocessing in ``slabs_get_single_hkl``, made so that the information on slab and vacuum thickness is noted and carried forward during multiprocessing. ``**mp_kwargs`` can take any ``SlabGenerator`` argument. Args: struc (`pymatgen Structure object`): Structure object decorated with oxidation states max_index (`int`): The maximum Miller index to go up to. thickness (`int`): Minimum slab thickness vacuum (`int`): Minimum vacuum thickness is_symmetric (`bool`, optional): Whether the slabs cleaved should have inversion symmetry. If bulk is non-centrosymmetric, ``is_symmetric`` needs to be ``False`` - the function will return no slabs as it looks for inversion symmetry. Take care checking the slabs for mirror plane symmetry before just using them. Defaults to ``True``. center_slab (`bool`, optional): The position of the slab in the simulation cell. * ``True``: the slab is centered with equal amounts of vacuum above and below. * ``False``: the slab is at the bottom of the simulation cell with all of the vacuum on top of it. Defaults to True. Returns List of dicts of slabs and relevant metadata """ SlabGenerator_kwargs = { 'in_unit_planes': False, 'primitive': True, 'max_normal_search': None, 'reorient_lattice': True, 'lll_reduce': True } SlabGenerator_kwargs.update( (k, mp_kwargs[k]) for k in SlabGenerator_kwargs.keys() & mp_kwargs.keys()) get_slabs_kwargs = { 'ftol': 0.1, 'tol': 0.1, 'max_broken_bonds': 0, 'symmetrize': False, 'repair': False, 'bonds': None } get_slabs_kwargs.update( (k, mp_kwargs[k]) for k in get_slabs_kwargs.keys() & mp_kwargs.keys()) slabs = [] slabgen = SlabGenerator(struc, hkl, thickness, vacuum, center_slab=center_slab, **SlabGenerator_kwargs) all_slabs = slabgen.get_slabs(**get_slabs_kwargs) h = slabgen._proj_height p = round(h / slabgen.parent.lattice.d_hkl(slabgen.miller_index), 8) if slabgen.in_unit_planes: nlayers_slab = int(math.ceil(slabgen.min_slab_size / p)) else: nlayers_slab = int(math.ceil(slabgen.min_slab_size / h)) for i, slab in enumerate(all_slabs): if is_symmetric == True: if not slab.is_polar() and slab.is_symmetric(): slabs.append({ 'hkl': ''.join(map(str, slab.miller_index)), 'slab_thickness': thickness, 'slab_layers': nlayers_slab, 'vac_thickness': vacuum, 'slab_index': i, 'slab': slab }) else: if not slab.is_polar(): slabs.append({ 'hkl': ''.join(map(str, slab.miller_index)), 'slab_thickness': thickness, 'slab_layers': nlayers_slab, 'vac_thickness': vacuum, 'slab_index': i, 'slab': slab }) return slabs
def generate_slabs(structure, hkl, thicknesses, vacuums, save_slabs=True, save_metadata=True, json_fname=None, make_fols=False, make_input_files=False, max_size=500, center_slab=True, ox_states=None, is_symmetric=True, layers_to_relax=None, fmt='poscar', name='POSCAR', config_dict=None, user_incar_settings=None, user_kpoints_settings=None, user_potcar_settings=None, parallelise=True, **kwargs): """ Generates all unique slabs for a specified Miller indices or up to a maximum Miller index with minimum slab and vacuum thicknesses. It includes all combinations for multiple zero dipole symmetric terminations for the same Miller index. The function returns None by default and generates either: (i) POSCAR_hkl_slab_vac_index.vasp (default) (ii) hkl/slab_vac_index folders with structure files (iii) hkl/slab_vac_index with all VASP input files Or if `save_slabs=False` a list of dicts of all unique slabs is returned. Args: structure (`str` or pmg Structure obj): Filename of structure file in any format supported by pymatgen or pymatgen structure object. hkl (`tuple`, `list` or `int`): Miller index as tuple, a list of Miller indices or a maximum index up to which the search should be performed. E.g. if searching for slabs up to (2,2,2) ``hkl=2`` thicknesses (`list`): The minimum size of the slab in Angstroms. vacuums (`list`): The minimum size of the vacuum in Angstroms. save_slabs (`bool`, optional): Whether to save the slabs to file. Defaults to ``True``. save_metadata (`bool`, optional): Whether to save the slabs' metadata to file. Saves the entire slab object to a json file Defaults to ``True``. json_fname (`str`, optional): Filename of json metadata file. Defaults to bulk_formula_metadata.json make_fols (`bool`, optional): Makes folders for each termination and slab/vacuum thickness combinations containing structure files. * ``True``: A Miller index folder is created, in which folders named slab_vac_index are created to which the relevant structure files are saved. E.g. for a (0,0,1) slab of index 1 with a slab thickness of 20 Å and vacuum thickness of 30 Å the folder structure would be: ``001/20_30_1/POSCAR`` * ``False``: The indexed structure files are put in a folder named after the bulk formula. E.g. for a (0,0,1) MgO slab of index 1 with a slab thickness of 20 Å and vacuum thickness of 30 Å the folder structure would be: ``MgO/POSCAR_001_20_30_1`` Defaults to ``False``. make_input_files (`bool`, optional): Makes INCAR, POTCAR and KPOINTS files in each folder. If ``make_input_files`` is ``True`` but ``make_files`` or ``save_slabs`` is ``False``, files will be saved to folders regardless. This only works with VASP input files, other formats are not yet supported. Defaults to ``False``. max_size (`int`, optional): The maximum number of atoms in the slab specified to raise warning about slab size. Even if the warning is raised, it still outputs the slabs regardless. Defaults to ``500``. center_slab (`bool`, optional): The position of the slab in the simulation cell. * ``True``: the slab is centered with equal amounts of vacuum above and below. * ``False``: the slab is at the bottom of the simulation cell with all of the vacuum on top of it. Defaults to True. ox_states (``None``, `list` or `dict`, optional): Add oxidation states to the bulk structure. Different types of oxidation states specified will result in different pymatgen functions used. The options are: * if supplied as ``list``: The oxidation states are added by site e.g. ``[3, 2, 2, 1, -2, -2, -2, -2]`` * if supplied as ``dict``: The oxidation states are added by element e.g. ``{'Fe': 3, 'O':-2}`` * if ``None``: The oxidation states are added by guess. Defaults to ``None``. is_symmetric (`bool`, optional): Whether the slabs cleaved should have inversion symmetry. If bulk is non-centrosymmetric, ``is_symmetric`` needs to be ``False`` - the function will return no slabs as it looks for inversion symmetry. Take care checking the slabs for mirror plane symmetry before just using them. Defaults to ``True``. layers_to_relax (`int`, optional): Specifies the number of layers at the top and bottom of the slab that should be relaxed, keeps the centre constrained using selective dynamics. NB only works for VASP files fmt (`str`, optional): The format of the output structure files. Options include 'cif', 'poscar', 'cssr', 'json', not case sensitive. Defaults to 'poscar'. name (`str`, optional): The name of the surface slab structure file created. Case sensitive. Defaults to 'POSCAR' config_dict (`dict` or `str`, optional): Specifies the dictionary used for the generation of the input files. Suppports already loaded dictionaires, yaml and json files. Surfaxe-supplied dictionaries are PBE (``pe``), PBEsol (``ps``) and HSE06 (``hse06``) for single shot calculations and PBE (``pe_relax``) and PBEsol (``ps_relax``) for relaxations. Not case sensitive. Defaults to PBEsol (``ps``). user_incar_settings (`dict`, optional): Overrides the default INCAR parameter settings. Defaults to ``None``. user_kpoints_settings (`dict` or Kpoints object, optional): Overrides the default kpoints settings. If it is supplied as `dict`, it should be as ``{'reciprocal_density': 100}``. Defaults to ``None``. user_potcar_settings (`dict`, optional): Overrides the default POTCAR settings. Defaults to ``None``. parallelise (`bool`, optional): Use multiprocessing to generate slabs. Defaults to ``True``. Returns: None (default) or unique_slabs (list of dicts) """ # Set up additional arguments for multiprocessing and saving slabs mp_kwargs = { 'in_unit_planes': False, 'primitive': True, 'max_normal_search': None, 'reorient_lattice': True, 'lll_reduce': True, 'ftol': 0.1, 'tol': 0.1, 'max_broken_bonds': 0, 'symmetrize': False, 'repair': False, 'bonds': None } mp_kwargs.update((k, kwargs[k]) for k in mp_kwargs.keys() & kwargs.keys()) save_slabs_kwargs = { 'user_incar_settings': None, 'user_kpoints_settings': None, 'user_potcar_settings': None, 'constrain_total_magmom': False, 'sort_structure': True, 'potcar_functional': None, 'user_potcar_functional': None, 'force_gamma': False, 'reduce_structure': None, 'vdw': None, 'use_structure_charge': False, 'standardize': False, 'sym_prec': 0.1, 'international_monoclinic': True } save_slabs_kwargs.update( (k, kwargs[k]) for k in save_slabs_kwargs.keys() & kwargs.keys()) save_slabs_kwargs.update({ 'user_incar_settings': user_incar_settings, 'user_kpoints_settings': user_kpoints_settings, 'user_potcar_settings': user_potcar_settings }) # Import bulk relaxed structure, add oxidation states for slab dipole # calculations struc = _instantiate_structure(structure) # Check structure is conventional standard and warn if not sga = SpacegroupAnalyzer(struc) cs = sga.get_conventional_standard_structure() if cs.lattice != struc.lattice: warnings.formatwarning = _custom_formatwarning warnings.warn( 'Lattice of the structure provided does not match the ' 'conventional standard structure. Miller indices are lattice dependent,' ' make sure you are using the correct bulk structure ') # Check if oxidation states were added to the bulk already struc = oxidation_states(struc, ox_states) # Check if hkl provided as tuple or int, find all available hkl if # provided as int; make into a list to iterate over if type(hkl) == tuple: miller = [hkl] elif type(hkl) == int: miller = get_symmetrically_distinct_miller_indices(struc, hkl) elif type(hkl) == list and all(isinstance(x, tuple) for x in hkl): miller = hkl else: raise TypeError( 'Miller index should be supplied as tuple, int or list ' 'of tuples') # create all combinations of hkl, slab and vacuum thicknesses combos = itertools.product(miller, thicknesses, vacuums) # Check if bulk structure is noncentrosymmetric if is_symmetric=True, # change to False if not to make sure slabs are produced, issues warning if is_symmetric: sg = SpacegroupAnalyzer(struc) if not sg.is_laue(): is_symmetric = False warnings.formatwarning = _custom_formatwarning warnings.warn( ('Inversion symmetry was not found in the bulk ' 'structure, slabs produced will be non-centrosymmetric')) # Check if multiple cores are available, then iterate through the slab and # vacuum thicknesses and get all non polar symmetric slabs if multiprocessing.cpu_count() > 1 and parallelise == True: with multiprocessing.Pool() as pool: nested_provisional = pool.starmap( functools.partial(_mp_generate_slabs, struc, is_symmetric=is_symmetric, center_slab=center_slab, **mp_kwargs), combos) provisional = list(itertools.chain.from_iterable(nested_provisional)) else: # Set up kwargs again SG_kwargs = { k: mp_kwargs[k] for k in [ 'in_unit_planes', 'primitive' 'max_normal_search', 'reorient_lattice', 'lll_reduce' ] } gs_kwargs = { k: mp_kwargs[k] for k in [ 'ftol', 'tol', 'max_broken_bonds', 'symmetrize', 'repair', 'bonds' ] } provisional = [] for hkl, thickness, vacuum in combos: slabgen = SlabGenerator(struc, hkl, thickness, vacuum, center_slab=center_slab, **SG_kwargs) # Get the number of layers in the slab h = slabgen._proj_height p = round(h / slabgen.parent.lattice.d_hkl(slabgen.miller_index), 8) if slabgen.in_unit_planes: nlayers_slab = int(math.ceil(slabgen.min_slab_size / p)) else: nlayers_slab = int(math.ceil(slabgen.min_slab_size / h)) slabs = slabgen.get_slabs(**gs_kwargs) for i, slab in enumerate(slabs): # Get all the zero-dipole slabs with inversion symmetry if is_symmetric: if slab.is_symmetric() and not slab.is_polar(): provisional.append({ 'hkl': ''.join(map(str, slab.miller_index)), 'slab_thickness': thickness, 'slab_layers': nlayers_slab, 'vac_thickness': vacuum, 'slab_index': i, 'slab': slab }) # Get all the zero-dipole slabs wihtout inversion symmetry else: if not slab.is_polar(): provisional.append({ 'hkl': ''.join(map(str, slab.miller_index)), 'slab_thickness': thickness, 'slab_layers': nlayers_slab, 'vac_thickness': vacuum, 'slab_index': i, 'slab': slab }) # Iterate though provisional slabs to extract the unique slabs unique_list_of_dicts, repeat, large = _filter_slabs(provisional, max_size) if layers_to_relax is not None and fmt.lower() == 'poscar': unique_list_of_dicts, small = _get_selective_dynamics( struc, unique_list_of_dicts, layers_to_relax) if small: warnings.formatwarning = _custom_formatwarning warnings.warn( 'Some slabs were too thin to fix the centre of the slab.' ' Slabs with no selective dynamics applied are: ' + ', '.join(map(str, small))) # Warnings for too large, too small, repeated and no slabs; if repeat: warnings.formatwarning = _custom_formatwarning warnings.warn('Not all combinations of hkl or slab/vac thicknesses ' 'were generated because of repeat structures. ' 'The repeat slabs are: ' + ', '.join(map(str, repeat))) if large: warnings.formatwarning = _custom_formatwarning warnings.warn('Some generated slabs exceed the max size specified.' ' Slabs that exceed the max size are: ' + ', '.join(map(str, large))) if len(unique_list_of_dicts) == 0: raise ValueError( 'No zero dipole slabs found for specified Miller index') # Save the metadata or slabs to file or return the list of dicts if save_metadata: bulk_name = struc.composition.reduced_formula if json_fname is None: json_fname = '{}_metadata.json'.format(bulk_name) unique_list_of_dicts_copy = deepcopy(unique_list_of_dicts) for i in unique_list_of_dicts_copy: i['slab'] = i['slab'].as_dict() with open(json_fname, 'w') as f: json.dump(unique_list_of_dicts_copy, f) if save_slabs: slabs_to_file(list_of_slabs=unique_list_of_dicts, structure=struc, make_fols=make_fols, make_input_files=make_input_files, config_dict=config_dict, fmt=fmt, name=name, **save_slabs_kwargs) else: return unique_list_of_dicts
# Import the neccesary tools to generate surfaces from pymatgen.core.surface import SlabGenerator, generate_all_slabs, Structure, Lattice # Import the neccesary tools for making a Wulff shape from pymatgen.analysis.wulff import WulffShape import os lattice = Lattice.cubic(3.508) Ni = Structure(lattice, ["Ni", "Ni", "Ni", "Ni"], [[0, 0, 0], [0, 0.5, 0], [0.5, 0, 0], [0, 0, 0.5]]) slabgen = SlabGenerator(Ni, (1, 1, 1), 10, 10) lattice = Lattice.cubic(5.46873) Si = Structure(lattice, ["Si", "Si", "Si", "Si", "Si", "Si", "Si", "Si"], [[0.00000, 0.00000, 0.50000], [0.75000, 0.75000, 0.75000], [0.00000, 0.50000, 0.00000], [0.75000, 0.25000, 0.25000], [0.50000, 0.00000, 0.00000], [0.25000, 0.75000, 0.25000], [0.50000, 0.50000, 0.50000], [0.25000, 0.25000, 0.75000]]) slabgen = SlabGenerator(Si, (1, 1, 1), 10, 10) print("Notice now there are actually now %s terminations that can be \ generated in the (111) direction for diamond Si" % (len(slabgen.get_slabs())))
def test_get_slab(self): s = self.get_structure("LiFePO4") gen = SlabGenerator(s, [0, 0, 1], 10, 10) s = gen.get_slab(0.25) self.assertAlmostEqual(s.lattice.abc[2], 20.820740000000001) fcc = Structure.from_spacegroup("Fm-3m", Lattice.cubic(3), ["Fe"], [[0, 0, 0]]) gen = SlabGenerator(fcc, [1, 1, 1], 10, 10, max_normal_search=1) slab = gen.get_slab() self.assertEqual(len(slab), 6) gen = SlabGenerator(fcc, [1, 1, 1], 10, 10, primitive=False, max_normal_search=1) slab_non_prim = gen.get_slab() self.assertEqual(len(slab_non_prim), len(slab) * 4) # Some randomized testing of cell vectors for i in range(1, 231): i = random.randint(1, 230) sg = SpaceGroup.from_int_number(i) if sg.crystal_system == "hexagonal" or ( sg.crystal_system == "trigonal" and ( sg.symbol.endswith("H") or sg.int_number in [ 143, 144, 145, 147, 149, 150, 151, 152, 153, 154, 156, 157, 158, 159, 162, 163, 164, 165, ] ) ): latt = Lattice.hexagonal(5, 10) else: # Cubic lattice is compatible with all other space groups. latt = Lattice.cubic(5) s = Structure.from_spacegroup(i, latt, ["H"], [[0, 0, 0]]) miller = (0, 0, 0) while miller == (0, 0, 0): miller = ( random.randint(0, 6), random.randint(0, 6), random.randint(0, 6), ) gen = SlabGenerator(s, miller, 10, 10) a, b, c = gen.oriented_unit_cell.lattice.matrix self.assertAlmostEqual(np.dot(a, gen._normal), 0) self.assertAlmostEqual(np.dot(b, gen._normal), 0)
def create_min_thickness_slab(structure, index, num_termination_min, layerthickness, layer_subtractions, tol, min_thickness=10): #Find final_slabthickness such that the cleaned slab is a least min_thickness thick after layer_subtractions times the average layerthickness #Cleaning consists of removing one layer on each side and then ensuring that the number of atomic layers is multiples of the number of unique terminations slabthickness = min_thickness final_slabthickness = 0 while final_slabthickness < min_thickness + layerthickness * layer_subtractions: final_slab = SlabGenerator(structure, index, min_slab_size=slabthickness, min_vacuum_size=5, primitive=True, center_slab=True, in_unit_planes=False, max_normal_search=7).get_slab() final_slabthickness = np.max(final_slab.cart_coords[:, 2]) - np.min( final_slab.cart_coords[:, 2]) slabthickness += 2 #increase slabthickness for potential next iteration if final_slabthickness > 5: #Ensure that slab is thick enough for subtractions #"Clean" the two outside surfaces, pymatgen makes weird terminations sometimes min_value = np.min(final_slab.cart_coords[:, 2]) first_layer_size = len( np.where( np.abs(min_value - final_slab.cart_coords[:, 2]) < tol)[0]) for i in range(first_layer_size): index_to_remove = np.argmin(final_slab.frac_coords[:, 2]) final_slab.remove_sites([index_to_remove]) max_value = np.max(final_slab.cart_coords[:, 2]) first_layer_size = len( np.where( np.abs(max_value - final_slab.cart_coords[:, 2]) < tol)[0]) for i in range(first_layer_size): index_to_remove = np.argmax(final_slab.frac_coords[:, 2]) final_slab.remove_sites([index_to_remove]) nlayers = number_of_atomic_layers(final_slab, tol) #Delete all layers that are not multiple of num_termination_min (e.g. make ABCABCAB -> ABCABC; important for slabs with mirror symmetry) n_extra_layers = nlayers % num_termination_min if n_extra_layers != 0 and n_extra_layers < nlayers: for layer in range(n_extra_layers): max_value = np.max(final_slab.cart_coords[:, 2]) first_layer_size = len( np.where( np.abs(max_value - final_slab.cart_coords[:, 2]) < tol)[0]) for i in range(first_layer_size): index_to_remove = np.argmax(final_slab.frac_coords[:, 2]) final_slab.remove_sites([index_to_remove]) final_slabthickness = np.max( final_slab.cart_coords[:, 2]) - np.min( final_slab.cart_coords[:, 2]) while final_slabthickness - 1.3 * num_termination_min * layerthickness > min_thickness + layerthickness * layer_subtractions: for nterm in range(num_termination_min): max_value = np.max(final_slab.cart_coords[:, 2]) first_layer_size = len( np.where( np.abs(max_value - final_slab.cart_coords[:, 2]) < tol)[0]) for i in range(first_layer_size): index_to_remove = np.argmax(final_slab.frac_coords[:, 2]) final_slab.remove_sites([index_to_remove]) final_slabthickness = np.max(final_slab.cart_coords[:, 2]) - np.min( final_slab.cart_coords[:, 2]) return final_slab
def get_slabs(self, max_index: int = 1, min_slab_size: float = 5.0, min_vacuum_size: float = 15.0, is_fix_vacuum_size: bool = False, bonds: Dict[Tuple[str, str], float] = None, tolerance: float = 0.001, max_broken_bonds: int = 0, is_lll_reduce: bool = False, is_center_slab: bool = False, is_primitive: bool = True, max_normal_search: int = None, is_symmetrize: bool = False, is_repair: bool = False, is_in_unit_planes: bool = False) -> List[Structure]: ''' Search and return the slabs found in the given structure. @in - max_index, int, max of miller index. e.g. when max_index = 1 for cubic structure, only (100), (110), (111) miller surfaces are searched. - min_slab_size, float, minimum size in angstroms of layers containing atoms - min_vacuum_size, float, minimum size in angstroms of vacuum layer - is_fix_vacuum_size, bool, Not implemented yet - bonds, {(str, str): float}, specify the maximum length of bond length for given atom pairs to avoid bond broken. - tolerance, float, accuracy - max_broken_bonds, int - is_lll_reduce, bool, whether or not the slabs will be orthogonalized - is_center_slab, bool, whether or not the slabs will be centered between the vacuum layer - is_primitive, bool, whether to reduce any generated slabs to a primitive cell - max_normal_search, If set to a positive integer, the code will conduct a search for a normal lattice vector that is as perpendicular to the surface as possible by considering multiples linear combinations of lattice vectors up to max_normal_search. - is_symmetrize, bool, Whether or not to ensure the surfaces of the slabs are equivalent - is_repair, bool, whether to repair terminations with broken bonds or just omit them - is_in_unit_planes, bool, whether to set min_slab_size and min_vac_size in units of hkl planes (True) or Angstrom (False/default) @out ''' st = self.old_structure.copy() all_slabs = [] for miller in get_symmetrically_distinct_miller_indices(st, max_index): if is_fix_vacuum_size: pass else: vacuum_size = random.random() * min_vacuum_size gen = SlabGenerator(st, miller, min_slab_size, vacuum_size, lll_reduce=is_lll_reduce, center_slab=is_center_slab, primitive=is_primitive, max_normal_search=max_normal_search, in_unit_planes=is_in_unit_planes) slabs = gen.get_slabs(bonds=bonds, tol=tolerance, symmetrize=is_symmetrize, max_broken_bonds=max_broken_bonds, repair=is_repair) if len(slabs) > 0: all_slabs.extend(slabs) self.operations.append({'slabs': len(slabs)}) return all_slabs
def pmg_surfer(mpid='', vacuum=15, mat=None, max_index=1, min_slab_size=15, write_file=True): """ Pymatgen surface builder for a Poscar Args: vacuum: vacuum region mat: Structure object max_index: maximum miller index min_slab_size: minimum slab size Returns: structures: list of surface Structure objects """ if mat == None: with MPRester() as mp: mat = mp.get_structure_by_material_id(mpid) if mpid == '': print('Provide structure') sg_mat = SpacegroupAnalyzer(mat) mat_cvn = sg_mat.get_conventional_standard_structure() mat_cvn.sort() indices = get_symmetrically_distinct_miller_indices(mat_cvn, max_index) #ase_atoms = AseAtomsAdaptor().get_atoms(mat_cvn) structures = [] pos = Poscar(mat_cvn) try: pos.comment = str('sbulk') + str('@') + str('vac') + str(vacuum) + str( '@') + str('size') + str(min_slab_size) except: pass structures.append(pos) if write_file == True: mat_cvn.to(fmt='poscar', filename=str('POSCAR-') + str('cvn') + str('.vasp')) for i in indices: slab = SlabGenerator(initial_structure=mat_cvn, miller_index=i, min_slab_size=min_slab_size, min_vacuum_size=vacuum, lll_reduce=False, center_slab=True, primitive=False).get_slab() normal_slab = slab.get_orthogonal_c_slab() slab_pymatgen = Poscar(normal_slab).structure #ase_slab.center(vacuum=vacuum, axis=2) #slab_pymatgen = AseAtomsAdaptor().get_structure(ase_slab) xy_size = min_slab_size dim1 = int((float(xy_size) / float(max(abs(slab_pymatgen.lattice.matrix[0]))))) + 1 dim2 = int( float(xy_size) / float(max(abs(slab_pymatgen.lattice.matrix[1])))) + 1 slab_pymatgen.make_supercell([dim1, dim2, 1]) slab_pymatgen.sort() surf_name = '_'.join(map(str, i)) pos = Poscar(slab_pymatgen) try: pos.comment = str("Surf-") + str(surf_name) + str('@') + str( 'vac') + str(vacuum) + str('@') + str('size') + str( min_slab_size) except: pass if write_file == True: pos.write_file(filename=str('POSCAR-') + str("Surf-") + str(surf_name) + str('.vasp')) structures.append(pos) return structures
def get_dimensionality_gorai( structure, max_hkl=2, el_radius_updates=None, min_slab_size=5, min_vacuum_size=5, standardize=True, bonds=None, ): """ This method returns whether a structure is 3D, 2D (layered), or 1D (linear chains or molecules) according to the algorithm published in Gorai, P., Toberer, E. & Stevanovic, V. Computational Identification of Promising Thermoelectric Materials Among Known Quasi-2D Binary Compounds. J. Mater. Chem. A 2, 4136 (2016). Note that a 1D structure detection might indicate problems in the bonding algorithm, particularly for ionic crystals (e.g., NaCl) Users can change the behavior of bonds detection by passing either el_radius_updates to update atomic radii for auto-detection of max bond distances, or bonds to explicitly specify max bond distances for atom pairs. Note that if you pass both, el_radius_updates are ignored. Args: structure: (Structure) structure to analyze dimensionality for max_hkl: (int) max index of planes to look for layers el_radius_updates: (dict) symbol->float to update atomic radii min_slab_size: (float) internal surface construction parameter min_vacuum_size: (float) internal surface construction parameter standardize (bool): whether to standardize the structure before analysis. Set to False only if you already have the structure in a convention where layers / chains will be along low <hkl> indexes. bonds ({(specie1, specie2): max_bond_dist}: bonds are specified as a dict of tuples: float of specie1, specie2 and the max bonding distance. For example, PO4 groups may be defined as {("P", "O"): 3}. Returns: (int) the dimensionality of the structure - 1 (molecules/chains), 2 (layered), or 3 (3D) """ if standardize: structure = SpacegroupAnalyzer( structure).get_conventional_standard_structure() if not bonds: bonds = get_max_bond_lengths(structure, el_radius_updates) num_surfaces = 0 for h in range(max_hkl): for k in range(max_hkl): for l in range(max_hkl): if max([h, k, l]) > 0 and num_surfaces < 2: sg = SlabGenerator( structure, (h, k, l), min_slab_size=min_slab_size, min_vacuum_size=min_vacuum_size, ) slabs = sg.get_slabs(bonds) for _ in slabs: num_surfaces += 1 return 3 - min(num_surfaces, 2)
def get_oriented_slabs(self, film_layers=3, substrate_layers=3, match_index=0, **kwargs): """ Get a list of oriented slabs for constructing interfaces and put them in self.film_structures, self.substrate_structures, self.modified_film_structures, and self.modified_substrate_structures. Currently only uses first match (lowest SA) in the list of matches Args: film_layers (int): number of layers of film to include in Interface structures. substrate_layers (int): number of layers of substrate to include in Interface structures. match_index (int): ZSL match from which to construct slabs. """ self.match_index = match_index self.substrate_layers = substrate_layers self.film_layers = film_layers if 'zslgen' in kwargs.keys(): sa = SubstrateAnalyzer(zslgen=kwargs.get('zslgen')) del kwargs['zslgen'] else: sa = SubstrateAnalyzer() # Generate all possible interface matches self.matches = list( sa.calculate(self.original_film_structure, self.original_substrate_structure, **kwargs)) match = self.matches[match_index] # Generate substrate slab and align x axis to (100) and slab normal to (001) ## Get no-vacuum structure for strained bulk calculation self.sub_sg = SlabGenerator(self.original_substrate_structure, match['sub_miller'], substrate_layers, 0, in_unit_planes=True, reorient_lattice=False, primitive=False) no_vac_sub_slab = self.sub_sg.get_slab() no_vac_sub_slab = get_shear_reduced_slab(no_vac_sub_slab) self.oriented_substrate = align_x(no_vac_sub_slab) self.oriented_substrate.sort() ## Get slab with vacuum self.sub_sg = SlabGenerator(self.original_substrate_structure, match['sub_miller'], substrate_layers, 1, in_unit_planes=True, reorient_lattice=False, primitive=False) sub_slabs = self.sub_sg.get_slabs() for i, sub_slab in enumerate(sub_slabs): sub_slab = get_shear_reduced_slab(sub_slab) sub_slab = align_x(sub_slab) sub_slab.sort() sub_slabs[i] = sub_slab self.substrate_structures = sub_slabs # Generate film slab and align x axis to (100) and slab normal to (001) ## Get no-vacuum structure for strained bulk calculation self.film_sg = SlabGenerator(self.original_film_structure, match['film_miller'], film_layers, 0, in_unit_planes=True, reorient_lattice=False, primitive=False) no_vac_film_slab = self.film_sg.get_slab() no_vac_film_slab = get_shear_reduced_slab(no_vac_film_slab) self.oriented_film = align_x(no_vac_film_slab) self.oriented_film.sort() ## Get slab with vacuum self.film_sg = SlabGenerator(self.original_film_structure, match['film_miller'], film_layers, 1, in_unit_planes=True, reorient_lattice=False, primitive=False) film_slabs = self.film_sg.get_slabs() for i, film_slab in enumerate(film_slabs): film_slab = get_shear_reduced_slab(film_slab) film_slab = align_x(film_slab) film_slab.sort() film_slabs[i] = film_slab self.film_structures = film_slabs # Apply transformation to produce matched area and a & b vectors self.apply_transformations(match) # Get non-stoichioimetric substrate slabs sym_sub_slabs = [] for sub_slab in self.modified_substrate_structures: sym_sub_slab = self.sub_sg.nonstoichiometric_symmetrized_slab( sub_slab) for slab in sym_sub_slab: if not slab == sub_slab: sym_sub_slabs.append(slab) self.sym_modified_substrate_structures = sym_sub_slabs # Get non-stoichioimetric film slabs sym_film_slabs = [] for film_slab in self.modified_film_structures: sym_film_slab = self.film_sg.nonstoichiometric_symmetrized_slab( film_slab) for slab in sym_film_slab: if not slab == film_slab: sym_film_slabs.append(slab) self.sym_modified_film_structures = sym_film_slabs # Strained film structures (No Vacuum) self.strained_substrate, self.strained_film = strain_slabs( self.oriented_substrate, self.oriented_film) return