def setUp(self): entrylist = list() weights = list() comp = Composition("Mn2O3") entry = PDEntry(comp, 49) entrylist.append(PourbaixEntry(entry)) weights.append(1.0) comp = Ion.from_formula("MnO4[-]") entry = IonEntry(comp, 25) entrylist.append(PourbaixEntry(entry)) weights.append(0.25) comp = Composition("Fe2O3") entry = PDEntry(comp, 50) entrylist.append(PourbaixEntry(entry)) weights.append(0.5) comp = Ion.from_formula("Fe[2+]") entry = IonEntry(comp, 15) entrylist.append(PourbaixEntry(entry)) weights.append(2.5) comp = Ion.from_formula("Fe[3+]") entry = IonEntry(comp, 20) entrylist.append(PourbaixEntry(entry)) weights.append(1.5) self.weights = weights self.entrylist = entrylist self.multientry = MultiEntry(entrylist, weights)
def get_e_above_hull(formula, energy): atoms = Atoms(formula) full_symbols = atoms.get_chemical_symbols() symbols, counts = np.unique(full_symbols, return_counts=True) # list(set(atoms.get_chemical_symbols())) with MPRester() as m: data = m.get_entries_in_chemsys(symbols, compatible_only=True, property_data=[ 'energy_per_atom', 'unit_cell_formula', 'pretty_formula' ]) PDentries = [] for d in data: d = d.as_dict() PDentries += [PDEntry(d['data']['unit_cell_formula'], d['energy'])] print(d['data']['pretty_formula'], d['energy']) PD = PhaseDiagram(PDentries) # Need to apply MP corrections to +U calculations # MP advanced correction + anion correction #print(energy * 2, energy * 2 + corr_energy * 2) PDE0 = PDEntry(formula, energy) e_hull = PD.get_e_above_hull(PDE0) return e_hull
def test_get_quasi_e_to_hull(self): for entry in self.pd.unstable_entries: # catch duplicated stable entries if entry.normalize( inplace=False) in self.pd.get_stable_entries_normed(): self.assertLessEqual( self.pd.get_quasi_e_to_hull(entry), 0, "Duplicated stable entries should have negative decomposition energy!" ) else: self.assertGreaterEqual( self.pd.get_quasi_e_to_hull(entry), 0, "Unstable entries should have positive decomposition energy!" ) for entry in self.pd.stable_entries: if entry.composition.is_element: self.assertEqual( self.pd.get_quasi_e_to_hull(entry), 0, "Stable elemental entries should have decomposition energy of zero!" ) else: self.assertLessEqual( self.pd.get_quasi_e_to_hull(entry), 0, "Stable entries should have negative decomposition energy!" ) novel_stable_entry = PDEntry("Li5FeO4", -999) self.assertLess( self.pd.get_quasi_e_to_hull(novel_stable_entry), 0, "Novel stable entries should have negative decomposition energy!") novel_unstable_entry = PDEntry("Li5FeO4", 999) self.assertGreater( self.pd.get_quasi_e_to_hull(novel_unstable_entry), 0, "Novel unstable entries should have positive decomposition energy!" ) duplicate_entry = PDEntry("Li2O", -14.31361175) scaled_dup_entry = PDEntry("Li4O2", -14.31361175 * 2) stable_entry = [e for e in self.pd.stable_entries if e.name == "Li2O"][0] self.assertEqual( self.pd.get_quasi_e_to_hull(duplicate_entry), self.pd.get_quasi_e_to_hull(stable_entry), "Novel duplicates of stable entries should have same decomposition energy!" ) self.assertEqual( self.pd.get_quasi_e_to_hull(scaled_dup_entry), self.pd.get_quasi_e_to_hull(stable_entry), "Novel scaled duplicates of stable entries should have same decomposition energy!" )
def _pd(structures, energies, ce): """ Generate a phase diagram with the structures and energies """ entries = [] for s, e in zip(structures, energies): entries.append(PDEntry(s.composition.element_composition, e)) max_e = max(entries, key=lambda e: e.energy_per_atom).energy_per_atom + 1000 for el in ce.structure.composition.keys(): entries.append(PDEntry(Composition({el: 1}).element_composition, max_e)) return PhaseDiagram(entries)
def get_convex_hull_area(self, composition_space): """ Prints out the area or volume of the convex hull defined by the organisms in the promotion set. """ # make a phase diagram of just the organisms in the promotion set # (on the lower convex hull) pdentries = [] for organism in self.promotion_set: pdentries.append( PDEntry(organism.composition, organism.total_energy)) compound_pd = CompoundPhaseDiagram(pdentries, composition_space.endpoints) # get the data for the convex hull qhull_data = compound_pd.qhull_data # for some reason, the last point is positive, so remove it hull_data = np.delete(qhull_data, -1, 0) # make a ConvexHull object from the hull data try: convex_hull = ConvexHull(hull_data) except: return None if len(composition_space.endpoints) == 2: return convex_hull.area else: return convex_hull.volume
def from_csv(cls, filename: str): """ Imports PDEntries from a csv. Args: filename: Filename to import from. Returns: List of Elements, List of PDEntries """ with open(filename, "r", encoding="utf-8") as f: reader = csv.reader(f, delimiter=unicode2str(","), quotechar=unicode2str("\""), quoting=csv.QUOTE_MINIMAL) entries = list() header_read = False elements = None for row in reader: if not header_read: elements = row[1:(len(row) - 1)] header_read = True else: name = row[0] energy = float(row[-1]) comp = dict() for ind in range(1, len(row) - 1): if float(row[ind]) > 0: comp[Element(elements[ind - 1])] = float(row[ind]) entries.append(PDEntry(Composition(comp), energy, name)) return cls(entries)
def test_read_write_csv(self): Zn_solids = ["Zn", "ZnO", "ZnO2"] sol_g = [0.0, -3.338, -1.315] Zn_ions = ["Zn[2+]", "ZnOH[+]", "HZnO2[-]", "ZnO2[2-]", "ZnO"] liq_g = [-1.527, -3.415, -4.812, -4.036, -2.921] liq_conc = [1e-6, 1e-6, 1e-6, 1e-6, 1e-6] solid_entry = list() for sol in Zn_solids: comp = Composition(sol) delg = sol_g[Zn_solids.index(sol)] solid_entry.append(PourbaixEntry(PDEntry(comp, delg))) ion_entry = list() for ion in Zn_ions: comp_ion = Ion.from_formula(ion) delg = liq_g[Zn_ions.index(ion)] conc = liq_conc[Zn_ions.index(ion)] PoE = PourbaixEntry(IonEntry(comp_ion, delg)) PoE.conc = conc ion_entry.append(PoE) entries = solid_entry + ion_entry PourbaixEntryIO.to_csv("pourbaix_test_entries.csv", entries) (elements, entries) = PourbaixEntryIO.from_csv("pourbaix_test_entries.csv") self.assertEqual( elements, [Element('Zn'), Element('H'), Element('O')], "Wrong elements!") self.assertEqual(len(entries), 8, "Wrong number of entries!") os.remove("pourbaix_test_entries.csv")
def Create_Compositional_PhaseDiagram(self): phasediagram_entries = [] for compound in self.compounds_info.keys(): compound_composition = {} # Disregard elements not included in main compound if (compound in self.all_elements) and (compound not in self.elements_list): continue # Elements if compound in self.elements_list: compound_composition[compound] = self.compounds_info[compound][ "dft_" + compound] # Compounds else: for element in self.compounds_info[compound]["elements_list"]: compound_composition[element] = self.compounds_info[ compound]["dft_" + element] compound_total_energy = self.compounds_info[compound][ "total_energy"] phasediagram_entries.append( PDEntry(compound_composition, compound_total_energy)) self.phasediagram = PhaseDiagram(phasediagram_entries)
def get_decomp_product_ids(structures, competing_phases): # Create phase diagrams for each of the new structure chemical systems phase_diagrams = [] for competing in competing_phases: # Add competing phases entries = [ PDEntry(Composition(i['full_formula']), i['final_energy'], name=i['task_id']) for i in competing ] pd = PhaseDiagram(entries) phase_diagrams.append(pd) # Put new structures on phase diagram to get the set of decomp products all_decomp_prods = [] for new_struc, pd in zip(structures, phase_diagrams): comp = new_struc['structure'].composition.element_composition decomp_prods = pd.get_decomposition(comp=comp) all_decomp_prods.extend([i.name for i in decomp_prods]) # Reduce decomposition products to unique set all_decomp_prods = list(set(all_decomp_prods)) print('{} unique competing phases to calculate'.format( len(all_decomp_prods))) return (all_decomp_prods)
def setUp(self): comp = Composition("Mn2O3") self.solentry = PDEntry(comp, 49) ion = Ion.from_formula("MnO4-") self.ionentry = IonEntry(ion, 25) self.PxIon = PourbaixEntry(self.ionentry) self.PxSol = PourbaixEntry(self.solentry) self.PxIon.conc = 1e-4
def setUp(self): comp = Composition("LiFeO2") entry = PDEntry(comp, 53) self.transformed_entry = TransformedPDEntry( { DummySpecies("Xa"): 1, DummySpecies("Xb"): 1 }, entry)
def from_entries(cls, entries, working_ion_entry): """ Create a new InsertionElectrode. Args: entries: A list of ComputedStructureEntries (or subclasses) representing the different topotactic states of the battery, e.g. TiO2 and LiTiO2. working_ion_entry: A single ComputedEntry or PDEntry representing the element that carries charge across the battery, e.g. Li. """ _working_ion = working_ion_entry.composition.elements[0] _working_ion_entry = working_ion_entry # Prepare to make phase diagram: determine elements and set their energy # to be very high elements = set() for entry in entries: elements.update(entry.composition.elements) # Set an artificial energy for each element for convex hull generation element_energy = max([entry.energy_per_atom for entry in entries]) + 10 pdentries = [] pdentries.extend(entries) pdentries.extend( [PDEntry(Composition({el: 1}), element_energy) for el in elements]) # Make phase diagram to determine which entries are stable vs. unstable pd = PhaseDiagram(pdentries) def lifrac(e): return e.composition.get_atomic_fraction(_working_ion) # stable entries ordered by amount of Li asc _stable_entries = tuple( sorted([e for e in pd.stable_entries if e in entries], key=lifrac)) # unstable entries ordered by amount of Li asc _unstable_entries = tuple( sorted([e for e in pd.unstable_entries if e in entries], key=lifrac)) # create voltage pairs _vpairs = tuple([ InsertionVoltagePair.from_entries( _stable_entries[i], _stable_entries[i + 1], working_ion_entry, ) for i in range(len(_stable_entries) - 1) ]) return cls( voltage_pairs=_vpairs, working_ion_entry=_working_ion_entry, _stable_entries=_stable_entries, _unstable_entries=_unstable_entries, )
def is_in_composition_space_pd(self, organism, composition_space, constraints, pool): """ Returns a boolean indicating whether the organism is in the composition space. Whether composition space endpoints are allowed is determined by the value of constraints.allow_endpoints. Args: organism: the Organism to check composition_space: the CompositionSpace of the search constraints: the Constraints of the search pool: the Pool """ # cast the endpoints to PDEntries (just make up some energies) pdentries = [] for endpoint in composition_space.endpoints: pdentries.append(PDEntry(endpoint, -10)) pdentries.append(PDEntry(organism.composition, -10)) # make a CompoundPhaseDiagram and use it to check if the organism # is in the composition space from how many entries it returns composition_checker = CompoundPhaseDiagram(pdentries, composition_space.endpoints) if len( composition_checker.transform_entries( pdentries, composition_space.endpoints)[0]) == len( composition_space.endpoints): print('Organism {} lies outside the composition space '.format( organism.id)) return False # check the composition space endpoints if specified if not constraints.allow_endpoints and len(pool.to_list()) > 0: for endpoint in composition_space.endpoints: if endpoint.almost_equals( organism.composition.reduced_composition): print('Organism {} is at a composition space ' 'endpoint '.format(organism.id)) return False return True
def setUp(self): comp = Composition("LiFeO2") entry = PDEntry(comp, 53) terminal_compositions = ["Li2O", "FeO", "LiO8"] terminal_compositions = [Composition(c) for c in terminal_compositions] sp_mapping = OrderedDict() for i, comp in enumerate(terminal_compositions): sp_mapping[comp] = DummySpecies("X" + chr(102 + i)) self.transformed_entry = TransformedPDEntry(entry, sp_mapping)
def setUp(self): entries = list(EntrySet.from_csv(os.path.join(module_dir, "pdentries_test.csv"))) self.pd_ternary = PhaseDiagram(entries) self.plotter_ternary_mpl = PDPlotter(self.pd_ternary, backend="matplotlib") self.plotter_ternary_plotly = PDPlotter(self.pd_ternary, backend="plotly") entrieslio = [e for e in entries if "Fe" not in e.composition] self.pd_binary = PhaseDiagram(entrieslio) self.plotter_binary_mpl = PDPlotter(self.pd_binary, backend="matplotlib") self.plotter_binary_plotly = PDPlotter(self.pd_binary, backend="plotly") entries.append(PDEntry("C", 0)) self.pd_quaternary = PhaseDiagram(entries) self.plotter_quaternary_mpl = PDPlotter(self.pd_quaternary, backend="matplotlib") self.plotter_quaternary_plotly = PDPlotter(self.pd_quaternary, backend="plotly")
def compute_pd_values(self, organisms_list, composition_space): """ Constructs a convex hull from the provided organisms and sets the organisms' values to their distances from the convex hull. Returns the CompoundPhaseDiagram object computed from the organisms in organisms_list. Args: organisms_list: a list of Organisms whose values we need to compute composition_space: the CompositionSpace of the search """ # create a PDEntry object for each organism in the list of organisms pdentries = {} for organism in organisms_list: pdentries[organism.id] = PDEntry(organism.composition, organism.total_energy) # put the pdentries in a list pdentries_list = [] for organism_id in pdentries: pdentries_list.append(pdentries[organism_id]) # create a compound phase diagram object from the list of pdentries compound_pd = CompoundPhaseDiagram(pdentries_list, composition_space.endpoints) # transform the pdentries and put them in a dictionary, with the # organism id's as the keys transformed_pdentries = {} for org_id in pdentries: transformed_pdentries[org_id] = compound_pd.transform_entries( [pdentries[org_id]], composition_space.endpoints)[0][0] # put the values in a dictionary, with the organism id's as the keys values = {} for org_id in pdentries: values[org_id] = compound_pd.get_e_above_hull( transformed_pdentries[org_id]) # assign values to the organisms for organism in organisms_list: organism.value = values[organism.id] return compound_pd
def test_planar_inputs(self): e1 = PDEntry("H", 0) e2 = PDEntry("He", 0) e3 = PDEntry("Li", 0) e4 = PDEntry("Be", 0) e5 = PDEntry("B", 0) e6 = PDEntry("Rb", 0) pd = PhaseDiagram([e1, e2, e3, e4, e5, e6], map(Element, ["Rb", "He", "B", "Be", "Li", "H"])) self.assertEqual(len(pd.facets), 1)
def getPourbaixEntryfromMongo(elements): entries = [] unique_entries = {} elements_with_H_O = elements elements_with_H_O.append('H') elements_with_H_O.append('O') elements_with_H_O.sort() print(elements) allcombinations = getAllCombinations(elements_with_H_O) myclient = pymongo.MongoClient("mongodb://localhost:27017/") mydb = myclient["mp"] mycol = mydb["aml_all5"] for c in allcombinations: c.sort() p = '^' for i in c: p = p + i + '[0-9]+\s*' p = p + '$' mq = {"elements": c} md = mycol.find(mq) for x in md: print(x['pretty_formula']) fe = x['formation_energy_per_atom'] if fe is not None: try: fe = float(x['formation_energy_per_atom']) if unique_entries.get(x['pretty_formula']) is not None: if unique_entries[x['pretty_formula']] > x[ 'formation_energy_per_atom'] * x['natoms']: unique_entries[x['pretty_formula']] = x[ 'formation_energy_per_atom'] * x['natoms'] else: unique_entries[x['pretty_formula']] = x[ 'formation_energy_per_atom'] * x['natoms'] except ValueError: fe = 100.0 print(x['task_id'], x['pretty_formula'], fe) for u in unique_entries: print(u, unique_entries[u]) ent = PDEntry(Composition(u), unique_entries[u], attribute={'task_id': x['task_id']}) ent = PourbaixEntry(ent) #ent.phase_type='Solid' entries.append(ent) return entries
def Create_Compositional_PhaseDiagram(self): # Record all entries for the phase diagram phasediagram_entries = [] for compound in self.compounds_info.keys(): # Disregard elements not included in main compound if (compound in self.all_elements) and (compound not in self.elements_list): continue # Get the compound's composition compound_composition = {} if compound in self.elements_list: # Elemental material compound_composition[compound] = self.compounds_info[compound][ "dft_" + compound] else: # Compound material for element in self.compounds_info[compound]["elements_list"]: compound_composition[element] = self.compounds_info[ compound]["dft_" + element] # Get the compound's total energy compound_total_energy = self.compounds_info[compound][ "total_energy"] # Record to list of entries phasediagram_entries.append( PDEntry(compound_composition, compound_total_energy)) # Calculate compositional phase diagram (using pymatgen) # The output data structure is as follows: # lines --> List of arrays, each array is 2x2 for ternary (3x3 for quaternary, etc.), column vector represents point on phase diagram. # ex: array([ [0.3, 0.5], [1.0, 0.0] ]) is a line that goes from point [x=0.3, y=1.0] to point [x=0.5, y=0.0] # labels --> Dictionary with point-PDEntry pairs. self.pmg_phasediagram = PhaseDiagram(phasediagram_entries) self.pmg_phasediagram_plot_object = PDPlotter(self.pmg_phasediagram) (lines, labels, unstable) = self.pmg_phasediagram_plot_object.pd_plot_data # Record all lines and points of the compositional phase diagram self.lines = lines self.labels = labels
def get_phase_diagram_plot(self): """ Returns a phase diagram plot, as a matplotlib plot object. """ # set the font to Times, rendered with Latex plt.rc('font', **{'family': 'serif', 'serif': ['Times']}) plt.rc('text', usetex=True) # parse the composition space endpoints endpoints_line = self.lines[0].split() endpoints = [] for word in endpoints_line[::-1]: if word == 'endpoints:': break else: endpoints.append(Composition(word)) if len(endpoints) < 2: print('There must be at least 2 endpoint compositions to make a ' 'phase diagram.') quit() # parse the compositions and total energies of all the structures compositions = [] total_energies = [] for i in range(4, len(self.lines)): line = self.lines[i].split() compositions.append(Composition(line[1])) total_energies.append(float(line[2])) # make a list of PDEntries pdentries = [] for i in range(len(compositions)): pdentries.append(PDEntry(compositions[i], total_energies[i])) # make a CompoundPhaseDiagram compound_pd = CompoundPhaseDiagram(pdentries, endpoints) # make a PhaseDiagramPlotter pd_plotter = PDPlotter(compound_pd, show_unstable=50) return pd_plotter.get_plot(label_unstable=False)
def from_csv(filename): """ Imports PourbaixEntries from a csv. Args: filename: Filename to import from. Returns: List of Entries """ with open(filename, "rt") as f: reader = csv.reader(f, delimiter=unicode2str(","), quotechar=unicode2str("\""), quoting=csv.QUOTE_MINIMAL) entries = list() header_read = False for row in reader: if not header_read: elements = row[1:(len(row) - 4)] header_read = True elif row: name = row[0] energy = float(row[-4]) conc = float(row[-1]) comp = dict() for ind in range(1, len(row) - 4): if float(row[ind]) > 0: comp[Element(elements[ind - 1])] = float(row[ind]) phase_type = row[-3] if phase_type == "Ion": PoE = PourbaixEntry( IonEntry(Ion.from_formula(name), energy)) PoE.conc = conc PoE.name = name entries.append(PoE) else: entries.append( PourbaixEntry(PDEntry(Composition(comp), energy))) elements = [Element(el) for el in elements] return elements, entries
def get_convex_hull_area(self, composition_space): """ Returns the area/volume of the current convex hull. Args: composition_space: the CompositionSpace of the search """ # check if the initial population contains organisms at all the # endpoints of the composition space if self.has_endpoints(composition_space) and \ self.has_non_endpoint(composition_space): # compute and print the area or volume of the convex hull pdentries = [] for organism in self.initial_population: pdentries.append( PDEntry(organism.composition, organism.total_energy)) compound_pd = CompoundPhaseDiagram(pdentries, composition_space.endpoints) # get the data for the convex hull qhull_data = compound_pd.qhull_data # for some reason, the last point is positive, so remove it hull_data = np.delete(qhull_data, -1, 0) # make a ConvexHull object from the hull data # Sometime this fails, saying that only two points were given to # construct the convex hull, even though the if statement above # checks that there are enough points. try: convex_hull = ConvexHull(hull_data) except: return None if len(composition_space.endpoints) == 2: return convex_hull.area else: return convex_hull.volume
def update_entries_store(rows): if rows is None: raise PreventUpdate entries = [] for row in rows: try: comp = Composition(row["Formula"]) energy = row["Formation Energy (eV/atom)"] if row["Material ID"] is None: attribute = "Custom Entry" else: attribute = row["Material ID"] entry = PDEntry(comp, float(energy) * comp.num_atoms, attribute=attribute) entries.append(entry) except: pass if not entries: raise PreventUpdate return self.to_data(entries)
def update_entries_store(rows): if rows is None: raise PreventUpdate entries = [] for row in rows: try: comp = Composition(row["Formula"]) energy = row["Formation Energy (eV/atom)"] if row["Material ID"] is None: attribute = "Custom Entry" else: attribute = row["Material ID"] # create new entry object containing mpid as attribute (to combine with custom entries) entry = PDEntry(comp, float(energy) * comp.num_atoms, attribute=attribute) entries.append(entry) except: continue if not entries: raise PreventUpdate return entries
def test_1d_pd(self): entry = PDEntry("H", 0) pd = PhaseDiagram([entry]) decomp, e = pd.get_decomp_and_e_above_hull(PDEntry("H", 1)) self.assertAlmostEqual(e, 1) self.assertAlmostEqual(decomp[entry], 1.0)
def setUp(self): comp = Composition("LiFeO2") self.entry = PDEntry(comp, 53) self.gpentry = GrandPotPDEntry(self.entry, {Element("O"): 1.5})
def from_entries(cls, entries, working_ion_entry, strip_structures=False): """ Create a new InsertionElectrode. Args: entries: A list of ComputedStructureEntries (or subclasses) representing the different topotactic states of the battery, e.g. TiO2 and LiTiO2. working_ion_entry: A single ComputedEntry or PDEntry representing the element that carries charge across the battery, e.g. Li. strip_structures: Since the electrode document only uses volume we can make the electrode object significantly leaner by dropping the structure data. If this parameter is set to True, the ComputedStructureEntry will be replaced with ComputedEntry and the volume will be stored in ComputedEntry.data['volume'] """ if strip_structures: ents = [] for ient in entries: dd = ient.as_dict() ent = ComputedEntry.from_dict(dd) ent.data["volume"] = ient.structure.volume ents.append(ent) entries = ents _working_ion = working_ion_entry.composition.elements[0] _working_ion_entry = working_ion_entry # Prepare to make phase diagram: determine elements and set their energy # to be very high elements = set() for entry in entries: elements.update(entry.composition.elements) # Set an artificial energy for each element for convex hull generation element_energy = max([entry.energy_per_atom for entry in entries]) + 10 pdentries = [] pdentries.extend(entries) pdentries.extend( [PDEntry(Composition({el: 1}), element_energy) for el in elements]) # Make phase diagram to determine which entries are stable vs. unstable pd = PhaseDiagram(pdentries) def lifrac(e): return e.composition.get_atomic_fraction(_working_ion) # stable entries ordered by amount of Li asc _stable_entries = tuple( sorted([e for e in pd.stable_entries if e in entries], key=lifrac)) # unstable entries ordered by amount of Li asc _unstable_entries = tuple( sorted([e for e in pd.unstable_entries if e in entries], key=lifrac)) # create voltage pairs _vpairs = tuple([ InsertionVoltagePair.from_entries( _stable_entries[i], _stable_entries[i + 1], working_ion_entry, ) for i in range(len(_stable_entries) - 1) ]) framework = _vpairs[0].framework return cls( voltage_pairs=_vpairs, working_ion_entry=_working_ion_entry, _stable_entries=_stable_entries, _unstable_entries=_unstable_entries, _framework_formula=framework.reduced_formula, )
def test_get_phase_separation_energy(self): for entry in self.pd.unstable_entries: if entry.composition.fractional_composition not in [ e.composition.fractional_composition for e in self.pd.stable_entries ]: self.assertGreaterEqual( self.pd.get_phase_separation_energy(entry), 0, "Unstable entries should have positive decomposition energy!", ) else: if entry.is_element: el_ref = self.pd.el_refs[entry.composition.elements[0]] e_d = entry.energy_per_atom - el_ref.energy_per_atom self.assertAlmostEqual( self.pd.get_phase_separation_energy(entry), e_d, 7) # NOTE the remaining materials would require explicit tests as they # could be either positive or negative pass for entry in self.pd.stable_entries: if entry.composition.is_element: self.assertEqual( self.pd.get_phase_separation_energy(entry), 0, "Stable elemental entries should have decomposition energy of zero!", ) else: self.assertLessEqual( self.pd.get_phase_separation_energy(entry), 0, "Stable entries should have negative decomposition energy!", ) self.assertAlmostEqual( self.pd.get_phase_separation_energy(entry, stable_only=True), self.pd.get_equilibrium_reaction_energy(entry), 7, ("Using `stable_only=True` should give decomposition energy equal to " "equilibrium reaction energy!"), ) # Test that we get correct behaviour with a polymorph toy_entries = { "Li": 0.0, "Li2O": -5, "LiO2": -4, "O2": 0.0, } toy_pd = PhaseDiagram([PDEntry(c, e) for c, e in toy_entries.items()]) # stable entry self.assertAlmostEqual( toy_pd.get_phase_separation_energy(PDEntry("Li2O", -5)), -1.0, 7, ) # polymorph self.assertAlmostEqual( toy_pd.get_phase_separation_energy(PDEntry("Li2O", -4)), -2.0 / 3.0, 7, ) # Test that the method works for novel entries novel_stable_entry = PDEntry("Li5FeO4", -999) self.assertLess( self.pd.get_phase_separation_energy(novel_stable_entry), 0, "Novel stable entries should have negative decomposition energy!", ) novel_unstable_entry = PDEntry("Li5FeO4", 999) self.assertGreater( self.pd.get_phase_separation_energy(novel_unstable_entry), 0, "Novel unstable entries should have positive decomposition energy!", ) duplicate_entry = PDEntry("Li2O", -14.31361175) scaled_dup_entry = PDEntry("Li4O2", -14.31361175 * 2) stable_entry = [e for e in self.pd.stable_entries if e.name == "Li2O"][0] self.assertEqual( self.pd.get_phase_separation_energy(duplicate_entry), self.pd.get_phase_separation_energy(stable_entry), "Novel duplicates of stable entries should have same decomposition energy!", ) self.assertEqual( self.pd.get_phase_separation_energy(scaled_dup_entry), self.pd.get_phase_separation_energy(stable_entry), "Novel scaled duplicates of stable entries should have same decomposition energy!", )
def from_entries( cls, entries: Iterable[Union[ComputedEntry, ComputedStructureEntry]], working_ion_entry: Union[ComputedEntry, ComputedStructureEntry, PDEntry], strip_structures: bool = False, ): """ Create a new InsertionElectrode. Args: entries: A list of ComputedEntries, ComputedStructureEntries, or subclasses representing the different topotactic states of the battery, e.g. TiO2 and LiTiO2. working_ion_entry: A single ComputedEntry or PDEntry representing the element that carries charge across the battery, e.g. Li. strip_structures: Since the electrode document only uses volume we can make the electrode object significantly leaner by dropping the structure data. If this parameter is set to True, the ComputedStructureEntry will be replaced with a ComputedEntry and the volume will be stored in ComputedEntry.data['volume']. If entries provided are ComputedEntries, must set strip_structures=False. """ if strip_structures: ents = [] for ient in entries: dd = ient.as_dict() ent = ComputedEntry.from_dict(dd) ent.data["volume"] = ient.structure.volume ents.append(ent) entries = ents _working_ion = working_ion_entry.composition.elements[0] _working_ion_entry = working_ion_entry # Prepare to make phase diagram: determine elements and set their energy # to be very high elements = set() for entry in entries: elements.update(entry.composition.elements) # Set an artificial high energy for each element for convex hull generation element_energy = max(entry.energy_per_atom for entry in entries) + 10 pdentries: List[Union[ComputedEntry, ComputedStructureEntry, PDEntry]] = [] pdentries.extend(entries) pdentries.extend( [PDEntry(Composition({el: 1}), element_energy) for el in elements]) # Make phase diagram to determine which entries are stable vs. unstable. # For each working ion concentration, we want one stable entry # to use in forming voltage pairs. PhaseDiagram allows for easy comparison # of entry energies. pd = PhaseDiagram(pdentries) def lifrac(e): return e.composition.get_atomic_fraction(_working_ion) # stable entries ordered by amount of Li asc _stable_entries = tuple( sorted((e for e in pd.stable_entries if e in entries), key=lifrac)) # unstable entries ordered by amount of Li asc _unstable_entries = tuple( sorted((e for e in pd.unstable_entries if e in entries), key=lifrac)) # create voltage pairs _vpairs: Tuple[AbstractVoltagePair] = tuple( # type: ignore InsertionVoltagePair.from_entries( _stable_entries[i], _stable_entries[i + 1], working_ion_entry, ) for i in range(len(_stable_entries) - 1)) framework = _vpairs[0].framework return cls( # pylint: disable=E1123 voltage_pairs=_vpairs, working_ion_entry=_working_ion_entry, stable_entries=_stable_entries, unstable_entries=_unstable_entries, framework_formula=framework.reduced_formula, )
def compute_composition_vector(self, composition_space): """ Returns the composition vector of the organism, as a numpy array. Args: composition_space: the CompositionSpace of the search. """ if composition_space.objective_function == 'epa': return None elif composition_space.objective_function == 'pd': # make CompoundPhaseDiagram and PDAnalyzer objects pdentries = [] for endpoint in composition_space.endpoints: pdentries.append(PDEntry(endpoint, -10)) compound_pd = CompoundPhaseDiagram(pdentries, composition_space.endpoints) # transform the organism's composition transformed_entry = compound_pd.transform_entries( [PDEntry(self.composition, 10)], composition_space.endpoints) # get the transformed species and amounts if len(transformed_entry[0]) == 0: return None transformed_list = str(transformed_entry[0][0]).split() del transformed_list[0] popped = '' while popped != 'with': popped = transformed_list.pop() # separate the dummy species symbols from the amounts symbols = [] amounts = [] for entry in transformed_list: split_entry = entry.split('0+') symbols.append(split_entry[0]) amounts.append(float(split_entry[1])) # make a dictionary mapping dummy species to amounts dummy_species_amounts = {} for i in range(len(symbols)): dummy_species_amounts[DummySpecie(symbol=symbols[i])] = \ amounts[i] # make Composition object with dummy species, get decomposition dummy_comp = Composition(dummy_species_amounts) decomp = compound_pd.get_decomposition(dummy_comp) # get amounts of the decomposition in terms of the (untransformed) # composition space endpoints formatted_decomp = {} for key in decomp: key_dict = key.as_dict() comp = Composition(key_dict['entry']['composition']) formatted_decomp[comp] = decomp[key] # make the composition vector composition_vector = [] # because the random organism creator shuffles the endpoints composition_space.endpoints.sort() for endpoint in composition_space.endpoints: if endpoint in formatted_decomp: composition_vector.append(formatted_decomp[endpoint]) else: composition_vector.append(0.0) return np.array(composition_vector)