def _make_title(self, legend): if not legend or (not legend.get("composition", None)): return H1(self.default_title, id=self.id("title")) composition = legend["composition"] if isinstance(composition, dict): try: composition = Composition.from_dict(composition) # strip DummySpecie if present (TODO: should be method in pymatgen) composition = Composition({ el: amt for el, amt in composition.items() if not isinstance(el, DummySpecie) }) composition = composition.get_reduced_composition_and_factor( )[0] formula = composition.reduced_formula formula_parts = re.findall(r"[^\d_]+|\d+", formula) formula_components = [ html.Sub(part.strip()) if part.isnumeric() else html.Span(part.strip()) for part in formula_parts ] except: formula_components = list(map(str, composition.keys())) return H1(formula_components, id=self.id("title"), style={"display": "inline-block"})
def __init__(self, entry1, entry2, working_ion_entry): #initialize some internal variables working_element = working_ion_entry.composition.elements[0] entry_charge = entry1 entry_discharge = entry2 if entry_charge.composition.get_atomic_fraction(working_element) \ > entry2.composition.get_atomic_fraction(working_element): (entry_charge, entry_discharge) = (entry_discharge, entry_charge) comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition ion_sym = working_element.symbol frame_charge_comp = Composition({ el: comp_charge[el] for el in comp_charge if el.symbol != ion_sym }) frame_discharge_comp = Composition({ el: comp_discharge[el] for el in comp_discharge if el.symbol != ion_sym }) #Data validation #check that the ion is just a single element if not working_ion_entry.composition.is_element: raise ValueError("VoltagePair: The working ion specified must be " "an element") #check that at least one of the entries contains the working element if not comp_charge.get_atomic_fraction(working_element) > 0 and \ not comp_discharge.get_atomic_fraction(working_element) > 0: raise ValueError("VoltagePair: The working ion must be present in " "one of the entries") #check that the entries do not contain the same amount of the workin #element if comp_charge.get_atomic_fraction(working_element) == \ comp_discharge.get_atomic_fraction(working_element): raise ValueError("VoltagePair: The working ion atomic percentage " "cannot be the same in both the entries") #check that the frameworks of the entries are equivalent if not frame_charge_comp.reduced_formula == \ frame_discharge_comp.reduced_formula: raise ValueError("VoltagePair: the specified entries must have the" " same compositional framework") #Initialize normalization factors, charged and discharged entries valence_list = Element(ion_sym).oxidation_states working_ion_valence = max(valence_list) (self.framework, norm_charge) = frame_charge_comp.get_reduced_composition_and_factor() norm_discharge = \ frame_discharge_comp.get_reduced_composition_and_factor()[1] self._working_ion_entry = working_ion_entry #Initialize normalized properties self._vol_charge = entry_charge.structure.volume / norm_charge self._vol_discharge = entry_discharge.structure.volume / norm_discharge comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition self._mass_charge = comp_charge.weight / norm_charge self._mass_discharge = comp_discharge.weight / norm_discharge self._num_ions_transferred = \ (comp_discharge[working_element] / norm_discharge) \ - (comp_charge[working_element] / norm_charge) self._voltage = \ (((entry_charge.energy / norm_charge) - (entry_discharge.energy / norm_discharge)) / \ self._num_ions_transferred + working_ion_entry.energy_per_atom) / working_ion_valence self._mAh = self._num_ions_transferred * Charge(1, "e").to("C") * \ Time(1, "s").to("h") * AVOGADROS_CONST * 1000 * working_ion_valence #Step 4: add (optional) hull and muO2 data self.decomp_e_charge = \ entry_charge.data.get("decomposition_energy", None) self.decomp_e_discharge = \ entry_discharge.data.get("decomposition_energy", None) self.muO2_charge = entry_charge.data.get("muO2", None) self.muO2_discharge = entry_discharge.data.get("muO2", None) self.entry_charge = entry_charge self.entry_discharge = entry_discharge self.normalization_charge = norm_charge self.normalization_discharge = norm_discharge self._frac_charge = comp_charge.get_atomic_fraction(working_element) self._frac_discharge = \ comp_discharge.get_atomic_fraction(working_element)
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)
def from_entries(cls, entry1, entry2, working_ion_entry): """ Args: entry1: Entry corresponding to one of the entries in the voltage step. entry2: Entry corresponding to the other entry in the voltage step. working_ion_entry: A single ComputedEntry or PDEntry representing the element that carries charge across the battery, e.g. Li. """ # initialize some internal variables working_element = working_ion_entry.composition.elements[0] entry_charge = entry1 entry_discharge = entry2 if entry_charge.composition.get_atomic_fraction( working_element) > entry2.composition.get_atomic_fraction( working_element): (entry_charge, entry_discharge) = (entry_discharge, entry_charge) comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition ion_sym = working_element.symbol frame_charge_comp = Composition({ el: comp_charge[el] for el in comp_charge if el.symbol != ion_sym }) frame_discharge_comp = Composition({ el: comp_discharge[el] for el in comp_discharge if el.symbol != ion_sym }) # Data validation # check that the ion is just a single element if not working_ion_entry.composition.is_element: raise ValueError( "VoltagePair: The working ion specified must be an element") # check that at least one of the entries contains the working element if (not comp_charge.get_atomic_fraction(working_element) > 0 and not comp_discharge.get_atomic_fraction(working_element) > 0): raise ValueError( "VoltagePair: The working ion must be present in one of the entries" ) # check that the entries do not contain the same amount of the workin # element if comp_charge.get_atomic_fraction( working_element) == comp_discharge.get_atomic_fraction( working_element): raise ValueError( "VoltagePair: The working ion atomic percentage cannot be the same in both the entries" ) # check that the frameworks of the entries are equivalent if not frame_charge_comp.reduced_formula == frame_discharge_comp.reduced_formula: raise ValueError( "VoltagePair: the specified entries must have the same compositional framework" ) # Initialize normalization factors, charged and discharged entries valence_list = Element(ion_sym).oxidation_states working_ion_valence = abs(max(valence_list)) ( framework, norm_charge, ) = frame_charge_comp.get_reduced_composition_and_factor() norm_discharge = frame_discharge_comp.get_reduced_composition_and_factor( )[1] # Initialize normalized properties if hasattr(entry_charge, "structure"): _vol_charge = entry_charge.structure.volume / norm_charge else: _vol_charge = entry_charge.data.get("volume") if hasattr(entry_discharge, "structure"): _vol_discharge = entry_discharge.structure.volume / norm_discharge else: _vol_discharge = entry_discharge.data.get("volume") comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition _mass_charge = comp_charge.weight / norm_charge _mass_discharge = comp_discharge.weight / norm_discharge _num_ions_transferred = ( comp_discharge[working_element] / norm_discharge) - (comp_charge[working_element] / norm_charge) _voltage = ( ((entry_charge.energy / norm_charge) - (entry_discharge.energy / norm_discharge)) / _num_ions_transferred + working_ion_entry.energy_per_atom) / working_ion_valence _mAh = _num_ions_transferred * Charge(1, "e").to("C") * Time( 1, "s").to("h") * N_A * 1000 * working_ion_valence _frac_charge = comp_charge.get_atomic_fraction(working_element) _frac_discharge = comp_discharge.get_atomic_fraction(working_element) vpair = InsertionVoltagePair( # pylint: disable=E1123 voltage=_voltage, mAh=_mAh, mass_charge=_mass_charge, mass_discharge=_mass_discharge, vol_charge=_vol_charge, vol_discharge=_vol_discharge, frac_charge=_frac_charge, frac_discharge=_frac_discharge, working_ion_entry=working_ion_entry, entry_charge=entry_charge, entry_discharge=entry_discharge, framework_formula=framework.reduced_formula, ) # Step 4: add (optional) hull and muO2 data vpair.decomp_e_charge = entry_charge.data.get("decomposition_energy", None) vpair.decomp_e_discharge = entry_discharge.data.get( "decomposition_energy", None) vpair.muO2_charge = entry_charge.data.get("muO2", None) vpair.muO2_discharge = entry_discharge.data.get("muO2", None) return vpair
def __init__(self, entry1, entry2, working_ion_entry): #initialize some internal variables working_element = working_ion_entry.composition.elements[0] entry_charge = entry1 entry_discharge = entry2 if entry_charge.composition.get_atomic_fraction(working_element) \ > entry2.composition.get_atomic_fraction(working_element): (entry_charge, entry_discharge) = (entry_discharge, entry_charge) comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition ion_sym = working_element.symbol frame_charge_comp = Composition({el: comp_charge[el] for el in comp_charge if el.symbol != ion_sym}) frame_discharge_comp = Composition({el: comp_discharge[el] for el in comp_discharge if el.symbol != ion_sym}) #Data validation #check that the ion is just a single element if not working_ion_entry.composition.is_element: raise ValueError("VoltagePair: The working ion specified must be " "an element") #check that at least one of the entries contains the working element if not comp_charge.get_atomic_fraction(working_element) > 0 and \ not comp_discharge.get_atomic_fraction(working_element) > 0: raise ValueError("VoltagePair: The working ion must be present in " "one of the entries") #check that the entries do not contain the same amount of the workin #element if comp_charge.get_atomic_fraction(working_element) == \ comp_discharge.get_atomic_fraction(working_element): raise ValueError("VoltagePair: The working ion atomic percentage " "cannot be the same in both the entries") #check that the frameworks of the entries are equivalent if not frame_charge_comp.reduced_formula == \ frame_discharge_comp.reduced_formula: raise ValueError("VoltagePair: the specified entries must have the" " same compositional framework") #Initialize normalization factors, charged and discharged entries valence_list = Element(ion_sym).oxidation_states working_ion_valence = max(valence_list) (self.framework, norm_charge) = frame_charge_comp.get_reduced_composition_and_factor() norm_discharge = \ frame_discharge_comp.get_reduced_composition_and_factor()[1] self._working_ion_entry = working_ion_entry #Initialize normalized properties self._vol_charge = entry_charge.structure.volume / norm_charge self._vol_discharge = entry_discharge.structure.volume / norm_discharge comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition self._mass_charge = comp_charge.weight / norm_charge self._mass_discharge = comp_discharge.weight / norm_discharge self._num_ions_transferred = \ (comp_discharge[working_element] / norm_discharge) \ - (comp_charge[working_element] / norm_charge) self._voltage = \ (((entry_charge.energy / norm_charge) - (entry_discharge.energy / norm_discharge)) / \ self._num_ions_transferred + working_ion_entry.energy_per_atom) / working_ion_valence self._mAh = self._num_ions_transferred * Charge(1, "e").to("C") * \ Time(1, "s").to("h") * AVOGADROS_CONST * 1000 * working_ion_valence #Step 4: add (optional) hull and muO2 data self.decomp_e_charge = \ entry_charge.data.get("decomposition_energy", None) self.decomp_e_discharge = \ entry_discharge.data.get("decomposition_energy", None) self.muO2_charge = entry_charge.data.get("muO2", None) self.muO2_discharge = entry_discharge.data.get("muO2", None) self.entry_charge = entry_charge self.entry_discharge = entry_discharge self.normalization_charge = norm_charge self.normalization_discharge = norm_discharge self._frac_charge = comp_charge.get_atomic_fraction(working_element) self._frac_discharge = \ comp_discharge.get_atomic_fraction(working_element)
class Entry(MSONable, metaclass=ABCMeta): """ A lightweight object containing the energy associated with a specific chemical composition. This base class is not intended to be instantiated directly. Note that classes which inherit from Entry must define a .energy property. """ def __init__(self, composition: Composition, energy: float): """ Initializes an Entry. Args: composition (Composition): Composition of the entry. For flexibility, this can take the form of all the typical input taken by a Composition, including a {symbol: amt} dict, a string formula, and others. energy (float): Energy of the entry. """ self._energy = energy self.composition = Composition(composition) @property def is_element(self) -> bool: """ :return: Whether composition of entry is an element. """ return self.composition.is_element @property @abstractmethod def energy(self) -> float: """ :return: the energy of the entry. """ @property def energy_per_atom(self) -> float: """ :return: the energy per atom of the entry. """ return self.energy / self.composition.num_atoms def __str__(self): return self.__repr__() def normalize( self, mode: str = "formula_unit", inplace: bool = True ) -> Optional["Entry"]: """ Normalize the entry's composition and energy. Args: mode: "formula_unit" is the default, which normalizes to composition.reduced_formula. The other option is "atom", which normalizes such that the composition amounts sum to 1. inplace: "True" is the default which normalises the current Entry object. Setting inplace to "False" returns a normalized copy of the Entry object. """ if inplace: factor = self._normalization_factor(mode) self.composition /= factor self._energy /= factor return None else: entry = copy.deepcopy(self) factor = entry._normalization_factor(mode) entry.composition /= factor entry._energy /= factor return entry def _normalization_factor(self, mode: str = "formula_unit") -> float: if mode == "atom": factor = self.composition.num_atoms else: comp, factor = self.composition.get_reduced_composition_and_factor() return factor def as_dict(self) -> dict: """ :return: MSONable dict. """ return { "@module": self.__class__.__module__, "@class": self.__class__.__name__, "energy": self._energy, "composition": self.composition.as_dict(), }
class ComputedEntry(MSONable): """ An lightweight ComputedEntry object containing key computed data for many purposes. Extends a PDEntry so that it can be used for phase diagram generation. The difference between a ComputedEntry and a standard PDEntry is that it includes additional parameters like a correction and run_parameters. """ def __init__(self, composition: Composition, energy: float, correction: float = 0.0, parameters: dict = None, data: dict = None, entry_id: object = None): """ Initializes a ComputedEntry. Args: composition (Composition): Composition of the entry. For flexibility, this can take the form of all the typical input taken by a Composition, including a {symbol: amt} dict, a string formula, and others. energy (float): Energy of the entry. Usually the final calculated energy from VASP or other electronic structure codes. correction (float): A correction to be applied to the energy. This is used to modify the energy for certain analyses. Defaults to 0.0. parameters (dict): An optional dict of parameters associated with the entry. Defaults to None. data (dict): An optional dict of any additional data associated with the entry. Defaults to None. entry_id (obj): An optional id to uniquely identify the entry. """ self.uncorrected_energy = energy self.composition = Composition(composition) self.correction = correction self.parameters = parameters if parameters else {} self.data = data if data else {} self.entry_id = entry_id self.name = self.composition.reduced_formula def normalize(self, mode: str = "formula_unit") -> None: """ Normalize the entry's composition, energy and any corrections. Generally, this would not have effect on any Args: mode: "formula_unit" is the default, which normalizes to composition.reduced_formula. The other option is "atom", which normalizes such that the composition amounts sum to 1. """ if mode == "atom": factor = self.composition.num_atoms comp = self.composition / factor else: comp, factor = self.composition.get_reduced_composition_and_factor( ) self.composition = comp self.uncorrected_energy /= factor self.correction /= factor @property def is_element(self) -> bool: """ :return: Whether composition of entry is an element. """ return self.composition.is_element @property def energy(self) -> float: """ :return: the *corrected* energy of the entry. """ return self.uncorrected_energy + self.correction @property def energy_per_atom(self) -> float: """ :return: the *corrected* energy per atom of the entry. """ return self.energy / self.composition.num_atoms def __repr__(self): output = [ "ComputedEntry {} - {}".format(self.entry_id, self.composition.formula), "Energy = {:.4f}".format(self.uncorrected_energy), "Correction = {:.4f}".format(self.correction), "Parameters:" ] for k, v in self.parameters.items(): output.append("{} = {}".format(k, v)) output.append("Data:") for k, v in self.data.items(): output.append("{} = {}".format(k, v)) return "\n".join(output) def __str__(self): return self.__repr__() @classmethod def from_dict(cls, d) -> 'ComputedEntry': """ :param d: Dict representation. :return: ComputedEntry """ dec = MontyDecoder() return cls(d["composition"], d["energy"], d["correction"], parameters={ k: dec.process_decoded(v) for k, v in d.get("parameters", {}).items() }, data={ k: dec.process_decoded(v) for k, v in d.get("data", {}).items() }, entry_id=d.get("entry_id", None)) def as_dict(self) -> dict: """ :return: MSONable dict. """ return { "@module": self.__class__.__module__, "@class": self.__class__.__name__, "energy": self.uncorrected_energy, "composition": self.composition.as_dict(), "correction": self.correction, "parameters": json.loads(json.dumps(self.parameters, cls=MontyEncoder)), "data": json.loads(json.dumps(self.data, cls=MontyEncoder)), "entry_id": self.entry_id }
def __init__(self, entry1, entry2, working_ion_entry): """ Args: entry1: Entry corresponding to one of the entries in the voltage step. entry2: Entry corresponding to the other entry in the voltage step. working_ion_entry: A single ComputedEntry or PDEntry representing the element that carries charge across the battery, e.g. Li. """ #initialize some internal variables working_element = working_ion_entry.composition.elements[0] entry_charge = entry1 entry_discharge = entry2 if entry_charge.composition.get_atomic_fraction(working_element) \ > entry2.composition.get_atomic_fraction(working_element): (entry_charge, entry_discharge) = (entry_discharge, entry_charge) comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition ion_sym = working_element.symbol frame_charge_comp = Composition({ el: comp_charge[el] for el in comp_charge if el.symbol != ion_sym }) frame_discharge_comp = Composition({ el: comp_discharge[el] for el in comp_discharge if el.symbol != ion_sym }) #Data validation #check that the ion is just a single element if not working_ion_entry.composition.is_element: raise ValueError("VoltagePair: The working ion specified must be " "an element") #check that at least one of the entries contains the working element if not comp_charge.get_atomic_fraction(working_element) > 0 and \ not comp_discharge.get_atomic_fraction(working_element) > 0: raise ValueError("VoltagePair: The working ion must be present in " "one of the entries") #check that the entries do not contain the same amount of the workin #element if comp_charge.get_atomic_fraction(working_element) == \ comp_discharge.get_atomic_fraction(working_element): raise ValueError("VoltagePair: The working ion atomic percentage " "cannot be the same in both the entries") #check that the frameworks of the entries are equivalent if not frame_charge_comp.reduced_formula == \ frame_discharge_comp.reduced_formula: raise ValueError("VoltagePair: the specified entries must have the" " same compositional framework") #Initialize normalization factors, charged and discharged entries (self.framework, norm_charge) = frame_charge_comp.get_reduced_composition_and_factor() norm_discharge = \ frame_discharge_comp.get_reduced_composition_and_factor()[1] self._working_ion_entry = working_ion_entry #Initialize normalized properties self._vol_charge = entry_charge.structure.volume / norm_charge self._vol_discharge = entry_discharge.structure.volume / norm_discharge comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition self._mass_charge = comp_charge.weight / norm_charge self._mass_discharge = comp_discharge.weight / norm_discharge self._num_ions_transferred = \ (comp_discharge[working_element] / norm_discharge) \ - (comp_charge[working_element] / norm_charge) self._voltage = \ ((entry_charge.energy / norm_charge) - (entry_discharge.energy / norm_discharge)) / \ self._num_ions_transferred + working_ion_entry.energy_per_atom self._mAh = self._num_ions_transferred * ELECTRON_TO_AMPERE_HOURS * 1e3 #Step 4: add (optional) hull and muO2 data self.decomp_e_charge = \ entry_charge.data.get("decomposition_energy", None) self.decomp_e_discharge = \ entry_discharge.data.get("decomposition_energy", None) self.muO2_charge = entry_charge.data.get("muO2", None) self.muO2_discharge = entry_discharge.data.get("muO2", None) self.entry_charge = entry_charge self.entry_discharge = entry_discharge self.normalization_charge = norm_charge self.normalization_discharge = norm_discharge self._frac_charge = comp_charge.get_atomic_fraction(working_element) self._frac_discharge = \ comp_discharge.get_atomic_fraction(working_element)
def __init__(self, entry1, entry2, working_ion_entry): """ Args: entry1: Entry corresponding to one of the entries in the voltage step. entry2: Entry corresponding to the other entry in the voltage step. working_ion_entry: A single ComputedEntry or PDEntry representing the element that carries charge across the battery, e.g. Li. """ #initialize some internal variables working_element = working_ion_entry.composition.elements[0] entry_charge = entry1 entry_discharge = entry2 if entry_charge.composition.get_atomic_fraction(working_element) \ > entry2.composition.get_atomic_fraction(working_element): (entry_charge, entry_discharge) = (entry_discharge, entry_charge) comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition ion_sym = working_element.symbol frame_charge_comp = Composition({el: comp_charge[el] for el in comp_charge if el.symbol != ion_sym}) frame_discharge_comp = Composition({el: comp_discharge[el] for el in comp_discharge if el.symbol != ion_sym}) #Data validation #check that the ion is just a single element if not working_ion_entry.composition.is_element: raise ValueError("VoltagePair: The working ion specified must be " "an element") #check that at least one of the entries contains the working element if not comp_charge.get_atomic_fraction(working_element) > 0 and \ not comp_discharge.get_atomic_fraction(working_element) > 0: raise ValueError("VoltagePair: The working ion must be present in " "one of the entries") #check that the entries do not contain the same amount of the workin #element if comp_charge.get_atomic_fraction(working_element) == \ comp_discharge.get_atomic_fraction(working_element): raise ValueError("VoltagePair: The working ion atomic percentage " "cannot be the same in both the entries") #check that the frameworks of the entries are equivalent if not frame_charge_comp.reduced_formula == \ frame_discharge_comp.reduced_formula: raise ValueError("VoltagePair: the specified entries must have the" " same compositional framework") #Initialize normalization factors, charged and discharged entries (self.framework, norm_charge) = frame_charge_comp.get_reduced_composition_and_factor() norm_discharge = \ frame_discharge_comp.get_reduced_composition_and_factor()[1] self._working_ion_entry = working_ion_entry #Initialize normalized properties self._vol_charge = entry_charge.structure.volume / norm_charge self._vol_discharge = entry_discharge.structure.volume / norm_discharge comp_charge = entry_charge.composition comp_discharge = entry_discharge.composition self._mass_charge = comp_charge.weight / norm_charge self._mass_discharge = comp_discharge.weight / norm_discharge self._num_ions_transferred = \ (comp_discharge[working_element] / norm_discharge) \ - (comp_charge[working_element] / norm_charge) self._voltage = \ ((entry_charge.energy / norm_charge) - (entry_discharge.energy / norm_discharge)) / \ self._num_ions_transferred + working_ion_entry.energy_per_atom self._mAh = self._num_ions_transferred * ELECTRON_TO_AMPERE_HOURS * 1e3 #Step 4: add (optional) hull and muO2 data self.decomp_e_charge = \ entry_charge.data.get("decomposition_energy", None) self.decomp_e_discharge = \ entry_discharge.data.get("decomposition_energy", None) self.muO2_charge = entry_charge.data.get("muO2", None) self.muO2_discharge = entry_discharge.data.get("muO2", None) self.entry_charge = entry_charge self.entry_discharge = entry_discharge self.normalization_charge = norm_charge self.normalization_discharge = norm_discharge self._frac_charge = comp_charge.get_atomic_fraction(working_element) self._frac_discharge = \ comp_discharge.get_atomic_fraction(working_element)