def get_hull_distance(competing_phase_directory='../competing_phases'): """ Calculate the material's distance to the thermodynamic hull, based on species in the Materials Project database. Args: competing_phase_directory (str): absolute or relative path to the location where your competing phases have been relaxed. The default expectation is that they are stored in a directory named 'competing_phases' at the same level as your material's relaxation directory. Returns: float. distance (eV/atom) between the material and the hull. """ finished_competitors = {} original_directory = os.getcwd() # Determine which competing phases have been relaxed in the current # framework and store them in a dictionary ({formula: entry}). if os.path.isdir(competing_phase_directory): os.chdir(competing_phase_directory) for comp_dir in [ dir for dir in os.listdir(os.getcwd()) if os.path.isdir(dir) and is_converged(dir) ]: vasprun = Vasprun('{}/vasprun.xml'.format(comp_dir)) composition = vasprun.final_structure.composition energy = vasprun.final_energy finished_competitors[comp_dir] = ComputedEntry(composition, energy) os.chdir(original_directory) else: raise ValueError('Competing phase directory does not exist.') composition = Structure.from_file('POSCAR').composition try: energy = Vasprun('vasprun.xml').final_energy except: raise ValueError('This directory does not have a converged vasprun.xml') my_entry = ComputedEntry(composition, energy) # 2D material entries = MPR.get_entries_in_chemsys( [elt.symbol for elt in composition] ) # If the energies of competing phases have been calculated in # the current framework, put them in the phase diagram instead # of the MP energies. for i in range(len(entries)): formula = entries[i].composition.reduced_formula if formula in finished_competitors: entries[i] = finished_competitors[formula] else: entries[i] = ComputedEntry(entries[i].composition, 100) entries.append(my_entry) # 2D material pda = PDAnalyzer(PhaseDiagram(entries)) decomp = pda.get_decomp_and_e_above_hull(my_entry, allow_negative=True) return decomp[1]
def get_competing_phases_new(structure): """ Collect the species to which the 2D materials might decompose to. Since a lot of 2D materials with similar compositions will have the same competing phases, duplicates aren't counted. """ total_competing_phases = [] composition = structure.composition energy = 100 my_entry = ComputedEntry(composition, energy) # 2D material entries = MPR.get_entries_in_chemsys( elements=[elt.symbol for elt in composition]) entries.append(my_entry) # 2D material pda = PDAnalyzer(PhaseDiagram(entries)) decomp = pda.get_decomp_and_e_above_hull(my_entry, allow_negative=True) competing_phases = [(entry.composition.reduced_formula, entry.entry_id) for entry in decomp[0]] # Keep a running list of all unique competing phases, since in # high throughput 2D searches there is usually some overlap in # competing phases for different materials. for specie in competing_phases: if specie not in total_competing_phases: total_competing_phases.append(specie) return total_competing_phases
def get_competing_phases(): """ Collect the species to which the material might decompose to. Returns: A list of phases as tuples formatted as [(formula_1, Materials_Project_ID_1), (formula_2, Materials_Project_ID_2), ...] """ composition = Structure.from_file('POSCAR').composition try: energy = Vasprun('vasprun.xml').final_energy except: energy = 100 # The function can work without a vasprun.xml entries = MPR.get_entries_in_chemsys([elt.symbol for elt in composition]) my_entry = ComputedEntry(composition, energy) entries.append(my_entry) pda = PDAnalyzer(PhaseDiagram(entries)) decomp = pda.get_decomp_and_e_above_hull(my_entry, allow_negative=True) competing_phases = [(entry.composition.reduced_formula, entry.entry_id) for entry in decomp[0]] return competing_phases
def test_1d_pd(self): entry = PDEntry('H', 0) pd = PhaseDiagram([entry]) pda = PDAnalyzer(pd) decomp, e = pda.get_decomp_and_e_above_hull(PDEntry('H', 1)) self.assertAlmostEqual(e, 1) self.assertAlmostEqual(decomp[entry], 1.0)
def test_get_stability(self): entries = self.rester.get_entries_in_chemsys(["Fe", "O"]) modified_entries = [] for entry in entries: # Create modified entries with energies that are 0.01eV higher # than the corresponding entries. if entry.composition.reduced_formula == "Fe2O3": modified_entries.append( ComputedEntry(entry.composition, entry.uncorrected_energy + 0.01, parameters=entry.parameters, entry_id="mod_{}".format(entry.entry_id))) rest_ehulls = self.rester.get_stability(modified_entries) all_entries = entries + modified_entries compat = MaterialsProjectCompatibility() all_entries = compat.process_entries(all_entries) pd = PhaseDiagram(all_entries) a = PDAnalyzer(pd) for e in all_entries: if str(e.entry_id).startswith("mod"): for d in rest_ehulls: if d["entry_id"] == e.entry_id: data = d break self.assertAlmostEqual(a.get_e_above_hull(e), data["e_above_hull"])
def get_competing_phases(): """ Collect the species to which the material might decompose to. Returns: A list of phases as tuples formatted as [(formula_1, Materials_Project_ID_1), (formula_2, Materials_Project_ID_2), ...] """ total_competing_phases = [] composition = Structure.from_file('POSCAR').composition try: energy = Vasprun('vasprun.xml').final_energy except: energy = 100 # The function can work without a vasprun.xml entries = MPR.get_entries_in_chemsys( [elt.symbol for elt in composition] ) my_entry = ComputedEntry(composition, energy) entries.append(my_entry) pda = PDAnalyzer(PhaseDiagram(entries)) decomp = pda.get_decomp_and_e_above_hull(my_entry, allow_negative=True) competing_phases = [ (entry.composition.reduced_formula, entry.entry_id) for entry in decomp[0] ] return competing_phases
def get_hull_distance(competing_phase_directory='../competing_phases'): """ Calculate the material's distance to the thermodynamic hull, based on species in the Materials Project database. Args: competing_phase_directory (str): absolute or relative path to the location where your competing phases have been relaxed. The default expectation is that they are stored in a directory named 'competing_phases' at the same level as your material's relaxation directory. Returns: float. distance (eV/atom) between the material and the hull. """ finished_competitors = {} original_directory = os.getcwd() # Determine which competing phases have been relaxed in the current # framework and store them in a dictionary ({formula: entry}). if os.path.isdir(competing_phase_directory): os.chdir(competing_phase_directory) for comp_dir in [ dir for dir in os.listdir(os.getcwd()) if os.path.isdir(dir) and is_converged(dir) ]: vasprun = Vasprun('{}/vasprun.xml'.format(comp_dir)) composition = vasprun.final_structure.composition energy = vasprun.final_energy finished_competitors[comp_dir] = ComputedEntry(composition, energy) os.chdir(original_directory) else: raise ValueError('Competing phase directory does not exist.') composition = Structure.from_file('POSCAR').composition try: energy = Vasprun('vasprun.xml').final_energy except: raise ValueError( 'This directory does not have a converged vasprun.xml') my_entry = ComputedEntry(composition, energy) # 2D material entries = MPR.get_entries_in_chemsys([elt.symbol for elt in composition]) # If the energies of competing phases have been calculated in # the current framework, put them in the phase diagram instead # of the MP energies. for i in range(len(entries)): formula = entries[i].composition.reduced_formula if formula in finished_competitors: entries[i] = finished_competitors[formula] else: entries[i] = ComputedEntry(entries[i].composition, 100) entries.append(my_entry) # 2D material pda = PDAnalyzer(PhaseDiagram(entries)) decomp = pda.get_decomp_and_e_above_hull(my_entry, allow_negative=True) return decomp[1]
def test_get_critical_compositions(self): c1 = Composition('Fe2O3') c2 = Composition('Li3FeO4') c3 = Composition('Li2O') comps = self.analyzer.get_critical_compositions(c1, c2) expected = [ Composition('Fe2O3'), Composition('Li0.3243244Fe0.1621621O0.51351349') * 7.4, Composition('Li3FeO4') ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) comps = self.analyzer.get_critical_compositions(c1, c3) expected = [ Composition('Fe2O3'), Composition('LiFeO2'), Composition('Li5FeO4') / 3, Composition('Li2O') ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) # Don't fail silently if input compositions aren't in phase diagram # Can be very confusing if you're working with a GrandPotentialPD self.assertRaises(ValueError, self.analyzer.get_critical_compositions, Composition('Xe'), Composition('Mn')) # For the moment, should also fail even if compositions are in the gppd # because it isn't handled properly gppd = GrandPotentialPhaseDiagram(self.pd.all_entries, {'Xe': 1}, self.pd.elements + [Element('Xe')]) pda = PDAnalyzer(gppd) self.assertRaises(ValueError, pda.get_critical_compositions, Composition('Fe2O3'), Composition('Li3FeO4Xe')) # check that the function still works though comps = pda.get_critical_compositions(c1, c2) expected = [ Composition('Fe2O3'), Composition('Li0.3243244Fe0.1621621O0.51351349') * 7.4, Composition('Li3FeO4') ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) # case where the endpoints are identical self.assertEqual(self.analyzer.get_critical_compositions(c1, c1 * 2), [c1, c1 * 2])
def get_e_above_hull(self): """ Get e_above_hull for this compound Args: allow_negative: whether to calculate negative energy above hull for stable compound Returns: decomposition, energy above hull """ pda = PDAnalyzer(self.pd) (decomp, hull) = pda.get_decomp_and_e_above_hull(self.entry) decomp = [compound.composition.reduced_formula for compound in decomp] return (decomp, hull)
def get_deco(e, pda): """get_deco Gets the non-stoichiometric decomposition products of e. :param e: Entry we want the decomposition of. :param pda: PDAnalzyer containing e. """ e_c = e.composition.reduced_composition shifted_entries = [copy.deepcopy(e) for e in pda._pd.all_entries] for o_e in shifted_entries: if o_e.composition.reduced_composition == e_c: o_e.uncorrected_energy += 10.0 new_pd = PhaseDiagram(shifted_entries) new_pda = PDAnalyzer(new_pd) decomp = new_pda.get_decomposition(e_c) return decomp
def test_dim1(self): #Ensure that dim 1 PDs can eb generated. for el in ["Li", "Fe", "O2"]: entries = [e for e in self.entries if e.composition.reduced_formula == el] pd = PhaseDiagram(entries) self.assertEqual(len(pd.stable_entries), 1) a = PDAnalyzer(pd) for e in entries: decomp, ehull = a.get_decomp_and_e_above_hull(e) self.assertGreaterEqual(ehull, 0) plotter = PDPlotter(pd) lines, stable_entries, unstable_entries = plotter.pd_plot_data self.assertEqual(lines[0][1], [0, 0])
def from_composition_and_pd(comp, pd, working_ion_symbol="Li"): """ Convenience constructor to make a ConversionElectrode from a composition and a phase diagram. Args: comp: Starting composition for ConversionElectrode, e.g., Composition("FeF3") pd: A PhaseDiagram of the relevant system (e.g., Li-Fe-F) working_ion_symbol: Element symbol of working ion. Defaults to Li. """ working_ion = Element(working_ion_symbol) entry = None working_ion_entry = None for e in pd.stable_entries: if e.composition.reduced_formula == comp.reduced_formula: entry = e elif e.is_element and \ e.composition.reduced_formula == working_ion_symbol: working_ion_entry = e if not entry: raise ValueError( "Not stable compound found at composition {}.".format(comp)) analyzer = PDAnalyzer(pd) profile = analyzer.get_element_profile(working_ion, comp) # Need to reverse because voltage goes form most charged to most # discharged. profile.reverse() if len(profile) < 2: return None working_ion_entry = working_ion_entry working_ion = working_ion_entry.composition.elements[0].symbol normalization_els = {} for el, amt in comp.items(): if el != Element(working_ion): normalization_els[el] = amt vpairs = [ ConversionVoltagePair.from_steps(profile[i], profile[i + 1], normalization_els) for i in range(len(profile) - 1) ] return ConversionElectrode(vpairs, working_ion_entry, comp)
def test_dim1(self): #Ensure that dim 1 PDs can eb generated. for el in ["Li", "Fe", "O2"]: entries = [ e for e in self.entries if e.composition.reduced_formula == el ] pd = PhaseDiagram(entries) self.assertEqual(len(pd.stable_entries), 1) a = PDAnalyzer(pd) for e in entries: decomp, ehull = a.get_decomp_and_e_above_hull(e) self.assertGreaterEqual(ehull, 0) plotter = PDPlotter(pd) lines, stable_entries, unstable_entries = plotter.pd_plot_data self.assertEqual(lines[0][1], [0, 0])
def from_composition_and_pd(comp, pd, working_ion_symbol="Li"): """ Convenience constructor to make a ConversionElectrode from a composition and a phase diagram. Args: comp: Starting composition for ConversionElectrode, e.g., Composition("FeF3") pd: A PhaseDiagram of the relevant system (e.g., Li-Fe-F) working_ion_symbol: Element symbol of working ion. Defaults to Li. """ working_ion = Element(working_ion_symbol) entry = None working_ion_entry = None for e in pd.stable_entries: if e.composition.reduced_formula == comp.reduced_formula: entry = e elif e.is_element and \ e.composition.reduced_formula == working_ion_symbol: working_ion_entry = e if not entry: raise ValueError("Not stable compound found at composition {}." .format(comp)) analyzer = PDAnalyzer(pd) profile = analyzer.get_element_profile(working_ion, comp) # Need to reverse because voltage goes form most charged to most # discharged. profile.reverse() if len(profile) < 2: return None working_ion_entry = working_ion_entry working_ion = working_ion_entry.composition.elements[0].symbol normalization_els = {} for el, amt in comp.items(): if el != Element(working_ion): normalization_els[el] = amt vpairs = [ConversionVoltagePair.from_steps(profile[i], profile[i + 1], normalization_els) for i in range(len(profile) - 1)] return ConversionElectrode(vpairs, working_ion_entry, comp)
def get_mu_range(mpid, ext_elts=[]): if 'hse' in mpid: mpid = mpid.split('_')[0] try: entry = m.get_entry_by_material_id(mpid) in_MP = True except: from high_throughput.defects.database import TasksOperater in_MP = False TO = TasksOperater() id_ = TO.groups['bulk'][mpid][0] rec = TO.collection.find_one({'_id':id_},['output']) stru_tmp =Structure.from_dict(rec['output']['crystal']) energy = rec['output']['final_energy'] entry = PDEntry(stru_tmp.composition, energy) elts = [i.symbol for i in entry.composition.elements] for i in ext_elts: elts.append(i) entries = m.get_entries_in_chemsys(elts) if not in_MP: entries.append(entry) for entry in entries: entry.correction = 0.0 pd=PhaseDiagram(entries) pda=PDAnalyzer(pd) chempots={} decompositions=pda.get_decomposition(entry.composition).keys() decomposition_inds=[pd.qhull_entries.index(entry) for entry in decompositions] facets_around=[] for facet in pd.facets: is_facet_around=True for ind in decomposition_inds: if ind not in facet: is_facet_around=False if is_facet_around==True: facets_around.append(facet) for facet in facets_around: s=[] for ind in facet: s.append(str(pd.qhull_entries[ind].name)) s.sort() chempots['-'.join(s)]=pda.get_facet_chempots(facet) chempots={i:{j.symbol:chempots[i][j] for j in chempots[i]} for i in chempots} return chempots
def test_get_critical_compositions(self): c1 = Composition('Fe2O3') c2 = Composition('Li3FeO4') c3 = Composition('Li2O') comps = self.analyzer.get_critical_compositions(c1, c2) expected = [Composition('Fe2O3'), Composition('Li0.3243244Fe0.1621621O0.51351349') * 7.4, Composition('Li3FeO4')] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) comps = self.analyzer.get_critical_compositions(c1, c3) expected = [Composition('Fe2O3'), Composition('LiFeO2'), Composition('Li5FeO4') / 3, Composition('Li2O')] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) # Don't fail silently if input compositions aren't in phase diagram # Can be very confusing if you're working with a GrandPotentialPD self.assertRaises(ValueError, self.analyzer.get_critical_compositions, Composition('Xe'), Composition('Mn')) # For the moment, should also fail even if compositions are in the gppd # because it isn't handled properly gppd = GrandPotentialPhaseDiagram(self.pd.all_entries, {'Xe': 1}, self.pd.elements + [Element('Xe')]) pda = PDAnalyzer(gppd) self.assertRaises(ValueError, pda.get_critical_compositions, Composition('Fe2O3'), Composition('Li3FeO4Xe')) # check that the function still works though comps = pda.get_critical_compositions(c1, c2) expected = [Composition('Fe2O3'), Composition('Li0.3243244Fe0.1621621O0.51351349') * 7.4, Composition('Li3FeO4')] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) # case where the endpoints are identical self.assertEqual(self.analyzer.get_critical_compositions(c1, c1 * 2), [c1, c1 * 2])
def main(): """ Main function. """ # Read the calculation results into a ComputedEntry. entl = VaspToComputedEntryDrone().assimilate(sys.argv[1]) # Check if our local calculation is compatible with MP. if MaterialsProjectCompatibility().process_entry(entl) is None: print("Calculation not compatible with MP.") sys.exit(0) # Get other entries sharing a chemical system with the results. chemsys = [ele for ele in entl.composition.as_dict()] with MPRester() as mpr: chemsys_entries = mpr.get_entries_in_chemsys(chemsys) # Append our local calculation to the list of entries. chemsys_entries.append(entl) # Process the entries. p_e = MaterialsProjectCompatibility().process_entries(chemsys_entries) # Build a phase diagram and an analyzer for it. p_d = PhaseDiagram(p_e) pda = PDAnalyzer(p_d) # Scan stable entries for our calculation. for ent in p_d.stable_entries: if ent.entry_id is None: print(str(ent.composition.reduced_formula) + " 0.0") sys.exit(0) # Scan unstable entries for our calculation and print decomposition. for ent in p_d.unstable_entries: if ent.entry_id is None: dco, ehull = pda.get_decomp_and_e_above_hull(ent) pretty_dc = [("{}:{}".format(k.composition.reduced_formula, k.entry_id), round(v, 2)) for k, v in dco.items()] print( str(ent.composition.reduced_formula) + " %.3f" % ehull + " " + str(pretty_dc))
def process_item(self, item): """ Process the list of entries into a phase diagram Args: item (set(entry)): a list of entries to process into a phase diagram Returns: [dict]: a list of thermo dictionaries to update thermo with """ entries = self.__compat.process_entries(item) try: pd = PhaseDiagram(entries) analyzer = PDAnalyzer(pd) docs = [] for e in entries: (decomp, ehull) = \ analyzer.get_decomp_and_e_above_hull(e) d = {"material_id": e.entry_id} d["thermo"] = {} d["thermo"][ "formation_energy_per_atom"] = pd.get_form_energy_per_atom( e) d["thermo"]["e_above_hull"] = ehull d["thermo"]["is_stable"] = e in stable_entries d["thermo"][ "eq_reaction_e"] = analyzer.get_equilibrium_reaction_energy( e) d["thermo"]["decomposes_to"] = [{ "material_id": de.entry_id, "formula": de.composition.formula, "amount": amt } for de, amt in decomp.items()] docs.append(d) except PhaseDiagramError as p: self.__logger.warning("Phase diagram error: {}".format(p)) return [] return docs
def get_deco_old(e, pda): """get_deco Gets the non-stoichiometric decomposition products of e. :param e: Entry we want the decomposition of. :param pda: PDAnalzyer containing e. """ # If the entry is stable, we need to lift it off the hull. e_c = e.composition.reduced_composition if "e_above_hull" in e.data and e.data["e_above_hull"] < 0.008: shifted_entries = [copy.deepcopy(e) for e in pda._pd.all_entries] for o_e in shifted_entries: if o_e.composition.reduced_composition == e_c: o_e.uncorrected_energy += 10.0 new_pd = PhaseDiagram(shifted_entries) new_pda = PDAnalyzer(new_pd) decomp = new_pda.get_decomposition(e_c) elif pda.get_decomp_and_e_above_hull(e)[1] < 0.008: shifted_entries = [copy.deepcopy(e) for e in pda._pd.all_entries] for o_e in shifted_entries: if o_e.composition.reduced_composition == e_c: o_e.uncorrected_energy += 10.0 new_pd = PhaseDiagram(shifted_entries) new_pda = PDAnalyzer(new_pd) decomp = new_pda.get_decomposition(e_c) else: decomp = pda.get_decomposition(e_c) return decomp
def get_equilibrium_reaction_energy(self): """ Adapted from pymatgen. Only work if entry is stable(hull = 0) Provides the reaction energy of a stable entry from the neighboring equilibrium stable entries (also known as the inverse distance to hull). Returns: Equilibrium reaction energy of entry. Stable entries should have equilibrium reaction energy <= 0. """ if self.entry not in self.pd.stable_entries: raise ValueError("Equilibrium reaction energy is available only " "for stable entries.") entries = [e for e in self.pd.stable_entries if e != self.entry ] #all stable entries without this stable entry modpd = PhaseDiagram(entries, self.pd.elements) analyzer = PDAnalyzer(modpd) (decomp, hull) = analyzer.get_decomp_and_e_above_hull(self.entry, allow_negative=True) decomp = [compound.composition.reduced_formula for compound in decomp] return (decomp, hull)
def get_contour_pd_plot(self): """ Plot a contour phase diagram plot, where phase triangles are colored according to degree of instability by interpolation. Currently only works for 3-component phase diagrams. Returns: A matplotlib plot object. """ from scipy import interpolate from matplotlib import cm pd = self._pd entries = pd.qhull_entries data = np.array(pd.qhull_data) plt = self._get_2d_plot() analyzer = PDAnalyzer(pd) data[:, 0:2] = triangular_coord(data[:, 0:2]).transpose() for i, e in enumerate(entries): data[i, 2] = analyzer.get_e_above_hull(e) gridsize = 0.005 xnew = np.arange(0, 1., gridsize) ynew = np.arange(0, 1, gridsize) f = interpolate.LinearNDInterpolator(data[:, 0:2], data[:, 2]) znew = np.zeros((len(ynew), len(xnew))) for (i, xval) in enumerate(xnew): for (j, yval) in enumerate(ynew): znew[j, i] = f(xval, yval) plt.contourf(xnew, ynew, znew, 1000, cmap=cm.autumn_r) plt.colorbar() return plt
def get_lowest_decomposition(self, composition): """ Get the decomposition leading to lowest cost Args: composition: Composition as a pymatgen.core.structure.Composition Returns: Decomposition as a dict of {Entry: amount} """ entries_list = [] elements = [e.symbol for e in composition.elements] for i in range(len(elements)): for combi in itertools.combinations(elements, i + 1): chemsys = [Element(e) for e in combi] x = self.costdb.get_entries(chemsys) entries_list.extend(x) try: pd = PhaseDiagram(entries_list) return PDAnalyzer(pd).get_decomposition(composition) except IndexError: raise ValueError("Error during PD building; most likely, " "cost data does not exist!")
outfile = open('./results.csv', 'w') outfile.write( "full_compound,working_ion,transition_metal,anion,v_int_max," "v_conv_max,max_rxn,max_species,v_int_min,v_conv_min,min_rxn," "min_species,v_int_gs,v_conv_gs,gs_rxn,gs_species,v_int_topo," "v_conv_topo,v_conv_rxn,v_conv_species\n") calclist = get_subdir(root_dir) for ion in working_ions: for tm in tms: for anion in anions: interc_formula = ion + tm + str(n_tm) + anion + str(n_anion) print("About to run", interc_formula) empty_formula = tm + str(n_tm) + anion + str(n_anion) pd_full = build_complete_pd(interc_formula, calclist) pda_full = PDAnalyzer(pd_full) structs_interc = get_polymorphs(interc_formula, pda_full) structs_empty = get_polymorphs(empty_formula, pda_full) voltage_dict = get_voltages(structs_interc, structs_empty, pda_full) outfile.write(str(interc_formula) + "," + str(ion) + "," + str(tm)\ + "," + str(anion) + "," + str(voltage_dict["max_intercalation"]) + "," + str(voltage_dict["max_conversion"]) + "," + str(voltage_dict["max_rxn"]) + "," + str(voltage_dict["max_dict"]) + "," + str(voltage_dict["min_intercalation"]) + "," + str(voltage_dict["min_conversion"]) + "," + str(voltage_dict["min_rxn"]) + "," + str(voltage_dict["min_dict"]) + "," + str(voltage_dict["ground_state_intercalation"]) + "," +
class PDAnalyzerTest(unittest.TestCase): def setUp(self): module_dir = os.path.dirname(os.path.abspath(__file__)) (elements, entries) = PDEntryIO.from_csv(os.path.join(module_dir, "pdentries_test.csv")) self.pd = PhaseDiagram(entries) self.analyzer = PDAnalyzer(self.pd) def test_get_e_above_hull(self): for entry in self.pd.stable_entries: self.assertLess(self.analyzer.get_e_above_hull(entry), 1e-11, "Stable entries should have e above hull of zero!") for entry in self.pd.all_entries: if entry not in self.pd.stable_entries: e_ah = self.analyzer.get_e_above_hull(entry) self.assertGreaterEqual(e_ah, 0) self.assertTrue(isinstance(e_ah, Number)) def test_get_equilibrium_reaction_energy(self): for entry in self.pd.stable_entries: self.assertLessEqual( self.analyzer.get_equilibrium_reaction_energy(entry), 0, "Stable entries should have negative equilibrium reaction energy!") def test_get_decomposition(self): for entry in self.pd.stable_entries: self.assertEqual(len(self.analyzer.get_decomposition(entry.composition)), 1, "Stable composition should have only 1 decomposition!") dim = len(self.pd.elements) for entry in self.pd.all_entries: ndecomp = len(self.analyzer.get_decomposition(entry.composition)) self.assertTrue(ndecomp > 0 and ndecomp <= dim, "The number of decomposition phases can at most be equal to the number of components.") #Just to test decomp for a ficitious composition ansdict = {entry.composition.formula: amt for entry, amt in self.analyzer.get_decomposition(Composition("Li3Fe7O11")).items()} expected_ans = {"Fe2 O2": 0.0952380952380949, "Li1 Fe1 O2": 0.5714285714285714, "Fe6 O8": 0.33333333333333393} for k, v in expected_ans.items(): self.assertAlmostEqual(ansdict[k], v) def test_get_transition_chempots(self): for el in self.pd.elements: self.assertLessEqual(len(self.analyzer.get_transition_chempots(el)), len(self.pd.facets)) def test_get_element_profile(self): for el in self.pd.elements: for entry in self.pd.stable_entries: if not (entry.composition.is_element): self.assertLessEqual(len(self.analyzer.get_element_profile(el, entry.composition)), len(self.pd.facets)) expected = [{'evolution': 1.0, 'chempot': -4.2582781416666666, 'reaction': 'Li2O + 0.5 O2 -> Li2O2'}, {'evolution': 0, 'chempot': -5.0885906699999968, 'reaction': 'Li2O -> Li2O'}, {'evolution': -1.0, 'chempot': -10.487582010000001, 'reaction': 'Li2O -> 2 Li + 0.5 O2'}] result = self.analyzer.get_element_profile(Element('O'), Composition('Li2O')) for d1, d2 in zip(expected, result): self.assertAlmostEqual(d1['evolution'], d2['evolution']) self.assertAlmostEqual(d1['chempot'], d2['chempot']) self.assertEqual(d1['reaction'], str(d2['reaction'])) def test_get_get_chempot_range_map(self): elements = [el for el in self.pd.elements if el.symbol != "Fe"] self.assertEqual(len(self.analyzer.get_chempot_range_map(elements)), 10) def test_getmu_vertices_stability_phase(self): results = self.analyzer.getmu_vertices_stability_phase(Composition("LiFeO2"), Element("O")) self.assertAlmostEqual(len(results), 6) test_equality = False for c in results: if abs(c[Element("O")]+7.115) < 1e-2 and abs(c[Element("Fe")]+6.596) < 1e-2 and \ abs(c[Element("Li")]+3.931) < 1e-2: test_equality = True self.assertTrue(test_equality,"there is an expected vertex missing in the list") def test_getmu_range_stability_phase(self): results = self.analyzer.get_chempot_range_stability_phase( Composition("LiFeO2"), Element("O")) self.assertAlmostEqual(results[Element("O")][1], -4.4501812249999997) self.assertAlmostEqual(results[Element("Fe")][0], -6.5961470999999996) self.assertAlmostEqual(results[Element("Li")][0], -3.6250022625000007) def test_get_hull_energy(self): for entry in self.pd.stable_entries: h_e = self.analyzer.get_hull_energy(entry.composition) self.assertAlmostEqual(h_e, entry.energy) n_h_e = self.analyzer.get_hull_energy(entry.composition.fractional_composition) self.assertAlmostEqual(n_h_e, entry.energy_per_atom) def test_1d_pd(self): entry = PDEntry('H', 0) pd = PhaseDiagram([entry]) pda = PDAnalyzer(pd) decomp, e = pda.get_decomp_and_e_above_hull(PDEntry('H', 1)) self.assertAlmostEqual(e, 1) self.assertAlmostEqual(decomp[entry], 1.0) def test_get_critical_compositions_fractional(self): c1 = Composition('Fe2O3').fractional_composition c2 = Composition('Li3FeO4').fractional_composition c3 = Composition('Li2O').fractional_composition comps = self.analyzer.get_critical_compositions(c1, c2) expected = [Composition('Fe2O3').fractional_composition, Composition('Li0.3243244Fe0.1621621O0.51351349'), Composition('Li3FeO4').fractional_composition] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) comps = self.analyzer.get_critical_compositions(c1, c3) expected = [Composition('Fe0.4O0.6'), Composition('LiFeO2').fractional_composition, Composition('Li5FeO4').fractional_composition, Composition('Li2O').fractional_composition] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) def test_get_critical_compositions(self): c1 = Composition('Fe2O3') c2 = Composition('Li3FeO4') c3 = Composition('Li2O') comps = self.analyzer.get_critical_compositions(c1, c2) expected = [Composition('Fe2O3'), Composition('Li0.3243244Fe0.1621621O0.51351349') * 7.4, Composition('Li3FeO4')] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) comps = self.analyzer.get_critical_compositions(c1, c3) expected = [Composition('Fe2O3'), Composition('LiFeO2'), Composition('Li5FeO4') / 3, Composition('Li2O')] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) # Don't fail silently if input compositions aren't in phase diagram # Can be very confusing if you're working with a GrandPotentialPD self.assertRaises(ValueError, self.analyzer.get_critical_compositions, Composition('Xe'), Composition('Mn')) # For the moment, should also fail even if compositions are in the gppd # because it isn't handled properly gppd = GrandPotentialPhaseDiagram(self.pd.all_entries, {'Xe': 1}, self.pd.elements + [Element('Xe')]) pda = PDAnalyzer(gppd) self.assertRaises(ValueError, pda.get_critical_compositions, Composition('Fe2O3'), Composition('Li3FeO4Xe')) # check that the function still works though comps = pda.get_critical_compositions(c1, c2) expected = [Composition('Fe2O3'), Composition('Li0.3243244Fe0.1621621O0.51351349') * 7.4, Composition('Li3FeO4')] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) # case where the endpoints are identical self.assertEqual(self.analyzer.get_critical_compositions(c1, c1 * 2), [c1, c1 * 2]) def test_get_composition_chempots(self): c1 = Composition('Fe3.1O4') c2 = Composition('Fe3.2O4.1Li0.01') e1 = self.analyzer.get_hull_energy(c1) e2 = self.analyzer.get_hull_energy(c2) cp = self.analyzer.get_composition_chempots(c1) calc_e2 = e1 + sum(cp[k] * v for k, v in (c2 - c1).items()) self.assertAlmostEqual(e2, calc_e2)
class PDAnalyzerTest(unittest.TestCase): def setUp(self): module_dir = os.path.dirname(os.path.abspath(__file__)) (elements, entries) = PDEntryIO.from_csv( os.path.join(module_dir, "pdentries_test.csv")) self.pd = PhaseDiagram(entries) self.analyzer = PDAnalyzer(self.pd) def test_get_e_above_hull(self): for entry in self.pd.stable_entries: self.assertLess( self.analyzer.get_e_above_hull(entry), 1e-11, "Stable entries should have e above hull of zero!") for entry in self.pd.all_entries: if entry not in self.pd.stable_entries: e_ah = self.analyzer.get_e_above_hull(entry) self.assertGreaterEqual(e_ah, 0) self.assertTrue(isinstance(e_ah, Number)) def test_get_equilibrium_reaction_energy(self): for entry in self.pd.stable_entries: self.assertLessEqual( self.analyzer.get_equilibrium_reaction_energy(entry), 0, "Stable entries should have negative equilibrium reaction energy!" ) def test_get_decomposition(self): for entry in self.pd.stable_entries: self.assertEqual( len(self.analyzer.get_decomposition(entry.composition)), 1, "Stable composition should have only 1 decomposition!") dim = len(self.pd.elements) for entry in self.pd.all_entries: ndecomp = len(self.analyzer.get_decomposition(entry.composition)) self.assertTrue( ndecomp > 0 and ndecomp <= dim, "The number of decomposition phases can at most be equal to the number of components." ) #Just to test decomp for a ficitious composition ansdict = { entry.composition.formula: amt for entry, amt in self.analyzer.get_decomposition( Composition("Li3Fe7O11")).items() } expected_ans = { "Fe2 O2": 0.0952380952380949, "Li1 Fe1 O2": 0.5714285714285714, "Fe6 O8": 0.33333333333333393 } for k, v in expected_ans.items(): self.assertAlmostEqual(ansdict[k], v) def test_get_transition_chempots(self): for el in self.pd.elements: self.assertLessEqual( len(self.analyzer.get_transition_chempots(el)), len(self.pd.facets)) def test_get_element_profile(self): for el in self.pd.elements: for entry in self.pd.stable_entries: if not (entry.composition.is_element): self.assertLessEqual( len( self.analyzer.get_element_profile( el, entry.composition)), len(self.pd.facets)) def test_get_get_chempot_range_map(self): elements = [el for el in self.pd.elements if el.symbol != "Fe"] self.assertEqual(len(self.analyzer.get_chempot_range_map(elements)), 10) def test_getmu_vertices_stability_phase(self): results = self.analyzer.getmu_vertices_stability_phase( Composition("LiFeO2"), Element("O")) self.assertAlmostEqual(len(results), 6) test_equality = False for c in results: if abs(c[Element("O")]+7.115) < 1e-2 and abs(c[Element("Fe")]+6.596) < 1e-2 and \ abs(c[Element("Li")]+3.931) < 1e-2: test_equality = True self.assertTrue(test_equality, "there is an expected vertex missing in the list") def test_getmu_range_stability_phase(self): results = self.analyzer.get_chempot_range_stability_phase( Composition("LiFeO2"), Element("O")) self.assertAlmostEqual(results[Element("O")][1], -4.4501812249999997) self.assertAlmostEqual(results[Element("Fe")][0], -6.5961470999999996) self.assertAlmostEqual(results[Element("Li")][0], -3.6250022625000007) def test_get_hull_energy(self): for entry in self.pd.stable_entries: h_e = self.analyzer.get_hull_energy(entry.composition) self.assertAlmostEqual(h_e, entry.energy) n_h_e = self.analyzer.get_hull_energy( entry.composition.fractional_composition) self.assertAlmostEqual(n_h_e, entry.energy_per_atom) def test_1d_pd(self): entry = PDEntry('H', 0) pd = PhaseDiagram([entry]) pda = PDAnalyzer(pd) decomp, e = pda.get_decomp_and_e_above_hull(PDEntry('H', 1)) self.assertAlmostEqual(e, 1) self.assertAlmostEqual(decomp[entry], 1.0)
] return relevant_entries def find_entry_index(formula, all_entries): entry_index = [ all_entries.index(entry) for entry in all_entries if entry.composition.reduced_formula == Composition( formula).reduced_formula ] return entry_index rester = MPRester( '5TxDLF4Iwa7rGcAl') #Generate your own key from materials project.. mp_entries = rester.get_entries_in_chemsys(["Li", "Fe", "O", "S"]) pd = PhaseDiagram(mp_entries) plotter = PDPlotter(pd) analyzer = PDAnalyzer(pd) li_entries = [e for e in mp_entries if e.composition.reduced_formula == "Li"] uli0 = min(li_entries, key=lambda e: e.energy_per_atom).energy_per_atom el_profile = analyzer.get_element_profile(Element("Li"), Composition("Li2FeSO")) for i, d in enumerate(el_profile): voltage = -(d["chempot"] - uli0) print("Voltage: %s V" % voltage) print(d["reaction"]) print("")
class PDAnalyzerTest(unittest.TestCase): def setUp(self): module_dir = os.path.dirname(os.path.abspath(__file__)) (elements, entries) = PDEntryIO.from_csv(os.path.join(module_dir, "pdentries_test.csv")) self.pd = PhaseDiagram(entries) self.analyzer = PDAnalyzer(self.pd) def test_get_e_above_hull(self): for entry in self.pd.stable_entries: self.assertLess(self.analyzer.get_e_above_hull(entry), 1e-11, "Stable entries should have e above hull of zero!") for entry in self.pd.all_entries: if entry not in self.pd.stable_entries: e_ah = self.analyzer.get_e_above_hull(entry) self.assertGreaterEqual(e_ah, 0) self.assertTrue(isinstance(e_ah, Number)) def test_get_equilibrium_reaction_energy(self): for entry in self.pd.stable_entries: self.assertLessEqual( self.analyzer.get_equilibrium_reaction_energy(entry), 0, "Stable entries should have negative equilibrium reaction energy!") def test_get_decomposition(self): for entry in self.pd.stable_entries: self.assertEqual(len(self.analyzer.get_decomposition(entry.composition)), 1, "Stable composition should have only 1 decomposition!") dim = len(self.pd.elements) for entry in self.pd.all_entries: ndecomp = len(self.analyzer.get_decomposition(entry.composition)) self.assertTrue(ndecomp > 0 and ndecomp <= dim, "The number of decomposition phases can at most be equal to the number of components.") #Just to test decomp for a ficitious composition ansdict = {entry.composition.formula: amt for entry, amt in self.analyzer.get_decomposition(Composition("Li3Fe7O11")).items()} expected_ans = {"Fe2 O2": 0.0952380952380949, "Li1 Fe1 O2": 0.5714285714285714, "Fe6 O8": 0.33333333333333393} for k, v in expected_ans.items(): self.assertAlmostEqual(ansdict[k], v) def test_get_transition_chempots(self): for el in self.pd.elements: self.assertLessEqual(len(self.analyzer.get_transition_chempots(el)), len(self.pd.facets)) def test_get_element_profile(self): for el in self.pd.elements: for entry in self.pd.stable_entries: if not (entry.composition.is_element): self.assertLessEqual(len(self.analyzer.get_element_profile(el, entry.composition)), len(self.pd.facets)) def test_get_get_chempot_range_map(self): elements = [el for el in self.pd.elements if el.symbol != "Fe"] self.assertEqual(len(self.analyzer.get_chempot_range_map(elements)), 10) def test_getmu_vertices_stability_phase(self): results = self.analyzer.getmu_vertices_stability_phase(Composition("LiFeO2"), Element("O")) self.assertAlmostEqual(len(results), 6) test_equality = False for c in results: if abs(c[Element("O")]+7.115) < 1e-2 and abs(c[Element("Fe")]+6.596) < 1e-2 and \ abs(c[Element("Li")]+3.931) < 1e-2: test_equality = True self.assertTrue(test_equality,"there is an expected vertex missing in the list") def test_getmu_range_stability_phase(self): results = self.analyzer.get_chempot_range_stability_phase( Composition("LiFeO2"), Element("O")) self.assertAlmostEqual(results[Element("O")][1], -4.4501812249999997) self.assertAlmostEqual(results[Element("Fe")][0], -6.5961470999999996) self.assertAlmostEqual(results[Element("Li")][0], -3.6250022625000007) def test_get_hull_energy(self): for entry in self.pd.stable_entries: h_e = self.analyzer.get_hull_energy(entry.composition) self.assertAlmostEqual(h_e, entry.energy) n_h_e = self.analyzer.get_hull_energy(entry.composition.fractional_composition) self.assertAlmostEqual(n_h_e, entry.energy_per_atom) def test_1d_pd(self): entry = PDEntry('H', 0) pd = PhaseDiagram([entry]) pda = PDAnalyzer(pd) decomp, e = pda.get_decomp_and_e_above_hull(PDEntry('H', 1)) self.assertAlmostEqual(e, 1) self.assertAlmostEqual(decomp[entry], 1.0)
Composition(names[11]), Composition(names[32]) ] # creating entries from for the LDA phase diagram entries_lda = [] for i in range(len(names)): # entries_lda.append(PDEntry(names[i],enlda[i], names[i], "LDA")) entries_lda.append(PDEntry(names[i], enlda[i], " ", "LDA")) # creating the phase diagram for LDA pd_lda = PhaseDiagram(entries_lda) cpd_lda = CompoundPhaseDiagram(entries_lda, term_comp, normalize_terminal_compositions=True) a_lda = PDAnalyzer(pd_lda) # creating entries from for the PBE phase diagram entries_pbe = [] for i in range(len(names)): # entries_pbe.append(PDEntry(names[i],enpbe[i], names[i], "PBE")) entries_pbe.append(PDEntry(names[i], enpbe[i], " ", "PBE")) # creating the phase diagram for PBE pd_pbe = PhaseDiagram(entries_pbe) cpd_pbe = CompoundPhaseDiagram(entries_pbe, term_comp, normalize_terminal_compositions=True) a_pbe = PDAnalyzer(pd_pbe) # # visualize quaternary phase diagrams
def setUp(self): module_dir = os.path.dirname(os.path.abspath(__file__)) (elements, entries) = PDEntryIO.from_csv( os.path.join(module_dir, "pdentries_test.csv")) self.pd = PhaseDiagram(entries) self.analyzer = PDAnalyzer(self.pd)
def _get_2d_plot(self, label_stable=True, label_unstable=True, ordering=None, energy_colormap=None, vmin_mev=-60.0, vmax_mev=60.0, show_colorbar=True, process_attributes=False): """ Shows the plot using pylab. Usually I won't do imports in methods, but since plotting is a fairly expensive library to load and not all machines have matplotlib installed, I have done it this way. """ plt = pretty_plot(8, 6) from matplotlib.font_manager import FontProperties if ordering is None: (lines, labels, unstable) = self.pd_plot_data else: (_lines, _labels, _unstable) = self.pd_plot_data (lines, labels, unstable) = order_phase_diagram( _lines, _labels, _unstable, ordering) if energy_colormap is None: if process_attributes: for x, y in lines: plt.plot(x, y, "k-", linewidth=3, markeredgecolor="k") # One should think about a clever way to have "complex" # attributes with complex processing options but with a clear # logic. At this moment, I just use the attributes to know # whether an entry is a new compound or an existing (from the # ICSD or from the MP) one. for x, y in labels.keys(): if labels[(x, y)].attribute is None or \ labels[(x, y)].attribute == "existing": plt.plot(x, y, "ko", linewidth=3, markeredgecolor="k", markerfacecolor="b", markersize=12) else: plt.plot(x, y, "k*", linewidth=3, markeredgecolor="k", markerfacecolor="g", markersize=18) else: for x, y in lines: plt.plot(x, y, "ko-", linewidth=3, markeredgecolor="k", markerfacecolor="b", markersize=15) else: from matplotlib.colors import Normalize, LinearSegmentedColormap from matplotlib.cm import ScalarMappable pda = PDAnalyzer(self._pd) for x, y in lines: plt.plot(x, y, "k-", linewidth=3, markeredgecolor="k") vmin = vmin_mev / 1000.0 vmax = vmax_mev / 1000.0 if energy_colormap == 'default': mid = - vmin / (vmax - vmin) cmap = LinearSegmentedColormap.from_list( 'my_colormap', [(0.0, '#005500'), (mid, '#55FF55'), (mid, '#FFAAAA'), (1.0, '#FF0000')]) else: cmap = energy_colormap norm = Normalize(vmin=vmin, vmax=vmax) _map = ScalarMappable(norm=norm, cmap=cmap) _energies = [pda.get_equilibrium_reaction_energy(entry) for coord, entry in labels.items()] energies = [en if en < 0.0 else -0.00000001 for en in _energies] vals_stable = _map.to_rgba(energies) ii = 0 if process_attributes: for x, y in labels.keys(): if labels[(x, y)].attribute is None or \ labels[(x, y)].attribute == "existing": plt.plot(x, y, "o", markerfacecolor=vals_stable[ii], markersize=12) else: plt.plot(x, y, "*", markerfacecolor=vals_stable[ii], markersize=18) ii += 1 else: for x, y in labels.keys(): plt.plot(x, y, "o", markerfacecolor=vals_stable[ii], markersize=15) ii += 1 font = FontProperties() font.set_weight("bold") font.set_size(24) # Sets a nice layout depending on the type of PD. Also defines a # "center" for the PD, which then allows the annotations to be spread # out in a nice manner. if len(self._pd.elements) == 3: plt.axis("equal") plt.xlim((-0.1, 1.2)) plt.ylim((-0.1, 1.0)) plt.axis("off") center = (0.5, math.sqrt(3) / 6) else: all_coords = labels.keys() miny = min([c[1] for c in all_coords]) ybuffer = max(abs(miny) * 0.1, 0.1) plt.xlim((-0.1, 1.1)) plt.ylim((miny - ybuffer, ybuffer)) center = (0.5, miny / 2) plt.xlabel("Fraction", fontsize=28, fontweight='bold') plt.ylabel("Formation energy (eV/fu)", fontsize=28, fontweight='bold') for coords in sorted(labels.keys(), key=lambda x: -x[1]): entry = labels[coords] label = entry.name # The follow defines an offset for the annotation text emanating # from the center of the PD. Results in fairly nice layouts for the # most part. vec = (np.array(coords) - center) vec = vec / np.linalg.norm(vec) * 10 if np.linalg.norm(vec) != 0 \ else vec valign = "bottom" if vec[1] > 0 else "top" if vec[0] < -0.01: halign = "right" elif vec[0] > 0.01: halign = "left" else: halign = "center" if label_stable: if process_attributes and entry.attribute == 'new': plt.annotate(latexify(label), coords, xytext=vec, textcoords="offset points", horizontalalignment=halign, verticalalignment=valign, fontproperties=font, color='g') else: plt.annotate(latexify(label), coords, xytext=vec, textcoords="offset points", horizontalalignment=halign, verticalalignment=valign, fontproperties=font) if self.show_unstable: font = FontProperties() font.set_size(16) pda = PDAnalyzer(self._pd) energies_unstable = [pda.get_e_above_hull(entry) for entry, coord in unstable.items()] if energy_colormap is not None: energies.extend(energies_unstable) vals_unstable = _map.to_rgba(energies_unstable) ii = 0 for entry, coords in unstable.items(): ehull = pda.get_e_above_hull(entry) if ehull < self.show_unstable: vec = (np.array(coords) - center) vec = vec / np.linalg.norm(vec) * 10 \ if np.linalg.norm(vec) != 0 else vec label = entry.name if energy_colormap is None: plt.plot(coords[0], coords[1], "ks", linewidth=3, markeredgecolor="k", markerfacecolor="r", markersize=8) else: plt.plot(coords[0], coords[1], "s", linewidth=3, markeredgecolor="k", markerfacecolor=vals_unstable[ii], markersize=8) if label_unstable: plt.annotate(latexify(label), coords, xytext=vec, textcoords="offset points", horizontalalignment=halign, color="b", verticalalignment=valign, fontproperties=font) ii += 1 if energy_colormap is not None and show_colorbar: _map.set_array(energies) cbar = plt.colorbar(_map) cbar.set_label( 'Energy [meV/at] above hull (in red)\nInverse energy [' 'meV/at] above hull (in green)', rotation=-90, ha='left', va='center') ticks = cbar.ax.get_yticklabels() # cbar.ax.set_yticklabels(['${v}$'.format( # v=float(t.get_text().strip('$'))*1000.0) for t in ticks]) f = plt.gcf() f.set_size_inches((8, 6)) plt.subplots_adjust(left=0.09, right=0.98, top=0.98, bottom=0.07) return plt
class PDAnalyzerTest(unittest.TestCase): def setUp(self): module_dir = os.path.dirname(os.path.abspath(__file__)) (elements, entries) = PDEntryIO.from_csv( os.path.join(module_dir, "pdentries_test.csv")) self.pd = PhaseDiagram(entries) self.analyzer = PDAnalyzer(self.pd) def test_get_e_above_hull(self): for entry in self.pd.stable_entries: self.assertLess( self.analyzer.get_e_above_hull(entry), 1e-11, "Stable entries should have e above hull of zero!") for entry in self.pd.all_entries: if entry not in self.pd.stable_entries: e_ah = self.analyzer.get_e_above_hull(entry) self.assertGreaterEqual(e_ah, 0) self.assertTrue(isinstance(e_ah, Number)) def test_get_equilibrium_reaction_energy(self): for entry in self.pd.stable_entries: self.assertLessEqual( self.analyzer.get_equilibrium_reaction_energy(entry), 0, "Stable entries should have negative equilibrium reaction energy!" ) def test_get_decomposition(self): for entry in self.pd.stable_entries: self.assertEqual( len(self.analyzer.get_decomposition(entry.composition)), 1, "Stable composition should have only 1 decomposition!") dim = len(self.pd.elements) for entry in self.pd.all_entries: ndecomp = len(self.analyzer.get_decomposition(entry.composition)) self.assertTrue( ndecomp > 0 and ndecomp <= dim, "The number of decomposition phases can at most be equal to the number of components." ) #Just to test decomp for a ficitious composition ansdict = { entry.composition.formula: amt for entry, amt in self.analyzer.get_decomposition( Composition("Li3Fe7O11")).items() } expected_ans = { "Fe2 O2": 0.0952380952380949, "Li1 Fe1 O2": 0.5714285714285714, "Fe6 O8": 0.33333333333333393 } for k, v in expected_ans.items(): self.assertAlmostEqual(ansdict[k], v) def test_get_transition_chempots(self): for el in self.pd.elements: self.assertLessEqual( len(self.analyzer.get_transition_chempots(el)), len(self.pd.facets)) def test_get_element_profile(self): for el in self.pd.elements: for entry in self.pd.stable_entries: if not (entry.composition.is_element): self.assertLessEqual( len( self.analyzer.get_element_profile( el, entry.composition)), len(self.pd.facets)) expected = [{ 'evolution': 1.0, 'chempot': -4.2582781416666666, 'reaction': 'Li2O + 0.5 O2 -> Li2O2' }, { 'evolution': 0, 'chempot': -5.0885906699999968, 'reaction': 'Li2O -> Li2O' }, { 'evolution': -1.0, 'chempot': -10.487582010000001, 'reaction': 'Li2O -> 2 Li + 0.5 O2' }] result = self.analyzer.get_element_profile(Element('O'), Composition('Li2O')) for d1, d2 in zip(expected, result): self.assertAlmostEqual(d1['evolution'], d2['evolution']) self.assertAlmostEqual(d1['chempot'], d2['chempot']) self.assertEqual(d1['reaction'], str(d2['reaction'])) def test_get_get_chempot_range_map(self): elements = [el for el in self.pd.elements if el.symbol != "Fe"] self.assertEqual(len(self.analyzer.get_chempot_range_map(elements)), 10) def test_getmu_vertices_stability_phase(self): results = self.analyzer.getmu_vertices_stability_phase( Composition("LiFeO2"), Element("O")) self.assertAlmostEqual(len(results), 6) test_equality = False for c in results: if abs(c[Element("O")]+7.115) < 1e-2 and abs(c[Element("Fe")]+6.596) < 1e-2 and \ abs(c[Element("Li")]+3.931) < 1e-2: test_equality = True self.assertTrue(test_equality, "there is an expected vertex missing in the list") def test_getmu_range_stability_phase(self): results = self.analyzer.get_chempot_range_stability_phase( Composition("LiFeO2"), Element("O")) self.assertAlmostEqual(results[Element("O")][1], -4.4501812249999997) self.assertAlmostEqual(results[Element("Fe")][0], -6.5961470999999996) self.assertAlmostEqual(results[Element("Li")][0], -3.6250022625000007) def test_get_hull_energy(self): for entry in self.pd.stable_entries: h_e = self.analyzer.get_hull_energy(entry.composition) self.assertAlmostEqual(h_e, entry.energy) n_h_e = self.analyzer.get_hull_energy( entry.composition.fractional_composition) self.assertAlmostEqual(n_h_e, entry.energy_per_atom) def test_1d_pd(self): entry = PDEntry('H', 0) pd = PhaseDiagram([entry]) pda = PDAnalyzer(pd) decomp, e = pda.get_decomp_and_e_above_hull(PDEntry('H', 1)) self.assertAlmostEqual(e, 1) self.assertAlmostEqual(decomp[entry], 1.0) def test_get_critical_compositions_fractional(self): c1 = Composition('Fe2O3').fractional_composition c2 = Composition('Li3FeO4').fractional_composition c3 = Composition('Li2O').fractional_composition comps = self.analyzer.get_critical_compositions(c1, c2) expected = [ Composition('Fe2O3').fractional_composition, Composition('Li0.3243244Fe0.1621621O0.51351349'), Composition('Li3FeO4').fractional_composition ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) comps = self.analyzer.get_critical_compositions(c1, c3) expected = [ Composition('Fe0.4O0.6'), Composition('LiFeO2').fractional_composition, Composition('Li5FeO4').fractional_composition, Composition('Li2O').fractional_composition ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) def test_get_critical_compositions(self): c1 = Composition('Fe2O3') c2 = Composition('Li3FeO4') c3 = Composition('Li2O') comps = self.analyzer.get_critical_compositions(c1, c2) expected = [ Composition('Fe2O3'), Composition('Li0.3243244Fe0.1621621O0.51351349') * 7.4, Composition('Li3FeO4') ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) comps = self.analyzer.get_critical_compositions(c1, c3) expected = [ Composition('Fe2O3'), Composition('LiFeO2'), Composition('Li5FeO4') / 3, Composition('Li2O') ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) # Don't fail silently if input compositions aren't in phase diagram # Can be very confusing if you're working with a GrandPotentialPD self.assertRaises(ValueError, self.analyzer.get_critical_compositions, Composition('Xe'), Composition('Mn')) # For the moment, should also fail even if compositions are in the gppd # because it isn't handled properly gppd = GrandPotentialPhaseDiagram(self.pd.all_entries, {'Xe': 1}, self.pd.elements + [Element('Xe')]) pda = PDAnalyzer(gppd) self.assertRaises(ValueError, pda.get_critical_compositions, Composition('Fe2O3'), Composition('Li3FeO4Xe')) # check that the function still works though comps = pda.get_critical_compositions(c1, c2) expected = [ Composition('Fe2O3'), Composition('Li0.3243244Fe0.1621621O0.51351349') * 7.4, Composition('Li3FeO4') ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) # case where the endpoints are identical self.assertEqual(self.analyzer.get_critical_compositions(c1, c1 * 2), [c1, c1 * 2]) def test_get_composition_chempots(self): c1 = Composition('Fe3.1O4') c2 = Composition('Fe3.2O4.1Li0.01') e1 = self.analyzer.get_hull_energy(c1) e2 = self.analyzer.get_hull_energy(c2) cp = self.analyzer.get_composition_chempots(c1) calc_e2 = e1 + sum(cp[k] * v for k, v in (c2 - c1).items()) self.assertAlmostEqual(e2, calc_e2)
def setUp(self): module_dir = os.path.dirname(os.path.abspath(__file__)) (elements, entries) = PDEntryIO.from_csv(os.path.join(module_dir, "pdentries_test.csv")) self.pd = PhaseDiagram(entries) self.analyzer = PDAnalyzer(self.pd)
def get_chempot_range_map_plot(self, elements,referenced=True): """ Returns a plot of the chemical potential range _map. Currently works only for 3-component PDs. Args: elements: Sequence of elements to be considered as independent variables. E.g., if you want to show the stability ranges of all Li-Co-O phases wrt to uLi and uO, you will supply [Element("Li"), Element("O")] referenced: if True, gives the results with a reference being the energy of the elemental phase. If False, gives absolute values. Returns: A matplotlib plot object. """ plt = pretty_plot(12, 8) analyzer = PDAnalyzer(self._pd) chempot_ranges = analyzer.get_chempot_range_map( elements, referenced=referenced) missing_lines = {} excluded_region = [] for entry, lines in chempot_ranges.items(): comp = entry.composition center_x = 0 center_y = 0 coords = [] contain_zero = any([comp.get_atomic_fraction(el) == 0 for el in elements]) is_boundary = (not contain_zero) and \ sum([comp.get_atomic_fraction(el) for el in elements]) == 1 for line in lines: (x, y) = line.coords.transpose() plt.plot(x, y, "k-") for coord in line.coords: if not in_coord_list(coords, coord): coords.append(coord.tolist()) center_x += coord[0] center_y += coord[1] if is_boundary: excluded_region.extend(line.coords) if coords and contain_zero: missing_lines[entry] = coords else: xy = (center_x / len(coords), center_y / len(coords)) plt.annotate(latexify(entry.name), xy, fontsize=22) ax = plt.gca() xlim = ax.get_xlim() ylim = ax.get_ylim() #Shade the forbidden chemical potential regions. excluded_region.append([xlim[1], ylim[1]]) excluded_region = sorted(excluded_region, key=lambda c: c[0]) (x, y) = np.transpose(excluded_region) plt.fill(x, y, "0.80") #The hull does not generate the missing horizontal and vertical lines. #The following code fixes this. el0 = elements[0] el1 = elements[1] for entry, coords in missing_lines.items(): center_x = sum([c[0] for c in coords]) center_y = sum([c[1] for c in coords]) comp = entry.composition is_x = comp.get_atomic_fraction(el0) < 0.01 is_y = comp.get_atomic_fraction(el1) < 0.01 n = len(coords) if not (is_x and is_y): if is_x: coords = sorted(coords, key=lambda c: c[1]) for i in [0, -1]: x = [min(xlim), coords[i][0]] y = [coords[i][1], coords[i][1]] plt.plot(x, y, "k") center_x += min(xlim) center_y += coords[i][1] elif is_y: coords = sorted(coords, key=lambda c: c[0]) for i in [0, -1]: x = [coords[i][0], coords[i][0]] y = [coords[i][1], min(ylim)] plt.plot(x, y, "k") center_x += coords[i][0] center_y += min(ylim) xy = (center_x / (n + 2), center_y / (n + 2)) else: center_x = sum(coord[0] for coord in coords) + xlim[0] center_y = sum(coord[1] for coord in coords) + ylim[0] xy = (center_x / (n + 1), center_y / (n + 1)) plt.annotate(latexify(entry.name), xy, horizontalalignment="center", verticalalignment="center", fontsize=22) plt.xlabel("$\\mu_{{{0}}} - \\mu_{{{0}}}^0$ (eV)" .format(el0.symbol)) plt.ylabel("$\\mu_{{{0}}} - \\mu_{{{0}}}^0$ (eV)" .format(el1.symbol)) plt.tight_layout() return plt
# Plot! plotter = PDPlotter(phase, show_unstable=True) #plotter.show() lines, stable, unstable = plotter.pd_plot_data ### NEED_TO_CHANGE ### plt = plotter.get_plot() f = plt.gcf() f.set_size_inches((15, 12)) fig_dir = output_Path + "/" + "phase.png" plt.savefig(fig_dir, image_format="png") # Analyze phase diagram! pda = PDAnalyzer(phase) # elements : enum Element to array elements_array = [] for ele in phase.elements: elements_array.append(ele.value) result["elements"] = sorted(elements_array, key=SortByAtomicNumber) # dimension result["dimension"] = len(phase.elements) # lines : numpy.ndarray to array lines_array = [] for line in lines: if isinstance(line, numpy.ndarray): ll = line.tolist()