def chemical_reaction(reactant_entries, product_entries, possible_yield_entries, mode="strict"): try: rxn = ComputedReaction(reactant_entries, product_entries) except: try: rxn = ComputedReaction( reactant_entries, product_entries + possible_yield_entries) except: return None if rxn.reactants[0] != rxn.products[0]: # avoid self reactions if mode == "strict": reactant_comp = [ entry.composition.reduced_composition for entry in reactant_entries ] product_comp = [ entry.composition.reduced_composition for entry in product_entries + possible_yield_entries ] if set(rxn.reactants).issubset(set(reactant_comp)) and set( rxn.products).issubset(set(product_comp)): return rxn else: return rxn
def get_decomposition_in_gppd(self, chempot, entries=None, exclusions=None, trypreload=False): gppd_entries = entries if entries \ else self.get_gppd_entries(chempot, exclusions=exclusions, trypreload=trypreload) pd = PhaseDiagram(gppd_entries) gppd_entries = pd.stable_entries open_el_entries = [ _ for _ in gppd_entries if _.is_element and _.composition.elements[0].symbol in chempot.keys() ] el_ref = { _.composition.elements[0].symbol: _.energy_per_atom for _ in open_el_entries } chempot_vaspref = {_: chempot[_] + el_ref[_] for _ in chempot} for open_entry in open_el_entries: open_entry.correction += chempot_vaspref[ open_entry.composition.elements[0].symbol] GPPD = GrandPotentialPhaseDiagram(gppd_entries, chempot_vaspref) GPComp = self.GPComp(chempot) decomp_GP_entries = GPPD.get_decomposition(GPComp) decomp_entries = [gpe.original_entry for gpe in decomp_GP_entries] rxn = ComputedReaction([self] + open_el_entries, decomp_entries) rxn.normalize_to(self.composition) return decomp_entries, rxn
def get_rxn_e_table_string(self, pure_el_ref, open_el, PE_list, oe_amt_list, mu_trans_list, plot_rxn_e): neg_flag = (max(mu_trans_list) > 1e-6) rxn_trans_list = [mu for mu in mu_trans_list] rxn_e_list = [] ext = 0.2 rxn_trans_list = [rxn_trans_list[0] + ext] + rxn_trans_list if neg_flag else [0] + rxn_trans_list for data in zip(oe_amt_list, PE_list, rxn_trans_list): oe_amt, PE, ext_miu = data rxn = ComputedReaction([self, pure_el_ref], PE) rxn.normalize_to(self.composition.reduced_composition) rxn_e_list.append(rxn.calculated_reaction_energy - oe_amt * ext_miu) rxn_trans_list = rxn_trans_list + [rxn_trans_list[-1] - ext] rxn_e_list = rxn_e_list + [rxn_e_list[-1] + ext * oe_amt_list[-1]] rxn_e_list = [e / self.composition.num_atoms for e in rxn_e_list] df = pandas.DataFrame() df["miu_{} (eV)".format(open_el)] = rxn_trans_list df["Rxn energy (eV/atom)"] = rxn_e_list if plot_rxn_e: plt.figure(figsize=(8, 6)) ax = plt.gca() ax.invert_xaxis() ax.axvline(0, linestyle='--', color='k', linewidth=0.5, zorder=1) ax.plot(rxn_trans_list, rxn_e_list, '-', linewidth=1.5, color='cornflowerblue', zorder=3) ax.scatter(rxn_trans_list[1:-1], rxn_e_list[1:-1], edgecolors='cornflowerblue', facecolors='w', linewidth=1.5, s=50, zorder=4) ax.set_xlabel('Chemical potential ref. to {}'.format(open_el)) ax.set_ylabel('Reaction energy (eV/atom)') ax.set_xlim([float(rxn_trans_list[0]), float(rxn_trans_list[-1])]) plt.show() print_df = df.to_string(index=False, float_format='{:,.2f}'.format, justify='center') return print_df
def get_full_evolution_profile(pd, entry1, entry2, x1, x2): """ This function is used to solve the transition points along a path on convex hull. The essence is to use binary search, which is more accurate and faster than brutal force screening This is a recursive function. :param pd: PhaseDiagram of GrandPotentialPhaseDiagram :param entry1 & entry2: mixing entry1/entry2, PDEntry for pd_mixing, GrandPotEntry for gppd_mixing :param x1 & x2: The mixing ratio range for binary search. :return: An uncleaned but complete profile with all transition points. """ evolution_profile = {} entry_left = get_mix_entry({entry1: x1, entry2: 1 - x1}) entry_right = get_mix_entry({entry1: x2, entry2: 1 - x2}) (decomp1, h1) = pd.get_decomp_and_e_above_hull(entry_left) (decomp2, h2) = pd.get_decomp_and_e_above_hull(entry_right) decomp1 = set(decomp1.keys()) decomp2 = set(decomp2.keys()) evolution_profile[x1] = (decomp1, h1) evolution_profile[x2] = (decomp2, h2) if decomp1 == decomp2: return evolution_profile intersect = decomp1 & decomp2 if len(intersect) > 0: # This is try to catch a single transition point try: rxn = ComputedReaction([entry_left, entry_right], list(intersect)) if not {entry_left, entry_right} < set(rxn.all_entries): return evolution_profile c1 = rxn.coeffs[rxn.all_entries.index(entry_left)] c2 = rxn.coeffs[rxn.all_entries.index( entry_right )] # I know this is tedious but this is the only way I found that works.. x = (c1 * x1 + c2 * x2) / (c1 + c2) if c1 * c2 == 0: return evolution_profile entry_mid = VirtualEntry.from_mixing({ entry_left: c1 / (c1 + c2), entry_right: c2 / (c1 + c2) }) h_mid = pd.get_decomp_and_e_above_hull(entry_mid)[1] evolution_profile[x] = (intersect, h_mid) return evolution_profile except ReactionError: pass x_mid = (x1 + x2) / 2.0 entry_mid = get_mix_entry({entry1: 0.5, entry2: 0.5}) (decomp_mid, h_mid) = pd.get_decomp_and_e_above_hull(entry_mid) decomp_mid = set(decomp_mid.keys()) evolution_profile[x_mid] = (decomp_mid, h_mid) part1 = get_full_evolution_profile(pd, entry1, entry2, x1, x_mid) part2 = get_full_evolution_profile(pd, entry1, entry2, x_mid, x2) evolution_profile.update(part1) evolution_profile.update(part2) return evolution_profile
def get_printable_PE_data_in_pd(self, entries=None): decomp, hull_e = self.get_decomp_entries_and_e_above_hull(entries=entries) output = ['-' * 60] PE = list(decomp.keys()) output.append("Reduced formula of the given composition: " + self.composition.reduced_formula) output.append("Calculated phase equilibria: " + "\t".join(i.name for i in PE)) rxn = ComputedReaction([self], PE) rxn.normalize_to(self.composition.reduced_composition) output.append(str(rxn)) output.append('-' * 60) string = '\n'.join(output) return string
def setUp(self): d = [ { "correction": 0.0, "data": {}, "energy": -108.56492362, "parameters": {}, "composition": { "Li": 54 }, }, { "correction": 0.0, "data": {}, "energy": -577.94689128, "parameters": {}, "composition": { "O": 32, "Li": 64 }, }, { "correction": 0.0, "data": {}, "energy": -17.02844794, "parameters": {}, "composition": { "O": 2 }, }, { "correction": 0.0, "data": {}, "energy": -959.64693323, "parameters": {}, "composition": { "O": 72, "Li": 72 }, }, ] entries = [] for e in d: entries.append(ComputedEntry.from_dict(e)) rcts = list( filter(lambda e: e.composition.reduced_formula in ["Li", "O2"], entries)) prods = list( filter(lambda e: e.composition.reduced_formula == "Li2O2", entries)) self.rxn = ComputedReaction(rcts, prods)
def get_evolution_phases_table_string(self, open_el, pure_el_ref, PE_list, oe_amt_list, mu_trans_list, allowpmu): if not allowpmu: mu_h_list = [0] + mu_trans_list mu_l_list = mu_h_list[1:] + ['-inf'] df = pandas.DataFrame() df['mu_high (eV)'] = mu_h_list df['mu_low (eV)'] = mu_l_list df['d(n_{})'.format(open_el)] = oe_amt_list PE_names = [] rxns = [] for PE in PE_list: rxn = ComputedReaction([self, pure_el_ref], PE) rxn.normalize_to(self.composition.reduced_composition) PE_names.append(', '.join(sorted([_.name for _ in PE]))) rxns.append(str(rxn)) df['Phase equilibria'] = PE_names df['Reaction'] = rxns print_df = df.to_string(index=False, float_format='{:,.2f}'.format, justify='center') return print_df
def compute_corrections(self, exp_entries: list, calc_entries: dict) -> dict: """ Computes the corrections and fills in correction, corrections_std_error, and corrections_dict. Args: exp_entries: list of dictionary objects with the following keys/values: {"formula": chemical formula, "exp energy": formation energy in eV/formula unit, "uncertainty": uncertainty in formation energy} calc_entries: dictionary of computed entries, of the form {chemical formula: ComputedEntry} Raises: ValueError: calc_compounds is missing an entry """ self.exp_compounds = exp_entries self.calc_compounds = calc_entries self.names: List[str] = [] self.diffs: List[float] = [] self.coeff_mat: List[List[float]] = [] self.exp_uncer: List[float] = [] # remove any corrections in calc_compounds for entry in self.calc_compounds.values(): entry.correction = 0 for cmpd_info in self.exp_compounds: # to get consistent element ordering in formula name = Composition(cmpd_info["formula"]).reduced_formula allow = True compound = self.calc_compounds.get(name, None) if not compound: warnings.warn( "Compound {} is not found in provided computed entries and is excluded from the fit" .format(name)) continue # filter out compounds with large uncertainties relative_uncertainty = abs(cmpd_info["uncertainty"] / cmpd_info["exp energy"]) if relative_uncertainty > self.max_error: allow = False warnings.warn( "Compound {} is excluded from the fit due to high experimental uncertainty ({}%)" .format(name, relative_uncertainty)) # filter out compounds containing certain polyanions for anion in self.exclude_polyanions: if anion in name or anion in cmpd_info["formula"]: allow = False warnings.warn( "Compound {} contains the polyanion {} and is excluded from the fit" .format(name, anion)) break # filter out compounds that are unstable if isinstance(self.allow_unstable, float): try: eah = compound.data["e_above_hull"] except KeyError: raise ValueError("Missing e above hull data") if eah > self.allow_unstable: allow = False warnings.warn( "Compound {} is unstable and excluded from the fit (e_above_hull = {})" .format(name, eah)) if allow: comp = Composition(name) elems = list(comp.as_dict()) reactants = [] for elem in elems: try: elem_name = Composition(elem).reduced_formula reactants.append(self.calc_compounds[elem_name]) except KeyError: raise ValueError("Computed entries missing " + elem) rxn = ComputedReaction(reactants, [compound]) rxn.normalize_to(comp) energy = rxn.calculated_reaction_energy coeff = [] for specie in self.species: if specie == "oxide": if compound.data["oxide_type"] == "oxide": coeff.append(comp["O"]) self.oxides.append(name) else: coeff.append(0) elif specie == "peroxide": if compound.data["oxide_type"] == "peroxide": coeff.append(comp["O"]) self.peroxides.append(name) else: coeff.append(0) elif specie == "superoxide": if compound.data["oxide_type"] == "superoxide": coeff.append(comp["O"]) self.superoxides.append(name) else: coeff.append(0) elif specie == "S": if Element("S") in comp: sf_type = "sulfide" if compound.data.get("sulfide_type"): sf_type = compound.data["sulfide_type"] elif hasattr(compound, "structure"): sf_type = sulfide_type(compound.structure) if sf_type == "sulfide": coeff.append(comp["S"]) self.sulfides.append(name) else: coeff.append(0) else: coeff.append(0) else: try: coeff.append(comp[specie]) except ValueError: raise ValueError( "We can't detect this specie: {}".format( specie)) self.names.append(name) self.diffs.append( (cmpd_info["exp energy"] - energy) / comp.num_atoms) self.coeff_mat.append([i / comp.num_atoms for i in coeff]) self.exp_uncer.append( (cmpd_info["uncertainty"]) / comp.num_atoms) # for any exp entries with no uncertainty value, assign average uncertainty value sigma = np.array(self.exp_uncer) sigma[sigma == 0] = np.nan with warnings.catch_warnings(): warnings.simplefilter( "ignore", category=RuntimeWarning ) # numpy raises warning if the entire array is nan values mean_uncer = np.nanmean(sigma) sigma = np.where(np.isnan(sigma), mean_uncer, sigma) if np.isnan(mean_uncer): # no uncertainty values for any compounds, don't try to weight popt, self.pcov = curve_fit(_func, self.coeff_mat, self.diffs, p0=np.ones(len(self.species))) else: popt, self.pcov = curve_fit( _func, self.coeff_mat, self.diffs, p0=np.ones(len(self.species)), sigma=sigma, absolute_sigma=True, ) self.corrections = popt.tolist() self.corrections_std_error = np.sqrt(np.diag(self.pcov)).tolist() for i in range(len(self.species)): self.corrections_dict[self.species[i]] = ( round(self.corrections[i], 3), round(self.corrections_std_error[i], 4), ) return self.corrections_dict
try: stable_result = get_most_stable_entry(result) except IndexError: error["error"] = "No structure with that formula" json_dir = output_path + "/" + "output.json" with open(json_dir, "w") as outfile: json.dump(error, outfile) print(error) exit() # print stable_result.name, phase.get_form_energy(stable_result) (compo, factor) = stable_result.composition.get_reduced_composition_and_factor() form_energy[stable_result.name] = phase.get_form_energy(stable_result) / factor reaction = ComputedReaction(stable_formula, [stable_result]) energy = FloatWithUnit(reaction.calculated_reaction_energy, "eV atom^-1") json_result = dict() json_result["reaction"] = str(reaction) value1 = dict() value1["value"] = energy.to("kJ mol^-1") value1["unit"] = "kJ mol^-1" value2 = dict() value2["value"] = energy.to("eV atom^-1") value2["unit"] = "eV" json_result["value1"] = value1 json_result["value2"] = value2
def test_calculated_reaction_energy_uncertainty_for_nan(self): # test that reaction_energy_uncertainty property is nan when the uncertainty # for any product/reactant is nan d = [ { "correction": 0.0, "data": {}, "energy": -108.56492362, "parameters": {}, "composition": { "Li": 54 }, }, { "correction": 0.0, "data": {}, "energy": -17.02844794, "parameters": {}, "composition": { "O": 2 }, }, { '@module': 'pymatgen.entries.computed_entries', '@class': 'ComputedEntry', 'energy': -38.76889738, 'composition': defaultdict(float, { 'Li': 4.0, 'O': 4.0 }), 'energy_adjustments': [{ '@module': 'pymatgen.entries.computed_entries', '@class': 'ConstantEnergyAdjustment', '@version': '2020.6.8', 'value': -1.864, 'uncertainty': np.nan, 'name': 'MP2020 Composition Correction', 'cls': { '@module': 'pymatgen.entries.compatibility', '@class': 'MaterialsProject2020Compatibility', '@version': '2020.6.8', 'compat_type': 'Advanced', 'correct_peroxide': True, 'check_potcar_hash': False }, 'description': 'Constant energy adjustment (-1.864 eV)' }], 'parameters': { 'run_type': 'GGA', 'is_hubbard': False, 'pseudo_potential': { 'functional': 'PBE', 'labels': ['Li_sv', 'O'], 'pot_type': 'paw' }, 'hubbards': {}, 'potcar_symbols': ['PBE Li_sv', 'PBE O'], 'oxide_type': 'peroxide' }, 'data': { 'oxide_type': 'peroxide' }, 'entry_id': 'mp-841', 'correction': -1.864 }, ] entries = [] for e in d: entries.append(ComputedEntry.from_dict(e)) rcts = list( filter(lambda e: e.composition.reduced_formula in ["Li", "O2"], entries)) prods = list( filter(lambda e: e.composition.reduced_formula == "Li2O2", entries)) rxn_with_uncertainty = ComputedReaction(rcts, prods) self.assertTrue( isnan(rxn_with_uncertainty.calculated_reaction_energy_uncertainty))
def test_calculated_reaction_energy_uncertainty_for_nan(self): # test that reaction_energy_uncertainty property is nan when the uncertainty # for any product/reactant is nan d = [ { "correction": 0.0, "data": {}, "energy": -108.56492362, "parameters": {}, "composition": {"Li": 54}, }, { "correction": 0.0, "data": {}, "energy": -17.02844794, "parameters": {}, "composition": {"O": 2}, }, { "@module": "pymatgen.entries.computed_entries", "@class": "ComputedEntry", "energy": -38.76889738, "composition": defaultdict(float, {"Li": 4.0, "O": 4.0}), "energy_adjustments": [ { "@module": "pymatgen.entries.computed_entries", "@class": "ConstantEnergyAdjustment", "@version": "2020.6.8", "value": -1.864, "uncertainty": np.nan, "name": "MP2020 Composition Correction", "cls": { "@module": "pymatgen.entries.compatibility", "@class": "MaterialsProject2020Compatibility", "@version": "2020.6.8", "compat_type": "Advanced", "correct_peroxide": True, "check_potcar_hash": False, }, "description": "Constant energy adjustment (-1.864 eV)", } ], "parameters": { "run_type": "GGA", "is_hubbard": False, "pseudo_potential": { "functional": "PBE", "labels": ["Li_sv", "O"], "pot_type": "paw", }, "hubbards": {}, "potcar_symbols": ["PBE Li_sv", "PBE O"], "oxide_type": "peroxide", }, "data": {"oxide_type": "peroxide"}, "entry_id": "mp-841", "correction": -1.864, }, ] entries = [] for e in d: entries.append(ComputedEntry.from_dict(e)) rcts = list( filter(lambda e: e.composition.reduced_formula in ["Li", "O2"], entries) ) prods = list( filter(lambda e: e.composition.reduced_formula == "Li2O2", entries) ) rxn_with_uncertainty = ComputedReaction(rcts, prods) self.assertTrue( isnan(rxn_with_uncertainty.calculated_reaction_energy_uncertainty) )
def find_theo_redenth(compstr): """ Finds theoretical redox enthalpies from the Materials Project from perovskite to brownmillerite based partially on https://github.com/materialsproject/pymatgen/blob/b3e972e293885c5b3c69fb3e9aa55287869d4d84/ examples/Calculating%20Reaction%20Energies%20with%20the%20Materials%20API.ipynb :param compstr: composition as a string :return: red_enth: redox enthalpy in kJ/mol O """ compstr_perovskite = compstr.split("O")[0] + "O3" comp_spl = split_comp(compstr) chem_sys = "" for i in range(len(comp_spl)): if comp_spl[i] is not None: chem_sys = chem_sys + comp_spl[i][0] + "-" chem_sys = chem_sys + "O" chem_sys = chem_sys.split("-") all_entries = mpr.get_entries_in_chemsys(chem_sys) # This method simply gets the lowest energy entry for all entries with the same composition. def get_most_stable_entry(formula): relevant_entries = [entry for entry in all_entries if entry.composition.reduced_formula == Composition(formula).reduced_formula] relevant_entries = sorted(relevant_entries, key=lambda e: e.energy_per_atom) return relevant_entries[0] formula_spl = [''.join(g) for _, g in groupby(str(compstr), str.isalpha)] perov_formula = [] for k in range(len(formula_spl)): try: perov_formula += str(int(float(formula_spl[k]) * 8)) except ValueError: perov_formula += str(formula_spl[k]) perov_formula = "".join(perov_formula) perov_formula = str(perov_formula).split("O")[0] + "O24" perovskite = get_most_stable_entry(perov_formula) brownm_formula = [] for k in range(len(formula_spl)): try: brownm_formula += str(int(float(formula_spl[k]) * 32)) except ValueError: brownm_formula += str(formula_spl[k]) brownm_formula = "".join(brownm_formula) brownm_formula = str(brownm_formula).split("O")[0] + "O80" brownmillerite = get_most_stable_entry(brownm_formula) # for oxygen: do not use the most stable phase O8 but the most stable O2 phase def get_oxygen(): relevant_entries = [entry for entry in all_entries if entry.composition == Composition("O2")] relevant_entries = sorted(relevant_entries, key=lambda e: e.energy_per_atom) return relevant_entries[0] oxygen = get_oxygen() reaction = ComputedReaction([perovskite], [brownmillerite, oxygen]) energy = FloatWithUnit(reaction.calculated_reaction_energy, "eV atom^-1") # figure out the stoichiometry of O2 in the reaction equation in order to normalize the energies per mol of O try: o_stoich = float(str(str(reaction.as_dict).split(" O2")[0]).split()[-1]) except ValueError: o_stoich = 1 # energy in J/mol per mol of O2 ener = (float(energy.to("kJ mol^-1")) * 1000) / o_stoich # per mol of O ener = ener / 2 return ener
def get_gibbs_rxn(self, rxn): reactant_gibbs_entries = self.get_gibbs_computed_structure_entries( rxn._reactant_entries) product_gibbs_entries = self.get_gibbs_computed_structure_entries( rxn._product_entries) return ComputedReaction(reactant_gibbs_entries, product_gibbs_entries)
#This method simply gets the lowest energy entry for all entry with the same composition. def get_most_stable_entry(formula): relevant_entries = [ entry for entry in all_entries if entry.composition.reduced_formula == Composition(formula).reduced_formula ] relevant_entries = sorted(relevant_entries, key=lambda e: e.energy_per_atom) return relevant_entries[0] CaO = get_most_stable_entry("CaO") CO2 = get_most_stable_entry("CO2") CaCO3 = get_most_stable_entry("CaCO3") reaction = ComputedReaction([CaO, CO2], [CaCO3]) print("Caculated") print(reaction) print("Reaction energy = {:.2f}".format(reaction.calculated_reaction_energy * EV_PER_ATOM_TO_KJ_PER_MOL) ) #Conversion needed since our computed energies are in eV. print() # The following portions demonstrate how to get the experimental values as well. exp_CaO = a.get_exp_entry("CaO") exp_CaCO3 = a.get_exp_entry("CaCO3") #Unfortunately, the Materials Project database does not have gas phase experimental entries. This is the value from NIST. We manually create the entry. #Exp entries should be in kJ/mol. exp_CO2 = ComputedEntry("CO2", -393.51)