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)) pda = 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 setup_phase_diagram_calculations(self, full_phase_diagram = False, energy_above_hull = 0, struct_fmt = 'poscar'): """ This method allows for setting up local phase diagram calculations so a user can calculate chemical potentials on a level of interest beyond PBE-GGA/GGA+U Method is to pull the MP phase diagram and use PBE-GGA level data to decide which phases need to be computed full_phase_diagram flag has two options: False: set up the structures/phases which are stable in GGA phase diagram and are relevant for defining the chemical potentials (exist to define the facets adjacent to composition of interest) True: set up the full phase diagram according to all the entries in the MP database with elements of interest entry_above_hull: allows for a range of energies above hull for each composition being set up default is 0, meaning just the PBE-GGA ground state phases are set up. If you set value to 0.5 then all phases within 0.5 eV/atom of PBE-GGA ground state hull will be set up etc. struct_fmt: is file format you want structure to be written as. Options are “cif”, “poscar”, “cssr”, and “json” """ #while GGA chem pots won't be used here; use this method for quickly gathering phase diagram object entries # AND to find phases of interest if you just want to re-calculate local facets MPgga_muvals = self.MPC.get_chempots_from_composition(self.bulk_composition) if full_phase_diagram: setupphases = set([localentry.name for entrykey in self.MPC.entries.keys() for localentry in self.MPC.entries[entrykey]]) #all elements in phase diagram else: if len(self.bulk_composition)==2: #neccessary because binary species have chempots written as "A-rich, B-rich" setupphases = set([phase.split('_')[0] for facet in MPgga_muvals.keys() for phase in facet.split('-')]) else: setupphases = set([phase for facet in MPgga_muvals.keys() for phase in facet.split('-')]) #just local facets structures_to_setup = {} #this will be a list of structure objects which need to be setup locally #create phase diagram object for analyzing PBE-GGA energetics of structures computed in MP database full_structure_entries = [struct for entrykey in self.MPC.entries.keys() for struct in self.MPC.entries[entrykey]] pd = PhaseDiagram(full_structure_entries) for entry in full_structure_entries: if (entry.name in setupphases) and (pd.get_decomp_and_e_above_hull(entry, allow_negative=True)[1] <= energy_above_hull): with MPRester(api_key=self.mapi_key) as mp: localstruct = mp.get_structure_by_material_id(entry.entry_id) structures_to_setup[str(entry.entry_id)+'_'+str(entry.name)] = localstruct #Set up structure files locally if desired if os.path.exists(os.path.join(self.path_base,'PhaseDiagram')): print ('phase diagram already exists! Dont overwrite...') else: os.makedirs(os.path.join(self.path_base,'PhaseDiagram')) for localname,localstruct in structures_to_setup.items(): filename = os.path.join(self.path_base,'PhaseDiagram',localname) os.makedirs(filename) if struct_fmt == 'poscar': outputname = 'POSCAR' else: outputname = 'structfile' localstruct.to(fmt=struct_fmt,filename=os.path.join(filename, outputname)) #NOTE TO USER. Can use pymatgen here to setup additional calculation files if interested... return structures_to_setup
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.compatibility.process_entries(item) try: pd = PhaseDiagram(entries) docs = [] for e in entries: (decomp, ehull) = \ pd.get_decomp_and_e_above_hull(e) d = { self.thermo.key: e.entry_id, "thermo": { "formation_energy_per_atom": pd.get_form_energy_per_atom(e), "e_above_hull": ehull, "is_stable": e in pd.stable_entries } } # Logic for if stable or decomposes if d["thermo"]["is_stable"]: d["thermo"][ "eq_reaction_e"] = pd.get_equilibrium_reaction_energy( e) else: d["thermo"]["decomposes_to"] = [{ "task_id": de.entry_id, "formula": de.composition.formula, "amount": amt } for de, amt in decomp.items()] d["thermo"]["entry"] = e.as_dict() d["thermo"][ "explanation"] = self.compatibility.get_explanation_dict(e) elsyms = sorted( set([el.symbol for el in e.composition.elements])) d["chemsys"] = "-".join(elsyms) d["nelements"] = len(elsyms) d["elements"] = list(elsyms) docs.append(d) except PhaseDiagramError as p: print(e.as_dict()) self.logger.warning("Phase diagram error: {}".format(p)) return [] return docs
def convexHull(entries, tolerance=0, printing=False): # This initializes the REST adaptor. You may need to put your own API key in as an arg. If we need MP DB #with open("key.txt", "r") as myfile: # data = myfile.readlines() #a = MPRester(data[0].rstrip()) # entries = a.get_entries_in_chemsys(elems) pd2 = PhaseDiagram(entries) data = collections.defaultdict(list) for e in pd2.stable_entries: ehull = pd2.get_equilibrium_reaction_energy(e) data["Composition"].append(e.composition.reduced_formula) data['NElements'].append(e.attribute) data["Ehull"].append(ehull) #data['Entropy'].append(e.data) #data['Gibbs'].append(ehull) data["Decomposition"].append("self") for e in pd2.unstable_entries: decomp, ehull = pd2.get_decomp_and_e_above_hull(e, allow_negative=True) # decomp, ehull = pd2.get_decomp_and_e_above_hull(e) data["Composition"].append(e.composition.reduced_formula) data['NElements'].append(e.attribute) data["Ehull"].append(ehull) #data['Entropy'].append(e.data) #data['Gibbs'].append(ehull) data["Decomposition"].append(" + ".join([ "%.2f %s" % (v, k.composition.formula) for k, v in decomp.items() ])[0:70]) df = pd.DataFrame( data, columns=["NElements", "Composition", "Ehull", "Decomposition"]) if printing == True: #print(df[df.Ehull<=tolerance]) print(df) return (df)
def get_ehull(structure_type, tot_e, species, unmix_entries=None, all_entries=None, debug=False, from_mp=False): """ Get Ehull predicted under given total energy and species. The composition can be either given by the species dict(for garnet only) or a formula. Args: structure_type(str): "garnet" or "perovskite" tot_e (float): total energy, the unit is in accordance with given composition. species (dict): species in dictionary. unmix_entries (list): additional list of unmix entries. all_entries(list): Manually supply the entries whithin the chemical space debug(bool): Whether or not to run it in debug mode. (For test only) from_mp(bool): Whether or not to query entries from MP (would take long) Returns: ehull (float): energy above hull. """ formula = spe2form(structure_type, species) composition = Composition(formula) elements = [i.name for i in composition.elements] unmix_entries = [] if unmix_entries is None else unmix_entries if not all_entries: if from_mp: all_entries = m.get_entries_in_chemsys( [el.name for el in composition], inc_structure=True) else: entries_dict = EHULL_ENTRIES[structure_type] all_entries = get_entries_in_chemsy(entries_dict, elements) all_entries = filter_entries(structure_type, all_entries, species) # For unmix: no need to find calc entries, for mix, # calc entries were provided through unmix_entries # calc_entries_dict = CALC_ENTRIES[structure_type] # all_calc_entries = get_entries_in_chemsy(calc_entries_dict, elements) # compat = MaterialsProjectCompatibility() # all_calc_entries = compat.process_entries(all_calc_entries) # if all_calc_entries: # all_entries = all_entries + all_calc_entries if not all_entries: raise ValueError("Incomplete") entry = prepare_entry(structure_type, tot_e, species) if debug: return entry, all_entries phase_diagram = PhaseDiagram(all_entries + [entry] + unmix_entries) return phase_diagram.get_decomp_and_e_above_hull(entry)
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)) pda = PhaseDiagram(entries) decomp = pda.get_decomp_and_e_above_hull(my_entry, allow_negative=True) return decomp[1]
def get_decomp_entries_and_e_above_hull(self, entries=None, exclusions=None, trypreload=None): if not entries: entries = self.get_PD_entries(exclusions=exclusions, trypreload=trypreload) pd = PhaseDiagram(entries) decomp_entries, hull_energy = pd.get_decomp_and_e_above_hull(self) return decomp_entries, hull_energy
def from_entries(cls, entries: List[ComputedEntry], sandboxes=None): pd = PhaseDiagram(entries) sandboxes = sandboxes or ["core"] docs = [] for e in entries: (decomp, ehull) = pd.get_decomp_and_e_above_hull(e) d = { "material_id": e.entry_id, "uncorrected_energy_per_atom": e.uncorrected_energy / e.composition.num_atoms, "energy_per_atom": e.uncorrected_energy / e.composition.num_atoms, "formation_energy_per_atom": pd.get_form_energy_per_atom(e), "energy_above_hull": ehull, "is_stable": e in pd.stable_entries, "sandboxes": sandboxes, } if "last_updated" in e.data: d["last_updated"] = e.data["last_updated"] # Store different info if stable vs decomposes if d["is_stable"]: d["equillibrium_reaction_energy_per_atom"] = pd.get_equilibrium_reaction_energy( e) else: d["decomposes_to"] = [{ "material_id": de.entry_id, "formula": de.composition.formula, "amount": amt, } for de, amt in decomp.items()] d["energy_type"] = e.parameters.get("run_type", "Unknown") d["entry_types"] = [e.parameters.get("run_type", "Unknown")] d["entries"] = {e.parameters.get("run_type", ""): e} for k in ["last_updated"]: if k in e.parameters: d[k] = e.parameters[k] elif k in e.data: d[k] = e.data[k] docs.append( ThermoDoc.from_composition(composition=e.composition, **d)) return docs
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) for e in entries: decomp, ehull = pd.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 get_ehull(structure_type, tot_e, species, unmix_entries=None, all_entries=None, debug=False): """ Get Ehull predicted under given total energy and species. The composition can be either given by the species dict(for garnet only) or a formula. Args: tot_e (float): total energy, the unit is in accordance with given composition. species (dict): species in dictionary. unmix_entries (list): additional list of unmix entries. Returns: ehull (float): energy above hull. """ formula = spe2form(structure_type, species) composition = Composition(formula) unmix_entries = [] if unmix_entries is None else unmix_entries if not all_entries: all_entries = m.get_entries_in_chemsys([el.name for el in composition], inc_structure=True) all_entries = filter_entries(structure_type, all_entries, species) all_calc_entries = [e for e in CALC_ENTRIES[structure_type] if set(e.composition).issubset(set(composition)) \ and e.name != composition.reduced_formula] if all_calc_entries: all_entries = all_entries + all_calc_entries compat = MaterialsProjectCompatibility() all_entries = compat.process_entries(all_entries) if not all_entries: raise ValueError("Incomplete") entry = prepare_entry(structure_type, tot_e, species) if debug: return entry, all_entries phase_diagram = PhaseDiagram(all_entries + [entry] + unmix_entries) return phase_diagram.get_decomp_and_e_above_hull(entry)
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 present(self, df=None, new_result_ids=None, all_result_ids=None, filename=None, save_hull_distance=False, finalize=False): """ Generate plots of convex hulls for each of the runs Args: df (DataFrame): dataframe with formation energies, compositions, ids new_result_ids ([]): list of new result ids (i. e. indexes in the updated dataframe) all_result_ids ([]): list of all result ids associated with the current run filename (str): filename to output, if None, no file output is produced Returns: (pyplot): plotter instance """ df = df if df is not None else self.df new_result_ids = new_result_ids if new_result_ids is not None \ else self.new_result_ids all_result_ids = all_result_ids if all_result_ids is not None \ else self.all_result_ids # TODO: consolidate duplicated code here # Generate all entries comps = df.loc[all_result_ids]['Composition'].dropna() system_elements = [] for comp in comps: system_elements += list(Composition(comp).as_dict().keys()) elems = set(system_elements) if len(elems) > 4: warnings.warn( "Number of elements too high for phase diagram plotting") return None ind_to_include = [] for ind in df.index: if set(Composition( df.loc[ind]['Composition']).as_dict().keys()).issubset( elems): ind_to_include.append(ind) _df = df.loc[ind_to_include] # Create computed entry column _df['entry'] = [ ComputedEntry( Composition(row['Composition']), row['delta_e'] * Composition( row['Composition']).num_atoms, # un-normalize the energy entry_id=index) for index, row in _df.iterrows() ] # Partition ids into sets of prior to CAMD run, from CAMD but prior to # current iteration, and new ids ids_prior_to_camd = list(set(_df.index) - set(all_result_ids)) ids_prior_to_run = list(set(all_result_ids) - set(new_result_ids)) # Create phase diagram based on everything prior to current run entries = list(_df.loc[ids_prior_to_run + ids_prior_to_camd]['entry']) # Filter for nans by checking if it's a computed entry entries = [ entry for entry in entries if isinstance(entry, ComputedEntry) ] pg_elements = [Element(el) for el in sorted(elems)] pd = PhaseDiagram(entries, elements=pg_elements) plotkwargs = { "markerfacecolor": "white", "markersize": 7, "linewidth": 2, } if finalize: plotkwargs.update({'linestyle': '--'}) else: plotkwargs.update({'linestyle': '-'}) plotter = PDPlotter(pd, **plotkwargs) getplotkwargs = {"label_stable": False} if finalize else {} plot = plotter.get_plot(**getplotkwargs) # Get valid results valid_results = [ new_result_id for new_result_id in new_result_ids if new_result_id in _df.index ] if finalize: # If finalize, we'll reset pd to all entries at this point # to measure stabilities wrt. the ultimate hull. pd = PhaseDiagram(_df['entry'].values, elements=pg_elements) plotter = PDPlotter( pd, **{ "markersize": 0, "linestyle": "-", "linewidth": 2 }) plot = plotter.get_plot(plt=plot) for entry in _df['entry'][valid_results]: decomp, e_hull = pd.get_decomp_and_e_above_hull( entry, allow_negative=True) if e_hull < self.hull_distance: color = 'g' marker = 'o' markeredgewidth = 1 else: color = 'r' marker = 'x' markeredgewidth = 1 # Get coords coords = [ entry.composition.get_atomic_fraction(el) for el in pd.elements ][1:] if pd.dim == 2: coords = coords + [pd.get_form_energy_per_atom(entry)] if pd.dim == 3: coords = triangular_coord(coords) elif pd.dim == 4: coords = tet_coord(coords) plot.plot(*coords, marker=marker, markeredgecolor=color, markerfacecolor="None", markersize=11, markeredgewidth=markeredgewidth) if filename is not None: plot.savefig(filename, dpi=70) plot.close() if filename is not None and save_hull_distance: if self.stabilities is None: print("ERROR: No stability information in analyzer.") return None with open(filename.split(".")[0] + '.json', 'w') as f: json.dump(self.stabilities, f)
class PhaseDiagramTest(unittest.TestCase): def setUp(self): self.entries = EntrySet.from_csv(str(module_dir / "pdentries_test.csv")) self.pd = PhaseDiagram(self.entries) warnings.simplefilter("ignore") def tearDown(self): warnings.simplefilter("default") def test_init(self): # Ensure that a bad set of entries raises a PD error. Remove all Li # from self.entries. entries = filter( lambda e: (not e.composition.is_element) or e.composition.elements[0] != Element("Li"), self.entries, ) self.assertRaises(ValueError, PhaseDiagram, entries) def test_dim1(self): # Ensure that dim 1 PDs can be 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) for e in entries: ehull = pd.get_e_above_hull(e) self.assertGreaterEqual(ehull, 0) plotter = PDPlotter(pd) lines, *_ = plotter.pd_plot_data self.assertEqual(lines[0][1], [0, 0]) def test_ordering(self): # Test sorting of elements entries = [ComputedEntry(Composition(formula), 0) for formula in ["O", "N", "Fe"]] pd = PhaseDiagram(entries) sorted_elements = (Element("Fe"), Element("N"), Element("O")) self.assertEqual(tuple(pd.elements), sorted_elements) entries.reverse() pd = PhaseDiagram(entries) self.assertEqual(tuple(pd.elements), sorted_elements) # Test manual specification of order ordering = [Element(elt_string) for elt_string in ["O", "N", "Fe"]] pd = PhaseDiagram(entries, elements=ordering) self.assertEqual(tuple(pd.elements), tuple(ordering)) def test_stable_entries(self): stable_formulas = [ent.composition.reduced_formula for ent in self.pd.stable_entries] expected_stable = [ "Fe2O3", "Li5FeO4", "LiFeO2", "Fe3O4", "Li", "Fe", "Li2O", "O2", "FeO", ] for formula in expected_stable: self.assertTrue(formula in stable_formulas, formula + " not in stable entries!") def test_get_formation_energy(self): stable_formation_energies = { ent.composition.reduced_formula: self.pd.get_form_energy(ent) for ent in self.pd.stable_entries } expected_formation_energies = { "Li5FeO4": -164.8117344866667, "Li2O2": -14.119232793333332, "Fe2O3": -16.574164339999996, "FeO": -5.7141519966666685, "Li": 0.0, "LiFeO2": -7.732752316666666, "Li2O": -6.229303868333332, "Fe": 0.0, "Fe3O4": -22.565714456666683, "Li2FeO3": -45.67166036000002, "O2": 0.0, } for formula, energy in expected_formation_energies.items(): self.assertAlmostEqual(energy, stable_formation_energies[formula], 7) def test_all_entries_hulldata(self): self.assertEqual(len(self.pd.all_entries_hulldata), 490) 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 test_str(self): self.assertIsNotNone(str(self.pd)) def test_get_e_above_hull(self): for entry in self.pd.all_entries: for entry in self.pd.stable_entries: decomp, e_hull = self.pd.get_decomp_and_e_above_hull(entry) self.assertLess( e_hull, 1e-11, "Stable entries should have e above hull of zero!", ) self.assertEqual(decomp[entry], 1, "Decomposition of stable entry should be itself.") else: e_ah = self.pd.get_e_above_hull(entry) self.assertTrue(isinstance(e_ah, Number)) self.assertGreaterEqual(e_ah, 0) def test_get_equilibrium_reaction_energy(self): for entry in self.pd.stable_entries: self.assertLessEqual( self.pd.get_equilibrium_reaction_energy(entry), 0, "Stable entries should have negative equilibrium reaction energy!", ) 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 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 test_get_decomposition(self): for entry in self.pd.stable_entries: self.assertEqual( len(self.pd.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.pd.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.pd.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, 7) def test_get_transition_chempots(self): for el in self.pd.elements: self.assertLessEqual(len(self.pd.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.pd.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.pd.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.pd.get_chempot_range_map(elements)), 10) def test_getmu_vertices_stability_phase(self): results = self.pd.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.pd.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.pd.get_hull_energy(entry.composition) self.assertAlmostEqual(h_e, entry.energy) n_h_e = self.pd.get_hull_energy(entry.composition.fractional_composition) self.assertAlmostEqual(n_h_e, entry.energy_per_atom) def test_get_hull_energy_per_atom(self): for entry in self.pd.stable_entries: h_e = self.pd.get_hull_energy_per_atom(entry.composition) self.assertAlmostEqual(h_e, entry.energy_per_atom) 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 test_get_critical_compositions_fractional(self): c1 = Composition("Fe2O3").fractional_composition c2 = Composition("Li3FeO4").fractional_composition c3 = Composition("Li2O").fractional_composition comps = self.pd.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.pd.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.pd.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.pd.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.pd.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")]) self.assertRaises( ValueError, gppd.get_critical_compositions, Composition("Fe2O3"), Composition("Li3FeO4Xe"), ) # check that the function still works though comps = gppd.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.pd.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.pd.get_hull_energy(c1) e2 = self.pd.get_hull_energy(c2) cp = self.pd.get_composition_chempots(c1) calc_e2 = e1 + sum(cp[k] * v for k, v in (c2 - c1).items()) self.assertAlmostEqual(e2, calc_e2) def test_get_all_chempots(self): c1 = Composition("Fe3.1O4") c2 = Composition("FeO") cp1 = self.pd.get_all_chempots(c1) cpresult = { Element("Li"): -4.077061954999998, Element("Fe"): -6.741593864999999, Element("O"): -6.969907375000003, } for elem, energy in cpresult.items(): self.assertAlmostEqual(cp1["Fe3O4-FeO-LiFeO2"][elem], energy) cp2 = self.pd.get_all_chempots(c2) cpresult = { Element("O"): -7.115354140000001, Element("Fe"): -6.5961471, Element("Li"): -3.9316151899999987, } for elem, energy in cpresult.items(): self.assertAlmostEqual(cp2["FeO-LiFeO2-Fe"][elem], energy) def test_to_from_dict(self): # test round-trip for other entry types such as ComputedEntry entry = ComputedEntry("H", 0.0, 0.0, entry_id="test") pd = PhaseDiagram([entry]) d = pd.as_dict() pd_roundtrip = PhaseDiagram.from_dict(d) self.assertEqual(pd.all_entries[0].entry_id, pd_roundtrip.all_entries[0].entry_id) dd = self.pd.as_dict() new_pd = PhaseDiagram.from_dict(dd) new_dd = new_pd.as_dict() self.assertEqual(new_dd, dd) self.assertIsInstance(pd.to_json(), str) def test_read_json(self): with ScratchDir("."): dumpfn(self.pd, "pd.json") loadfn("pd.json")
class PhaseDiagramResults(object): """ Simplified interface to phase-diagram pymatgen API. Inspired to: https://anaconda.org/matsci/plotting-and-analyzing-a-phase-diagram-using-the-materials-api/notebook See also: :cite:`Ong2008,Ong2010` """ def __init__(self, entries): self.entries = entries from abipy.core.structure import Structure for e in entries: e.structure = Structure.as_structure(e.structure) self.structures = [e.structure for e in entries] self.mpids = [e.entry_id for e in entries] # Create phase diagram. from pymatgen.analysis.phase_diagram import PhaseDiagram self.phasediagram = PhaseDiagram(self.entries) def plot(self, show_unstable=True, show=True): """ Plot phase diagram. Args: show_unstable (float): Whether unstable phases will be plotted as well as red crosses. If a number > 0 is entered, all phases with ehull < show_unstable will be shown. show: True to show plot. Return: plotter object. """ from pymatgen.analysis.phase_diagram import PDPlotter plotter = PDPlotter(self.phasediagram, show_unstable=show_unstable) if show: plotter.show() return plotter @lazy_property def dataframe(self): """Pandas dataframe with the most important results.""" rows = [] for e in self.entries: d = e.structure.get_dict4pandas(with_spglib=True) decomp, ehull = self.phasediagram.get_decomp_and_e_above_hull(e) rows.append( OrderedDict([ ("Materials ID", e.entry_id), ("spglib_symb", d["spglib_symb"]), ("spglib_num", d["spglib_num"]), ("Composition", e.composition.reduced_formula), ( "Ehull", ehull ), # ("Equilibrium_reaction_energy", pda.get_equilibrium_reaction_energy(e)), ("Decomposition", " + ".join([ "%.2f %s" % (v, k.composition.formula) for k, v in decomp.items() ])), ])) import pandas as pd return pd.DataFrame(rows, columns=list(rows[0].keys()) if rows else None) def print_dataframes(self, with_spglib=False, file=sys.stdout, verbose=0): """ Print pandas dataframe to file `file`. Args: with_spglib: True to compute spacegroup with spglib. file: Output stream. verbose: Verbosity level. """ print_dataframe(self.dataframe, file=file) if verbose: from abipy.core.structure import dataframes_from_structures dfs = dataframes_from_structures(self.structures, index=self.mpids, with_spglib=with_spglib) print_dataframe(dfs.lattice, title="Lattice parameters:", file=file) if verbose > 1: print_dataframe( dfs.coords, title="Atomic positions (columns give the site index):", file=file)
def process_item(self, item): """ Process the list of entries into thermo docs for each sandbox 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 """ docs = [] sandboxes, entries = item entries = self.compatibility.process_entries(entries) # determine chemsys chemsys = "-".join( sorted( set([ el.symbol for e in entries for el in e.composition.elements ]))) self.logger.debug( f"Procesing {len(entries)} entries for {chemsys} - {sandboxes}") try: pd = PhaseDiagram(entries) docs = [] for e in entries: (decomp, ehull) = pd.get_decomp_and_e_above_hull(e) d = { self.thermo.key: e.entry_id, "thermo": { "energy": e.uncorrected_energy, "energy_per_atom": e.uncorrected_energy / e.composition.num_atoms, "formation_energy_per_atom": pd.get_form_energy_per_atom(e), "e_above_hull": ehull, "is_stable": e in pd.stable_entries, }, } # Store different info if stable vs decomposes if d["thermo"]["is_stable"]: d["thermo"][ "eq_reaction_e"] = pd.get_equilibrium_reaction_energy( e) else: d["thermo"]["decomposes_to"] = [{ "task_id": de.entry_id, "formula": de.composition.formula, "amount": amt, } for de, amt in decomp.items()] d["thermo"]["entry"] = e.as_dict() d["thermo"][ "explanation"] = self.compatibility.get_explanation_dict(e) elsyms = sorted( set([el.symbol for el in e.composition.elements])) d["chemsys"] = "-".join(elsyms) d["nelements"] = len(elsyms) d["elements"] = list(elsyms) d["_sbxn"] = list(sandboxes) docs.append(d) except PhaseDiagramError as p: elsyms = [] for e in entries: elsyms.extend([el.symbol for el in e.composition.elements]) self.logger.warning("Phase diagram errorin chemsys {}: {}".format( "-".join(sorted(set(elsyms))), p)) return [] return docs
def analyze_GGA_chempots(self, full_sub_approach=False): """ For calculating GGA-PBE atomic chemical potentials by using Materials Project pre-computed data Args: full_sub_approach: generate chemical potentials by looking at full phase diagram (setting to True is NOT recommended if subs_species set has more than one element in it...) This code retrieves atomic chempots from Materials Project (MP) entries by making use of the pymatgen phase diagram (PD) object and computed entries from the MP database. There are debug notes that are made based on the stability of the structure of interest with respect to the phase diagram generated from MP NOTE on 'full_sub_approach': The default approach for substitutional elements (full_sub_approach = False) is to only consider facets which have a maximum of 1 composition with the extrinsic species present (see PyCDT paper for chemical potential methodology DOI: 10.1016/j.cpc.2018.01.004). This default approach speeds up analysis when analyzing several substitutional species at the same time. If you prefer to consider the full phase diagram (not recommended when you have more than 2 substitutional defects), then set full_sub_approach to True. """ logger = logging.getLogger(__name__) #gather entries self.get_mp_entries(full_sub_approach=full_sub_approach) # figure out how system should be treated for chemical potentials # based on phase diagram entry_list = self.entries['bulk_derived'] pd = PhaseDiagram(entry_list) decomp_en = round( pd.get_decomp_and_e_above_hull(self.bulk_ce, allow_negative=True)[1], 4) stable_composition_exists = False for i in pd.stable_entries: if i.composition.reduced_composition == self.redcomp: stable_composition_exists = True if (decomp_en <= 0) and stable_composition_exists: logger.debug( "Bulk Computed Entry found to be stable with respect " "to MP Phase Diagram (e_above_hull = {} eV/atom).".format( decomp_en)) elif (decomp_en <= 0) and not stable_composition_exists: logger.info( "Bulk Computed Entry found to be stable with respect " "to MP Phase Diagram (e_above_hull = {} eV/atom).\n" "However, no stable entry with this composition exists " "in the MP database!\nPlease consider submitting the " "POSCAR to the MP xtaltoolkit, so future users will " "know about this structure:" " https://materialsproject.org/#apps/xtaltoolkit\n" "Manually inserting structure into phase diagram and " "proceeding as normal.".format(decomp_en)) entry_list.append(self.bulk_ce) elif stable_composition_exists: logger.warning( "Bulk Computed Entry not stable with respect to MP " "Phase Diagram (e_above_hull = {} eV/atom), but found " "stable MP composition to exist.\nProducing chemical " "potentials with respect to stable phase.".format(decomp_en)) else: logger.warning( "Bulk Computed Entry not stable with respect to MP " "Phase Diagram (e_above_hull = {} eV/atom) and no " "stable structure with this composition exists in the " "MP database.\nProceeding with atomic chemical " "potentials according to composition position within " "phase diagram.".format(decomp_en)) pd = PhaseDiagram(entry_list) chem_lims = self.get_chempots_from_pd(pd) logger.debug("Bulk Chemical potential facets: {}".format( chem_lims.keys())) if not full_sub_approach: # NOTE if full_sub_approach was True, then all the sub_entries # would be ported into the bulk_derived list finchem_lims = {} # this will be final chem_lims dictionary for key in chem_lims.keys(): face_list = key.split('-') blk, blknom, subnom = self.diff_bulk_sub_phases(face_list) finchem_lims[blknom] = {} finchem_lims[blknom] = chem_lims[key] # Now add single elements to extend the phase diagram, # adding new additions to chemical potentials ONLY for the cases # where the phases in equilibria are those from the bulk phase # diagram. This is essentially the assumption that the majority of # the elements in the total composition will be from the native # species present rather than the sub species (a good approximation) for sub_el in self.sub_species: sub_specie_entries = entry_list[:] for entry in self.entries['subs_set'][sub_el]: sub_specie_entries.append(entry) pd = PhaseDiagram(sub_specie_entries) chem_lims = self.get_chempots_from_pd(pd) for key in chem_lims.keys(): face_list = key.split('-') blk, blknom, subnom = self.diff_bulk_sub_phases( face_list, sub_el=sub_el) # if number of facets from bulk phase diagram is # equal to bulk species then full_sub_approach says this # can be grouped with rest of structures if len(blk) == len(self.bulk_species_symbol): if blknom not in finchem_lims.keys(): finchem_lims[blknom] = chem_lims[key] else: finchem_lims[blknom][Element(sub_el)] = \ chem_lims[key][Element(sub_el)] if 'name-append' not in finchem_lims[blknom].keys(): finchem_lims[blknom]['name-append'] = subnom else: finchem_lims[blknom]['name-append'] += '-' + subnom else: # if chem pots determined by two (or more) sub-specie # containing phases, skip this facet! continue #run a check to make sure all facets dominantly defined by bulk species overdependent_chempot = False facets_to_delete = [] for facet_name, cps in finchem_lims.items(): cp_key_num = (len(cps.keys()) - 1) if 'name-append' in cps else len(cps.keys()) if cp_key_num != (len(self.bulk_species_symbol) + len(self.sub_species)): facets_to_delete.append(facet_name) logger.info( "Not using facet {} because insufficient number of bulk facets for " "bulk set {} with sub_species set {}. (only dependent on {})." "".format(facet_name, self.bulk_species_symbol, self.sub_species, cps.get('name-append'))) if len(facets_to_delete) == len(finchem_lims): overdependent_chempot = True logger.warning( "Determined chemical potentials to be over dependent" " on a substitutional specie. Needing to revert to full_sub_approach. If " "multiple sub species exist this could take a while/break the code..." ) else: finchem_lims = { k: v for k, v in finchem_lims.items() if k not in facets_to_delete } if not overdependent_chempot: chem_lims = {} for orig_facet, fc_cp_dict in finchem_lims.items(): if 'name-append' not in fc_cp_dict: facet_nom = orig_facet else: full_facet_list = orig_facet.split('-') full_facet_list.extend( fc_cp_dict['name-append'].split('-')) full_facet_list.sort() facet_nom = '-'.join(full_facet_list) chem_lims[facet_nom] = { k: v for k, v in fc_cp_dict.items() if k != 'name-append' } else: #This is for when overdetermined chempots occur, forcing the full_sub_approach to happen for sub, subentries in self.entries['subs_set'].items(): for subentry in subentries: entry_list.append(subentry) pd = PhaseDiagram(entry_list) chem_lims = self.get_chempots_from_pd(pd) return chem_lims
class PhaseDiagramResults(object): """ Simplified interface to phase-diagram pymatgen API. Inspired to: https://anaconda.org/matsci/plotting-and-analyzing-a-phase-diagram-using-the-materials-api/notebook See also: :cite:`Ong2008,Ong2010` """ def __init__(self, entries): self.entries = entries from abipy.core.structure import Structure for e in entries: e.structure = Structure.as_structure(e.structure) self.structures = [e.structure for e in entries] self.mpids = [e.entry_id for e in entries] # Create phase diagram. from pymatgen.analysis.phase_diagram import PhaseDiagram self.phasediagram = PhaseDiagram(self.entries) def plot(self, show_unstable=True, show=True): """ Plot phase diagram. Args: show_unstable (float): Whether unstable phases will be plotted as well as red crosses. If a number > 0 is entered, all phases with ehull < show_unstable will be shown. show: True to show plot. Return: plotter object. """ from pymatgen.analysis.phase_diagram import PDPlotter plotter = PDPlotter(self.phasediagram, show_unstable=show_unstable) if show: plotter.show() return plotter @lazy_property def dataframe(self): """Pandas dataframe with the most important results.""" rows = [] for e in self.entries: d = e.structure.get_dict4pandas(with_spglib=True) decomp, ehull = self.phasediagram.get_decomp_and_e_above_hull(e) rows.append(OrderedDict([ ("Materials ID", e.entry_id), ("spglib_symb", d["spglib_symb"]), ("spglib_num", d["spglib_num"]), ("Composition", e.composition.reduced_formula), ("Ehull", ehull), # ("Equilibrium_reaction_energy", pda.get_equilibrium_reaction_energy(e)), ("Decomposition", " + ".join(["%.2f %s" % (v, k.composition.formula) for k, v in decomp.items()])), ])) import pandas as pd return pd.DataFrame(rows, columns=list(rows[0].keys()) if rows else None) def print_dataframes(self, with_spglib=False, file=sys.stdout, verbose=0): """ Print pandas dataframe to file `file`. Args: with_spglib: True to compute spacegroup with spglib. file: Output stream. verbose: Verbosity level. """ print_dataframe(self.dataframe, file=file) if verbose: from abipy.core.structure import dataframes_from_structures dfs = dataframes_from_structures(self.structures, index=self.mpids, with_spglib=with_spglib) print_dataframe(dfs.lattice, title="Lattice parameters:", file=file) if verbose > 1: print_dataframe(dfs.coords, title="Atomic positions (columns give the site index):", file=file)