def _get_valences(self): """ Computes ionic valences of elements for all sites in the structure. """ try: bv = BVAnalyzer() self._structure = bv.get_oxi_state_decorated_structure( self._structure) valences = bv.get_valences(self._structure) except: try: bv = BVAnalyzer(symm_tol=0.0) self._structure = bv.get_oxi_state_decorated_structure( self._structure) valences = bv.get_valences(self._structure) except: valences = [0] * self._structure.num_sites #raise #el = [site.specie.symbol for site in self._structure.sites] #el = [site.species_string for site in self._structure.sites] #el = [site.specie for site in self._structure.sites] #valence_dict = dict(zip(el, valences)) #print valence_dict return valences
class BVAnalyzerTest(PymatgenTest): def setUp(self): self.analyzer = BVAnalyzer() def test_get_valence(self): s = Structure.from_file(os.path.join(test_dir, "LiMn2O4.json")) ans = [1, 1, 3, 3, 4, 4, -2, -2, -2, -2, -2, -2, -2, -2] self.assertEqual(self.analyzer.get_valences(s), ans) s = self.get_structure("LiFePO4") ans = [1, 1, 1, 1, 2, 2, 2, 2, 5, 5, 5, 5, -2, -2, -2, -2, -2, -2, -2, - 2, -2, -2, -2, -2, -2, -2, -2, -2] self.assertEqual(self.analyzer.get_valences(s), ans) s = self.get_structure("Li3V2(PO4)3") ans = [1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 5, 5, 5, 5, 5, 5, -2, -2, -2, -2, - 2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, - 2, -2, -2, -2] self.assertEqual(self.analyzer.get_valences(s), ans) s = Structure.from_file(os.path.join(test_dir, "Li4Fe3Mn1(PO4)4.json")) ans = [1, 1, 1, 1, 2, 2, 2, 2, 5, 5, 5, 5, -2, -2, -2, -2, -2, -2, -2, - 2, -2, -2, -2, -2, -2, -2, -2, -2] self.assertEqual(self.analyzer.get_valences(s), ans) s = self.get_structure("NaFePO4") ans = [1, 1, 1, 1, 2, 2, 2, 2, 5, 5, 5, 5, -2, -2, -2, -2, -2, -2, -2, - 2, -2, -2, -2, -2, -2, -2, -2, -2] self.assertEqual(self.analyzer.get_valences(s), ans) def test_get_oxi_state_structure(self): s = Structure.from_file(os.path.join(test_dir, "LiMn2O4.json")) news = self.analyzer.get_oxi_state_decorated_structure(s) self.assertIn(Specie("Mn", 3), news.composition.elements) self.assertIn(Specie("Mn", 4), news.composition.elements)
def from_py_struct(structure: pymatgen.core.Structure): """Create a SmactStructure from a pymatgen Structure object. Args: structure: A pymatgen Structure. Returns: :class:`~.SmactStructure` """ if not isinstance(structure, pymatgen.core.Structure): raise TypeError("Structure must be a pymatgen.core.Structure instance.") bva = BVAnalyzer() struct = bva.get_oxi_state_decorated_structure(structure) sites, species = SmactStructure.__parse_py_sites(struct) lattice_mat = struct.lattice.matrix lattice_param = 1.0 return SmactStructure( species, lattice_mat, sites, lattice_param, sanitise_species=True, )
def get_basic_analysis_and_error_checks(d): initial_vol = d["input"]["crystal"]["lattice"]["volume"] final_vol = d["output"]["crystal"]["lattice"]["volume"] delta_vol = final_vol - initial_vol percent_delta_vol = delta_vol / initial_vol coord_num = get_coordination_numbers(d) calc = d["calculations"][-1] gap = calc["output"]["bandgap"] cbm = calc["output"]["cbm"] vbm = calc["output"]["vbm"] is_direct = calc["output"]["is_gap_direct"] if abs(percent_delta_vol) > 0.20: warning_msgs = ["Volume change > 20%"] else: warning_msgs = [] bv_struct = Structure.from_dict(d["output"]["crystal"]) try: bva = BVAnalyzer() bv_struct = bva.get_oxi_state_decorated_structure(bv_struct) except ValueError as e: logger.error("Valence cannot be determined due to {e}." .format(e=e)) except Exception as ex: logger.error("BVAnalyzer error {e}.".format(e=str(ex))) return {"delta_volume": delta_vol, "percent_delta_volume": percent_delta_vol, "warnings": warning_msgs, "coordination_numbers": coord_num, "bandgap": gap, "cbm": cbm, "vbm": vbm, "is_gap_direct": is_direct, "bv_structure": bv_struct.to_dict}
def predict(self, structure, ref_structure, test_isostructural=True): """ Given a structure, returns back the predicted volume. Args: structure (Structure): structure w/unknown volume ref_structure (Structure): A reference structure with a similar structure but different species. test_isostructural (bool): Whether to test that the two structures are isostructural. This algo works best for isostructural compounds. Defaults to True. Returns: a float value of the predicted volume """ if not is_ox(structure): a = BVAnalyzer() structure = a.get_oxi_state_decorated_structure(structure) if not is_ox(ref_structure): a = BVAnalyzer() ref_structure = a.get_oxi_state_decorated_structure(ref_structure) if test_isostructural: m = StructureMatcher() mapping = m.get_best_electronegativity_anonymous_mapping( structure, ref_structure) if mapping is None: raise ValueError("Input structures do not match!") comp = structure.composition ref_comp = ref_structure.composition numerator = 0 denominator = 0 # Here, the 1/3 factor on the composition accounts for atomic # packing. We want the number per unit length. # TODO: AJ doesn't understand the (1/3). It would make sense to him # if you were doing atomic volume and not atomic radius for k, v in comp.items(): numerator += k.ionic_radius * v**(1 / 3) for k, v in ref_comp.items(): denominator += k.ionic_radius * v**(1 / 3) # The scaling factor is based on lengths. We apply a power of 3. return ref_structure.volume * (numerator / denominator)**3
def add_bv_structure(doc): struc = Structure.from_dict(doc["structure"]) try: bva = BVAnalyzer() bv_struct = bva.get_oxi_state_decorated_structure(struc) doc["bv_structure"] = bv_struct.as_dict() except Exception as e: print("BVAnalyzer error: {}".format(e))
def predict(self, structure, ref_structure, test_isostructural=True): """ Given a structure, returns back the predicted volume. Args: structure (Structure): structure w/unknown volume ref_structure (Structure): A reference structure with a similar structure but different species. test_isostructural (bool): Whether to test that the two structures are isostructural. This algo works best for isostructural compounds. Defaults to True. Returns: a float value of the predicted volume """ if not is_ox(structure): a = BVAnalyzer() structure = a.get_oxi_state_decorated_structure(structure) if not is_ox(ref_structure): a = BVAnalyzer() ref_structure = a.get_oxi_state_decorated_structure(ref_structure) if test_isostructural: m = StructureMatcher() mapping = m.get_best_electronegativity_anonymous_mapping(structure, ref_structure) if mapping is None: raise ValueError("Input structures do not match!") comp = structure.composition ref_comp = ref_structure.composition numerator = 0 denominator = 0 # Here, the 1/3 factor on the composition accounts for atomic # packing. We want the number per unit length. # TODO: AJ doesn't understand the (1/3). It would make sense to him # if you were doing atomic volume and not atomic radius for k, v in comp.items(): numerator += k.ionic_radius * v ** (1 / 3) for k, v in ref_comp.items(): denominator += k.ionic_radius * v ** (1 / 3) # The scaling factor is based on lengths. We apply a power of 3. return ref_structure.volume * (numerator / denominator) ** 3
def get_basic_analysis_and_error_checks(d, max_force_threshold=0.5, volume_change_threshold=0.2): initial_vol = d["input"]["crystal"]["lattice"]["volume"] final_vol = d["output"]["crystal"]["lattice"]["volume"] delta_vol = final_vol - initial_vol percent_delta_vol = delta_vol / initial_vol coord_num = get_coordination_numbers(d) calc = d["calculations"][-1] gap = calc["output"]["bandgap"] cbm = calc["output"]["cbm"] vbm = calc["output"]["vbm"] is_direct = calc["output"]["is_gap_direct"] warning_msgs = [] error_msgs = [] if abs(percent_delta_vol) > volume_change_threshold: warning_msgs.append("Volume change > {}%" .format(volume_change_threshold * 100)) bv_struct = Structure.from_dict(d["output"]["crystal"]) try: bva = BVAnalyzer() bv_struct = bva.get_oxi_state_decorated_structure(bv_struct) except ValueError as e: logger.error("Valence cannot be determined due to {e}." .format(e=e)) except Exception as ex: logger.error("BVAnalyzer error {e}.".format(e=str(ex))) max_force = None if d["state"] == "successful" and \ d["calculations"][0]["input"]["parameters"].get("NSW", 0) > 0: # handle the max force and max force error max_force = max([np.linalg.norm(a) for a in d["calculations"][-1]["output"] ["ionic_steps"][-1]["forces"]]) if max_force > max_force_threshold: error_msgs.append("Final max force exceeds {} eV" .format(max_force_threshold)) d["state"] = "error" s = Structure.from_dict(d["output"]["crystal"]) if not s.is_valid(): error_msgs.append("Bad structure (atoms are too close!)") d["state"] = "error" return {"delta_volume": delta_vol, "max_force": max_force, "percent_delta_volume": percent_delta_vol, "warnings": warning_msgs, "errors": error_msgs, "coordination_numbers": coord_num, "bandgap": gap, "cbm": cbm, "vbm": vbm, "is_gap_direct": is_direct, "bv_structure": bv_struct.as_dict()}
class AutoOxiStateDecorationTransformation(AbstractTransformation): """ This transformation automatically decorates a structure with oxidation states using a bond valence approach. """ def __init__( self, symm_tol=0.1, max_radius=4, max_permutations=100000, distance_scale_factor=1.015, ): """ Args: symm_tol (float): Symmetry tolerance used to determine which sites are symmetrically equivalent. Set to 0 to turn off symmetry. max_radius (float): Maximum radius in Angstrom used to find nearest neighbors. max_permutations (int): Maximum number of permutations of oxidation states to test. distance_scale_factor (float): A scale factor to be applied. This is useful for scaling distances, esp in the case of calculation-relaxed structures, which may tend to under (GGA) or over bind (LDA). The default of 1.015 works for GGA. For experimental structure, set this to 1. """ self.symm_tol = symm_tol self.max_radius = max_radius self.max_permutations = max_permutations self.distance_scale_factor = distance_scale_factor self.analyzer = BVAnalyzer(symm_tol, max_radius, max_permutations, distance_scale_factor) def apply_transformation(self, structure): """ Apply the transformation. Args: structure (Structure): Input Structure Returns: Oxidation state decorated Structure. """ return self.analyzer.get_oxi_state_decorated_structure(structure) @property def inverse(self): """ Returns: None """ return None @property def is_one_to_many(self): """ Returns: False """ return False
class AutoOxiStateDecorationTransformation(AbstractTransformation): """ This transformation automatically decorates a structure with oxidation states using a bond valence approach. """ def __init__(self, symm_tol=0.1, max_radius=4, max_permutations=100000, distance_scale_factor=1.015): """ Args: symm_tol: Symmetry tolerance used to determine which sites are symmetrically equivalent. Set to 0 to turn off symmetry. max_radius: Maximum radius in Angstrom used to find nearest neighbors. max_permutations: The maximum number of permutations of oxidation states to test. distance_scale_factor: A scale factor to be applied. This is useful for scaling distances, esp in the case of calculation-relaxed structures which may tend to under (GGA) or over bind (LDA). The default of 1.015 works for GGA. For experimental structure, set this to 1. """ self.analyzer = BVAnalyzer(symm_tol, max_radius, max_permutations, distance_scale_factor) def apply_transformation(self, structure): return self.analyzer.get_oxi_state_decorated_structure(structure) @property def inverse(self): return None @property def is_one_to_many(self): return False @property def to_dict(self): return { "name": self.__class__.__name__, "version": __version__, "init_args": { "symm_tol": self.analyzer.symm_tol, "max_radius": self.analyzer.max_radius, "max_permutations": self.analyzer.max_permutations, "distance_scale_factor": self.analyzer.dist_scale_factor }, "@module": self.__class__.__module__, "@class": self.__class__.__name__ }
class AutoOxiStateDecorationTransformation(AbstractTransformation): """ This transformation automatically decorates a structure with oxidation states using a bond valence approach. """ def __init__(self, symm_tol=0.1, max_radius=4, max_permutations=100000, distance_scale_factor=1.015): """ Args: symm_tol: Symmetry tolerance used to determine which sites are symmetrically equivalent. Set to 0 to turn off symmetry. max_radius: Maximum radius in Angstrom used to find nearest neighbors. max_permutations: The maximum number of permutations of oxidation states to test. distance_scale_factor: A scale factor to be applied. This is useful for scaling distances, esp in the case of calculation-relaxed structures which may tend to under (GGA) or over bind (LDA). The default of 1.015 works for GGA. For experimental structure, set this to 1. """ self.analyzer = BVAnalyzer(symm_tol, max_radius, max_permutations, distance_scale_factor) def apply_transformation(self, structure): return self.analyzer.get_oxi_state_decorated_structure(structure) @property def inverse(self): return None @property def is_one_to_many(self): return False @property def to_dict(self): return { "name": self.__class__.__name__, "version": __version__, "init_args": { "symm_tol": self.analyzer.symm_tol, "max_radius": self.analyzer.max_radius, "max_permutations": self.analyzer.max_permutations, "distance_scale_factor": self.analyzer.dist_scale_factor, }, "@module": self.__class__.__module__, "@class": self.__class__.__name__, }
def _get_valences(self): """ Computes ionic valences of elements for all sites in the structure. """ try: bv = BVAnalyzer() self._structure = bv.get_oxi_state_decorated_structure(self._structure) valences = bv.get_valences(self._structure) except: try: bv = BVAnalyzer(symm_tol=0.0) self._structure = bv.get_oxi_state_decorated_structure(self._structure) valences = bv.get_valences(self._structure) except: raise #print valences #el = [site.specie.symbol for site in self._structure.sites] #el = [site.species_string for site in self._structure.sites] #el = [site.specie for site in self._structure.sites] #valence_dict = dict(zip(el, valences)) #print valence_dict return valences
def from_mp( species: List[Union[Tuple[str, int, int], Tuple[smact.Species, int]]], api_key: str, ): """Create a SmactStructure using the first Materials Project entry for a composition. Args: species: See :meth:`~.__init__`. api_key: A www.materialsproject.org API key. Returns: :class:`~.SmactStructure` """ sanit_species = SmactStructure._sanitise_species(species) with MPRester(api_key) as m: eles = SmactStructure._get_ele_stoics(sanit_species) formula = "".join(f"{ele}{stoic}" for ele, stoic in eles.items()) structs = m.query( criteria={"reduced_cell_formula": formula}, properties=["structure"], ) if len(structs) == 0: raise ValueError( "Could not find composition in Materials Project Database, " "please supply a structure.") struct = structs[0][ 'structure'] # Default to first found structure if 0 not in (spec[1] for spec in sanit_species): # If everything's charged bva = BVAnalyzer() struct = bva.get_oxi_state_decorated_structure(struct) lattice_mat = struct.lattice.matrix lattice_param = 1.0 # TODO Use actual lattice parameter sites, _ = SmactStructure.__parse_py_sites(struct) return SmactStructure( sanit_species, lattice_mat, sites, lattice_param, sanitise_species=False, )
def setUp(self): filepath = os.path.join(test_dir, 'POSCAR') p = Poscar.from_file(filepath) self.structure = p.structure bv = BVAnalyzer() self.structure = bv.get_oxi_state_decorated_structure(self.structure) valences = bv.get_valences(self.structure) radii = [] for i in range(len(valences)): el = self.structure.sites[i].specie.symbol radius = Specie(el, valences[i]).ionic_radius radii.append(radius) el = [site.species_string for site in self.structure.sites] self.rad_dict = dict(zip(el, radii)) for el in self.rad_dict.keys(): print((el, self.rad_dict[el].real))
def setUp(self): filepath = os.path.join(test_dir, 'POSCAR') p = Poscar.from_file(filepath) self.structure = p.structure bv = BVAnalyzer() self.structure = bv.get_oxi_state_decorated_structure(self.structure) valences = bv.get_valences(self.structure) radii = [] for i in range(len(valences)): el = self.structure.sites[i].specie.symbol radius = Specie(el, valences[i]).ionic_radius radii.append(radius) el = [site.species_string for site in self.structure.sites] self.rad_dict = dict(zip(el, radii)) for el in self.rad_dict.keys(): print((el, self.rad_dict[el].real))
def setUp(self): """ Setup MgO rocksalt structure for testing Vacancy """ mgo_latt = [[4.212, 0, 0], [0, 4.212, 0], [0, 0, 4.212]] mgo_specie = ["Mg"] * 4 + ["O"] * 4 mgo_frac_cord = [[0, 0, 0], [0.5, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0.5], [0.5, 0, 0], [0, 0.5, 0], [0, 0, 0.5], [0.5, 0.5, 0.5]] self._mgo_uc = Structure(mgo_latt, mgo_specie, mgo_frac_cord, True, True) bv = BVAnalyzer() self._mgo_uc = bv.get_oxi_state_decorated_structure(self._mgo_uc) self._mgo_val_rad_eval = ValenceIonicRadiusEvaluator(self._mgo_uc) self._mgo_val = self._mgo_val_rad_eval.valences self._mgo_rad = self._mgo_val_rad_eval.radii self._mgo_vac = Vacancy(self._mgo_uc, self._mgo_val, self._mgo_rad)
def setUp(self): """ Setup MgO rocksalt structure for testing Vacancy """ mgo_latt = [[4.212, 0, 0], [0, 4.212, 0], [0, 0, 4.212]] mgo_specie = ["Mg"] * 4 + ["O"] * 4 mgo_frac_cord = [[0, 0, 0], [0.5, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0.5], [0.5, 0, 0], [0, 0.5, 0], [0, 0, 0.5], [0.5, 0.5, 0.5]] self._mgo_uc = Structure(mgo_latt, mgo_specie, mgo_frac_cord, True, True) bv = BVAnalyzer() self._mgo_uc = bv.get_oxi_state_decorated_structure(self._mgo_uc) self._mgo_val_rad_eval = ValenceIonicRadiusEvaluator(self._mgo_uc) self._mgo_val = self._mgo_val_rad_eval.valences self._mgo_rad = self._mgo_val_rad_eval.radii self._mgo_vac = Vacancy(self._mgo_uc, self._mgo_val, self._mgo_rad)
class BVAnalyzerTest(unittest.TestCase): def setUp(self): self.analyzer = BVAnalyzer() def test_get_valence(self): parser = CifParser(os.path.join(test_dir, "LiMn2O4.cif")) s = parser.get_structures()[0] ans = [1, 1, 3, 3, 4, 4, -2, -2, -2, -2, -2, -2, -2, -2] self.assertEqual(self.analyzer.get_valences(s), ans) parser = CifParser(os.path.join(test_dir, "LiFePO4.cif")) s = parser.get_structures()[0] ans = [ 1, 1, 1, 1, 2, 2, 2, 2, 5, 5, 5, 5, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 ] self.assertEqual(self.analyzer.get_valences(s), ans) parser = CifParser(os.path.join(test_dir, "Li3V2(PO4)3.cif")) s = parser.get_structures()[0] ans = [ 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 5, 5, 5, 5, 5, 5, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 ] self.assertEqual(self.analyzer.get_valences(s), ans) parser = CifParser(os.path.join(test_dir, "Li4Fe3Mn1(PO4)4.cif")) s = parser.get_structures()[0] ans = [ 1, 1, 1, 1, 2, 2, 2, 2, 5, 5, 5, 5, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 ] self.assertEqual(self.analyzer.get_valences(s), ans) parser = CifParser(os.path.join(test_dir, "NaFePO4.cif")) s = parser.get_structures()[0] ans = [ 1, 1, 1, 1, 2, 2, 2, 2, 5, 5, 5, 5, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 ] self.assertEqual(self.analyzer.get_valences(s), ans) def test_get_oxi_state_structure(self): parser = CifParser(os.path.join(test_dir, "LiMn2O4.cif")) s = parser.get_structures()[0] news = self.analyzer.get_oxi_state_decorated_structure(s) self.assertIn(Specie("Mn", 3), news.composition.elements) self.assertIn(Specie("Mn", 4), news.composition.elements)
def get_valences(self): """ Uses Pymatgen to obtain likely valence states of every element in structure Returns vals: dictionary of average valence state for every element in composition """ struct = self.structure bv = BVAnalyzer() try: valences = bv.get_valences(struct) struct = bv.get_oxi_state_decorated_structure(struct) except: return None if isinstance(valences[0], list): valences = [item for sublist in valences for item in sublist] stoich = defaultdict(int) for site in struct.as_dict()['sites']: elem = site['species'][0]['element'] stoich[elem] += 1 vals = {} for spec in stoich.keys(): vals[spec] = 0.0 for atom in range(len(struct)): try: vals[struct.as_dict()['sites'][atom]['species'][0] ['element']] += valences[atom] except Exception as e: print("Trouble with {}".format(struct.formula)) print('Do you have partial occupancies?') return None for spec in vals: vals[spec] = vals[spec] / stoich[spec] vals[spec] = int(round(vals[spec])) return vals
def __init__(self, structure, valences, radii): """ Given a structure, generate symmetrically distinct interstitial sites. Args: structure: pymatgen.core.structure.Structure valences: Dictionary of oxidation states of elements in { El:valence} form radii: Radii of elemnts in the structure """ bv = BVAnalyzer() self._structure = bv.get_oxi_state_decorated_structure(structure) #self._structure = structure self._valence_dict = valences self._rad_dict = radii #Use Zeo++ to obtain the voronoi nodes. Apply symmetry reduction and #the symmetry reduced voronoi nodes #are possible candidates for interstitial sites #try: possible_interstitial_sites = symmetry_reduced_voronoi_nodes( self._structure, self._rad_dict) #except: # raise ValueError("Symmetry_reduced_voronoi_nodes failed") #Do futher processing on possibleInterstitialSites to obtain #interstitial sites self._defect_sites = possible_interstitial_sites self._defectsite_coord_no = [] self._defect_coord_sites = [] self._defect_coord_charge = [] self._radii = [] for site in self._defect_sites: coord_no, coord_sites, chrg = self._get_coord_no_sites_chrg(site) self._defectsite_coord_no.append(coord_no) self._defect_coord_sites.append(coord_sites) self._defect_coord_charge.append(chrg) for site in self._defect_sites: self._radii.append(float(site.properties['voronoi_radius']))
class AutoOxiStateDecorationTransformation(AbstractTransformation): """ This transformation automatically decorates a structure with oxidation states using a bond valence approach. Args: symm_tol (float): Symmetry tolerance used to determine which sites are symmetrically equivalent. Set to 0 to turn off symmetry. max_radius (float): Maximum radius in Angstrom used to find nearest neighbors. max_permutations (int): Maximum number of permutations of oxidation states to test. distance_scale_factor (float): A scale factor to be applied. This is useful for scaling distances, esp in the case of calculation-relaxed structures, which may tend to under (GGA) or over bind (LDA). The default of 1.015 works for GGA. For experimental structure, set this to 1. """ def __init__(self, symm_tol=0.1, max_radius=4, max_permutations=100000, distance_scale_factor=1.015): self.symm_tol = symm_tol self.max_radius = max_radius self.max_permutations = max_permutations self.distance_scale_factor = distance_scale_factor self.analyzer = BVAnalyzer(symm_tol, max_radius, max_permutations, distance_scale_factor) def apply_transformation(self, structure): return self.analyzer.get_oxi_state_decorated_structure(structure) @property def inverse(self): return None @property def is_one_to_many(self): return False
class BVAnalyzerTest(unittest.TestCase): def setUp(self): self.analyzer = BVAnalyzer() def test_get_valence(self): parser = CifParser(os.path.join(test_dir, "LiMn2O4.cif")) s = parser.get_structures()[0] ans = [1, 1, 3, 3, 4, 4, -2, -2, -2, -2, -2, -2, -2, -2] self.assertEqual(self.analyzer.get_valences(s), ans) parser = CifParser(os.path.join(test_dir, "LiFePO4.cif")) s = parser.get_structures()[0] ans = [1, 1, 1, 1, 2, 2, 2, 2, 5, 5, 5, 5, -2, -2, -2, -2, -2, -2, -2, - 2, -2, -2, -2, -2, -2, -2, -2, -2] self.assertEqual(self.analyzer.get_valences(s), ans) parser = CifParser(os.path.join(test_dir, "Li3V2(PO4)3.cif")) s = parser.get_structures()[0] ans = [1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 5, 5, 5, 5, 5, 5, -2, -2, -2, -2, - 2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, - 2, -2, -2, -2] self.assertEqual(self.analyzer.get_valences(s), ans) parser = CifParser(os.path.join(test_dir, "Li4Fe3Mn1(PO4)4.cif")) s = parser.get_structures()[0] ans = [1, 1, 1, 1, 2, 2, 2, 2, 5, 5, 5, 5, -2, -2, -2, -2, -2, -2, -2, - 2, -2, -2, -2, -2, -2, -2, -2, -2] self.assertEqual(self.analyzer.get_valences(s), ans) parser = CifParser(os.path.join(test_dir, "NaFePO4.cif")) s = parser.get_structures()[0] ans = [1, 1, 1, 1, 2, 2, 2, 2, 5, 5, 5, 5, -2, -2, -2, -2, -2, -2, -2, - 2, -2, -2, -2, -2, -2, -2, -2, -2] self.assertEqual(self.analyzer.get_valences(s), ans) def test_get_oxi_state_structure(self): parser = CifParser(os.path.join(test_dir, "LiMn2O4.cif")) s = parser.get_structures()[0] news = self.analyzer.get_oxi_state_decorated_structure(s) self.assertIn(Specie("Mn", 3), news.composition.elements) self.assertIn(Specie("Mn", 4), news.composition.elements)
def get_basic_analysis_and_error_checks(d): initial_vol = d["input"]["crystal"]["lattice"]["volume"] final_vol = d["output"]["crystal"]["lattice"]["volume"] delta_vol = final_vol - initial_vol percent_delta_vol = delta_vol / initial_vol coord_num = get_coordination_numbers(d) calc = d["calculations"][-1] gap = calc["output"]["bandgap"] cbm = calc["output"]["cbm"] vbm = calc["output"]["vbm"] is_direct = calc["output"]["is_gap_direct"] if abs(percent_delta_vol) > 0.20: warning_msgs = ["Volume change > 20%"] else: warning_msgs = [] bv_struct = Structure.from_dict(d["output"]["crystal"]) try: bva = BVAnalyzer() bv_struct = bva.get_oxi_state_decorated_structure(bv_struct) except ValueError as e: logger.error("Valence cannot be determined due to {e}.".format(e=e)) except Exception as ex: logger.error("BVAnalyzer error {e}.".format(e=str(ex))) return { "delta_volume": delta_vol, "percent_delta_volume": percent_delta_vol, "warnings": warning_msgs, "coordination_numbers": coord_num, "bandgap": gap, "cbm": cbm, "vbm": vbm, "is_gap_direct": is_direct, "bv_structure": bv_struct.to_dict }
def Calc_Ewald(pmg_struct, formal_val=[]): """ input: pmg_struct: pymatgen structure formal_val: list - list of valence for each atom TBD: use input formal valence list to decorate the structure """ # GET valence list if len(formal_val)==0: bv_analyzer=BVAnalyzer(max_radius=4) #max_radius default is 4 formal_val=bv_analyzer.get_valences(pmg_struct) # default for cutoff is set to None real_cutoff = None rec_cutoff = None # Oxidation states are decorated automatically. decorated_struct = bv_analyzer.get_oxi_state_decorated_structure(pmg_struct) NUM_sites=pmg_struct.num_sites # Per atom ewald_per_atom=1/NUM_sites*EwaldSummation(decorated_struct, real_space_cut=real_cutoff, recip_space_cut=rec_cutoff).total_energy return ewald_per_atom
def __init__(self, structure, valences, radii, site_type='voronoi_vertex', accuracy='Normal', symmetry_flag=True, oxi_state=False): """ Given a structure, generate symmetrically distinct interstitial sites. Args: structure: pymatgen.core.structure.Structure valences: Dictionary of oxidation states of elements in {el:valence} form radii: Radii of elemnts in the structure site_type: "voronoi_vertex" uses voronoi nodes "voronoi_facecenter" uses voronoi polyhedra face centers Default is "voronoi_vertex" accuracy: Flag denoting whether to use high accuracy version of Zeo++. Options are "Normal" and "High". Default is normal. symmetry_flag: If True, only returns symmetrically distinct sites oxi_state: If False, input structure is considered devoid of oxidation-state decoration. And oxi-state for each site is determined. Use True, if input structure is oxi-state decorated. This option is useful when the structure is not electro-neutral after deleting/adding sites. In that case oxi-decorate the structure before deleting/adding the sites. """ if not oxi_state: try: bv = BVAnalyzer() self._structure = bv.get_oxi_state_decorated_structure( structure) except: try: bv = BVAnalyzer(symm_tol=0.0) self._structure = bv.get_oxi_state_decorated_structure( structure) except: raise else: self._structure = structure self._valence_dict = valences self._rad_dict = radii """ Use Zeo++ to obtain the voronoi nodes. Apply symmetry reduction and the symmetry reduced voronoi nodes are possible candidates for interstitial sites. """ if accuracy == "Normal": high_accuracy_flag = False elif accuracy == "High": high_accuracy_flag = True else: raise ValueError("Accuracy setting not understood.") vor_node_sites, vor_facecenter_sites = symmetry_reduced_voronoi_nodes( self._structure, self._rad_dict, high_accuracy_flag, symmetry_flag) if site_type == 'voronoi_vertex': possible_interstitial_sites = vor_node_sites elif site_type == 'voronoi_facecenter': possible_interstitial_sites = vor_facecenter_sites else: raise ValueError("Input site type not implemented") #Do futher processing on possibleInterstitialSites to obtain #interstitial sites self._defect_sites = possible_interstitial_sites self._defectsite_coord_no = [] self._defect_coord_sites = [] self._defect_coord_charge = [] self._radii = [] for site in self._defect_sites: coord_no, coord_sites, chrg = self._get_coord_no_sites_chrg(site) self._defectsite_coord_no.append(coord_no) self._defect_coord_sites.append(coord_sites) self._defect_coord_charge.append(chrg) for site in self._defect_sites: self._radii.append(float(site.properties['voronoi_radius']))
def __init__( self, structure, valences, radii, site_type="voronoi_vertex", accuracy="Normal", symmetry_flag=True, oxi_state=False, ): """ Given a structure, generate symmetrically distinct interstitial sites. Args: structure: pymatgen.core.structure.Structure valences: Dictionary of oxidation states of elements in {el:valence} form radii: Radii of elemnts in the structure site_type: "voronoi_vertex" uses voronoi nodes "voronoi_facecenter" uses voronoi polyhedra face centers Default is "voronoi_vertex" accuracy: Flag denoting whether to use high accuracy version of Zeo++. Options are "Normal" and "High". Default is normal. symmetry_flag: If True, only returns symmetrically distinct sites oxi_state: If False, input structure is considered devoid of oxidation-state decoration. And oxi-state for each site is determined. Use True, if input structure is oxi-state decorated. This option is useful when the structure is not electro-neutral after deleting/adding sites. In that case oxi-decorate the structure before deleting/adding the sites. """ if not oxi_state: try: bv = BVAnalyzer() self._structure = bv.get_oxi_state_decorated_structure(structure) except: try: bv = BVAnalyzer(symm_tol=0.0) self._structure = bv.get_oxi_state_decorated_structure(structure) except: raise else: self._structure = structure self._valence_dict = valences self._rad_dict = radii """ Use Zeo++ to obtain the voronoi nodes. Apply symmetry reduction and the symmetry reduced voronoi nodes are possible candidates for interstitial sites. """ if accuracy == "Normal": high_accuracy_flag = False elif accuracy == "High": high_accuracy_flag = True else: raise ValueError("Accuracy setting not understood.") vor_node_sites, vor_facecenter_sites = symmetry_reduced_voronoi_nodes( self._structure, self._rad_dict, high_accuracy_flag, symmetry_flag ) if site_type == "voronoi_vertex": possible_interstitial_sites = vor_node_sites elif site_type == "voronoi_facecenter": possible_interstitial_sites = vor_facecenter_sites else: raise ValueError("Input site type not implemented") # Do futher processing on possibleInterstitialSites to obtain # interstitial sites self._defect_sites = possible_interstitial_sites self._defectsite_coord_no = [] self._defect_coord_sites = [] self._defect_coord_charge = [] self._radii = [] for site in self._defect_sites: coord_no, coord_sites, chrg = self._get_coord_no_sites_chrg(site) self._defectsite_coord_no.append(coord_no) self._defect_coord_sites.append(coord_sites) self._defect_coord_charge.append(chrg) for site in self._defect_sites: self._radii.append(float(site.properties["voronoi_radius"]))
def predict(self, structure, ref_structure): """ Given a structure, returns the predicted volume. Args: structure (Structure): structure w/unknown volume ref_structure (Structure): A reference structure with a similar structure but different species. Returns: a float value of the predicted volume """ if self.check_isostructural: m = StructureMatcher() mapping = m.get_best_electronegativity_anonymous_mapping( structure, ref_structure) if mapping is None: raise ValueError("Input structures do not match!") if "ionic" in self.radii_type: try: # Use BV analyzer to determine oxidation states only if the # oxidation states are not already specified in the structure # and use_bv is true. if (not is_ox(structure)) and self.use_bv: a = BVAnalyzer() structure = a.get_oxi_state_decorated_structure(structure) if (not is_ox(ref_structure)) and self.use_bv: a = BVAnalyzer() ref_structure = a.get_oxi_state_decorated_structure( ref_structure) comp = structure.composition ref_comp = ref_structure.composition # Check if all the associated ionic radii are available. if any([k.ionic_radius is None for k in list(comp.keys())]) or \ any([k.ionic_radius is None for k in list(ref_comp.keys())]): raise ValueError("Not all the ionic radii are available!") numerator = 0 denominator = 0 # Here, the 1/3 factor on the composition accounts for atomic # packing. We want the number per unit length. for k, v in comp.items(): numerator += k.ionic_radius * v ** (1 / 3) for k, v in ref_comp.items(): denominator += k.ionic_radius * v ** (1 / 3) return ref_structure.volume * (numerator / denominator) ** 3 except Exception as ex: warnings.warn("Exception occured. Will attempt atomic radii.") # If error occurs during use of ionic radii scheme, pass # and see if we can resolve it using atomic radii. pass if "atomic" in self.radii_type: comp = structure.composition ref_comp = ref_structure.composition # Here, the 1/3 factor on the composition accounts for atomic # packing. We want the number per unit length. numerator = 0 denominator = 0 for k, v in comp.items(): numerator += k.atomic_radius * v ** (1 / 3) for k, v in ref_comp.items(): denominator += k.atomic_radius * v ** (1 / 3) return ref_structure.volume * (numerator / denominator) ** 3 raise ValueError("Cannot find volume scaling based on radii choices " "specified!")
def __init__(self, structure, valences, radii, site_type='voronoi_vertex', accuracy='Normal', symmetry_flag=True): """ Given a structure, generate symmetrically distinct interstitial sites. Args: structure: pymatgen.core.structure.Structure valences: Dictionary of oxidation states of elements in {el:valence} form radii: Radii of elemnts in the structure site_type: "voronoi_vertex" uses voronoi nodes "voronoi_facecenter" uses voronoi polyhedra face centers Default is "voronoi_vertex" accuracy: Flag denoting whether to use high accuracy version of Zeo++. Options are "Normal" and "High". Default is normal. """ try: bv = BVAnalyzer() self._structure = bv.get_oxi_state_decorated_structure(structure) except: try: bv = BVAnalyzer(symm_tol=0.0) self._structure = bv.get_oxi_state_decorated_structure( structure ) except: raise self._valence_dict = valences self._rad_dict = radii """ Use Zeo++ to obtain the voronoi nodes. Apply symmetry reduction and the symmetry reduced voronoi nodes are possible candidates for interstitial sites. """ if accuracy == "Normal": high_accuracy_flag = False elif accuracy == "High": high_accuracy_flag = True else: raise ValueError("Accuracy setting not understood.") vor_node_sites, vor_facecenter_sites = symmetry_reduced_voronoi_nodes( self._structure, self._rad_dict, high_accuracy_flag, symmetry_flag ) if site_type == 'voronoi_vertex': possible_interstitial_sites = vor_node_sites elif site_type == 'voronoi_facecenter': possible_interstitial_sites = vor_facecenter_sites else: raise ValueError("Input site type not implemented") #Do futher processing on possibleInterstitialSites to obtain #interstitial sites self._defect_sites = possible_interstitial_sites self._defectsite_coord_no = [] self._defect_coord_sites = [] self._defect_coord_charge = [] self._radii = [] for site in self._defect_sites: coord_no, coord_sites, chrg = self._get_coord_no_sites_chrg(site) self._defectsite_coord_no.append(coord_no) self._defect_coord_sites.append(coord_sites) self._defect_coord_charge.append(chrg) for site in self._defect_sites: self._radii.append(float(site.properties['voronoi_radius']))
def get_analysis_and_structure(self, structure, calculate_valences=True, guesstimate_spin=False, op_threshold=0.1): """ Obtain an analysis of a given structure and if it may be Jahn-Teller active or not. This is a heuristic, and may give false positives and false negatives (false positives are preferred). :param structure: input structure :param calculate_valences (bool): whether to attempt to calculate valences or not, structure should have oxidation states to perform analysis :param guesstimate_spin (bool): whether to guesstimate spin state from magnetic moments or not, use with caution :param op_threshold (float): threshold for order parameter above which to consider site to match an octahedral or tetrahedral motif, since Jahn-Teller structures can often be quite distorted, this threshold is smaller than one might expect :return (dict): analysis of structure, with key 'strength' which may be 'none', 'strong', 'weak', or 'unknown' """ structure = structure.get_primitive_structure() if calculate_valences: bva = BVAnalyzer() structure = bva.get_oxi_state_decorated_structure(structure) # no point testing multiple equivalent sites, doesn't make any difference to analysis # but makes returned symmetrized_structure = SpacegroupAnalyzer(structure).get_symmetrized_structure() # to detect structural motifs of a given site op = LocalStructOrderParams(['oct', 'tet']) # dict of site index to the Jahn-Teller analysis of that site jt_sites = [] non_jt_sites = [] for indices in symmetrized_structure.equivalent_indices: idx = indices[0] site = symmetrized_structure[idx] # only interested in sites with oxidation states if isinstance(site.specie, Specie) and site.specie.element.is_transition_metal: # get motif around site order_params = op.get_order_parameters(symmetrized_structure, idx) if order_params[0] > order_params[1] and order_params[0] > op_threshold: motif = 'oct' motif_order_parameter = order_params[0] elif order_params[1] > op_threshold: motif = 'tet' motif_order_parameter = order_params[1] else: motif = 'unknown' motif_order_parameter = None if motif == "oct" or motif == "tet": # guess spin of metal ion if guesstimate_spin and 'magmom' in site.properties: # estimate if high spin or low spin magmom = site.properties['magmom'] spin_state = self._estimate_spin_state(site.specie, motif, magmom) else: spin_state = "unknown" magnitude = self.get_magnitude_of_effect_from_species(site.specie, spin_state, motif) if magnitude != "none": ligands = get_neighbors_of_site_with_index(structure, idx, approach="min_dist", delta=0.15) ligand_bond_lengths = [ligand.distance(structure[idx]) for ligand in ligands] ligands_species = list(set([str(ligand.specie) for ligand in ligands])) ligand_bond_length_spread = max(ligand_bond_lengths) - \ min(ligand_bond_lengths) def trim(f): # avoid storing to unreasonable precision, hurts readability return float("{:.4f}".format(f)) # to be Jahn-Teller active, all ligands have to be the same if len(ligands_species) == 1: jt_sites.append({'strength': magnitude, 'motif': motif, 'motif_order_parameter': trim(motif_order_parameter), 'spin_state': spin_state, 'species': str(site.specie), 'ligand': ligands_species[0], 'ligand_bond_lengths': [trim(length) for length in ligand_bond_lengths], 'ligand_bond_length_spread': trim(ligand_bond_length_spread), 'site_indices': indices}) # store reasons for not being J-T active else: non_jt_sites.append({'site_indices': indices, 'strength': "none", 'reason': "Not Jahn-Teller active for this " "electronic configuration."}) else: non_jt_sites.append({'site_indices': indices, 'strength': "none", 'reason': "motif is {}".format(motif)}) # perform aggregation of all sites if jt_sites: analysis = {'active': True} # if any site could exhibit 'strong' Jahn-Teller effect # then mark whole structure as strong strong_magnitudes = [site['strength'] == "strong" for site in jt_sites] if any(strong_magnitudes): analysis['strength'] = "strong" else: analysis['strength'] = "weak" analysis['sites'] = jt_sites return analysis, structure else: return {'active': False, 'sites': non_jt_sites}, structure
def predict(self, structure, ref_structure): """ Given a structure, returns the predicted volume. Args: structure (Structure): structure w/unknown volume ref_structure (Structure): A reference structure with a similar structure but different species. Returns: a float value of the predicted volume """ if self.check_isostructural: m = StructureMatcher() mapping = m.get_best_electronegativity_anonymous_mapping(structure, ref_structure) if mapping is None: raise ValueError("Input structures do not match!") if "ionic" in self.radii_type: try: # Use BV analyzer to determine oxidation states only if the # oxidation states are not already specified in the structure # and use_bv is true. if (not _is_ox(structure)) and self.use_bv: a = BVAnalyzer() structure = a.get_oxi_state_decorated_structure(structure) if (not _is_ox(ref_structure)) and self.use_bv: a = BVAnalyzer() ref_structure = a.get_oxi_state_decorated_structure(ref_structure) comp = structure.composition ref_comp = ref_structure.composition # Check if all the associated ionic radii are available. if any(k.ionic_radius is None for k in list(comp.keys())) or any( k.ionic_radius is None for k in list(ref_comp.keys()) ): raise ValueError("Not all the ionic radii are available!") numerator = 0 denominator = 0 # Here, the 1/3 factor on the composition accounts for atomic # packing. We want the number per unit length. for k, v in comp.items(): numerator += k.ionic_radius * v ** (1 / 3) for k, v in ref_comp.items(): denominator += k.ionic_radius * v ** (1 / 3) return ref_structure.volume * (numerator / denominator) ** 3 except Exception: warnings.warn("Exception occured. Will attempt atomic radii.") # If error occurs during use of ionic radii scheme, pass # and see if we can resolve it using atomic radii. pass if "atomic" in self.radii_type: comp = structure.composition ref_comp = ref_structure.composition # Here, the 1/3 factor on the composition accounts for atomic # packing. We want the number per unit length. numerator = 0 denominator = 0 for k, v in comp.items(): numerator += k.atomic_radius * v ** (1 / 3) for k, v in ref_comp.items(): denominator += k.atomic_radius * v ** (1 / 3) return ref_structure.volume * (numerator / denominator) ** 3 raise ValueError("Cannot find volume scaling based on radii choices specified!")
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 get_analysis_and_structure(self, structure, calculate_valences=True, guesstimate_spin=False, op_threshold=0.1): """ Obtain an analysis of a given structure and if it may be Jahn-Teller active or not. This is a heuristic, and may give false positives and false negatives (false positives are preferred). :param structure: input structure :param calculate_valences (bool): whether to attempt to calculate valences or not, structure should have oxidation states to perform analysis :param guesstimate_spin (bool): whether to guesstimate spin state from magnetic moments or not, use with caution :param op_threshold (float): threshold for order parameter above which to consider site to match an octahedral or tetrahedral motif, since Jahn-Teller structures can often be quite distorted, this threshold is smaller than one might expect :return (dict): analysis of structure, with key 'strength' which may be 'none', 'strong', 'weak', or 'unknown' """ structure = structure.get_primitive_structure() if calculate_valences: bva = BVAnalyzer() structure = bva.get_oxi_state_decorated_structure(structure) # no point testing multiple equivalent sites, doesn't make any difference to analysis # but makes returned symmetrized_structure = SpacegroupAnalyzer( structure).get_symmetrized_structure() # to detect structural motifs of a given site op = LocalStructOrderParams(['oct', 'tet']) # dict of site index to the Jahn-Teller analysis of that site jt_sites = [] non_jt_sites = [] for indices in symmetrized_structure.equivalent_indices: idx = indices[0] site = symmetrized_structure[idx] # only interested in sites with oxidation states if isinstance(site.specie, Specie) and site.specie.element.is_transition_metal: # get motif around site order_params = op.get_order_parameters(symmetrized_structure, idx) if order_params[0] > order_params[1] and order_params[ 0] > op_threshold: motif = 'oct' motif_order_parameter = order_params[0] elif order_params[1] > op_threshold: motif = 'tet' motif_order_parameter = order_params[1] else: motif = 'unknown' motif_order_parameter = None if motif == "oct" or motif == "tet": # guess spin of metal ion if guesstimate_spin and 'magmom' in site.properties: # estimate if high spin or low spin magmom = site.properties['magmom'] spin_state = self._estimate_spin_state( site.specie, motif, magmom) else: spin_state = "unknown" magnitude = self.get_magnitude_of_effect_from_species( site.specie, spin_state, motif) if magnitude != "none": ligands = get_neighbors_of_site_with_index( structure, idx, approach="min_dist", delta=0.15) ligand_bond_lengths = [ ligand.distance(structure[idx]) for ligand in ligands ] ligands_species = list( set([str(ligand.specie) for ligand in ligands])) ligand_bond_length_spread = max(ligand_bond_lengths) - \ min(ligand_bond_lengths) def trim(f): # avoid storing to unreasonable precision, hurts readability return float("{:.4f}".format(f)) # to be Jahn-Teller active, all ligands have to be the same if len(ligands_species) == 1: jt_sites.append({ 'strength': magnitude, 'motif': motif, 'motif_order_parameter': trim(motif_order_parameter), 'spin_state': spin_state, 'species': str(site.specie), 'ligand': ligands_species[0], 'ligand_bond_lengths': [ trim(length) for length in ligand_bond_lengths ], 'ligand_bond_length_spread': trim(ligand_bond_length_spread), 'site_indices': indices }) # store reasons for not being J-T active else: non_jt_sites.append({ 'site_indices': indices, 'strength': "none", 'reason': "Not Jahn-Teller active for this " "electronic configuration." }) else: non_jt_sites.append({ 'site_indices': indices, 'strength': "none", 'reason': "motif is {}".format(motif) }) # perform aggregation of all sites if jt_sites: analysis = {'active': True} # if any site could exhibit 'strong' Jahn-Teller effect # then mark whole structure as strong strong_magnitudes = [ site['strength'] == "strong" for site in jt_sites ] if any(strong_magnitudes): analysis['strength'] = "strong" else: analysis['strength'] = "weak" analysis['sites'] = jt_sites return analysis, structure else: return {'active': False, 'sites': non_jt_sites}, structure
def get_analysis_and_structure( self, structure: Structure, calculate_valences: bool = True, guesstimate_spin: bool = False, op_threshold: float = 0.1, ) -> Tuple[Dict, Structure]: """Obtain an analysis of a given structure and if it may be Jahn-Teller active or not. This is a heuristic, and may give false positives and false negatives (false positives are preferred). Args: structure: input structure calculate_valences: whether to attempt to calculate valences or not, structure should have oxidation states to perform analysis (Default value = True) guesstimate_spin: whether to guesstimate spin state from magnetic moments or not, use with caution (Default value = False) op_threshold: threshold for order parameter above which to consider site to match an octahedral or tetrahedral motif, since Jahn-Teller structures can often be quite distorted, this threshold is smaller than one might expect Returns: analysis of structure, with key 'strength' which may be 'none', 'strong', 'weak', or 'unknown' (Default value = 0.1) and decorated structure """ structure = structure.get_primitive_structure() if calculate_valences: bva = BVAnalyzer() structure = bva.get_oxi_state_decorated_structure(structure) # no point testing multiple equivalent sites, doesn't make any difference to analysis # but makes returned symmetrized_structure = SpacegroupAnalyzer( structure).get_symmetrized_structure() # to detect structural motifs of a given site op = LocalStructOrderParams(["oct", "tet"]) # dict of site index to the Jahn-Teller analysis of that site jt_sites = [] non_jt_sites = [] for indices in symmetrized_structure.equivalent_indices: idx = indices[0] site = symmetrized_structure[idx] # only interested in sites with oxidation states if isinstance(site.specie, Species) and site.specie.element.is_transition_metal: # get motif around site order_params = op.get_order_parameters(symmetrized_structure, idx) if order_params[0] > order_params[1] and order_params[ 0] > op_threshold: motif = "oct" motif_order_parameter = order_params[0] elif order_params[1] > op_threshold: motif = "tet" motif_order_parameter = order_params[1] else: motif = "unknown" motif_order_parameter = None if motif in ["oct", "tet"]: motif = cast(Literal["oct", "tet"], motif) # mypy needs help # guess spin of metal ion if guesstimate_spin and "magmom" in site.properties: # estimate if high spin or low spin magmom = site.properties["magmom"] spin_state = self._estimate_spin_state( site.specie, motif, magmom) else: spin_state = "unknown" magnitude = self.get_magnitude_of_effect_from_species( site.specie, spin_state, motif) if magnitude != "none": ligands = get_neighbors_of_site_with_index( structure, idx, approach="min_dist", delta=0.15) ligand_bond_lengths = [ ligand.distance(structure[idx]) for ligand in ligands ] ligands_species = list( {str(ligand.specie) for ligand in ligands}) ligand_bond_length_spread = max( ligand_bond_lengths) - min(ligand_bond_lengths) def trim(f): """ Avoid storing to unreasonable precision, hurts readability. """ return float(f"{f:.4f}") # to be Jahn-Teller active, all ligands have to be the same if len(ligands_species) == 1: jt_sites.append({ "strength": magnitude, "motif": motif, "motif_order_parameter": trim(motif_order_parameter), "spin_state": spin_state, "species": str(site.specie), "ligand": ligands_species[0], "ligand_bond_lengths": [ trim(length) for length in ligand_bond_lengths ], "ligand_bond_length_spread": trim(ligand_bond_length_spread), "site_indices": indices, }) # store reasons for not being J-T active else: non_jt_sites.append({ "site_indices": indices, "strength": "none", "reason": "Not Jahn-Teller active for this electronic configuration.", }) else: non_jt_sites.append({ "site_indices": indices, "strength": "none", "reason": f"motif is {motif}", }) # perform aggregation of all sites if jt_sites: analysis = {"active": True} # type: Dict[str, Any] # if any site could exhibit 'strong' Jahn-Teller effect # then mark whole structure as strong strong_magnitudes = [ site["strength"] == "strong" for site in jt_sites ] if any(strong_magnitudes): analysis["strength"] = "strong" else: analysis["strength"] = "weak" analysis["sites"] = jt_sites return analysis, structure return {"active": False, "sites": non_jt_sites}, structure