def test_get_stability(self): entries = self.rester.get_entries_in_chemsys(["Fe", "O"]) modified_entries = [] for entry in entries: # Create modified entries with energies that are 0.01eV higher # than the corresponding entries. if entry.composition.reduced_formula == "Fe2O3": modified_entries.append( ComputedEntry(entry.composition, entry.uncorrected_energy + 0.01, parameters=entry.parameters, entry_id="mod_{}".format(entry.entry_id))) rest_ehulls = self.rester.get_stability(modified_entries) all_entries = entries + modified_entries compat = MaterialsProjectCompatibility() all_entries = compat.process_entries(all_entries) pd = PhaseDiagram(all_entries) for e in all_entries: if str(e.entry_id).startswith("mod"): for d in rest_ehulls: if d["entry_id"] == e.entry_id: data = d break self.assertAlmostEqual(pd.get_e_above_hull(e), data["e_above_hull"])
def get_competing_phases(): """ Collect the species to which the material might decompose to. Returns: A list of phases as tuples formatted as [(formula_1, Materials_Project_ID_1), (formula_2, Materials_Project_ID_2), ...] """ 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 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_pourbaix_entries(self, chemsys): """ A helper function to get all entries necessary to generate a pourbaix diagram from the rest interface. Args: chemsys ([str]): A list of elements comprising the chemical system, e.g. ['Li', 'Fe'] """ from pymatgen.analysis.pourbaix.entry import PourbaixEntry, IonEntry from pymatgen.analysis.phase_diagram import PhaseDiagram from pymatgen.core.ion import Ion from pymatgen.entries.compatibility import\ MaterialsProjectAqueousCompatibility chemsys = list(set(chemsys + ['O', 'H'])) entries = self.get_entries_in_chemsys( chemsys, property_data=['e_above_hull'], compatible_only=False) compat = MaterialsProjectAqueousCompatibility("Advanced") entries = compat.process_entries(entries) solid_pd = PhaseDiagram(entries) # Need this to get ion formation energy url = '/pourbaix_diagram/reference_data/' + '-'.join(chemsys) ion_data = self._make_request(url) pbx_entries = [] for entry in entries: if not set(entry.composition.elements)\ <= {Element('H'), Element('O')}: pbx_entry = PourbaixEntry(entry) pbx_entry.g0_replace(solid_pd.get_form_energy(entry)) pbx_entry.reduced_entry() pbx_entries.append(pbx_entry) # position the ion energies relative to most stable reference state for n, i_d in enumerate(ion_data): ion_entry = IonEntry(Ion.from_formula(i_d['Name']), i_d['Energy']) refs = [e for e in entries if e.composition.reduced_formula == i_d['Reference Solid']] if not refs: raise ValueError("Reference solid not contained in entry list") stable_ref = sorted(refs, key=lambda x: x.data['e_above_hull'])[0] rf = stable_ref.composition.get_reduced_composition_and_factor()[1] solid_diff = solid_pd.get_form_energy(stable_ref)\ - i_d['Reference solid energy'] * rf elt = i_d['Major_Elements'][0] correction_factor = ion_entry.ion.composition[elt]\ / stable_ref.composition[elt] correction = solid_diff * correction_factor pbx_entries.append(PourbaixEntry(ion_entry, correction, 'ion-{}'.format(n))) return pbx_entries
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 get_lowest_decomposition(self, composition): """ Get the decomposition leading to lowest cost Args: composition: Composition as a pymatgen.core.structure.Composition Returns: Decomposition as a dict of {Entry: amount} """ entries_list = [] elements = [e.symbol for e in composition.elements] for i in range(len(elements)): for combi in itertools.combinations(elements, i + 1): chemsys = [Element(e) for e in combi] x = self.costdb.get_entries(chemsys) entries_list.extend(x) try: pd = PhaseDiagram(entries_list) return pd.get_decomposition(composition) except IndexError: raise ValueError("Error during PD building; most likely, " "cost data does not exist!")
class ReactionNetwork: """ This class creates and stores a weighted, directed graph in graph-tool that is a dense network of all possible chemical reactions (edges) between phase combinations (vertices) in a chemical system. Reaction pathway hypotheses are generated using pathfinding methods. """ def __init__( self, entries, n=2, temp=300, interpolate_comps=None, extend_entries=None, include_metastable=False, include_polymorphs=False, filter_rxn_energies=0.5, ): """Initializes ReactionNetwork object with necessary preprocessing steps. This does not yet compute the graph. Args: entries ([ComputedStructureEntry]): list of ComputedStructureEntry- like objects to consider in network. These can be acquired from Materials Project (using MPRester) or created manually in pymatgen. Entries should have same compatability (e.g. MPCompability) for phase diagram generation. n (int): maximum number of phases allowed on each side of the reaction (default 2). Note that n > 2 leads to significant ( and often intractable) combinatorial explosion. temp (int): Temperature (in Kelvin) used for estimating Gibbs free energy of formation, as well as scaling the cost function later during network generation. Must select from [300, 400, 500, ... 2000] K. extend_entries ([ComputedStructureEntry]): list of ComputedStructureEntry-like objects which will be included in the network even after filtering for thermodynamic stability. Helpful if target phase has a significantly high energy above the hull. include_metastable (float or bool): either a) the specified cutoff for energy per atom (eV/atom) above hull, or b) True/False if considering only stable vs. all entries. An energy cutoff of 0.1 eV/atom is a reasonable starting threshold for thermodynamic stability. Defaults to False. include_polymorphs (bool): Whether or not to consider non-ground state polymorphs. Defaults to False. Note this is not useful unless structural metrics are considered in the cost function (to be added!) filter_rxn_energies (float): Energy filter. Reactions with energy_per_atom > filter will be excluded from network. """ self.logger = logging.getLogger("ReactionNetwork") self.logger.setLevel("INFO") # Chemical system / phase diagram variables self._all_entries = entries self._max_num_phases = n self._temp = temp self._e_above_hull = include_metastable self._include_polymorphs = include_polymorphs self._elements = { elem for entry in self.all_entries for elem in entry.composition.elements } self._pd_dict, self._filtered_entries = self._filter_entries( entries, include_metastable, temp, include_polymorphs) self._entry_mu_ranges = {} self._pd = None self._rxn_e_filter = filter_rxn_energies if (len(self._elements) <= 10 ): # phase diagrams take considerable time to build with 10+ elems self._pd = PhaseDiagram(self._filtered_entries) if interpolate_comps: interpolated_entries = [] for comp in interpolate_comps: energy = self._pd.get_hull_energy(Composition(comp)) interpolated_entries.append( PDEntry(comp, energy, attribute={"interpolated": True})) print("Interpolated entries:", "\n") print(interpolated_entries) self._filtered_entries.extend(interpolated_entries) if extend_entries: self._filtered_entries.extend(extend_entries) for idx, e in enumerate(self._filtered_entries): e.entry_idx = idx self.num_entries = len(self._filtered_entries) self._all_entry_combos = [ set(combo) for combo in generate_all_combos( self._filtered_entries, self._max_num_phases) ] self.entry_set = EntrySet(self._filtered_entries) self.entry_indices = { e: idx for idx, e in enumerate(self._filtered_entries) } # Graph variables used during graph creation self._precursors = None self._all_targets = None self._current_target = None self._cost_function = None self._complex_loopback = None self._precursors_entries = None self._most_negative_rxn = None # used in piecewise cost function self._g = None # Graph object in graph-tool filtered_entries_str = ", ".join([ entry.composition.reduced_formula for entry in self._filtered_entries ]) self.logger.info( f"Initializing network with {len(self._filtered_entries)} " f"entries: \n{filtered_entries_str}") def generate_rxn_network( self, precursors=None, targets=None, cost_function="softplus", complex_loopback=True, ): """ Generates and stores the reaction network (weighted, directed graph) using graph-tool. Args: precursors ([ComputedEntry]): entries for all phases which serve as the main reactants; if None, a "dummy" node is used to represent any possible set of precursors. targets ([ComputedEntry]): entries for all phases which are the final products; if None, a "dummy" node is used to represent any possible set of targets. cost_function (str): name of cost function to use for entire network (e.g. "softplus"). complex_loopback (bool): if True, adds zero-weight edges which "loop back" to allow for multi-step or autocatalytic-like reactions, i.e. original precursors can reappear many times and in different steps. """ self._precursors = set(precursors) if precursors else None self._all_targets = set(targets) if targets else None self._current_target = ( {targets[0]} if targets else None ) # take first entry to be first designated target self._cost_function = cost_function self._complex_loopback = complex_loopback if not self._precursors: precursors_entries = RxnEntries(None, "d") # use dummy precursors node if self._complex_loopback: raise ValueError( "Complex loopback can't be enabled when using a dummy precursors " "node!") else: precursors_entries = RxnEntries(precursors, "s") self._precursors_entries = precursors_entries g = gt.Graph() # initialization of graph obj in graph-tool g.vp["entries"] = g.new_vertex_property("object") g.vp["type"] = g.new_vertex_property( "int") # Type 0: precursors, 1: reactants, 2: products, 3: target g.vp["bool"] = g.new_vertex_property("bool") g.vp["path"] = g.new_vertex_property( "bool") # whether node is part of path g.vp["chemsys"] = g.new_vertex_property("string") g.ep["weight"] = g.new_edge_property("double") g.ep["rxn"] = g.new_edge_property("object") g.ep["bool"] = g.new_edge_property("bool") g.ep["path"] = g.new_edge_property( "bool") # whether edge is part of path precursors_v = g.add_vertex() self._update_vertex_properties( g, precursors_v, { "entries": precursors_entries, "type": 0, "bool": True, "path": True, "chemsys": precursors_entries.chemsys, }, ) target_chemsys = set( list(self._current_target)[0].composition.chemical_system.split( "-")) entries_dict = {} idx = 1 for entries in self._all_entry_combos: reactants = RxnEntries(entries, "R") products = RxnEntries(entries, "P") chemsys = reactants.chemsys if (self._precursors_entries.description == "D" and not target_chemsys.issubset(chemsys.split("-"))): continue if chemsys not in entries_dict: entries_dict[chemsys] = dict({"R": {}, "P": {}}) entries_dict[chemsys]["R"][reactants] = idx self._update_vertex_properties( g, idx, { "entries": reactants, "type": 1, "bool": True, "path": False, "chemsys": chemsys, }, ) if (self._precursors_entries.description == "D" and not self._all_targets.issubset(entries)): idx = idx + 1 continue entries_dict[chemsys]["P"][products] = idx + 1 self._update_vertex_properties( g, idx + 1, { "entries": products, "type": 2, "bool": True, "path": False, "chemsys": chemsys, }, ) idx = idx + 2 g.add_vertex(idx) # add ALL precursors, reactant, and product vertices target_v = g.add_vertex() # add target vertex target_entries = RxnEntries(self._current_target, "t") self._update_vertex_properties( g, target_v, { "entries": target_entries, "type": 3, "bool": True, "path": True, "chemsys": target_entries.chemsys, }, ) self.logger.info("Generating reactions by chemical subsystem...") all_edges = [] all_rxn_combos = [] start_time = time() for chemsys, vertices in entries_dict.items(): precursor_edges = [[precursors_v, v, 0, None, True, False] for entry, v in vertices["R"].items() if self._precursors_entries.description == "D" or entry.entries.issubset(self._precursors)] target_edges = [ edge for edge_list in map( partial( self._get_target_edges, current_target=self._current_target, target_v=int(target_v), precursors=self._precursors, max_num_phases=self._max_num_phases, entries_dict=entries_dict, complex_loopback=complex_loopback, ), vertices["P"].items(), ) for edge in edge_list if edge ] rxn_combos = product(vertices["R"].items(), vertices["P"].items()) all_rxn_combos.append(rxn_combos) all_edges.extend(precursor_edges) all_edges.extend(target_edges) db = bag.from_sequence(chain.from_iterable(all_rxn_combos), partition_size=100000) reaction_edges = db.map_partitions( find_rxn_edges, cost_function=cost_function, rxn_e_filter=self._rxn_e_filter, temp=self.temp, num_entries=self.num_entries, ).compute() all_edges.extend(reaction_edges) g.add_edge_list( all_edges, eprops=[g.ep["weight"], g.ep["rxn"], g.ep["bool"], g.ep["path"]]) end_time = time() self.logger.info( f"Graph creation took {round(end_time - start_time, 1)} seconds.") self.logger.info( f"Created graph with {g.num_vertices()} nodes and {g.num_edges()} edges." ) self._g = g def find_k_shortest_paths(self, k, verbose=True): """ Finds k shortest paths to current target using Yen's Algorithm. Args: k (int): desired number of shortest pathways (ranked by cost) verbose (bool): whether to print all identified pathways to the console. Returns: [RxnPathway]: list of RxnPathway objects containing reactions traversed on each path. """ g = self._g paths = [] precursors_v = gt.find_vertex(g, g.vp["type"], 0)[0] target_v = gt.find_vertex(g, g.vp["type"], 3)[0] for num, path in enumerate(self._yens_ksp(g, k, precursors_v, target_v)): rxns = [] weights = [] for step, v in enumerate(path): g.vp["path"][v] = True if (g.vp["type"][v] == 2 ): # add rxn step if current node in path is a product e = g.edge(path[step - 1], v) g.ep["path"][ e] = True # mark this edge as occurring on a path rxns.append(g.ep["rxn"][e]) weights.append(g.ep["weight"][e]) rxn_pathway = RxnPathway(rxns, weights) paths.append(rxn_pathway) if verbose: for path in paths: print(path, "\n") return paths def find_all_rxn_pathways( self, k=15, precursors=None, targets=None, max_num_combos=4, chempots=None, consider_crossover_rxns=10, filter_interdependent=True, ): """ Builds the k shortest paths to provided targets and then seeks to combine them to achieve a "net reaction" with balanced stoichiometry. In other words, the full conversion of all intermediates to final products. Warning: this method can take a significant amount of time depending on the size of the network and the max_num_combos parameter. General recommendations are k = 15 and max_num_combos = 4, although a higher max_num_combos may be required to capture the full pathway. Args: k (int): Number of shortest paths to calculate to each target (i.e. if there are 3 targets and k=15, then 3x15 = 45 paths will be generated and the reactions from these will be combined. precursors ([ComputedEntry]): list of all precursor ComputedEntry objects; defaults to precursors provided when network was created. targets ([ComputedEntry]): list of all target ComputedEntry objects; defaults to targets provided when network was created. max_num_combos (int): upper limit on how many reactions to consider at a time (default 4). chempots ({Element: float}): dictionary of chemical potentials, used for identifying intermediate reactions open to a specific element consider_crossover_rxns (bool): Whether to consider "crossover" reactions between intermediates in other pathways. This can be crucial for generating realistic predictions and it is highly recommended; generally the added computational cost is extremely low. filter_interdependent (bool): Returns: ([CombinedPathway], PathwayAnalysis): Tuple containing list of CombinedPathway objects (sorted by total cost) and a PathwayAnalysis object with helpful analysis methods for hypothesized pathways. """ paths_to_all_targets = dict() if not targets: targets = self._all_targets else: targets = set(targets) if not precursors or set(precursors) == self._precursors: precursors = self._precursors else: self.set_precursors(precursors, self._complex_loopback) try: net_rxn = ComputedReaction(list(precursors), list(targets), num_entries=self.num_entries) except ReactionError: raise ReactionError( "Net reaction must be balanceable to find all reaction pathways." ) self.logger.info(f"NET RXN: {net_rxn} \n") for target in targets: print(f"PATHS to {target.composition.reduced_formula} \n") print("--------------------------------------- \n") self.set_target(target) paths = self.find_k_shortest_paths(k) paths = { rxn: cost for path in paths for (rxn, cost) in zip(path.rxns, path.costs) } paths_to_all_targets.update(paths) print("Finding crossover reactions paths...") if consider_crossover_rxns: intermediates = { entry for rxn in paths_to_all_targets for entry in rxn.all_entries } - targets intermediate_rxns = self.find_intermediate_rxns( intermediates, targets, chempots) paths = { rxn: get_rxn_cost(rxn, self._cost_function, self.temp) for rxn in intermediate_rxns } paths_to_all_targets.update(paths) paths_to_all_targets.update( self.find_crossover_rxns(intermediates, targets)) paths_to_all_targets.pop(net_rxn, None) paths_to_all_targets = { k: v for k, v in paths_to_all_targets.items() if not (self._precursors.intersection(k._product_entries) or self._all_targets.intersection(k._reactant_entries) or len(k._product_entries) > 3) } rxn_list = [r for r in paths_to_all_targets.keys()] pprint(rxn_list) normalized_rxns = [ Reaction.from_string(r.normalized_repr) for r in rxn_list ] num_rxns = len(rxn_list) self.logger.info(f"Considering {num_rxns} reactions...") batch_size = 500000 total_paths = [] for n in range(1, max_num_combos + 1): if n >= 4: self.logger.info( f"Generating and filtering size {n} pathways...") all_c_mats, all_m_mats = [], [] for combos in tqdm( grouper(combinations(range(num_rxns), n), batch_size), total=int(comb(num_rxns, n) / batch_size), ): comp_matrices = np.stack([ np.vstack([rxn_list[r].vector for r in combo]) for combo in combos if combo ]) c_mats, m_mats = self._balance_path_arrays( comp_matrices, net_rxn.vector) all_c_mats.extend(c_mats) all_m_mats.extend(m_mats) for c_mat, m_mat in zip(all_c_mats, all_m_mats): rxn_dict = {} for rxn_mat in c_mat: reactant_entries = [ self._filtered_entries[i] for i in range(len(rxn_mat)) if rxn_mat[i] < 0 ] product_entries = [ self._filtered_entries[i] for i in range(len(rxn_mat)) if rxn_mat[i] > 0 ] rxn = ComputedReaction(reactant_entries, product_entries, entries=self.num_entries) cost = paths_to_all_targets[rxn_list[normalized_rxns.index( Reaction.from_string(rxn.normalized_repr))]] rxn_dict[rxn] = cost p = BalancedPathway(rxn_dict, net_rxn, balance=False) p.set_multiplicities(m_mat.flatten()) total_paths.append(p) if filter_interdependent: final_paths = set() for p in total_paths: interdependent, combined_rxn = find_interdependent_rxns( p, [c.composition for c in precursors]) if interdependent: continue final_paths.add(p) else: final_paths = total_paths return sorted(list(final_paths), key=lambda x: x.total_cost) def find_crossover_rxns(self, intermediates, targets): """ Identifies possible "crossover" reactions (i.e., reactions where the predicted intermediate phases result in one or more targets phases. Args: intermediates ([ComputedEntry]): List of intermediate entries targets ([ComputedEntry]): List of target entries Returns: [ComputedReaction]: List of crossover reactions """ all_crossover_rxns = dict() for reactants_combo in generate_all_combos(intermediates, self._max_num_phases): for products_combo in generate_all_combos(targets, self._max_num_phases): try: rxn = ComputedReaction( list(reactants_combo), list(products_combo), num_entries=self.num_entries, ) except ReactionError: continue if rxn._lowest_num_errors > 0: continue path = { rxn: get_rxn_cost(rxn, self._cost_function, self._temp) } all_crossover_rxns.update(path) return all_crossover_rxns def find_intermediate_rxns(self, intermediates, targets, chempots=None): """ Identifies thermodynamically predicted reactions from intermediate to one or more targets using interfacial reaction method. This method has the unique benefit ( compared to the find_crossover_rxns method) of identifying reactions open to a specfic element or producing 3 or more products. Args: intermediates ([ComputedEntry]): List of intermediate entries targets ([ComputedEntry]): List of target entries chempots ({Element: float}): Dictionary of chemical potentials used to create grand potential phase diagram by which interfacial reactions are predicted. Returns: [ComputedReaction]: List of intermediate reactions """ all_rxns = set() combos = list(generate_all_combos(intermediates, 2)) for entries in tqdm(combos): n = len(entries) r1 = entries[0].composition.reduced_composition chemsys = { str(el) for entry in entries for el in entry.composition.elements } elem = None if chempots: elem = str(list(chempots.keys())[0]) chemsys.update(elem) if chemsys == {elem}: continue if n == 1: r2 = entries[0].composition.reduced_composition elif n == 2: r2 = entries[1].composition.reduced_composition else: raise ValueError( "Can't have an interface that is not 1 to 2 entries!") if chempots: elem_comp = Composition(elem).reduced_composition if r1 == elem_comp or r2 == elem_comp: continue entry_subset = self.entry_set.get_subset_in_chemsys(list(chemsys)) pd = PhaseDiagram(entry_subset) grand_pd = None if chempots: grand_pd = GrandPotentialPhaseDiagram(entry_subset, chempots) rxns = react_interface(r1, r2, pd, self.num_entries, grand_pd) rxns_filtered = { r for r in rxns if set(r._product_entries) & targets } if rxns_filtered: most_favorable_rxn = min( rxns_filtered, key=lambda x: (x.calculated_reaction_energy / sum( [x.get_el_amount(elem) for elem in x.elements])), ) all_rxns.add(most_favorable_rxn) return all_rxns def set_precursors(self, precursors=None, complex_loopback=True): """ Replaces network's previous precursor node with provided new precursors. Finds new edges that link products back to reactants as dependent on the complex_loopback parameter. Args: precursors ([ComputedEntry]): list of new precursor entries complex_loopback (bool): if True, adds zero-weight edges which "loop back" to allow for multi-step or autocatalytic-like reactions, i.e. original precursors can reappear many times and in different steps. Returns: None """ g = self._g self._precursors = set(precursors) if precursors else None if not self._precursors: precursors_entries = RxnEntries(None, "d") # use dummy precursors node if complex_loopback: raise ValueError( "Complex loopback can't be enabled when using a dummy precursors " "node!") else: precursors_entries = RxnEntries(precursors, "s") g.remove_vertex(gt.find_vertex(g, g.vp["type"], 0)) new_precursors_v = g.add_vertex() self._update_vertex_properties( g, new_precursors_v, { "entries": precursors_entries, "type": 0, "bool": True, "path": True, "chemsys": precursors_entries.chemsys, }, ) new_edges = [] remove_edges = [] for v in gt.find_vertex(g, g.vp["type"], 1): # iterate over all reactants phases = g.vp["entries"][v].entries remove_edges.extend(list(v.in_edges())) if precursors_entries.description == "D" or phases.issubset( self._precursors): new_edges.append([new_precursors_v, v, 0, None, True, False]) for v in gt.find_vertex(g, g.vp["type"], 2): # iterate over all products phases = g.vp["entries"][v].entries if complex_loopback: combos = generate_all_combos(phases.union(self._precursors), self._max_num_phases) else: combos = generate_all_combos(phases, self._max_num_phases) for c in combos: combo_phases = set(c) if complex_loopback and combo_phases.issubset( self._precursors): continue combo_entry = RxnEntries(combo_phases, "R") loopback_v = gt.find_vertex(g, g.vp["entries"], combo_entry)[0] new_edges.append([v, loopback_v, 0, None, True, False]) for e in remove_edges: g.remove_edge(e) g.add_edge_list( new_edges, eprops=[g.ep["weight"], g.ep["rxn"], g.ep["bool"], g.ep["path"]]) def set_target(self, target): """ Replaces network's current target phase with new target phase. Args: target (ComputedEntry): ComputedEntry-like object for new target phase. Returns: None """ g = self._g if target in self._current_target: return else: self._current_target = {target} g.remove_vertex(gt.find_vertex(g, g.vp["type"], 3)) new_target_entry = RxnEntries(self._current_target, "t") new_target_v = g.add_vertex() self._update_vertex_properties( g, new_target_v, { "entries": new_target_entry, "type": 3, "bool": True, "path": True, "chemsys": new_target_entry.chemsys, }, ) new_edges = [] for v in gt.find_vertex(g, g.vp["type"], 2): # search for all products if self._current_target.issubset(g.vp["entries"][v].entries): new_edges.append([v, new_target_v, 0, None, True, False]) # link all products to new target g.add_edge_list( new_edges, eprops=[g.ep["weight"], g.ep["rxn"], g.ep["bool"], g.ep["path"]]) def set_cost_function(self, cost_function): """ Replaces network's current cost function with new function by recomputing edge weights. Args: cost_function (str): name of cost function. Current options are ["softplus", "relu", "piecewise"]. Returns: None """ g = self._g self._cost_function = cost_function for e in gt.find_edge_range(g, g.ep["weight"], (1e-8, 1e8)): g.ep["weight"][e] = get_rxn_cost(g.ep["rxn"][e], ) @staticmethod def _get_target_edges( vertex, current_target, target_v, precursors, max_num_phases, entries_dict, complex_loopback, ): entry = vertex[0] v = vertex[1] edge_list = [] phases = entry.entries if current_target.issubset(phases): edge_list.append([v, target_v, 0, None, True, False]) if complex_loopback: combos = generate_all_combos(phases.union(precursors), max_num_phases) else: combos = generate_all_combos(phases, max_num_phases) if complex_loopback: for c in combos: combo_phases = set(c) if combo_phases.issubset(precursors): continue combo_entry = RxnEntries(combo_phases, "R") loopback_v = entries_dict[ combo_entry.chemsys]["R"][combo_entry] edge_list.append([v, loopback_v, 0, None, True, False]) return edge_list @staticmethod def _update_vertex_properties(g, v, prop_dict): """ Helper method for updating several vertex properties at once in a graph-tool graph. Args: g (gt.Graph): a graph-tool Graph object. v (gt.Vertex or int): a graph-tool Vertex object (or its index) for a vertex in the provided graph. prop_dict (dict): a dictionary of the form {"prop": val}, where prop is the name of a VertexPropertyMap of the graph and val is the new updated value for that vertex's property. Returns: None """ for prop, val in prop_dict.items(): g.vp[prop][v] = val return None @staticmethod def _yens_ksp(g, num_k, precursors_v, target_v, edge_prop="bool", weight_prop="weight"): """ Yen's Algorithm for k-shortest paths. Inspired by igraph implementation by Antonin Lenfant. Ref: Jin Y. Yen, "Finding the K Shortest Loopless Paths in a Network", Management Science, Vol. 17, No. 11, Theory Series (Jul., 1971), pp. 712-716. Args: g (gt.Graph): the graph-tool graph object. num_k (int): number of k shortest paths that should be found. precursors_v (gt.Vertex): graph-tool vertex object containing precursors. target_v (gt.Vertex): graph-tool vertex object containing target. edge_prop (str): name of edge property map which allows for filtering edges. Defaults to the word "bool". weight_prop (str): name of edge property map that stores edge weights/costs. Defaults to the word "weight". Returns: List of lists of graph vertices corresponding to each shortest path (sorted in increasing order by cost). """ def path_cost(vertices): """Calculates path cost given a list of vertices.""" cost = 0 for j in range(len(vertices) - 1): cost += g.ep[weight_prop][g.edge(vertices[j], vertices[j + 1])] return cost path = gt.shortest_path(g, precursors_v, target_v, weights=g.ep[weight_prop])[0] if not path: return [] a = [path] a_costs = [path_cost(path)] b = queue.PriorityQueue( ) # automatically sorts by path cost (priority) for k in range(1, num_k): try: prev_path = a[k - 1] except IndexError: print(f"Identified only k={k} paths before exiting. \n") break for i in range(len(prev_path) - 1): spur_v = prev_path[i] root_path = prev_path[:i] filtered_edges = [] for path in a: if len(path) - 1 > i and root_path == path[:i]: e = g.edge(path[i], path[i + 1]) if not e: continue g.ep[edge_prop][e] = False filtered_edges.append(e) gv = gt.GraphView(g, efilt=g.ep[edge_prop]) spur_path = gt.shortest_path(gv, spur_v, target_v, weights=g.ep[weight_prop])[0] for e in filtered_edges: g.ep[edge_prop][e] = True if spur_path: total_path = root_path + spur_path total_path_cost = path_cost(total_path) b.put((total_path_cost, total_path)) while True: try: cost_, path_ = b.get(block=False) except queue.Empty: break if path_ not in a: a.append(path_) a_costs.append(cost_) break return a @staticmethod @njit(parallel=True) def _balance_path_arrays( comp_matrices, net_coeffs, tol=1e-6, ): """ Fast solution for reaction multiplicities via mass balance stochiometric constraints. Parallelized using Numba. Args: comp_matrices ([np.array]): list of numpy arrays containing stoichiometric coefficients of all compositions in all reactions, for each trial combination. net_coeffs ([np.array]): list of numpy arrays containing stoichiometric coefficients of net reaction. tol (float): numerical tolerance for determining if a multiplicity is zero (reaction was removed). Returns: ([bool],[np.array]): Tuple containing bool identifying which trial BalancedPathway objects were successfully balanced, and a list of all multiplicities arrays. """ shape = comp_matrices.shape net_coeff_filter = np.argwhere(net_coeffs != 0).flatten() len_net_coeff_filter = len(net_coeff_filter) all_multiplicities = np.zeros((shape[0], shape[1]), np.float64) indices = np.full(shape[0], False) for i in prange(shape[0]): correct = True for j in range(len_net_coeff_filter): idx = net_coeff_filter[j] if not comp_matrices[i][:, idx].any(): correct = False break if not correct: continue comp_pinv = np.linalg.pinv(comp_matrices[i]).T multiplicities = comp_pinv @ net_coeffs solved_coeffs = comp_matrices[i].T @ multiplicities if (multiplicities < tol).any(): continue elif not (np.abs(solved_coeffs - net_coeffs) <= (1e-08 + 1e-05 * np.abs(net_coeffs))).all(): continue all_multiplicities[i] = multiplicities indices[i] = True filtered_indices = np.argwhere(indices != 0).flatten() length = filtered_indices.shape[0] filtered_comp_matrices = np.empty((length, shape[1], shape[2]), np.float64) filtered_multiplicities = np.empty((length, shape[1]), np.float64) for i in range(length): idx = filtered_indices[i] filtered_comp_matrices[i] = comp_matrices[idx] filtered_multiplicities[i] = all_multiplicities[idx] return filtered_comp_matrices, filtered_multiplicities @staticmethod def _filter_entries(all_entries, e_above_hull, temp, include_polymorphs=False): """ Helper method for filtering entries by specified energy above hull Args: all_entries ([ComputedEntry]): List of ComputedEntry-like objects to be filtered e_above_hull (float): Thermodynamic stability threshold (energy above hull) [eV/atom] include_polymorphs (bool): whether to include higher energy polymorphs of existing structures Returns: [ComputedEntry]: list of all entries with energies above hull equal to or less than the specified e_above_hull. """ pd_dict = expand_pd(all_entries) pd_dict = { chemsys: PhaseDiagram(GibbsComputedStructureEntry.from_pd(pd, temp)) for chemsys, pd in pd_dict.items() } filtered_entries = set() all_comps = dict() for chemsys, pd in pd_dict.items(): for entry in pd.all_entries: if (entry in filtered_entries or pd.get_e_above_hull(entry) > e_above_hull): continue formula = entry.composition.reduced_formula if not include_polymorphs and (formula in all_comps): if all_comps[ formula].energy_per_atom < entry.energy_per_atom: continue filtered_entries.remove(all_comps[formula]) all_comps[formula] = entry filtered_entries.add(entry) return pd_dict, list(filtered_entries) @property def g(self): return self._g @property def pd(self): return self._pd @property def all_entries(self): return self._all_entries @property def filtered_entries(self): return self._filtered_entries @property def precursors(self): return self._precursors @property def all_targets(self): return self._all_targets @property def temp(self): return self._temp def __repr__(self): return (f"ReactionNetwork for chemical system: " f"{'-'.join(sorted([str(e) for e in self._pd.elements]))}, " f"with Graph: {str(self._g)}")
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)
from pymatgen.apps.borg.hive import VaspToComputedEntryDrone from pymatgen.apps.borg.queen import BorgQueen from pymatgen.entries.compatibility import MaterialsProjectCompatibility #pd = pd2.get_phase_diagram_data() # ler o tamanho da lista # cria uma lista só com os potenciais químicos # chempot_list = [all_phase_diagrams[pd_index][1] for pd_index in range(number_of_phase_diagrams)] drone = VaspToComputedEntryDrone() queen = BorgQueen(drone, rootpath=".") entries = queen.get_data() pd2 = PhaseDiagram(entries) all_phase_diagrams = pd.get_phase_diagram_data() number_of_phase_diagrams = len(all_phase_diagrams) open_elements_specific = None open_element_all = Element("O") mpr = MPRester("######") mp_entries = mpr.get_entries_in_chemsys(['Li', 'Ca', 'O'], compatible_only=True) pd = PhaseDiagramOpenAnalyzer(mp_entries[0], open_element_all) entries.extend(mp_entries)
from pymatgen.ext.matproj import MPRester from pymatgen.apps.borg.hive import VaspToComputedEntryDrone from pymatgen.apps.borg.queen import BorgQueen from pymatgen.entries.compatibility import MaterialsProjectCompatibility from pymatgen.analysis.phase_diagram import PhaseDiagram from pymatgen.analysis.phase_diagram import PDPlotter # Assimilate VASP calculations into ComputedEntry object. Let's assume that # the calculations are for a series of new LixFeyOz phases that we want to # know the phase stability. drone = VaspToComputedEntryDrone() queen = BorgQueen(drone, rootpath=".") entries = queen.get_data() # Obtain all existing Li-Fe-O phases using the Materials Project REST API with MPRester("key") as m: mp_entries = m.get_entries_in_chemsys(["Li", "Sn", "S"]) # Combined entry from calculated run with Materials Project entries entries.extend(mp_entries) # Process entries using the MaterialsProjectCompatibility compat = MaterialsProjectCompatibility() entries = compat.process_entries(entries) # Generate and plot Li-Fe-O phase diagram pd = PhaseDiagram(entries) plotter = PDPlotter(pd) plotter.show()
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 get_mixing_state_data(self, entries: list[ComputedStructureEntry], verbose: bool = False): """ Generate internal state data to be passed to get_adjustments. Args: entries: The list of ComputedStructureEntry to process. It is assumed that the entries have already been filtered using _filter_and_sort_entries() to remove any irrelevant run types, apply compat_1 and compat_2, and confirm that all have unique entry_id. Returns: DataFrame: A pandas DataFrame that contains information associating structures from different functionals with specific materials and establishing how many run_type_1 ground states have been computed with run_type_2. The DataFrame contains one row for each distinct material (Structure), with the following columns: formula: str the reduced_formula spacegroup: int the spacegroup num_sites: int the number of sites in the Structure entry_id_1: the entry_id of the run_type_1 entry entry_id_2: the entry_id of the run_type_2 entry run_type_1: Optional[str] the run_type_1 value run_type_2: Optional[str] the run_type_2 value energy_1: float or nan the ground state energy in run_type_1 in eV/atom energy_2: float or nan the ground state energy in run_type_2 in eV/atom is_stable_1: bool whether this material is stable on the run_type_1 PhaseDiagram hull_energy_1: float or nan the energy of the run_type_1 hull at this composition in eV/atom hull_energy_2: float or nan the energy of the run_type_1 hull at this composition in eV/atom None: Returns None if the supplied ComputedStructureEntry are insufficient for applying the mixing scheme. """ filtered_entries = [] for entry in entries: if not isinstance(entry, ComputedStructureEntry): warnings.warn( "Entry {} is not a ComputedStructureEntry and will be" "ignored. The DFT mixing scheme requires structures for" " all entries".format(entry.entry_id) ) continue filtered_entries.append(entry) # separate by run_type entries_type_1 = [e for e in filtered_entries if e.parameters["run_type"] in self.valid_rtypes_1] entries_type_2 = [e for e in filtered_entries if e.parameters["run_type"] in self.valid_rtypes_2] # construct PhaseDiagram for each run_type, if possible pd_type_1, pd_type_2 = None, None try: pd_type_1 = PhaseDiagram(entries_type_1) except ValueError: warnings.warn(f"{self.run_type_1} entries do not form a complete PhaseDiagram.") try: pd_type_2 = PhaseDiagram(entries_type_2) except ValueError: warnings.warn(f"{self.run_type_2} entries do not form a complete PhaseDiagram.") # Objective: loop through all the entries, group them by structure matching (or fuzzy structure matching # where relevant). For each group, put a row in a pandas DataFrame with the composition of the run_type_1 entry, # the run_type_2 entry, whether or not that entry is a ground state (not necessarily on the hull), its energy, # and the energy of the hull at that composition all_entries = list(entries_type_1) + list(entries_type_2) row_list = [] columns = [ "formula", "spacegroup", "num_sites", "is_stable_1", "entry_id_1", "entry_id_2", "run_type_1", "run_type_2", "energy_1", "energy_2", "hull_energy_1", "hull_energy_2", ] def _get_sg(struc) -> int: """helper function to get spacegroup with a loose tolerance""" try: return struc.get_space_group_info(symprec=0.1)[1] except Exception: return -1 # loop through all structures # this logic follows emmet.builders.vasp.materials.MaterialsBuilder.filter_and_group_tasks structures = [] for entry in all_entries: s = entry.structure s.entry_id = entry.entry_id structures.append(s) # First group by composition, then by spacegroup number, then by structure matching for comp, compgroup in groupby(sorted(structures, key=lambda s: s.composition), key=lambda s: s.composition): l_compgroup = list(compgroup) # group by spacegroup, then by number of sites (for diatmics) or by structure matching for sg, pregroup in groupby(sorted(l_compgroup, key=_get_sg), key=_get_sg): l_pregroup = list(pregroup) if comp.reduced_formula in ["O2", "H2", "Cl2", "F2", "N2", "I", "Br", "H2O"] and self.fuzzy_matching: # group by number of sites for n, sitegroup in groupby( sorted(l_pregroup, key=lambda s: s.num_sites), key=lambda s: s.num_sites ): l_sitegroup = list(sitegroup) row_list.append( self._populate_df_row(l_sitegroup, comp, sg, n, pd_type_1, pd_type_2, all_entries) ) else: for group in self.structure_matcher.group_structures(l_pregroup): grp = list(group) n = group[0].num_sites # StructureMatcher.group_structures returns a list of lists, # so each group should be a list containing matched structures row_list.append(self._populate_df_row(grp, comp, sg, n, pd_type_1, pd_type_2, all_entries)) mixing_state_data = pd.DataFrame(row_list, columns=columns) mixing_state_data.sort_values( ["formula", "energy_1", "spacegroup", "num_sites"], inplace=True, ignore_index=True ) return mixing_state_data
def __init__(self, entries, comp_dict=None, conc_dict=None, filter_solids=False, nproc=None): """ Args: entries ([PourbaixEntry] or [MultiEntry]): Entries list containing Solids and Ions or a list of MultiEntries comp_dict ({str: float}): Dictionary of compositions, defaults to equal parts of each elements conc_dict ({str: float}): Dictionary of ion concentrations, defaults to 1e-6 for each element filter_solids (bool): applying this filter to a pourbaix diagram ensures all included phases are filtered by stability on the compositional phase diagram. This breaks some of the functionality of the analysis, though, so use with caution. nproc (int): number of processes to generate multientries with in parallel. Defaults to None (serial processing) """ entries = deepcopy(entries) # Get non-OH elements self.pbx_elts = set( itertools.chain.from_iterable( [entry.composition.elements for entry in entries])) self.pbx_elts = list(self.pbx_elts - ELEMENTS_HO) self.dim = len(self.pbx_elts) - 1 # Process multientry inputs if isinstance(entries[0], MultiEntry): self._processed_entries = entries # Extract individual entries single_entries = list( set( itertools.chain.from_iterable( [e.entry_list for e in entries]))) self._unprocessed_entries = single_entries self._filtered_entries = single_entries self._conc_dict = None self._elt_comp = { k: v for k, v in entries[0].composition.items() if k not in ELEMENTS_HO } self._multielement = True # Process single entry inputs else: # Set default conc/comp dicts if not comp_dict: comp_dict = { elt.symbol: 1. / len(self.pbx_elts) for elt in self.pbx_elts } if not conc_dict: conc_dict = {elt.symbol: 1e-6 for elt in self.pbx_elts} self._conc_dict = conc_dict self._elt_comp = comp_dict self.pourbaix_elements = self.pbx_elts solid_entries = [ entry for entry in entries if entry.phase_type == "Solid" ] ion_entries = [ entry for entry in entries if entry.phase_type == "Ion" ] # If a conc_dict is specified, override individual entry concentrations for entry in ion_entries: ion_elts = list(set(entry.composition.elements) - ELEMENTS_HO) # TODO: the logic here for ion concentration setting is in two # places, in PourbaixEntry and here, should be consolidated if len(ion_elts) == 1: entry.concentration = conc_dict[ion_elts[0].symbol] \ * entry.normalization_factor elif len(ion_elts) > 1 and not entry.concentration: raise ValueError("Elemental concentration not compatible " "with multi-element ions") self._unprocessed_entries = solid_entries + ion_entries if not len(solid_entries + ion_entries) == len(entries): raise ValueError( "All supplied entries must have a phase type of " "either \"Solid\" or \"Ion\"") if filter_solids: # O is 2.46 b/c pbx entry finds energies referenced to H2O entries_HO = [ComputedEntry('H', 0), ComputedEntry('O', 2.46)] solid_pd = PhaseDiagram(solid_entries + entries_HO) solid_entries = list( set(solid_pd.stable_entries) - set(entries_HO)) self._filtered_entries = solid_entries + ion_entries if len(comp_dict) > 1: self._multielement = True self._processed_entries = self._preprocess_pourbaix_entries( self._filtered_entries, nproc=nproc) else: self._processed_entries = self._filtered_entries self._multielement = False self._stable_domains, self._stable_domain_vertices = \ self.get_pourbaix_domains(self._processed_entries)
def setUp(self): self.entries = EntrySet.from_csv(str(module_dir / "pdentries_test.csv")) self.pd = PhaseDiagram(self.entries) warnings.simplefilter("ignore")
app.config["suppress_callback_exceptions"] = True # tell Crystal Toolkit about the app ctc.register_app(app) # first, retrieve entries from Materials Project with MPRester() as mpr: # li_entries = mpr.get_entries_in_chemsys(["Li"]) # li_o_entries = mpr.get_entries_in_chemsys(["Li", "O"]) li_co_o_entries = mpr.get_entries_in_chemsys(["Li", "O", "Co"]) # li_co_o_fe_entries = mpr.get_entries_in_chemsys(["Li", "O", "Co", "Fe"]) # and then create the phase diagrams # li_phase_diagram = PhaseDiagram(li_entries) # li_o_phase_diagram = PhaseDiagram(li_o_entries) li_co_o_phase_diagram = PhaseDiagram(li_co_o_entries) # li_co_o_fe_phase_diagram = PhaseDiagram(li_co_o_fe_entries) # and the corresponding Crystal Toolkit components # we're creating four components here to demonstrate # visualizing 1-D, 2-D, 3-D and 4-D phase diagrams # li_phase_diagram_component = ctc.PhaseDiagramComponent(li_phase_diagram) # li_o_phase_diagram_component = ctc.PhaseDiagramComponent(li_o_phase_diagram) li_co_o_phase_diagram_component = ctc.PhaseDiagramComponent( li_co_o_phase_diagram) # li_co_o_fe_phase_diagram_component = ctc.PhaseDiagramComponent(li_co_o_fe_phase_diagram) print(li_co_o_entries) # example layout to demonstrate capabilities of component my_layout = html.Div([
from pymatgen.ext.matproj import MPRester from pymatgen.apps.borg.hive import VaspToComputedEntryDrone from pymatgen.apps.borg.queen import BorgQueen from pymatgen.entries.compatibility import MaterialsProjectCompatibility from pymatgen.analysis.phase_diagram import PhaseDiagram from pymatgen.analysis.phase_diagram import PDPlotter from pymatgen.entries.computed_entries import ComputedEntry entrada2 = [ComputedEntry("Li16Sn4S16", -150.73334384, -9.5)] # Obtain all existing Li-Fe-O phases using the Materials Project REST API with MPRester("Fvlb5EsNq71JxDy3") as m: mp_entries = m.get_entries_in_chemsys(["Li", "Sn", "S"]) # Process entries using the MaterialsProjectCompatibility compat = MaterialsProjectCompatibility() mp_entries = compat.process_entries(mp_entries) entrada2.extend(mp_entries) # Generate and plot Li-Fe-O phase diagram pd = PhaseDiagram(entrada2) plotter = PDPlotter(pd) plotter.show()
def setUp(self): self.entries = [ ComputedEntry(Composition("Li"), 0), ComputedEntry(Composition("Mn"), 0), ComputedEntry(Composition("O2"), 0), ComputedEntry(Composition("MnO2"), -10), ComputedEntry(Composition("Mn2O4"), -60), ComputedEntry(Composition("MnO3"), 20), ComputedEntry(Composition("Li2O"), -10), ComputedEntry(Composition("Li2O2"), -8), ComputedEntry(Composition("LiMnO2"), -30), ] self.pd = PhaseDiagram(self.entries) chempots = {"Li": -3} self.gpd = GrandPotentialPhaseDiagram(self.entries, chempots) self.ir = [] # ir[0] self.ir.append( InterfacialReactivity( Composition("O2"), Composition("Mn"), self.pd, norm=0, include_no_mixing_energy=0, pd_non_grand=None, use_hull_energy=False, ) ) # ir[1] self.ir.append( InterfacialReactivity( Composition("MnO2"), Composition("Mn"), self.gpd, norm=0, include_no_mixing_energy=1, pd_non_grand=self.pd, use_hull_energy=False, ) ) # ir[2] self.ir.append( InterfacialReactivity( Composition("Mn"), Composition("O2"), self.gpd, norm=1, include_no_mixing_energy=1, pd_non_grand=self.pd, use_hull_energy=False, ) ) # ir[3] self.ir.append( InterfacialReactivity( Composition("Li2O"), Composition("Mn"), self.gpd, norm=0, include_no_mixing_energy=1, pd_non_grand=self.pd, use_hull_energy=False, ) ) # ir[4] self.ir.append( InterfacialReactivity( Composition("Mn"), Composition("O2"), self.gpd, norm=1, include_no_mixing_energy=0, pd_non_grand=self.pd, use_hull_energy=False, ) ) # ir[5] self.ir.append( InterfacialReactivity( Composition("Mn"), Composition("Li2O"), self.gpd, norm=1, include_no_mixing_energy=1, pd_non_grand=self.pd, use_hull_energy=False, ) ) # ir[6] self.ir.append( InterfacialReactivity( Composition("Li2O2"), Composition("Li"), self.pd, norm=0, include_no_mixing_energy=0, pd_non_grand=None, use_hull_energy=True, ) ) # ir[7] self.ir.append( InterfacialReactivity( Composition("Li2O2"), Composition("Li"), self.pd, norm=0, include_no_mixing_energy=0, pd_non_grand=None, use_hull_energy=False, ) ) # ir[8] self.ir.append( InterfacialReactivity( Composition("Li2O2"), Composition("MnO2"), self.gpd, norm=0, include_no_mixing_energy=0, pd_non_grand=self.pd, use_hull_energy=True, ) ) # ir[9] self.ir.append( InterfacialReactivity( Composition("Li2O2"), Composition("MnO2"), self.gpd, norm=0, include_no_mixing_energy=0, pd_non_grand=self.pd, use_hull_energy=False, ) ) # ir[10] self.ir.append( InterfacialReactivity( Composition("O2"), Composition("Mn"), self.pd, norm=1, include_no_mixing_energy=0, pd_non_grand=None, use_hull_energy=False, ) ) # ir[11] self.ir.append( InterfacialReactivity( Composition("Li2O2"), Composition("Li2O2"), self.gpd, norm=1, include_no_mixing_energy=1, pd_non_grand=self.pd, use_hull_energy=False, ) ) # ir[12] self.ir.append( InterfacialReactivity( Composition("Li2O2"), Composition("Li2O2"), self.pd, norm=1, include_no_mixing_energy=0, pd_non_grand=None, use_hull_energy=False, ) ) with self.assertRaises(Exception) as context1: self.ir.append( InterfacialReactivity( Composition("Li2O2"), Composition("Li"), self.pd, norm=1, include_no_mixing_energy=1, pd_non_grand=None, ) ) self.assertTrue("Please provide grand phase diagram to compute no_mixing_energy!" == str(context1.exception)) with self.assertRaises(Exception) as context2: self.ir.append( InterfacialReactivity( Composition("O2"), Composition("Mn"), self.gpd, norm=0, include_no_mixing_energy=1, pd_non_grand=None, ) ) self.assertTrue( "Please provide non-grand phase diagram to compute no_mixing_energy!" == str(context2.exception) )
def find_intermediate_rxns(self, intermediates, targets, chempots=None): """ Identifies thermodynamically predicted reactions from intermediate to one or more targets using interfacial reaction method. This method has the unique benefit ( compared to the find_crossover_rxns method) of identifying reactions open to a specfic element or producing 3 or more products. Args: intermediates ([ComputedEntry]): List of intermediate entries targets ([ComputedEntry]): List of target entries chempots ({Element: float}): Dictionary of chemical potentials used to create grand potential phase diagram by which interfacial reactions are predicted. Returns: [ComputedReaction]: List of intermediate reactions """ all_rxns = set() combos = list(generate_all_combos(intermediates, 2)) for entries in tqdm(combos): n = len(entries) r1 = entries[0].composition.reduced_composition chemsys = { str(el) for entry in entries for el in entry.composition.elements } elem = None if chempots: elem = str(list(chempots.keys())[0]) chemsys.update(elem) if chemsys == {elem}: continue if n == 1: r2 = entries[0].composition.reduced_composition elif n == 2: r2 = entries[1].composition.reduced_composition else: raise ValueError( "Can't have an interface that is not 1 to 2 entries!") if chempots: elem_comp = Composition(elem).reduced_composition if r1 == elem_comp or r2 == elem_comp: continue entry_subset = self.entry_set.get_subset_in_chemsys(list(chemsys)) pd = PhaseDiagram(entry_subset) grand_pd = None if chempots: grand_pd = GrandPotentialPhaseDiagram(entry_subset, chempots) rxns = react_interface(r1, r2, pd, self.num_entries, grand_pd) rxns_filtered = { r for r in rxns if set(r._product_entries) & targets } if rxns_filtered: most_favorable_rxn = min( rxns_filtered, key=lambda x: (x.calculated_reaction_energy / sum( [x.get_el_amount(elem) for elem in x.elements])), ) all_rxns.add(most_favorable_rxn) return all_rxns
def __init__( self, entries, n=2, temp=300, interpolate_comps=None, extend_entries=None, include_metastable=False, include_polymorphs=False, filter_rxn_energies=0.5, ): """Initializes ReactionNetwork object with necessary preprocessing steps. This does not yet compute the graph. Args: entries ([ComputedStructureEntry]): list of ComputedStructureEntry- like objects to consider in network. These can be acquired from Materials Project (using MPRester) or created manually in pymatgen. Entries should have same compatability (e.g. MPCompability) for phase diagram generation. n (int): maximum number of phases allowed on each side of the reaction (default 2). Note that n > 2 leads to significant ( and often intractable) combinatorial explosion. temp (int): Temperature (in Kelvin) used for estimating Gibbs free energy of formation, as well as scaling the cost function later during network generation. Must select from [300, 400, 500, ... 2000] K. extend_entries ([ComputedStructureEntry]): list of ComputedStructureEntry-like objects which will be included in the network even after filtering for thermodynamic stability. Helpful if target phase has a significantly high energy above the hull. include_metastable (float or bool): either a) the specified cutoff for energy per atom (eV/atom) above hull, or b) True/False if considering only stable vs. all entries. An energy cutoff of 0.1 eV/atom is a reasonable starting threshold for thermodynamic stability. Defaults to False. include_polymorphs (bool): Whether or not to consider non-ground state polymorphs. Defaults to False. Note this is not useful unless structural metrics are considered in the cost function (to be added!) filter_rxn_energies (float): Energy filter. Reactions with energy_per_atom > filter will be excluded from network. """ self.logger = logging.getLogger("ReactionNetwork") self.logger.setLevel("INFO") # Chemical system / phase diagram variables self._all_entries = entries self._max_num_phases = n self._temp = temp self._e_above_hull = include_metastable self._include_polymorphs = include_polymorphs self._elements = { elem for entry in self.all_entries for elem in entry.composition.elements } self._pd_dict, self._filtered_entries = self._filter_entries( entries, include_metastable, temp, include_polymorphs) self._entry_mu_ranges = {} self._pd = None self._rxn_e_filter = filter_rxn_energies if (len(self._elements) <= 10 ): # phase diagrams take considerable time to build with 10+ elems self._pd = PhaseDiagram(self._filtered_entries) if interpolate_comps: interpolated_entries = [] for comp in interpolate_comps: energy = self._pd.get_hull_energy(Composition(comp)) interpolated_entries.append( PDEntry(comp, energy, attribute={"interpolated": True})) print("Interpolated entries:", "\n") print(interpolated_entries) self._filtered_entries.extend(interpolated_entries) if extend_entries: self._filtered_entries.extend(extend_entries) for idx, e in enumerate(self._filtered_entries): e.entry_idx = idx self.num_entries = len(self._filtered_entries) self._all_entry_combos = [ set(combo) for combo in generate_all_combos( self._filtered_entries, self._max_num_phases) ] self.entry_set = EntrySet(self._filtered_entries) self.entry_indices = { e: idx for idx, e in enumerate(self._filtered_entries) } # Graph variables used during graph creation self._precursors = None self._all_targets = None self._current_target = None self._cost_function = None self._complex_loopback = None self._precursors_entries = None self._most_negative_rxn = None # used in piecewise cost function self._g = None # Graph object in graph-tool filtered_entries_str = ", ".join([ entry.composition.reduced_formula for entry in self._filtered_entries ]) self.logger.info( f"Initializing network with {len(self._filtered_entries)} " f"entries: \n{filtered_entries_str}")
import json from pymatgen import MPRester from pymatgen.entries.compatibility import MaterialsProjectCompatibility from pymatgen.analysis.phase_diagram import PhaseDiagram, PDPlotter, PDEntry from pymatgen.core.periodic_table import Element from pymatgen.core.composition import Composition from pymatgen.io.vasp.outputs import Vasprun #if __name__ == "__main__": MAPI_KEY = 'DSR45TfHVuyuB1WvP1' # You must change this to your Materials API key! (or set MAPI_KEY env variable) system = ['Na', 'C', 'O'] # system we want to get PD for system_name = '-'.join(system) mpr = MPRester(MAPI_KEY) # object for connecting to MP Rest interface compat = MaterialsProjectCompatibility( ) # sets energy corrections and +U/pseudopotential choice unprocessed_entries = mpr.get_entries_in_chemsys(system) processed_entries = compat.process_entries( unprocessed_entries) # filter and add energy corrections # vasprun = Vasprun('vasprun.xml') # comp = vasprun.initial_structure.composition # energy = vasprun.final_energy # NBTentry = PDEntry(comp, energy) # processed_entries.append(NBTentry) pd = PhaseDiagram(processed_entries)
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 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 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 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 read_phase_diagram_and_chempots(self, full_sub_approach=False, include_mp_entries=True): """ Once phase diagram has been set up and run by user (in a folder called "PhaseDiagram"), this method parses and prints the chemical potentials based on the computed entries. The methodology is basically identical to that in the analyze_GGA_chempots method. Will supplement unfinished entries with MP database entries unless no_mp_entries is set to False Args: full_sub_approach: same attribute as described at length in the analyze_GGA_chempots method. Basically, the user can set this to True if they want to mix extrinsic species in the phase diagram include_mp_entries: if set to True, extra entries from Materials Project will be added to phase diagram according to phases that are stable in the Materials Project database """ pdfile = os.path.join(self.path_base, 'PhaseDiagram') if not os.path.exists(pdfile): print('Phase diagram file does not exist at ', pdfile) return # this is where we read computed entries into a list for parsing... # NOTE TO USER: If not running with VASP need to use another # pymatgen functionality for importing computed entries below... personal_entry_list = [] for structfile in os.listdir(pdfile): if os.path.exists(os.path.join(pdfile, structfile, 'vasprun.xml')): try: print('loading ', structfile) vr = Vasprun(os.path.join(pdfile, structfile, 'vasprun.xml'), parse_potcar_file=False) personal_entry_list.append(vr.get_computed_entry()) except: print('Could not load ', structfile) #add bulk computed entry to phase diagram, and see if it is stable if not self.bulk_ce: vr_path = os.path.join(self.path_base, 'bulk', 'vasprun.xml') if os.path.exists(vr_path): print('loading bulk computed entry') bulkvr = Vasprun(vr_path) self.bulk_ce = bulkvr.get_computed_entry() else: print ('No bulk entry given locally. Phase diagram ' + \ 'calculations cannot be set up without this') return self.bulk_composition = self.bulk_ce.composition self.redcomp = self.bulk_composition.reduced_composition # Supplement entries to phase diagram with those from MP database if include_mp_entries: mpcpa = MPChemPotAnalyzer(bulk_ce=self.bulk_ce, sub_species=self.sub_species, mapi_key=self.mapi_key) tempcl = mpcpa.analyze_GGA_chempots( full_sub_approach=full_sub_approach) # Use MPentries curr_pd = PhaseDiagram( list(set().union(mpcpa.entries['bulk_derived'], mpcpa.entries['subs_set']))) stable_idlist = { i.composition.reduced_composition: [i.energy_per_atom, i.entry_id, i] for i in curr_pd.stable_entries } for mpcomp, mplist in stable_idlist.items(): matched = False for pe in personal_entry_list: if (pe.composition.reduced_composition == mpcomp): # #USER: uncomment this if you want additional stable phases of identical composition included in your phase diagram # if personalentry.energy_per_atom > mplist[0]: # print('Adding entry from MP-database:',mpcomp,'(entry-id:',mplist[1]) # personal_entry_list.append(mplist[2]) matched = True if not matched: print('Adding entry from MP-database:', mpcomp, '(entry-id:', mplist[1]) personal_entry_list.append(mplist[2]) else: personal_entry_list.append(self.bulk_ce) #if you dont have entries for elemental corners of phase diagram then code breaks #manually inserting entries with energies of zero for competeness...USER DO NOT USE THIS eltcount = { elt: 0 for elt in set(self.bulk_ce.composition.elements) } for pentry in personal_entry_list: if pentry.is_element: eltcount[pentry.composition.elements[0]] += 1 for elt, eltnum in eltcount.items(): if not eltnum: s = Structure([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]], [elt], [[0, 0, 0]]) eltentry = ComputedStructureEntry(s, 0.) print( 'USER! Note that you have added a fake ' + str(elt) + ' structure to prevent from breaking the ' 'Phase Diagram Analyzer.\n As a result DO NOT trust the chemical potential results for regions ' 'of phase diagram that involve the element ' + str(elt)) personal_entry_list.append(eltentry) personal_entry_list.append(self.bulk_ce) #compute chemical potentials if full_sub_approach: pd = PhaseDiagram(personal_entry_list) chem_lims = self.get_chempots_from_pd(pd) else: #first seperate out the bulk associated elements from those of substitutional elements entry_list = [] sub_associated_entry_list = [] for localentry in personal_entry_list: bulk_associated = True for elt in localentry.composition.elements: if elt not in self.bulk_composition.elements: bulk_associated = False if bulk_associated: entry_list.append(localentry) else: sub_associated_entry_list(localentry) #now iterate through and collect chemical potentials pd = PhaseDiagram(entry_list) chem_lims = self.get_chempots_from_pd(pd) 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 consider adding 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 sub_associated_entry_list: if sub_el in entry.composition.elements: 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 one less than number of bulk species then 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][sub_el] = \ chem_lims[key][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()) bulk_species_symbol = [ s.symbol for s in self.bulk_composition.elements ] if cp_key_num != (len(bulk_species_symbol) + len(self.sub_species)): facets_to_delete.append(facet_name) print( "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 print( "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
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 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 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 plot_hull(self, df, new_result_ids, filename=None, finalize=False): """ Generate plots of convex hulls for each of the runs Args: df (DataFrame): dataframe with formation energies and formulas new_result_ids ([]): list of new result ids (i. e. indexes in the updated dataframe) filename (str): filename to output, if None, no file output is produced finalize (bool): flag indicating whether to include all new results Returns: (pyplot): plotter instance """ # Generate all entries total_comp = Composition(df['Composition'].sum()) if len(total_comp) > 4: warnings.warn( "Number of elements too high for phase diagram plotting") return None filtered = filter_dataframe_by_composition(df, total_comp) filtered = filtered[['delta_e', 'Composition']] filtered = filtered.dropna() # Create computed entry column with un-normalized energies filtered["entry"] = [ ComputedEntry( Composition(row["Composition"]), row["delta_e"] * Composition(row["Composition"]).num_atoms, entry_id=index, ) for index, row in filtered.iterrows() ] ids_prior_to_run = list(set(filtered.index) - set(new_result_ids)) if not ids_prior_to_run: warnings.warn( "No prior data, prior phase diagram cannot be constructed") return None # Create phase diagram based on everything prior to current run entries = filtered.loc[ids_prior_to_run]["entry"].dropna() # Filter for nans by checking if it's a computed entry pg_elements = sorted(total_comp.keys()) 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, backend='matplotlib', **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 filtered.index ] if finalize: # If finalize, we'll reset pd to all entries at this point to # measure stabilities wrt. the ultimate hull. pd = PhaseDiagram(filtered["entry"].values, elements=pg_elements) plotter = PDPlotter(pd, backend="matplotlib", **{ "markersize": 0, "linestyle": "-", "linewidth": 2 }) plot = plotter.get_plot(plt=plot) for entry in filtered["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()
def __init__(self, entries, comp_dict=None, conc_dict=None, filter_solids=False, nproc=None): entries = deepcopy(entries) # Get non-OH elements pbx_elts = set(itertools.chain.from_iterable( [entry.composition.elements for entry in entries])) pbx_elts = list(pbx_elts - elements_HO) # Process multientry inputs if isinstance(entries[0], MultiEntry): self._processed_entries = entries # Extract individual entries single_entries = list(set(itertools.chain.from_iterable( [e.entry_list for e in entries]))) self._unprocessed_entries = single_entries self._filtered_entries = single_entries self._conc_dict = None self._elt_comp = {k: v for k, v in entries[0].composition.items() if not k in elements_HO} self._multielement = True # Process single entry inputs else: # Set default conc/comp dicts if not comp_dict: comp_dict = {elt.symbol: 1. / len(pbx_elts) for elt in pbx_elts} if not conc_dict: conc_dict = {elt.symbol: 1e-6 for elt in pbx_elts} self._conc_dict = conc_dict self._elt_comp = comp_dict self.pourbaix_elements = pbx_elts solid_entries = [entry for entry in entries if entry.phase_type == "Solid"] ion_entries = [entry for entry in entries if entry.phase_type == "Ion"] # If a conc_dict is specified, override individual entry concentrations for entry in ion_entries: ion_elts = list(set(entry.composition.elements) - elements_HO) # TODO: the logic here for ion concentration setting is in two # places, in PourbaixEntry and here, should be consolidated if len(ion_elts) == 1: entry.concentration = conc_dict[ion_elts[0].symbol] \ * entry.normalization_factor elif len(ion_elts) > 1 and not entry.concentration: raise ValueError("Elemental concentration not compatible " "with multi-element ions") self._unprocessed_entries = solid_entries + ion_entries if not len(solid_entries + ion_entries) == len(entries): raise ValueError("All supplied entries must have a phase type of " "either \"Solid\" or \"Ion\"") if filter_solids: # O is 2.46 b/c pbx entry finds energies referenced to H2O entries_HO = [ComputedEntry('H', 0), ComputedEntry('O', 2.46)] solid_pd = PhaseDiagram(solid_entries + entries_HO) solid_entries = list(set(solid_pd.stable_entries) - set(entries_HO)) self._filtered_entries = solid_entries + ion_entries if len(comp_dict) > 1: self._multielement = True self._processed_entries = self._generate_multielement_entries( self._filtered_entries, nproc=nproc) else: self._processed_entries = self._filtered_entries self._multielement = False self._stable_domains, self._stable_domain_vertices = \ self.get_pourbaix_domains(self._processed_entries)
def test_from_pd(self): pd = PhaseDiagram(self.mp_entries) gibbs_entries = GibbsComputedStructureEntry.from_pd(pd) self.assertIsNotNone(gibbs_entries)
def get_pourbaix_entries(self, chemsys): """ A helper function to get all entries necessary to generate a pourbaix diagram from the rest interface. Args: chemsys ([str]): A list of elements comprising the chemical system, e.g. ['Li', 'Fe'] """ from pymatgen.analysis.pourbaix_diagram import PourbaixEntry, IonEntry from pymatgen.analysis.phase_diagram import PhaseDiagram from pymatgen.core.ion import Ion from pymatgen.entries.compatibility import\ MaterialsProjectAqueousCompatibility pbx_entries = [] # Get ion entries first, because certain ions have reference # solids that aren't necessarily in the chemsys (Na2SO4) url = '/pourbaix_diagram/reference_data/' + '-'.join(chemsys) ion_data = self._make_request(url) ion_ref_comps = [Composition(d['Reference Solid']) for d in ion_data] ion_ref_elts = list(itertools.chain.from_iterable( i.elements for i in ion_ref_comps)) ion_ref_entries = self.get_entries_in_chemsys( list(set([str(e) for e in ion_ref_elts] + ['O', 'H'])), property_data=['e_above_hull'], compatible_only=False) compat = MaterialsProjectAqueousCompatibility("Advanced") ion_ref_entries = compat.process_entries(ion_ref_entries) ion_ref_pd = PhaseDiagram(ion_ref_entries) # position the ion energies relative to most stable reference state for n, i_d in enumerate(ion_data): ion_entry = IonEntry(Ion.from_formula(i_d['Name']), i_d['Energy']) refs = [e for e in ion_ref_entries if e.composition.reduced_formula == i_d['Reference Solid']] if not refs: raise ValueError("Reference solid not contained in entry list") stable_ref = sorted(refs, key=lambda x: x.data['e_above_hull'])[0] rf = stable_ref.composition.get_reduced_composition_and_factor()[1] solid_diff = ion_ref_pd.get_form_energy(stable_ref)\ - i_d['Reference solid energy'] * rf elt = i_d['Major_Elements'][0] correction_factor = ion_entry.ion.composition[elt]\ / stable_ref.composition[elt] ion_entry.energy += solid_diff * correction_factor pbx_entries.append(PourbaixEntry(ion_entry, 'ion-{}'.format(n))) # import nose; nose.tools.set_trace() # Construct the solid pourbaix entries from filtered ion_ref entries extra_elts = set(ion_ref_elts) - {Element(s) for s in chemsys}\ - {Element('H'), Element('O')} for entry in ion_ref_entries: entry_elts = set(entry.composition.elements) # Ensure no OH chemsys or extraneous elements from ion references if not (entry_elts <= {Element('H'), Element('O')} or \ extra_elts.intersection(entry_elts)): # replace energy with formation energy, use dict to # avoid messing with the ion_ref_pd and to keep all old params form_e = ion_ref_pd.get_form_energy(entry) new_entry = deepcopy(entry) new_entry.uncorrected_energy = form_e new_entry.correction = 0.0 pbx_entry = PourbaixEntry(new_entry) if entry.entry_id == "mp-697146": pass # import nose; nose.tools.set_trace() # pbx_entry.reduced_entry() pbx_entries.append(pbx_entry) return pbx_entries
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 setUp(self): self.entries = [ComputedEntry(Composition('Li'), 0), ComputedEntry(Composition('Mn'), 0), ComputedEntry(Composition('O2'), 0), ComputedEntry(Composition('MnO2'), -10), ComputedEntry(Composition('Mn2O4'), -60), ComputedEntry(Composition('MnO3'), 20), ComputedEntry(Composition('Li2O'), -10), ComputedEntry(Composition('Li2O2'), -8), ComputedEntry(Composition('LiMnO2'), -30) ] self.pd = PhaseDiagram(self.entries) chempots = {'Li': -3} self.gpd = GrandPotentialPhaseDiagram(self.entries, chempots) self.ir = [] self.ir.append( InterfacialReactivity(Composition('O2'), Composition('Mn'), self.pd, norm=0, include_no_mixing_energy=0, pd_non_grand=None, use_hull_energy=False)) self.ir.append( InterfacialReactivity(Composition('MnO2'), Composition('Mn'), self.gpd, norm=0, include_no_mixing_energy=1, pd_non_grand=self.pd, use_hull_energy=False)) self.ir.append( InterfacialReactivity(Composition('Mn'), Composition('O2'), self.gpd, norm=1, include_no_mixing_energy=1, pd_non_grand=self.pd, use_hull_energy=False)) self.ir.append( InterfacialReactivity(Composition('Li2O'), Composition('Mn'), self.gpd, norm=0, include_no_mixing_energy=1, pd_non_grand=self.pd, use_hull_energy=False)) self.ir.append( InterfacialReactivity(Composition('Mn'), Composition('O2'), self.gpd, norm=1, include_no_mixing_energy=0, pd_non_grand=self.pd, use_hull_energy=False)) self.ir.append( InterfacialReactivity(Composition('Mn'), Composition('Li2O'), self.gpd, norm=1, include_no_mixing_energy=1, pd_non_grand=self.pd, use_hull_energy=False)) self.ir.append( InterfacialReactivity(Composition('Li2O2'), Composition('Li'), self.pd, norm=0, include_no_mixing_energy=0, pd_non_grand=None, use_hull_energy=True)) self.ir.append( InterfacialReactivity(Composition('Li2O2'), Composition('Li'), self.pd, norm=0, include_no_mixing_energy=0, pd_non_grand=None, use_hull_energy=False)) self.ir.append( InterfacialReactivity(Composition('Li2O2'), Composition('MnO2'), self.gpd, norm=0, include_no_mixing_energy=0, pd_non_grand=self.pd, use_hull_energy=True)) self.ir.append( InterfacialReactivity(Composition('Li2O2'), Composition('MnO2'), self.gpd, norm=0, include_no_mixing_energy=0, pd_non_grand=self.pd, use_hull_energy=False)) self.ir.append( InterfacialReactivity(Composition('O2'), Composition('Mn'), self.pd, norm=1, include_no_mixing_energy=0, pd_non_grand=None, use_hull_energy=False)) self.ir.append( InterfacialReactivity(Composition('Li2O2'), Composition('Li2O2'), self.gpd, norm=1, include_no_mixing_energy=1, pd_non_grand=self.pd, use_hull_energy=False)) self.ir.append( InterfacialReactivity(Composition('Li2O2'), Composition('Li2O2'), self.pd, norm=1, include_no_mixing_energy=0, pd_non_grand=None, use_hull_energy=False)) with self.assertRaises(Exception) as context1: self.ir.append( InterfacialReactivity(Composition('Li2O2'), Composition('Li'), self.pd, norm=1, include_no_mixing_energy=1, pd_non_grand=None)) self.assertTrue( 'Please provide grand phase diagram ' 'to compute no_mixing_energy!' == str(context1.exception)) with self.assertRaises(Exception) as context2: self.ir.append( InterfacialReactivity(Composition('O2'), Composition('Mn'), self.gpd, norm=0, include_no_mixing_energy=1, pd_non_grand=None)) self.assertTrue( 'Please provide non-grand phase diagram ' 'to compute no_mixing_energy!' == str(context2.exception))
############################################################################### print('') print('Calculating Phase Diagram...\n') for phase in computed_phases: # getting Composition Object comp = Composition(phase) # getting entry for PD Object entry = PDEntry(comp, computed_phases[phase]) # building list of entries entries.append(entry) # getting PD from list of entries pd = PhaseDiagram(entries) # get distance from convex hull for cubic phase comp = Composition('NaNbO3') energy = -38.26346361 entry = PDEntry(comp, energy) cubic_instability = pd.get_e_above_hull(entry) pd_dict = pd.as_dict() # Getting Plot plt = PDPlotter(pd, show_unstable=False) # you can also try show_unstable=True #plt_data = plt.pd_plot_data # getting plot for chem potential - variables 'fontsize' for labels size and 'plotsize' for fig size have been added (not present in original pymatgen) to get_chempot_range_map_plot function chem_pot_plot = plt.get_chempot_range_map_plot(
from pymatgen.analysis.phase_diagram import PhaseDiagram, PDPlotter, PDEntry from pymatgen.core.periodic_table import Element from pymatgen.core.composition import Composition from pymatgen.io.vasp.outputs import Vasprun import sys print("Usage: get_phase_diagram_from_MP.py 'Element1,Element2,Element3,...'") system = [el for el in sys.argv[1].split(',')] # system we want to get PD for MAPI_KEY = 'DSR45TfHVuyuB1WvP1' # You must change this to your Materials API key! (or set MAPI_KEY env variable) system_name = '-'.join(system) mpr = MPRester(MAPI_KEY) # object for connecting to MP Rest interface compat = MaterialsProjectCompatibility( ) # sets energy corrections and +U/pseudopotential choice unprocessed_entries = mpr.get_entries_in_chemsys(system, inc_structure=True) processed_entries = compat.process_entries( unprocessed_entries) # filter and add energy corrections pd = PhaseDiagram(processed_entries) pd_dict = pd.as_dict() filename = f'PD_{system_name}.json' with open(filename, 'w') as f: json.dump(pd_dict, f) print(f"PhaseDiagram object saved as dict in {filename}")
def __init__( self, entries: Union[List[PourbaixEntry], List[MultiEntry]], comp_dict: Optional[Dict[str, float]] = None, conc_dict: Optional[Dict[str, float]] = None, filter_solids: bool = True, nproc: Optional[int] = None, ): """ Args: entries ([PourbaixEntry] or [MultiEntry]): Entries list containing Solids and Ions or a list of MultiEntries comp_dict ({str: float}): Dictionary of compositions, defaults to equal parts of each elements conc_dict ({str: float}): Dictionary of ion concentrations, defaults to 1e-6 for each element filter_solids (bool): applying this filter to a Pourbaix diagram ensures all included solid phases are filtered by stability on the compositional phase diagram. Defaults to True. The practical consequence of this is that highly oxidized or reduced phases that might show up in experiments due to kinetic limitations on oxygen/hydrogen evolution won't appear in the diagram, but they are not actually "stable" (and are frequently overstabilized from DFT errors). Hence, including only the stable solid phases generally leads to the most accurate Pourbaix diagrams. nproc (int): number of processes to generate multientries with in parallel. Defaults to None (serial processing) """ entries = deepcopy(entries) self.filter_solids = filter_solids # Get non-OH elements self.pbx_elts = list( set( itertools.chain.from_iterable( [entry.composition.elements for entry in entries])) - ELEMENTS_HO) self.dim = len(self.pbx_elts) - 1 # Process multientry inputs if isinstance(entries[0], MultiEntry): self._processed_entries = entries # Extract individual entries single_entries = list( set( itertools.chain.from_iterable( [e.entry_list for e in entries]))) self._unprocessed_entries = single_entries self._filtered_entries = single_entries self._conc_dict = None self._elt_comp = { k: v for k, v in entries[0].composition.items() if k not in ELEMENTS_HO } self._multielement = True # Process single entry inputs else: # Set default conc/comp dicts if not comp_dict: comp_dict = { elt.symbol: 1.0 / len(self.pbx_elts) for elt in self.pbx_elts } if not conc_dict: conc_dict = {elt.symbol: 1e-6 for elt in self.pbx_elts} self._conc_dict = conc_dict self._elt_comp = comp_dict self.pourbaix_elements = self.pbx_elts solid_entries = [ entry for entry in entries if entry.phase_type == "Solid" ] ion_entries = [ entry for entry in entries if entry.phase_type == "Ion" ] # If a conc_dict is specified, override individual entry concentrations for entry in ion_entries: ion_elts = list(set(entry.composition.elements) - ELEMENTS_HO) # TODO: the logic here for ion concentration setting is in two # places, in PourbaixEntry and here, should be consolidated if len(ion_elts) == 1: entry.concentration = conc_dict[ ion_elts[0].symbol] * entry.normalization_factor elif len(ion_elts) > 1 and not entry.concentration: raise ValueError( "Elemental concentration not compatible with multi-element ions" ) self._unprocessed_entries = solid_entries + ion_entries if not len(solid_entries + ion_entries) == len(entries): raise ValueError( "All supplied entries must have a phase type of " 'either "Solid" or "Ion"') if self.filter_solids: # O is 2.46 b/c pbx entry finds energies referenced to H2O entries_HO = [ComputedEntry("H", 0), ComputedEntry("O", 2.46)] solid_pd = PhaseDiagram(solid_entries + entries_HO) solid_entries = list( set(solid_pd.stable_entries) - set(entries_HO)) self._filtered_entries = solid_entries + ion_entries if len(comp_dict) > 1: self._multielement = True self._processed_entries = self._preprocess_pourbaix_entries( self._filtered_entries, nproc=nproc) else: self._processed_entries = self._filtered_entries self._multielement = False self._stable_domains, self._stable_domain_vertices = self.get_pourbaix_domains( self._processed_entries)
def biased_hull(atomate_db, comp_list, anions=['N', 'O'], bias=[0]): with MPRester() as mpr: for pretty in comp_list: composition = Composition(pretty) composition = [str(i) for i in composition.elements] # anion_num = composition[2] # composition.pop() # composition.append(anions[0]) # composition.append(anions[1]) #First, build the phase diagram and hull orig_entries = mpr.get_entries_in_chemsys(composition) #orig_entries = mpr.get_entries_in_chemsys(chemsys_list[k]) entries = [] for i in range(len(bias)): entries.append(copy.deepcopy(orig_entries)) for j in range(0, len(entries[i])): temp = entries[i][j].parameters['potcar_symbols'] if temp in [['PBE ' + anions[0]], ['PBE ' + anions[1]], ['PBE ' + anions[0], 'PBE ' + anions[1]], ['PBE ' + anions[1], 'PBE ' + anions[0]]]: new_entry = ComputedEntry( entries[i][j].composition, entries[i][j].energy + bias[i]) #add arbitrary energy to gas phase entries[i][j] = copy.deepcopy(new_entry) #Then, find each entry in atomate_db which has this composition and get its hull energy print(pretty) structures = [] cursor = atomate_db.collection.find({ 'task_label': 'static', 'formula_pretty': pretty }) for structure in cursor: structures.append(structure) struct_entries = [] for structure in structures: temp = structure['calcs_reversed'][0] struct_entry = ComputedEntry( temp['composition_unit_cell'], temp['output']['energy'], parameters={ 'run_type': temp['run_type'], 'is_hubbard': structure['input']['is_hubbard'], 'pseudo_potential': structure['input']['pseudo_potential'], 'hubbards': structure['input']['hubbards'], 'potcar_symbols': structure['orig_inputs']['potcar']['symbols'], 'oxide_type': 'oxide' }, data={'oxide_type': 'oxide'}) for i in range(0, 4): struct_entry.parameters['potcar_symbols'][ i] = 'PBE ' + struct_entry.parameters[ 'potcar_symbols'][i] struct_entry = MaterialsProjectCompatibility().process_entries( [struct_entry ])[0] #takes list as argument and returns list struct_entries.append(struct_entry) bias_strings = [] stable_polymorph = {'id': 0, 'tilt_order': ''} for i in range(len(bias)): entries[i].extend(struct_entries) pd = PhaseDiagram(entries[i]) bias_string = 'ehull_' + str(bias[i]) + 'eV' bias_strings.append(bias_string) stable_polymorph[bias_strings[i]] = 1000 print(bias_strings) for j in range(0, len(struct_entries)): stability = pd.get_decomp_and_e_above_hull( struct_entries[j]) print(structures[j]['formula_pretty'], structures[j]['task_id'], [phase.composition for phase in stability[0]], stability[1]) if stability[1] < stable_polymorph[bias_strings[i]]: stable_polymorph['id'] = structures[j]['task_id'] stable_polymorph[bias_strings[i]] = stability[1] if 'tags' in structures[j]: if structures[j]['tags'][1] == 'tetra': stable_polymorph['tilt_order'] = structures[j][ 'tags'][2] else: stable_polymorph['tilt_order'] = structures[j][ 'tags'][1] output_dict[structures[j] ['formula_pretty']] = stable_polymorph return output_dict
def _solve_gs_preserve(self, A, f, mu, subsample_mapping, skip_gs=False): """ Code notes from Daniil Kitchaev ([email protected]) - 2018-09-10 This is a WORK IN PROGRESS based on Wenxuan's ground-state preservation fitting code. A, f, and mu as as in the other routines subsample mapping deals with the fact that weights change when fitting on a partial set (when figuring out mu) skin_gs gives the option of ignoring the constrained fitting part, which is helpful when figuring out mu In general, this code is really not production ready - the algorithm that serious numerical issues, and getting around them involved lots of fiddling with eigenvalue roundoffs, etc, as is commented below. There are also issues with the fact that constraints can be very difficult to satisfy, causing the solver to diverge (or just quit silently giving absurd results) - ths solution here appears to be to use MOSEK instead of cvxopt, and to iteratively remove constraints when they cause problems. Usually after cleaning up the data, everything can be fit though without removing constraints. At the end of the day, this algorithm seems to only be useful for niche applications because enforcing ground state preservation causes a giant bias in the fit and makes the error in E-above-hull highly correlated with the value of E-above-hull. The result is entropies are completely wrong, which is what you usually want out of a cluster expansion. So, use the code at your own risk. AFAIK, it works as described in Wenxuans paper, with various additions from me for numerical stability. It has not been extensively tested though or used in real projects due to the bias issue I described above. I think that unless the bias problem is resolved, this fitting scheme will not be of much practical use. """ if not subsample_mapping: assert A.shape[0] == self.feature_matrix.shape[0] subsample_mapping = {} for i in range(self.feature_matrix.shape[0]): subsample_mapping[i] = i from cvxopt import matrix from cvxopt import solvers from pymatgen.core.periodic_table import get_el_sp try: import mosek except: raise ValueError("GS preservation fitting is finicky and MOSEK solvers are typically required for numerical stability.") solvers.options['show_progress'] = False solvers.options['MOSEK'] = {mosek.dparam.check_convexity_rel_tol: 1e-6} ehull = list(self.e_above_hull_input) structure_index_at_hull = [i for (i,e) in enumerate(ehull) if e < 1e-5] reduce_composition_at_hull = [ self.structures[i].composition.element_composition.reduced_composition.element_composition for i in structure_index_at_hull] all_corr_in = np.array(self.feature_matrix) all_engr_in = np.array(self.normalized_energies) # Some structures can be degenerate in correlation space, even if they are distinct in reality. We can't # constrain their energies since as far as the CE is concerned, same correlation = same structure duplicated_correlation_set = [] for i in range(len(all_corr_in)): if i not in structure_index_at_hull: for j in structure_index_at_hull: if np.max(np.abs(all_corr_in[i] - all_corr_in[j])) < 1e-6: logging.info("Structure {} ({} - {}) has the same correlation as hull structure {} ({} {})".format(i, self.structures[i].composition.element_composition.reduced_formula, self.spacegroups[i], j, self.structures[j].composition.element_composition.reduced_formula, self.spacegroups[j])) duplicated_correlation_set.append(i) all_engr_in.shape = (len(all_engr_in), 1) f.shape = (f.shape[0], 1) # Adjust weights if subsample changed whats included and whats not weights_tmp = [] for i in range(A.shape[0]): weights_tmp.append(self.weights[subsample_mapping[i]]) subsample_mapping_inv = {} for i, j in subsample_mapping.items(): subsample_mapping_inv[j] = i for i in duplicated_correlation_set: if i in subsample_mapping_inv.keys(): weights_tmp[subsample_mapping_inv[i]] = 0 weight_vec = np.array(weights_tmp) weight_matrix = np.diag(weight_vec.transpose()) N_corr = A.shape[1] # Deal with roundoff error making P not positive semidefinite by using the SVD of A # At = USV* # At A = U S St Ut -> any negatives in S get squared # Unfortunately, this is usually not enough, so the next step is to explicitly add something small (1e-10) # to all eigenvalues so that eigenvalues close to zero are instead very slightly positive. # Otherwise, random numerical error makes the matrix not positive semidefinite, and the convex optimization # gets confused Aw = weight_matrix.dot(A) u, s, v = la.svd(Aw.transpose()) Ss = np.pad(np.diag(s), ((0, u.shape[0] - len(s)),(0,0)), mode='constant', constant_values=0) P_corr_part = 2 * u.dot((Ss.dot(Ss.transpose()))).dot(u.transpose()) P = np.lib.pad(P_corr_part, ((0, N_corr), (0, N_corr)), mode='constant', constant_values=0) P = 0.5 * (P + P.transpose()) ev, Q = la.eigh(P) Qi = la.inv(Q) P = Q.dot(np.diag(np.abs(ev)+1e-10)).dot(Qi) q_corr_part = -2 * ((weight_matrix.dot(A)).transpose()).dot(f) q_z_part = np.ones((N_corr, 1)) / mu q = np.concatenate((q_corr_part, q_z_part), axis=0) G_1 = np.concatenate((np.identity(N_corr), -np.identity(N_corr)), axis=1) G_2 = np.concatenate((-np.identity(N_corr), -np.identity(N_corr)), axis=1) G_3 = np.concatenate((G_1, G_2), axis=0) h_3 = np.zeros((2 * N_corr, 1)) # formulation is min 1/2 x'Px+ q'x s.t.: Gx<=h, Ax=b # P = 2 * A^T A # q = -2 * E^T A = q^T -> q = -2 * A^T E # See Wenxuan npjCompMat paper for derivation. All of the above mess is implementing this formula, plus dealing # with numerical issues with zero eigenvalues getting rounded off to something slightly negative init_vals = matrix(np.linalg.lstsq(self.feature_matrix, self.normalized_energies)[0]) input_entries = [] for s, e in zip(self.structures, self.energies): input_entries.append(PDEntry(s.composition.element_composition, e)) max_e = max(input_entries, key=lambda e: e.energy_per_atom).energy_per_atom + 1000 for el in self.ce.structure.composition.keys(): input_entries.append(PDEntry(Composition({el: 1}).element_composition, max_e)) pd_input = PhaseDiagram(input_entries) constraint_strings = [] # Uncomment to save various matrices for debugging purposes #np.save("A.npy", A) #np.save("f.npy", f) #np.save("w.npy", weight_vec) #np.save("P.npy", P) #np.save("q.npy", q) #np.save("G_noC.npy", G_3) #np.save("h_noC.npy", h_3) # The next part deals with adding constraints based on on-hull/off-hull compositions # Once again, there are numerical errors that arise when some structures are very close in correlation space # or in energy, such that the solver runs into either numerical issues or something else. The solution seems # to be to add constraints in batches, and try the increasingly constrained fit every once in a while. # When the fitting fails, roll back to find the problematic constraint and remove it. Usually there isnt more # than one or two bad constrains, and looking at them by hand is enough to figure out why they are causing # problems. BATCH_SIZE = int(np.sqrt(len(all_corr_in))) tot_constraints = 0 removed_constraints = 0 if not skip_gs: for i in range(len(all_corr_in)): if i not in structure_index_at_hull and i not in duplicated_correlation_set: reduced_comp = self.structures[i].composition.element_composition.reduced_composition.element_composition if reduced_comp in reduce_composition_at_hull: ## in hull composition hull_idx = reduce_composition_at_hull.index(reduced_comp) global_index = structure_index_at_hull[hull_idx] G_3_new_line = np.concatenate((all_corr_in[global_index] - all_corr_in[i], np.zeros((N_corr)))) G_3_new_line.shape = (1, 2 * N_corr) G_3 = np.concatenate((G_3, G_3_new_line), axis=0) small_error = np.array(-1e-3) # TODO: This tolerance is actually quite big, but it can be reduced as needed small_error.shape = (1, 1) h_3 = np.concatenate((h_3, small_error), axis=0) tot_constraints += 1 string = "{}|Added constraint from {}({} - {}) structure at hull comp".format(h_3.shape[0], reduced_comp, self.spacegroups[i], i) print(string) constraint_strings.append(string) else: # out of hull composition comp_now = self.structures[i].composition.element_composition.reduced_composition.element_composition decomposition_now = pd_input.get_decomposition(comp_now) new_vector = -1.0 * all_corr_in[i] for decompo_keys, decompo_values in decomposition_now.items(): reduced_decompo_keys = decompo_keys.composition.element_composition.reduced_composition.element_composition index_1 = reduce_composition_at_hull.index(reduced_decompo_keys) vertex_index_global = structure_index_at_hull[index_1] new_vector = new_vector + decompo_values * all_corr_in[vertex_index_global] G_3_new_line = np.concatenate((new_vector, np.zeros(N_corr))) G_3_new_line.shape = (1, 2 * N_corr) G_3 = np.concatenate((G_3, G_3_new_line), axis=0) small_error = np.array(-1e-3) small_error.shape = (1, 1) h_3 = np.concatenate((h_3, small_error), axis=0) tot_constraints += 1 string = "{}|Added constraint from {}({}) structure not at hull comp".format(h_3.shape[0], reduced_comp, i) print(string) constraint_strings.append(string) elif i in structure_index_at_hull: if self.structures[i].composition.element_composition.is_element: continue entries_new = [] for j in structure_index_at_hull: if not j == i: entries_new.append( PDEntry(self.structures[j].composition.element_composition, self.energies[j])) for el in self.ce.structure.composition.keys(): entries_new.append(PDEntry(Composition({el: 1}).element_composition, max(self.normalized_energies) + 1000)) pd_new = PhaseDiagram(entries_new) comp_now = self.structures[i].composition.element_composition.reduced_composition.element_composition decomposition_now = pd_new.get_decomposition(comp_now) new_vector = all_corr_in[i] abandon = False print("Constraining gs of {}({})".format(self.structures[i].composition, self.structures[i].composition)) for decompo_keys, decompo_values in decomposition_now.items(): reduced_decompo_keys = decompo_keys.composition.element_composition.reduced_composition.element_composition if not reduced_decompo_keys in reduce_composition_at_hull: abandon = True break index = reduce_composition_at_hull.index(reduced_decompo_keys) vertex_index_global = structure_index_at_hull[index] new_vector = new_vector - decompo_values * all_corr_in[vertex_index_global] if abandon: continue G_3_new_line = np.concatenate((new_vector, np.zeros(N_corr))) G_3_new_line.shape = (1, 2 * N_corr) G_3 = np.concatenate((G_3, G_3_new_line), axis=0) small_error = np.array(-1e-3) # TODO: Same tolerance as above small_error.shape = (1, 1) h_3 = np.concatenate((h_3, small_error), axis=0) tot_constraints += 1 string = "{}|Added constraint from {}({}) structure on hull, decomp".format(h_3.shape[0], comp_now, i) print(string) constraint_strings.append(string) if i % BATCH_SIZE == 0 or i == len(all_corr_in)-1: valid = False const_remove = 0 G_t = deepcopy(G_3) h_t = deepcopy(h_3) # Remove constraints until fit works while not valid: sol = solvers.qp(matrix(P), matrix(q), matrix(G_3), matrix(h_3), initvals=init_vals, solver='mosek') if sol['status'] == 'optimal': valid = True else: const_remove += 1 G_3 = G_t[:-1 * (const_remove),:] h_3 = h_t[:-1 * (const_remove)] removed_constraints += 1 if const_remove > 0: constraint_strings.append("{}|Removed".format(G_t.shape[0] - const_remove + 1)) # Add constraints back in one by one and remove if they cause problems for num_new in range(1, const_remove): G_new_line = G_t[-1 * (const_remove - num_new),:] h_new_line = h_t[-1 * (const_remove - num_new)] G_new_line.shape = (1, 2 * N_corr) h_new_line.shape = (1,1) G_3 = np.concatenate((G_3, G_new_line), axis=0) h_3 = np.concatenate((h_3, h_new_line), axis=0) sol = solvers.qp(matrix(P), matrix(q), matrix(G_3), matrix(h_3), initvals=init_vals, solver='mosek') removed_constraints -= 1 if sol['status'] != 'optimal': G_3 = G_3[:-1, :] h_3 = h_3[:-1] removed_constraints += 1 constraint_strings.append("{}|Removed".format(G_t.shape[0] - const_remove + num_new + 1)) # Uncomment for iterative saving matricex #np.save("G.npy", G_3) #np.save("h.npy", h_3) # Uncomment for debugging #np.save("G.npy", G_3) #np.save("h.npy", h_3) sol = solvers.qp(matrix(P), matrix(q), matrix(G_3), matrix(h_3), initvals=init_vals, solver='mosek') print("Final status: {}".format(sol['status'])) print("Mu: {}".format(mu)) print("Constrants: {}/{}".format(tot_constraints - removed_constraints, tot_constraints)) ecis = np.array(sol['x'])[:N_corr, 0] # Uncomment for some debugging info #print(ecis) #for string in constraint_strings: # print(string) return ecis
def get_phase_diagram_data(self): """ Returns grand potential phase diagram data to external plot Assumes openelement specific element equals None :return: Data to external plot """ open_elements_specific = None open_element_all = Element(self.open_element) mpr = MPRester("key") # import do dados dos arquivos tipo vasp drone = VaspToComputedEntryDrone() queen = BorgQueen(drone, rootpath=".") entries = queen.get_data() # Get data to make phase diagram mp_entries = mpr.get_entries_in_chemsys(self.system, compatible_only=True) entries.extend(mp_entries) compat = MaterialsProjectCompatibility() entries = compat.process_entries(entries) #explanation_output = open("explain.txt",'w') entries_output = open("entries.txt", 'w') compat.explain(entries[0]) print(entries, file=entries_output) #print(entries) if open_elements_specific: gcpd = GrandPotentialPhaseDiagram(entries, open_elements_specific) self.plot_phase_diagram(gcpd, False) self.analyze_phase_diagram(gcpd) if open_element_all: pd = PhaseDiagram(entries) chempots = pd.get_transition_chempots(open_element_all) #print(chempots) #all_gcpds = list() toplot = [] # dic = {} for idx in range(len(chempots)): if idx == len(chempots) - 1: avgchempot = chempots[idx] - 0.1 else: avgchempot = 0.5 * (chempots[idx] + chempots[idx + 1]) gcpd = GrandPotentialPhaseDiagram( entries, {open_element_all: avgchempot}, pd.elements) # toplot.append(self.get_grand_potential_phase_diagram(gcpd)) min_chempot = None if idx == len(chempots) - 1 else chempots[ idx + 1] max_chempot = chempots[idx] #gcpd = GrandPotentialPhaseDiagram(entries, {open_element_all: max_chempot}, pd.elements) toplot.append(self.get_grand_potential_phase_diagram(gcpd)) #toplot.append(max_chempot) #self.plot_phase_diagram(gcpd, False) #print({open_element_all: max_chempot}) # Data to plot phase diagram return toplot
def __init__(self, material_id, vasprun_dict, ref_element, exclude_ids=[], custom_entries=[], mapi_key=None): """ Analyzes surface energies and Wulff shape of a particular material using the chemical potential. Args: material_id (str): Materials Project material_id (a string, e.g., mp-1234). vasprun_dict (dict): Dictionary containing a list of Vaspruns for slab calculations as items and the corresponding Miller index of the slab as the key. eg. vasprun_dict = {(1,1,1): [vasprun_111_1, vasprun_111_2, vasprun_111_3], (1,1,0): [vasprun_111_1, vasprun_111_2], ...} element: element to be considered as independent variables. E.g., if you want to show the stability ranges of all Li-Co-O phases wrt to uLi exclude_ids (list of material_ids): List of material_ids to exclude when obtaining the decomposition components to calculate the chemical potential custom_entries (list of pymatgen-db type entries): List of user specified pymatgen-db type entries to use in finding decomposition components for the chemical potential mapi_key (str): Materials Project API key for accessing the MP database via MPRester """ self.ref_element = ref_element self.mprester = MPRester(mapi_key) if mapi_key else MPRester() self.ucell_entry = \ self.mprester.get_entry_by_material_id(material_id, inc_structure=True, property_data= ["formation_energy_per_atom"]) ucell = self.ucell_entry.structure # Get x and y, the number of species in a formula unit of the bulk reduced_comp = ucell.composition.reduced_composition.as_dict() if len(reduced_comp.keys()) == 1: x = y = reduced_comp[ucell[0].species_string] else: for el in reduced_comp.keys(): if self.ref_element == el: y = reduced_comp[el] else: x = reduced_comp[el] # Calculate Gibbs free energy of the bulk per unit formula gbulk = self.ucell_entry.energy /\ (len([site for site in ucell if site.species_string == self.ref_element]) / y) entries = [entry for entry in self.mprester.get_entries_in_chemsys(list(reduced_comp.keys()), property_data=["e_above_hull", "material_id"]) if entry.data["e_above_hull"] == 0 and entry.data["material_id"] not in exclude_ids] \ if not custom_entries else custom_entries pd = PhaseDiagram(entries) chempot_ranges = pd.get_chempot_range_map([Element(self.ref_element)]) # If no chemical potential is found, we return u=0, eg. # for a elemental system, the relative u of Cu for Cu is 0 chempot_range = [chempot_ranges[entry] for entry in chempot_ranges.keys() if entry.composition == self.ucell_entry.composition][0][0]._coords if \ chempot_ranges else [[0,0], [0,0]] e_of_element = [entry.energy_per_atom for entry in entries if str(entry.composition.reduced_composition) == self.ref_element + "1"][0] self.x = x self.y = y self.gbulk = gbulk chempot_range = list(chempot_range) self.chempot_range = sorted([chempot_range[0][0], chempot_range[1][0]]) self.e_of_element = e_of_element self.vasprun_dict = vasprun_dict