def get_integer_formula_and_factor(self, max_denominator=10000): """ Calculates an integer formula and factor. Args: max_denominator (int): all amounts in the el:amt dict are first converted to a Fraction with this maximum denominator Returns: A pretty normalized formula and a multiplicative factor, i.e., Li0.5O0.25 returns (Li2O, 0.25). O0.25 returns (O2, 0.125) """ vals = [ Fraction(v).limit_denominator(max_denominator) for v in self.values() ] denom = lcm(*(f.denominator for f in vals)) mul = gcd(*[int(v * denom) for v in vals]) d = { k: round(v / mul * denom) for k, v in self.get_el_amt_dict().items() } (formula, factor) = reduce_formula(d) if formula in Composition.special_formulas: formula = Composition.special_formulas[formula] factor /= 2 return formula, factor * mul / denom
def determine_min_cell(cls, structure, mag_species_spin, order_parameter): """ Determine the smallest supercell that is able to enumerate the provided structure with the given order parameter """ def lcm(n1, n2): """ Find least common multiple of two numbers """ return n1 * n2 / gcd(n1, n2) denom = Fraction(order_parameter).limit_denominator(100).denominator atom_per_specie = [structure.composition[m] for m in mag_species_spin.keys()] n_gcd = six.moves.reduce(gcd, atom_per_specie) if not n_gcd: raise ValueError( 'The specified species do not exist in the structure' ' to be enumerated') return lcm(int(n_gcd), denom) / n_gcd
def determine_min_cell(cls, structure, mag_species_spin, order_parameter): """ Determine the smallest supercell that is able to enumerate the provided structure with the given order parameter """ def lcm(n1, n2): """ Find least common multiple of two numbers """ return n1 * n2 / gcd(n1, n2) denom = Fraction(order_parameter).limit_denominator(100).denominator atom_per_specie = [ structure.composition[m] for m in mag_species_spin.keys() ] n_gcd = six.moves.reduce(gcd, atom_per_specie) if not n_gcd: raise ValueError( 'The specified species do not exist in the structure' ' to be enumerated') return lcm(n_gcd, denom) / n_gcd
def get_integer_formula_and_factor(self, max_denominator=10000): """ Calculates an integer formula and factor. Args: max_denominator (int): all amounts in the el:amt dict are first converted to a Fraction with this maximum denominator Returns: A pretty normalized formula and a multiplicative factor, i.e., Li0.5O0.25 returns (Li2O, 0.25). O0.25 returns (O2, 0.125) """ vals = [Fraction(v).limit_denominator(max_denominator) for v in self.values()] denom = lcm(*(f.denominator for f in vals)) mul = gcd(*[int(v * denom) for v in vals]) d = {k: round(v / mul * denom) for k, v in self.get_el_amt_dict().items()} (formula, factor) = reduce_formula(d) if formula in Composition.special_formulas: formula = Composition.special_formulas[formula] factor /= 2 return formula, factor * mul / denom
def determine_min_cell(disordered_structure): """ Determine the smallest supercell that is able to enumerate the provided structure with the given order parameter """ def lcm(n1, n2): """ Find least common multiple of two numbers """ return n1 * n2 / gcd(n1, n2) # assumes all order parameters for a given species are the same mag_species_order_parameter = {} mag_species_occurrences = {} for idx, site in enumerate(disordered_structure): if not site.is_ordered: op = max(site.species_and_occu.values()) # this very hacky bit of code only works because we know # that on disordered sites in this class, all species are the same # but have different spins, and this is comma-delimited sp = str(list(site.species_and_occu.keys())[0]).split(",")[0] if sp in mag_species_order_parameter: mag_species_occurrences[sp] += 1 else: mag_species_order_parameter[sp] = op mag_species_occurrences[sp] = 1 smallest_n = [] for sp, order_parameter in mag_species_order_parameter.items(): denom = Fraction(order_parameter).limit_denominator( 100).denominator num_atom_per_specie = mag_species_occurrences[sp] n_gcd = gcd(denom, num_atom_per_specie) smallest_n.append(lcm(int(n_gcd), denom) / n_gcd) return max(smallest_n)
def determine_min_cell(disordered_structure): """ Determine the smallest supercell that is able to enumerate the provided structure with the given order parameter """ def lcm(n1, n2): """ Find least common multiple of two numbers """ return n1 * n2 / gcd(n1, n2) # assumes all order parameters for a given species are the same mag_species_order_parameter = {} mag_species_occurrences = {} for idx, site in enumerate(disordered_structure): if not site.is_ordered: op = max(site.species_and_occu.values()) # this very hacky bit of code only works because we know # that on disordered sites in this class, all species are the same # but have different spins, and this is comma-delimited sp = str(list(site.species_and_occu.keys())[0]).split(",")[0] if sp in mag_species_order_parameter: mag_species_occurrences[sp] += 1 else: mag_species_order_parameter[sp] = op mag_species_occurrences[sp] = 1 smallest_n = [] for sp, order_parameter in mag_species_order_parameter.items(): denom = Fraction(order_parameter).limit_denominator(100).denominator num_atom_per_specie = mag_species_occurrences[sp] n_gcd = gcd(denom, num_atom_per_specie) smallest_n.append(lcm(int(n_gcd), denom) / n_gcd) return max(smallest_n)
def __init__(self, initial_structure, miller_index, min_slab_size, min_vacuum_size, lll_reduce=False, center_slab=False, primitive=True, max_normal_search=None): """ Calculates the slab scale factor and uses it to generate a unit cell of the initial structure that has been oriented by its miller index. Also stores the initial information needed later on to generate a slab. Args: initial_structure (Structure): Initial input structure. Note that to ensure that the miller indices correspond to usual crystallographic definitions, you should supply a conventional unit cell structure. miller_index ([h, k, l]): Miller index of plane parallel to surface. Note that this is referenced to the input structure. If you need this to be based on the conventional cell, you should supply the conventional structure. min_slab_size (float): In Angstroms min_vac_size (float): In Angstroms lll_reduce (bool): Whether to perform an LLL reduction on the eventual structure. center_slab (bool): Whether to center the slab in the cell with equal vacuum spacing from the top and bottom. primitive (bool): Whether to reduce any generated slabs to a primitive cell (this does **not** mean the slab is generated from a primitive cell, it simply means that after slab generation, we attempt to find shorter lattice vectors, which lead to less surface area and smaller cells). max_normal_search (int): 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. This has no bearing on surface energies, but may be useful as a preliminary step to generating slabs for absorption and other sizes. It is typical that this will not be the smallest possible cell for simulation. Normality is not guaranteed, but the oriented cell will have the c vector as normal as possible (within the search range) to the surface. A value of up to the max absolute Miller index is usually sufficient. """ latt = initial_structure.lattice miller_index = reduce_vector(miller_index) #Calculate the surface normal using the reciprocal lattice vector. recp = latt.reciprocal_lattice_crystallographic normal = recp.get_cartesian_coords(miller_index) normal /= np.linalg.norm(normal) slab_scale_factor = [] non_orth_ind = [] eye = np.eye(3, dtype=np.int) for i, j in enumerate(miller_index): if j == 0: # Lattice vector is perpendicular to surface normal, i.e., # in plane of surface. We will simply choose this lattice # vector as one of the basis vectors. slab_scale_factor.append(eye[i]) else: #Calculate projection of lattice vector onto surface normal. d = abs(np.dot(normal, latt.matrix[i])) / latt.abc[i] non_orth_ind.append((i, d)) # We want the vector that has maximum magnitude in the # direction of the surface normal as the c-direction. # Results in a more "orthogonal" unit cell. c_index, dist = max(non_orth_ind, key=lambda t: t[1]) if len(non_orth_ind) > 1: lcm_miller = lcm(*[miller_index[i] for i, d in non_orth_ind]) for (i, di), (j, dj) in itertools.combinations(non_orth_ind, 2): l = [0, 0, 0] l[i] = -int(round(lcm_miller / miller_index[i])) l[j] = int(round(lcm_miller / miller_index[j])) slab_scale_factor.append(l) if len(slab_scale_factor) == 2: break if max_normal_search is None: slab_scale_factor.append(eye[c_index]) else: index_range = sorted(reversed( range(-max_normal_search, max_normal_search + 1)), key=lambda x: abs(x)) candidates = [] for uvw in itertools.product(index_range, index_range, index_range): if (not any(uvw)) or abs( np.linalg.det(slab_scale_factor + [uvw])) < 1e-8: continue vec = latt.get_cartesian_coords(uvw) l = np.linalg.norm(vec) cosine = abs(np.dot(vec, normal) / l) candidates.append((uvw, cosine, l)) if abs(abs(cosine) - 1) < 1e-8: # If cosine of 1 is found, no need to search further. break # We want the indices with the maximum absolute cosine, # but smallest possible length. uvw, cosine, l = max(candidates, key=lambda x: (x[1], -l)) slab_scale_factor.append(uvw) slab_scale_factor = np.array(slab_scale_factor) # Let's make sure we have a left-handed crystallographic system if np.linalg.det(slab_scale_factor) < 0: slab_scale_factor *= -1 # Make sure the slab_scale_factor is reduced to avoid # unnecessarily large slabs reduced_scale_factor = [reduce_vector(v) for v in slab_scale_factor] slab_scale_factor = np.array(reduced_scale_factor) single = initial_structure.copy() single.make_supercell(slab_scale_factor) self.oriented_unit_cell = Structure.from_sites(single, to_unit_cell=True) self.parent = initial_structure self.lll_reduce = lll_reduce self.center_slab = center_slab self.slab_scale_factor = slab_scale_factor self.miller_index = miller_index self.min_vac_size = min_vacuum_size self.min_slab_size = min_slab_size self.primitive = primitive self._normal = normal a, b, c = self.oriented_unit_cell.lattice.matrix self._proj_height = abs(np.dot(normal, c))
def test_lcm(self): self.assertEqual(lcm(2, 3, 4), 12)
def _gen_input_file(self): """ Generate the necessary struct_enum.in file for enumlib. See enumlib documentation for details. """ coord_format = "{:.6f} {:.6f} {:.6f}" # Using symmetry finder, get the symmetrically distinct sites. fitter = SpacegroupAnalyzer(self.structure, self.symm_prec) symmetrized_structure = fitter.get_symmetrized_structure() logger.debug("Spacegroup {} ({}) with {} distinct sites".format( fitter.get_space_group_symbol(), fitter.get_space_group_number(), len(symmetrized_structure.equivalent_sites))) """ Enumlib doesn"t work when the number of species get too large. To simplify matters, we generate the input file only with disordered sites and exclude the ordered sites from the enumeration. The fact that different disordered sites with the exact same species may belong to different equivalent sites is dealt with by having determined the spacegroup earlier and labelling the species differently. """ # index_species and index_amounts store mappings between the indices # used in the enum input file, and the actual species and amounts. index_species = [] index_amounts = [] # Stores the ordered sites, which are not enumerated. ordered_sites = [] disordered_sites = [] coord_str = [] for sites in symmetrized_structure.equivalent_sites: if sites[0].is_ordered: ordered_sites.append(sites) else: sp_label = [] species = {k: v for k, v in sites[0].species.items()} if sum(species.values()) < 1 - EnumlibAdaptor.amount_tol: # Let us first make add a dummy element for every single # site whose total occupancies don't sum to 1. species[DummySpecie("X")] = 1 - sum(species.values()) for sp in species.keys(): if sp not in index_species: index_species.append(sp) sp_label.append(len(index_species) - 1) index_amounts.append(species[sp] * len(sites)) else: ind = index_species.index(sp) sp_label.append(ind) index_amounts[ind] += species[sp] * len(sites) sp_label = "/".join(["{}".format(i) for i in sorted(sp_label)]) for site in sites: coord_str.append("{} {}".format( coord_format.format(*site.coords), sp_label)) disordered_sites.append(sites) def get_sg_info(ss): finder = SpacegroupAnalyzer(Structure.from_sites(ss), self.symm_prec) return finder.get_space_group_number() target_sgnum = get_sg_info(symmetrized_structure.sites) curr_sites = list(itertools.chain.from_iterable(disordered_sites)) sgnum = get_sg_info(curr_sites) ordered_sites = sorted(ordered_sites, key=lambda sites: len(sites)) logger.debug("Disordered sites has sg # %d" % (sgnum)) self.ordered_sites = [] # progressively add ordered sites to our disordered sites # until we match the symmetry of our input structure if self.check_ordered_symmetry: while sgnum != target_sgnum and len(ordered_sites) > 0: sites = ordered_sites.pop(0) temp_sites = list(curr_sites) + sites new_sgnum = get_sg_info(temp_sites) if sgnum != new_sgnum: logger.debug("Adding %s in enum. New sg # %d" % (sites[0].specie, new_sgnum)) index_species.append(sites[0].specie) index_amounts.append(len(sites)) sp_label = len(index_species) - 1 for site in sites: coord_str.append("{} {}".format( coord_format.format(*site.coords), sp_label)) disordered_sites.append(sites) curr_sites = temp_sites sgnum = new_sgnum else: self.ordered_sites.extend(sites) for sites in ordered_sites: self.ordered_sites.extend(sites) self.index_species = index_species lattice = self.structure.lattice output = [self.structure.formula, "bulk"] for vec in lattice.matrix: output.append(coord_format.format(*vec)) output.append("%d" % len(index_species)) output.append("%d" % len(coord_str)) output.extend(coord_str) output.append("{} {}".format(self.min_cell_size, self.max_cell_size)) output.append(str(self.enum_precision_parameter)) output.append("full") ndisordered = sum([len(s) for s in disordered_sites]) base = int(ndisordered * lcm(*[ f.limit_denominator(ndisordered * self.max_cell_size).denominator for f in map(fractions.Fraction, index_amounts) ])) # This multiplicative factor of 10 is to prevent having too small bases # which can lead to rounding issues in the next step. # An old bug was that a base was set to 8, with a conc of 0.4:0.6. That # resulted in a range that overlaps and a conc of 0.5 satisfying this # enumeration. See Cu7Te5.cif test file. base *= 10 # base = ndisordered #10 ** int(math.ceil(math.log10(ndisordered))) # To get a reasonable number of structures, we fix concentrations to the # range expected in the original structure. total_amounts = sum(index_amounts) for amt in index_amounts: conc = amt / total_amounts if abs(conc * base - round(conc * base)) < 1e-5: output.append("{} {} {}".format(int(round(conc * base)), int(round(conc * base)), base)) else: min_conc = int(math.floor(conc * base)) output.append("{} {} {}".format(min_conc - 1, min_conc + 1, base)) output.append("") logger.debug("Generated input file:\n{}".format("\n".join(output))) with open("struct_enum.in", "w") as f: f.write("\n".join(output))
def apply_transformation(self, structure, return_ranked_list=False): """ Args: structure (Structure): Input structure to dope Returns: [{"structure": Structure, "energy": float}] """ comp = structure.composition logger.info("Composition: %s" % comp) for sp in comp: try: sp.oxi_state except AttributeError: analyzer = BVAnalyzer() structure = analyzer.get_oxi_state_decorated_structure( structure) comp = structure.composition break ox = self.dopant.oxi_state radius = self.dopant.ionic_radius compatible_species = [ sp for sp in comp if sp.oxi_state == ox and abs(sp.ionic_radius / radius - 1) < self.ionic_radius_tol] if (not compatible_species) and self.alio_tol: # We only consider aliovalent doping if there are no compatible # isovalent species. compatible_species = [ sp for sp in comp if abs(sp.oxi_state - ox) <= self.alio_tol and abs(sp.ionic_radius / radius - 1) < self.ionic_radius_tol and sp.oxi_state * ox >= 0] if self.allowed_doping_species is not None: # Only keep allowed doping species. compatible_species = [ sp for sp in compatible_species if sp in [get_el_sp(s) for s in self.allowed_doping_species]] logger.info("Compatible species: %s" % compatible_species) lengths = structure.lattice.abc scaling = [max(1, int(round(math.ceil(self.min_length/x)))) for x in lengths] logger.info("Lengths are %s" % str(lengths)) logger.info("Scaling = %s" % str(scaling)) all_structures = [] t = EnumerateStructureTransformation(**self.kwargs) for sp in compatible_species: supercell = structure * scaling nsp = supercell.composition[sp] if sp.oxi_state == ox: supercell.replace_species({sp: {sp: (nsp - 1)/nsp, self.dopant: 1/nsp}}) logger.info("Doping %s for %s at level %.3f" % ( sp, self.dopant, 1 / nsp)) elif self.codopant: codopant = _find_codopant(sp, 2 * sp.oxi_state - ox) supercell.replace_species({sp: {sp: (nsp - 2) / nsp, self.dopant: 1 / nsp, codopant: 1 / nsp}}) logger.info("Doping %s for %s + %s at level %.3f" % ( sp, self.dopant, codopant, 1 / nsp)) elif abs(sp.oxi_state) < abs(ox): # Strategy: replace the target species with a # combination of dopant and vacancy. # We will choose the lowest oxidation state species as a # vacancy compensation species as it is likely to be lower in # energy sp_to_remove = min([s for s in comp if s.oxi_state * ox > 0], key=lambda ss: abs(ss.oxi_state)) if sp_to_remove == sp: common_charge = lcm(int(abs(sp.oxi_state)), int(abs(ox))) ndopant = common_charge / abs(ox) nsp_to_remove = common_charge / abs(sp.oxi_state) logger.info("Doping %d %s with %d %s." % (nsp_to_remove, sp, ndopant, self.dopant)) supercell.replace_species( {sp: {sp: (nsp - nsp_to_remove) / nsp, self.dopant: ndopant / nsp}}) else: ox_diff = int(abs(round(sp.oxi_state - ox))) vac_ox = int(abs(sp_to_remove.oxi_state)) common_charge = lcm(vac_ox, ox_diff) ndopant = common_charge / ox_diff nx_to_remove = common_charge / vac_ox nx = supercell.composition[sp_to_remove] logger.info("Doping %d %s with %s and removing %d %s." % (ndopant, sp, self.dopant, nx_to_remove, sp_to_remove)) supercell.replace_species( {sp: {sp: (nsp - ndopant) / nsp, self.dopant: ndopant / nsp}, sp_to_remove: { sp_to_remove: (nx - nx_to_remove) / nx}}) elif abs(sp.oxi_state) > abs(ox): # Strategy: replace the target species with dopant and also # remove some opposite charged species for charge neutrality if ox > 0: sp_to_remove = max(supercell.composition.keys(), key=lambda el: el.X) else: sp_to_remove = min(supercell.composition.keys(), key=lambda el: el.X) # Confirm species are of opposite oxidation states. assert sp_to_remove.oxi_state * sp.oxi_state < 0 ox_diff = int(abs(round(sp.oxi_state - ox))) anion_ox = int(abs(sp_to_remove.oxi_state)) nx = supercell.composition[sp_to_remove] common_charge = lcm(anion_ox, ox_diff) ndopant = common_charge / ox_diff nx_to_remove = common_charge / anion_ox logger.info("Doping %d %s with %s and removing %d %s." % (ndopant, sp, self.dopant, nx_to_remove, sp_to_remove)) supercell.replace_species( {sp: {sp: (nsp - ndopant) / nsp, self.dopant: ndopant / nsp}, sp_to_remove: {sp_to_remove: (nx - nx_to_remove)/nx}}) ss = t.apply_transformation( supercell, return_ranked_list=self.max_structures_per_enum) logger.info("%s distinct structures" % len(ss)) all_structures.extend(ss) logger.info("Total %s doped structures" % len(all_structures)) if return_ranked_list: return all_structures[:return_ranked_list] return all_structures[0]["structure"]
def apply_transformation(self, structure, return_ranked_list=False): """ Args: structure (Structure): Input structure to dope Returns: [{"structure": Structure, "energy": float}] """ comp = structure.composition logger.info("Composition: %s" % comp) for sp in comp: try: sp.oxi_state except AttributeError: analyzer = BVAnalyzer() structure = analyzer.get_oxi_state_decorated_structure( structure) comp = structure.composition break ox = self.dopant.oxi_state radius = self.dopant.ionic_radius compatible_species = [ sp for sp in comp if sp.oxi_state == ox and abs(sp.ionic_radius / radius - 1) < self.ionic_radius_tol ] if (not compatible_species) and self.alio_tol: # We only consider aliovalent doping if there are no compatible # isovalent species. compatible_species = [ sp for sp in comp if abs(sp.oxi_state - ox) <= self.alio_tol and abs(sp.ionic_radius / radius - 1) < self.ionic_radius_tol and sp.oxi_state * ox >= 0 ] if self.allowed_doping_species is not None: # Only keep allowed doping species. compatible_species = [ sp for sp in compatible_species if sp in [get_el_sp(s) for s in self.allowed_doping_species] ] logger.info("Compatible species: %s" % compatible_species) lengths = structure.lattice.abc scaling = [ max(1, int(round(math.ceil(self.min_length / x)))) for x in lengths ] logger.info("Lengths are %s" % str(lengths)) logger.info("Scaling = %s" % str(scaling)) all_structures = [] t = EnumerateStructureTransformation(**self.kwargs) for sp in compatible_species: supercell = structure * scaling nsp = supercell.composition[sp] if sp.oxi_state == ox: supercell.replace_species( {sp: { sp: (nsp - 1) / nsp, self.dopant: 1 / nsp }}) logger.info("Doping %s for %s at level %.3f" % (sp, self.dopant, 1 / nsp)) elif self.codopant: codopant = _find_codopant(sp, 2 * sp.oxi_state - ox) supercell.replace_species({ sp: { sp: (nsp - 2) / nsp, self.dopant: 1 / nsp, codopant: 1 / nsp } }) logger.info("Doping %s for %s + %s at level %.3f" % (sp, self.dopant, codopant, 1 / nsp)) elif abs(sp.oxi_state) < abs(ox): # Strategy: replace the target species with a # combination of dopant and vacancy. # We will choose the lowest oxidation state species as a # vacancy compensation species as it is likely to be lower in # energy sp_to_remove = min([s for s in comp if s.oxi_state * ox > 0], key=lambda ss: abs(ss.oxi_state)) if sp_to_remove == sp: common_charge = lcm(int(abs(sp.oxi_state)), int(abs(ox))) ndopant = common_charge / abs(ox) nsp_to_remove = common_charge / abs(sp.oxi_state) logger.info("Doping %d %s with %d %s." % (nsp_to_remove, sp, ndopant, self.dopant)) supercell.replace_species({ sp: { sp: (nsp - nsp_to_remove) / nsp, self.dopant: ndopant / nsp } }) else: ox_diff = int(abs(round(sp.oxi_state - ox))) vac_ox = int(abs(sp_to_remove.oxi_state)) common_charge = lcm(vac_ox, ox_diff) ndopant = common_charge / ox_diff nx_to_remove = common_charge / vac_ox nx = supercell.composition[sp_to_remove] logger.info( "Doping %d %s with %s and removing %d %s." % (ndopant, sp, self.dopant, nx_to_remove, sp_to_remove)) supercell.replace_species({ sp: { sp: (nsp - ndopant) / nsp, self.dopant: ndopant / nsp }, sp_to_remove: { sp_to_remove: (nx - nx_to_remove) / nx } }) elif abs(sp.oxi_state) > abs(ox): # Strategy: replace the target species with dopant and also # remove some opposite charged species for charge neutrality if ox > 0: sp_to_remove = max(supercell.composition.keys(), key=lambda el: el.X) else: sp_to_remove = min(supercell.composition.keys(), key=lambda el: el.X) # Confirm species are of opposite oxidation states. assert sp_to_remove.oxi_state * sp.oxi_state < 0 ox_diff = int(abs(round(sp.oxi_state - ox))) anion_ox = int(abs(sp_to_remove.oxi_state)) nx = supercell.composition[sp_to_remove] common_charge = lcm(anion_ox, ox_diff) ndopant = common_charge / ox_diff nx_to_remove = common_charge / anion_ox logger.info( "Doping %d %s with %s and removing %d %s." % (ndopant, sp, self.dopant, nx_to_remove, sp_to_remove)) supercell.replace_species({ sp: { sp: (nsp - ndopant) / nsp, self.dopant: ndopant / nsp }, sp_to_remove: { sp_to_remove: (nx - nx_to_remove) / nx } }) ss = t.apply_transformation( supercell, return_ranked_list=self.max_structures_per_enum) logger.info("%s distinct structures" % len(ss)) all_structures.extend(ss) logger.info("Total %s doped structures" % len(all_structures)) if return_ranked_list: return all_structures[:return_ranked_list] return all_structures[0]["structure"]
def __init__(self, initial_structure, miller_index, min_slab_size, min_vacuum_size, lll_reduce=False, center_slab=False, primitive=True, max_normal_search=None): """ Calculates the slab scale factor and uses it to generate a unit cell of the initial structure that has been oriented by its miller index. Also stores the initial information needed later on to generate a slab. Args: initial_structure (Structure): Initial input structure. Note that to ensure that the miller indices correspond to usual crystallographic definitions, you should supply a conventional unit cell structure. miller_index ([h, k, l]): Miller index of plane parallel to surface. Note that this is referenced to the input structure. If you need this to be based on the conventional cell, you should supply the conventional structure. min_slab_size (float): In Angstroms min_vac_size (float): In Angstroms lll_reduce (bool): Whether to perform an LLL reduction on the eventual structure. center_slab (bool): Whether to center the slab in the cell with equal vacuum spacing from the top and bottom. primitive (bool): Whether to reduce any generated slabs to a primitive cell (this does **not** mean the slab is generated from a primitive cell, it simply means that after slab generation, we attempt to find shorter lattice vectors, which lead to less surface area and smaller cells). max_normal_search (int): 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. This has no bearing on surface energies, but may be useful as a preliminary step to generating slabs for absorption and other sizes. It is typical that this will not be the smallest possible cell for simulation. Normality is not guaranteed, but the oriented cell will have the c vector as normal as possible (within the search range) to the surface. A value of up to the max absolute Miller index is usually sufficient. """ latt = initial_structure.lattice miller_index = reduce_vector(miller_index) #Calculate the surface normal using the reciprocal lattice vector. recp = latt.reciprocal_lattice_crystallographic normal = recp.get_cartesian_coords(miller_index) normal /= np.linalg.norm(normal) slab_scale_factor = [] non_orth_ind = [] eye = np.eye(3, dtype=np.int) for i, j in enumerate(miller_index): if j == 0: # Lattice vector is perpendicular to surface normal, i.e., # in plane of surface. We will simply choose this lattice # vector as one of the basis vectors. slab_scale_factor.append(eye[i]) else: #Calculate projection of lattice vector onto surface normal. d = abs(np.dot(normal, latt.matrix[i])) / latt.abc[i] non_orth_ind.append((i, d)) # We want the vector that has maximum magnitude in the # direction of the surface normal as the c-direction. # Results in a more "orthogonal" unit cell. c_index, dist = max(non_orth_ind, key=lambda t: t[1]) if len(non_orth_ind) > 1: lcm_miller = lcm(*[miller_index[i] for i, d in non_orth_ind]) for (i, di), (j, dj) in itertools.combinations(non_orth_ind, 2): l = [0, 0, 0] l[i] = -int(round(lcm_miller / miller_index[i])) l[j] = int(round(lcm_miller / miller_index[j])) slab_scale_factor.append(l) if len(slab_scale_factor) == 2: break if max_normal_search is None: slab_scale_factor.append(eye[c_index]) else: index_range = sorted( reversed(range(-max_normal_search, max_normal_search + 1)), key=lambda x: abs(x)) candidates = [] for uvw in itertools.product(index_range, index_range, index_range): if (not any(uvw)) or abs( np.linalg.det(slab_scale_factor + [uvw])) < 1e-8: continue vec = latt.get_cartesian_coords(uvw) l = np.linalg.norm(vec) cosine = abs(np.dot(vec, normal) / l) candidates.append((uvw, cosine, l)) if abs(abs(cosine) - 1) < 1e-8: # If cosine of 1 is found, no need to search further. break # We want the indices with the maximum absolute cosine, # but smallest possible length. uvw, cosine, l = max(candidates, key=lambda x: (x[1], -l)) slab_scale_factor.append(uvw) slab_scale_factor = np.array(slab_scale_factor) # Let's make sure we have a left-handed crystallographic system if np.linalg.det(slab_scale_factor) < 0: slab_scale_factor *= -1 # Make sure the slab_scale_factor is reduced to avoid # unnecessarily large slabs reduced_scale_factor = [reduce_vector(v) for v in slab_scale_factor] slab_scale_factor = np.array(reduced_scale_factor) single = initial_structure.copy() single.make_supercell(slab_scale_factor) self.oriented_unit_cell = Structure.from_sites(single, to_unit_cell=True) self.parent = initial_structure self.lll_reduce = lll_reduce self.center_slab = center_slab self.slab_scale_factor = slab_scale_factor self.miller_index = miller_index self.min_vac_size = min_vacuum_size self.min_slab_size = min_slab_size self.primitive = primitive self._normal = normal a, b, c = self.oriented_unit_cell.lattice.matrix self._proj_height = abs(np.dot(normal, c))
def __init__(self, initial_structure, miller_index, min_slab_size, min_vacuum_size, lll_reduce=False, center_slab=False, primitive=True): """ Calculates the slab scale factor and uses it to generate a unit cell of the initial structure that has been oriented by its miller index. Also stores the initial information needed later on to generate a slab. Args: initial_structure (Structure): Initial input structure. miller_index ([h, k, l]): Miller index of plane parallel to surface. Note that this is referenced to the input structure. If you need this to be based on the conventional cell, you should supply the conventional structure. min_slab_size (float): In Angstroms min_vac_size (float): In Angstroms lll_reduce (bool): Whether to perform an LLL reduction on the eventual structure. center_slab (bool): Whether to center the slab in the cell with equal vacuum spacing from the top and bottom. primitive (bool): Whether to reduce any generated slabs to a primitive cell (this does **not** mean the slab is generated from a primitive cell, it simply means that after slab generation, we attempt to find shorter lattice vectors, which lead to less surface area and smaller cells). """ latt = initial_structure.lattice d = abs(reduce(gcd, miller_index)) miller_index = tuple([int(i / d) for i in miller_index]) #Calculate the surface normal using the reciprocal lattice vector. recp = latt.reciprocal_lattice_crystallographic normal = recp.get_cartesian_coords(miller_index) normal /= np.linalg.norm(normal) slab_scale_factor = [] non_orth_ind = [] eye = np.eye(3, dtype=np.int) for i, j in enumerate(miller_index): if j == 0: # Lattice vector is perpendicular to surface normal, i.e., # in plane of surface. We will simply choose this lattice # vector as one of the basis vectors. slab_scale_factor.append(eye[i]) else: #Calculate projection of lattice vector onto surface normal. d = abs(np.dot(normal, latt.matrix[i])) / latt.abc[i] non_orth_ind.append((i, d)) # We want the vector that has maximum magnitude in the # direction of the surface normal as the c-direction. # Results in a more "orthogonal" unit cell. c_index, dist = max(non_orth_ind, key=lambda t: t[1]) if len(non_orth_ind) > 1: lcm_miller = lcm(*[miller_index[i] for i, d in non_orth_ind]) for (i, di), (j, dj) in itertools.combinations(non_orth_ind, 2): l = [0, 0, 0] l[i] = -int(round(lcm_miller / miller_index[i])) l[j] = int(round(lcm_miller / miller_index[j])) slab_scale_factor.append(l) if len(slab_scale_factor) == 2: break slab_scale_factor.append(eye[c_index]) slab_scale_factor = np.array(slab_scale_factor) # Let's make sure we have a left-handed crystallographic system if np.linalg.det(slab_scale_factor) < 0: slab_scale_factor *= -1 single = initial_structure.copy() single.make_supercell(slab_scale_factor) self.oriented_unit_cell = Structure.from_sites(single, to_unit_cell=True) self.parent = initial_structure self.lll_reduce = lll_reduce self.center_slab = center_slab self.slab_scale_factor = slab_scale_factor self.miller_index = miller_index self.min_vac_size = min_vacuum_size self.min_slab_size = min_slab_size self.primitive = primitive self._normal = normal a, b, c = self.oriented_unit_cell.lattice.matrix self._proj_height = abs(np.dot(normal, c))
def _gen_input_file(self): """ Generate the necessary struct_enum.in file for enumlib. See enumlib documentation for details. """ coord_format = "{:.6f} {:.6f} {:.6f}" # Using symmetry finder, get the symmetrically distinct sites. fitter = SpacegroupAnalyzer(self.structure, self.symm_prec) symmetrized_structure = fitter.get_symmetrized_structure() logger.debug("Spacegroup {} ({}) with {} distinct sites".format( fitter.get_space_group_symbol(), fitter.get_space_group_number(), len(symmetrized_structure.equivalent_sites)) ) """ Enumlib doesn"t work when the number of species get too large. To simplify matters, we generate the input file only with disordered sites and exclude the ordered sites from the enumeration. The fact that different disordered sites with the exact same species may belong to different equivalent sites is dealt with by having determined the spacegroup earlier and labelling the species differently. """ # index_species and index_amounts store mappings between the indices # used in the enum input file, and the actual species and amounts. index_species = [] index_amounts = [] # Stores the ordered sites, which are not enumerated. ordered_sites = [] disordered_sites = [] coord_str = [] for sites in symmetrized_structure.equivalent_sites: if sites[0].is_ordered: ordered_sites.append(sites) else: sp_label = [] species = {k: v for k, v in sites[0].species.items()} if sum(species.values()) < 1 - EnumlibAdaptor.amount_tol: # Let us first make add a dummy element for every single # site whose total occupancies don't sum to 1. species[DummySpecie("X")] = 1 - sum(species.values()) for sp in species.keys(): if sp not in index_species: index_species.append(sp) sp_label.append(len(index_species) - 1) index_amounts.append(species[sp] * len(sites)) else: ind = index_species.index(sp) sp_label.append(ind) index_amounts[ind] += species[sp] * len(sites) sp_label = "/".join(["{}".format(i) for i in sorted(sp_label)]) for site in sites: coord_str.append("{} {}".format( coord_format.format(*site.coords), sp_label)) disordered_sites.append(sites) def get_sg_info(ss): finder = SpacegroupAnalyzer(Structure.from_sites(ss), self.symm_prec) return finder.get_space_group_number() target_sgnum = get_sg_info(symmetrized_structure.sites) curr_sites = list(itertools.chain.from_iterable(disordered_sites)) sgnum = get_sg_info(curr_sites) ordered_sites = sorted(ordered_sites, key=lambda sites: len(sites)) logger.debug("Disordered sites has sg # %d" % (sgnum)) self.ordered_sites = [] # progressively add ordered sites to our disordered sites # until we match the symmetry of our input structure if self.check_ordered_symmetry: while sgnum != target_sgnum and len(ordered_sites) > 0: sites = ordered_sites.pop(0) temp_sites = list(curr_sites) + sites new_sgnum = get_sg_info(temp_sites) if sgnum != new_sgnum: logger.debug("Adding %s in enum. New sg # %d" % (sites[0].specie, new_sgnum)) index_species.append(sites[0].specie) index_amounts.append(len(sites)) sp_label = len(index_species) - 1 for site in sites: coord_str.append("{} {}".format( coord_format.format(*site.coords), sp_label)) disordered_sites.append(sites) curr_sites = temp_sites sgnum = new_sgnum else: self.ordered_sites.extend(sites) for sites in ordered_sites: self.ordered_sites.extend(sites) self.index_species = index_species lattice = self.structure.lattice output = [self.structure.formula, "bulk"] for vec in lattice.matrix: output.append(coord_format.format(*vec)) output.append("%d" % len(index_species)) output.append("%d" % len(coord_str)) output.extend(coord_str) output.append("{} {}".format(self.min_cell_size, self.max_cell_size)) output.append(str(self.enum_precision_parameter)) output.append("partial") ndisordered = sum([len(s) for s in disordered_sites]) base = int(ndisordered*lcm(*[f.limit_denominator(ndisordered * self.max_cell_size).denominator for f in map(fractions.Fraction, index_amounts)])) # This multiplicative factor of 10 is to prevent having too small bases # which can lead to rounding issues in the next step. # An old bug was that a base was set to 8, with a conc of 0.4:0.6. That # resulted in a range that overlaps and a conc of 0.5 satisfying this # enumeration. See Cu7Te5.cif test file. base *= 10 # base = ndisordered #10 ** int(math.ceil(math.log10(ndisordered))) # To get a reasonable number of structures, we fix concentrations to the # range expected in the original structure. total_amounts = sum(index_amounts) for amt in index_amounts: conc = amt / total_amounts if abs(conc * base - round(conc * base)) < 1e-5: output.append("{} {} {}".format(int(round(conc * base)), int(round(conc * base)), base)) else: min_conc = int(math.floor(conc * base)) output.append("{} {} {}".format(min_conc - 1, min_conc + 1, base)) output.append("") logger.debug("Generated input file:\n{}".format("\n".join(output))) with open("struct_enum.in", "w") as f: f.write("\n".join(output))