def test_oxi_state_decoration(self): # Basic test: Get compositions where each element is in a single charge state decorated = Composition("H2O").add_charges_from_oxi_state_guesses() self.assertIn(Specie("H", 1), decorated) self.assertEqual(2, decorated.get(Specie("H", 1))) # Test: More than one charge state per element decorated = Composition("Fe3O4").add_charges_from_oxi_state_guesses() self.assertEqual(1, decorated.get(Specie("Fe", 2))) self.assertEqual(2, decorated.get(Specie("Fe", 3))) self.assertEqual(4, decorated.get(Specie("O", -2))) # Test: No possible charge states # It should return an uncharged composition decorated = Composition("NiAl").add_charges_from_oxi_state_guesses() self.assertEqual(1, decorated.get(Specie("Ni", 0))) self.assertEqual(1, decorated.get(Specie("Al", 0)))
def test_oxi_state_decoration(self): # Basic test: Get compositions where each element is in a single charge state decorated = Composition("H2O").add_charges_from_oxi_state_guesses() self.assertIn(Specie("H", 1), decorated) self.assertEqual(2, decorated.get(Specie("H", 1))) # Test: More than one charge state per element decorated = Composition("Fe3O4").add_charges_from_oxi_state_guesses() self.assertEqual(1, decorated.get(Specie("Fe", 2))) self.assertEqual(2, decorated.get(Specie("Fe", 3))) self.assertEqual(4, decorated.get(Specie("O", -2))) # Test: No possible charge states # It should return an uncharged composition decorated = Composition("NiAl").add_charges_from_oxi_state_guesses() self.assertEqual(1, decorated.get(Specie("Ni", 0))) self.assertEqual(1, decorated.get(Specie("Al", 0)))
class Ion(MSONable): """ Basic ion object. It is just a Composition object with an additional variable to store charge. The net charge can either be represented as Mn++, or Mn+2, or Mn[2+]. Note the order of the sign and magnitude in each representation. """ def __init__(self, composition, charge=0.0, properties=None): """ Flexible Ion construction, similar to Composition. For more information, please see pymatgen.core.Composition """ self._composition = Composition(composition) self._charge = charge self._properties = properties if properties else {} def __getattr__(self, a): if a in self._properties: return self._properties[a] try: return getattr(self._composition, a) except: raise AttributeError(a) @staticmethod def from_formula(formula): charge = 0.0 f = formula m = re.search(r"\[([^\[\]]+)\]", f) if m: m_chg = re.search("([\.\d]*)([+-])", m.group(1)) if m_chg: if m_chg.group(1) != "": charge += float(m_chg.group(1)) * \ (float(m_chg.group(2) + "1")) else: charge += float(m_chg.group(2) + "1") f = f.replace(m.group(), "", 1) m = re.search(r"\(aq\)", f) if m: f = f.replace(m.group(), "", 1) for m_chg in re.finditer("([+-])([\.\d]*)", f): sign = m_chg.group(1) sgn = float(str(sign + "1")) if m_chg.group(2).strip() != "": charge += float(m_chg.group(2)) * sgn else: charge += sgn f = f.replace(m_chg.group(), "", 1) composition = Composition(f) return Ion(composition, charge) @property def formula(self): """ Returns a formula string, with elements sorted by electronegativity, e.g., Li4 Fe4 P4 O16. """ formula = self._composition.formula chg_str = "" if self._charge > 0: chg_str = " +" + formula_double_format(self._charge, False) elif self._charge < 0: chg_str = " " + formula_double_format(self._charge, False) return formula + chg_str @property def anonymized_formula(self): """ An anonymized formula. Appends charge to the end of anonymized composition """ anon_formula = self._composition.anonymized_formula chg = self._charge chg_str = "" if chg > 0: chg_str += ("{}{}".format('+', str(int(chg)))) elif chg < 0: chg_str += ("{}{}".format('-', str(int(np.abs(chg))))) return anon_formula + chg_str @property def reduced_formula(self): """ Returns a reduced formula string with appended charge. """ reduced_formula = self._composition.reduced_formula charge = self._charge / float( self._composition.get_reduced_composition_and_factor()[1]) if charge > 0: if abs(charge) == 1: chg_str = "[+]" else: chg_str = "[" + formula_double_format(charge, False) + "+]" elif charge < 0: if abs(charge) == 1: chg_str = "[-]" else: chg_str = "[{}-]".format( formula_double_format(abs(charge), False)) else: chg_str = "(aq)" return reduced_formula + chg_str @property def alphabetical_formula(self): """ Returns a reduced formula string with appended charge """ alph_formula = self._composition.alphabetical_formula chg_str = "" if self._charge > 0: chg_str = " +" + formula_double_format(self._charge, False) elif self._charge < 0: chg_str = " " + formula_double_format(self._charge, False) return alph_formula + chg_str @property def charge(self): """ Charge of the ion """ return self._charge @property def composition(self): """ Return composition object """ return self._composition @property def to_dict(self): """ Returns: dict with composition, as well as charge """ d = self._composition.to_dict d['charge'] = self._charge return d @classmethod def from_dict(cls, d): """ Generates an ion object from a dict created by to_dict. Args: d: {symbol: amount} dict. """ # composition = Composition.from_dict(d['composition']) charge = d['charge'] composition = Composition({i: d[i] for i in d if i != 'charge'}) return Ion(composition, charge) @property def to_reduced_dict(self): """ Returns: dict with element symbol and reduced amount e.g., {"Fe": 2.0, "O":3.0}. """ reduced_formula = self._composition.reduced_formula c = Composition(reduced_formula) d = c.to_dict d['charge'] = self._charge return d def __eq__(self, other): if self.composition != other.composition: return False if self.charge != other.charge: return False return True def __ne__(self, other): return not self.__eq__(other) def __add__(self, other): """ Addition of two ions. """ new_composition = self.composition + other.composition new_charge = self.charge + other.charge return Ion(new_composition, new_charge) def __sub__(self, other): """ Subtraction of two ions """ new_composition = self.composition - other.composition new_charge = self.charge - other.charge return Ion(new_composition, new_charge) def __mul__(self, other): """ Multiplication of an Ion with a factor """ new_composition = self.composition * other new_charge = self.charge * other return Ion(new_composition, new_charge) def __hash__(self): #for now, just use the composition hash code. return self._composition.__hash__() def __len__(self): return len(self._composition) def __str__(self): return self.formula def __repr__(self): return "Ion: " + self.formula def __getitem__(self, el): return self._composition.get(el, 0)
class Ion(MSONable): """ Basic ion object. It is just a Composition object with an additional variable to store charge. The net charge can either be represented as Mn++, or Mn+2, or Mn[2+]. Note the order of the sign and magnitude in each representation. """ def __init__(self, composition, charge=0.0, properties=None): """ Flexible Ion construction, similar to Composition. For more information, please see pymatgen.core.Composition """ self._composition = Composition(composition) self._charge = charge self._properties = properties if properties else {} def __getattr__(self, a): if a in self._properties: return self._properties[a] try: return getattr(self._composition, a) except: raise AttributeError(a) @staticmethod def from_formula(formula): charge = 0.0 f = formula m = re.search(r"\[([^\[\]]+)\]", f) if m: m_chg = re.search(r"([\.\d]*)([+-])", m.group(1)) if m_chg: if m_chg.group(1) != "": charge += float(m_chg.group(1)) * \ (float(m_chg.group(2) + "1")) else: charge += float(m_chg.group(2) + "1") f = f.replace(m.group(), "", 1) m = re.search(r"\(aq\)", f) if m: f = f.replace(m.group(), "", 1) for m_chg in re.finditer(r"([+-])([\.\d]*)", f): sign = m_chg.group(1) sgn = float(str(sign + "1")) if m_chg.group(2).strip() != "": charge += float(m_chg.group(2)) * sgn else: charge += sgn f = f.replace(m_chg.group(), "", 1) composition = Composition(f) return Ion(composition, charge) @property def formula(self): """ Returns a formula string, with elements sorted by electronegativity, e.g., Li4 Fe4 P4 O16. """ formula = self._composition.formula chg_str = "" if self._charge > 0: chg_str = " +" + formula_double_format(self._charge, False) elif self._charge < 0: chg_str = " " + formula_double_format(self._charge, False) return formula + chg_str @property def anonymized_formula(self): """ An anonymized formula. Appends charge to the end of anonymized composition """ anon_formula = self._composition.anonymized_formula chg = self._charge chg_str = "" if chg > 0: chg_str += ("{}{}".format('+', str(int(chg)))) elif chg < 0: chg_str += ("{}{}".format('-', str(int(np.abs(chg))))) return anon_formula + chg_str @property def reduced_formula(self): """ Returns a reduced formula string with appended charge. """ reduced_formula = self._composition.reduced_formula charge = self._charge / float(self._composition. get_reduced_composition_and_factor()[1]) if charge > 0: if abs(charge) == 1: chg_str = "[+]" else: chg_str = "[" + formula_double_format(charge, False) + "+]" elif charge < 0: if abs(charge) == 1: chg_str = "[-]" else: chg_str = "[{}-]".format(formula_double_format(abs(charge), False)) else: chg_str = "(aq)" return reduced_formula + chg_str @property def alphabetical_formula(self): """ Returns a reduced formula string with appended charge """ alph_formula = self._composition.alphabetical_formula chg_str = "" if self._charge > 0: chg_str = " +" + formula_double_format(self._charge, False) elif self._charge < 0: chg_str = " " + formula_double_format(self._charge, False) return alph_formula + chg_str @property def charge(self): """ Charge of the ion """ return self._charge @property def composition(self): """ Return composition object """ return self._composition def as_dict(self): """ Returns: dict with composition, as well as charge """ d = self._composition.as_dict() d['charge'] = self._charge return d @classmethod def from_dict(cls, d): """ Generates an ion object from a dict created by as_dict(). Args: d: {symbol: amount} dict. """ # composition = Composition.from_dict(d['composition']) charge = d['charge'] composition = Composition({i: d[i] for i in d if i != 'charge'}) return Ion(composition, charge) @property def to_reduced_dict(self): """ Returns: dict with element symbol and reduced amount e.g., {"Fe": 2.0, "O":3.0}. """ reduced_formula = self._composition.reduced_formula c = Composition(reduced_formula) d = c.as_dict() d['charge'] = self._charge return d def __eq__(self, other): if self.composition != other.composition: return False if self.charge != other.charge: return False return True def __ne__(self, other): return not self.__eq__(other) def __add__(self, other): """ Addition of two ions. """ new_composition = self.composition + other.composition new_charge = self.charge + other.charge return Ion(new_composition, new_charge) def __sub__(self, other): """ Subtraction of two ions """ new_composition = self.composition - other.composition new_charge = self.charge - other.charge return Ion(new_composition, new_charge) def __mul__(self, other): """ Multiplication of an Ion with a factor """ new_composition = self.composition * other new_charge = self.charge * other return Ion(new_composition, new_charge) def __hash__(self): #for now, just use the composition hash code. return self._composition.__hash__() def __len__(self): return len(self._composition) def __str__(self): return self.formula def __repr__(self): return "Ion: " + self.formula def __getitem__(self, el): return self._composition.get(el, 0)
def is_ABO3_struc(row): comp = Composition(row) if round(comp.get('O')%3)==0: # get Oxygen fraction, this should be a multiple of 3 to have a final formula in the format of A(A')B(B')O3 return True return False
def get_ionic_properties(row): # getting oxidation state of fractional formula is not implemented yet in pymatgen # round the fractional formular to 1 decimal place in order to speed up guessed_oxidation_state calculation in pymatgen # sum of fractions is preserved (=1) elem_frac = {row.A1:row.A1_frac, row.A2:row.A2_frac, row.B1:row.B1_frac, row.B2:row.B2_frac, row.O:row.O_frac} # _=elem_frac.pop('_', None) elem_frac_red = { k:v for k, v in elem_frac.items() if (v<1 and v>0)} # remove empty cation and oxygen anion # ceil and floor to 1 decimal places and create list frac_list=[(k,math.ceil(v*10)/10.0) if v==min(list(elem_frac_red.values())) else (k,math.floor(v*10)/10.0) for k, v in elem_frac_red.items()] frac_dict = {k:v for k,v in frac_list} elem_frac_copy = elem_frac.copy() # make a copy so that original element fractions are not updated elem_frac_copy.update(frac_dict) # update the dictionary with 1 decimal precision {Element: fraction} where fraction ceiled/floored for 1 decimal place # get fractional formula with 1 decimal point rounded fractions l=[] [l.append(k+str(v)) for k,v in elem_frac_copy.items() if k!='_'][0] formula_1deci = ''.join(l) comp = Composition(formula_1deci) # create pymatgen Composition object int_comp = Composition(comp.get_integer_formula_and_factor()[0]) # get integer formular elem_ionic_radius = {} # try: if len(int_comp.oxi_state_guesses())>0: ox_state_dict = int_comp.oxi_state_guesses()[0] # get the best oxidation state guess / pymatgen outputs fractional oxidation states where necessary for (elem,ox_st) in list(ox_state_dict.items()): if not ox_st.is_integer() or ox_st not in ElementSym(elem).ox_st_dict[elem]: avg_ionic_radius = get_avg_ionic_radius(elem, int(int_comp.get(elem)), ox_st) if avg_ionic_radius == -1: # oxidation states cannot be solved with all available oxidation states break elem_ionic_radius[elem] = avg_ionic_radius else: ionic_radius = ElementSym(elem).ionic_radii(ox_st) elem_ionic_radius[elem] = ionic_radius if len(elem_ionic_radius)==4: # now update the first elem_frac dict with the found ionic radius values (some oinic radii may be averages because of fractional oxidation state) elem_radii = elem_frac.copy() # for clarity elem_radii.update(elem_ionic_radius) rA_avg = (elem_frac[row.A1]*elem_radii[row.A1] + elem_frac[row.A2]*elem_radii[row.A2]) rB_avg = (elem_frac[row.B1]*elem_radii[row.B1] + elem_frac[row.B2]*elem_radii[row.B2]) rO = ElementSym('O').ionic_radii(-2) # oxygen's oxidation state is always -2 ox_st_dict_copy = elem_frac.copy() # to find nA, nB, nO ox_st_dict_copy.update(ox_state_dict) nA = elem_frac[row.A1]*ox_st_dict_copy[row.A1] + elem_frac[row.A2]*ox_st_dict_copy[row.A2] nB = elem_frac[row.B1]*ox_st_dict_copy[row.B1] + elem_frac[row.B2]*ox_st_dict_copy[row.B2] nO = -2 # Oxygent oxidation state # to make it easy to understand that these materials are discarded, the else statements are added below // not necessary if Ra_avg, Rb_avg etc. were initialized with -1 at the top else: rA_avg = -1 # discard these materials / oxidation states could not be solved by the algorithm rB_avg = -1 rO = ElementSym('O').ionic_radii(-2) nA = -1 nB = -1 nO = -2 # Oxygen oxidation state else: rA_avg = -1 # discard these materials where the ions show unknown oxidation states/ typically higher that the max oxi_state of a particular element rB_avg = -1 # pymatgen's oxi_state_guesses() couldn't solve these even with fractional oxidation states rO = ElementSym('O').ionic_radii(-2) nA = -1 # discard nB = -1 # discard nO = -2 # Oxygent oxidation state return nA, nB, nO, rA_avg, rB_avg, rO