"""A reaction class that builds reactions based on ComputedEntry objects and providesinformation about reaction thermodynamics."""from__future__importannotationsfromfunctoolsimportcached_propertyfromtypingimportTYPE_CHECKINGimportnumpyasnpfromuncertaintiesimportufloatfromrxn_network.reactions.basicimportBasicReactionifTYPE_CHECKING:frompymatgen.core.compositionimportElementfrompymatgen.entries.computed_entriesimportComputedEntryfromrxn_network.coreimportComposition
[docs]classComputedReaction(BasicReaction):"""Convenience class to generate a reaction from ComputedEntry objects, with some additional attributes, such as a reaction energy based on computed energies. This class also balances the reaction. """def__init__(self,entries:list[ComputedEntry],coefficients:np.ndarray|list[float],data:dict|None=None,lowest_num_errors:int=0,):""" Args: entries: List of ComputedEntry objects. coefficients: List or array of reaction coefficients. data: Optional dict of data lowest_num_errors: number of "errors" encountered during reaction balancing. """self._entries=list(entries)self.reactant_entries=[entryforentry,coeffinzip(entries,coefficients)ifcoeff<0]self.product_entries=[entryforentry,coeffinzip(entries,coefficients)ifcoeff>0]compositions=[e.composition.reduced_compositionforeinentries]super().__init__(compositions,coefficients,data=data,lowest_num_errors=lowest_num_errors)
[docs]@classmethoddefbalance(cls,reactant_entries:list[ComputedEntry],product_entries:list[ComputedEntry],data:dict|None=None,)->ComputedReaction:"""Balances and returns a new ComputedReaction. Reactants and products to be specified as a collection (list, set, etc.) of ComputedEntry objects. Args: reactant_entries: List of reactant entries product_entries: List of product entries data: Optional dict of data """reactant_comps=[e.composition.reduced_compositionforeinreactant_entries]product_comps=[e.composition.reduced_compositionforeinproduct_entries]coefficients,lowest_num_errors,num_constraints=cls._balance_coeffs(reactant_comps,product_comps)ifnotdata:data={}data["num_constraints"]=num_constraintsreturncls(entries=list(reactant_entries)+list(product_entries),coefficients=coefficients,data=data,lowest_num_errors=lowest_num_errors,)
[docs]defget_new_temperature(self,new_temperature:float)->ComputedReaction:"""Returns a new reaction with the temperature changed. Args: new_temperature: New temperature in Kelvin """try:new_entries=[e.get_new_temperature(new_temperature)foreinself.entries]exceptAttributeErrorase:raiseAttributeError("One or more of the entries in the reaction is not associated with a"" temperature. Please use the GibbsComputedEntry class for all entries"" in the reaction.")fromereturnComputedReaction(new_entries,self.coefficients,data=self.data,lowest_num_errors=self.lowest_num_errors,)
@cached_propertydefenergy(self)->float:"""Returns (float): The calculated reaction energy. """calc_energies:dict[Composition,float]={}forentryinself._entries:(comp,factor)=entry.composition.get_reduced_composition_and_factor()calc_energies[comp]=min(calc_energies.get(comp,float("inf")),entry.energy/factor)returnsum(amt*calc_energies[c]foramt,cinzip(self.coefficients,self.compositions))@cached_propertydefenergy_per_atom(self)->float:"""Returns (float): The calculated reaction energy in eV, divided by the total number of atoms in the reaction. Cached for speedup. """returnself.energy/self.num_atoms@cached_propertydefenergy_uncertainty(self)->float:"""Calculates the uncertainty in the reaction energy based on the uncertainty in the energies of the reactants/products. Cached for speedup. """calc_energies:dict[Composition,ufloat]={}forentryinself._entries:(comp,factor)=entry.composition.get_reduced_composition_and_factor()energy_ufloat=ufloat(entry.energy,entry.correction_uncertainty)calc_energies[comp]=min(calc_energies.get(comp,float("inf")),energy_ufloat/factor)energy_with_uncertainty=sum(amt*calc_energies[c]foramt,cinzip(self.coefficients,self.compositions))returnenergy_with_uncertainty.s# type: ignore@cached_propertydefenergy_uncertainty_per_atom(self)->float:"""Returns the energy_uncertainty divided by the total number of atoms in the reaction. """returnself.energy_uncertainty/self.num_atoms@propertydefentries(self)->list[ComputedEntry]:"""Returns a copy of the entries."""returnself._entries
[docs]defcopy(self)->ComputedReaction:"""Returns a copy of the Reaction object."""returnComputedReaction(self.entries,self.coefficients,self.data,self.lowest_num_errors)
[docs]defreverse(self)->ComputedReaction:"""Returns a reversed reaction (i.e. sides flipped)."""returnComputedReaction(self.entries,-1*self.coefficients,self.data,self.lowest_num_errors)
[docs]defnormalize_to(self,comp:Composition,factor:float=1)->ComputedReaction:"""Normalizes the reaction to one of the compositions via the provided factor. By default, normalizes such that the composition given has a coefficient of 1. Args: comp: Composition object to normalize to factor: factor to normalize to. Defaults to 1. """coeffs=self.coefficients.copy()scale_factor=abs(1/coeffs[self.compositions.index(comp)]*factor)coeffs*=scale_factorreturnComputedReaction(self.entries,coeffs,self.data,self.lowest_num_errors)
[docs]defnormalize_to_element(self,element:Element,factor:float=1)->ComputedReaction:"""Normalizes the reaction to one of the elements. By default, normalizes such that the amount of the element is 1. Another factor can be specified. Args: element (Element/Species): Element to normalize to. factor (float): Factor to normalize to. Defaults to 1. """all_comp=self.compositionscoeffs=self.coefficients.copy()current_el_amount=sum(all_comp[i][element]*abs(coeffs[i])foriinrange(len(all_comp)))/2scale_factor=factor/current_el_amountcoeffs*=scale_factorreturnComputedReaction(self.entries,coeffs,self.data,self.lowest_num_errors)
[docs]defget_entry_idx_vector(self,n:int)->np.ndarray:"""Maps coefficients to a vector of length n on corresponding entry indices. Args: n: Total number of entries (determines length of vector) Returns: Vector containing reaction coefficients on the corresponding entry indices. """indices=[e.data["idx"]foreinself.entries]v=np.zeros(n)v[indices]=self.coefficientsreturnv
def__eq__(self,other)->bool:is_equal=BasicReaction.__eq__(self,other)ifis_equal:is_equal=np.isclose(# type: ignoreself.energy_per_atom,other.energy_per_atom)returnis_equaldef__hash__(self):"""Must be redefined due to overriding __eq__."""returnhash((self.chemical_system,tuple(sorted(self.coefficients))))# not checking here for reactions that are multiples (too expensive)