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
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 _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)) 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)
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)
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)