class CompoundPhaseDiagramTest(unittest.TestCase): def setUp(self): self.entries = EntrySet.from_csv(str(module_dir / "pdentries_test.csv")) self.pd = CompoundPhaseDiagram( self.entries, [Composition("Li2O"), Composition("Fe2O3")]) def test_stable_entries(self): stable_formulas = [ent.name for ent in self.pd.stable_entries] expected_stable = ["Fe2O3", "Li5FeO4", "LiFeO2", "Li2O"] for formula in expected_stable: self.assertTrue(formula in stable_formulas) def test_get_formation_energy(self): stable_formation_energies = { ent.name: self.pd.get_form_energy(ent) for ent in self.pd.stable_entries } expected_formation_energies = { "Li5FeO4": -7.0773284399999739, "Fe2O3": 0, "LiFeO2": -0.47455929750000081, "Li2O": 0, } for formula, energy in expected_formation_energies.items(): self.assertAlmostEqual(energy, stable_formation_energies[formula], 7) def test_str(self): self.assertIsNotNone(str(self.pd))
def get_convex_hull_area(self, composition_space): """ Prints out the area or volume of the convex hull defined by the organisms in the promotion set. """ # make a phase diagram of just the organisms in the promotion set # (on the lower convex hull) pdentries = [] for organism in self.promotion_set: pdentries.append( PDEntry(organism.composition, organism.total_energy)) compound_pd = CompoundPhaseDiagram(pdentries, composition_space.endpoints) # get the data for the convex hull qhull_data = compound_pd.qhull_data # for some reason, the last point is positive, so remove it hull_data = np.delete(qhull_data, -1, 0) # make a ConvexHull object from the hull data try: convex_hull = ConvexHull(hull_data) except: return None if len(composition_space.endpoints) == 2: return convex_hull.area else: return convex_hull.volume
def compute_pd_values(self, organisms_list, composition_space): """ Constructs a convex hull from the provided organisms and sets the organisms' values to their distances from the convex hull. Returns the CompoundPhaseDiagram object computed from the organisms in organisms_list. Args: organisms_list: a list of Organisms whose values we need to compute composition_space: the CompositionSpace of the search """ # create a PDEntry object for each organism in the list of organisms pdentries = {} for organism in organisms_list: pdentries[organism.id] = PDEntry(organism.composition, organism.total_energy) # put the pdentries in a list pdentries_list = [] for organism_id in pdentries: pdentries_list.append(pdentries[organism_id]) # create a compound phase diagram object from the list of pdentries compound_pd = CompoundPhaseDiagram(pdentries_list, composition_space.endpoints) # transform the pdentries and put them in a dictionary, with the # organism id's as the keys transformed_pdentries = {} for org_id in pdentries: transformed_pdentries[org_id] = compound_pd.transform_entries( [pdentries[org_id]], composition_space.endpoints)[0][0] # put the values in a dictionary, with the organism id's as the keys values = {} for org_id in pdentries: values[org_id] = compound_pd.get_e_above_hull( transformed_pdentries[org_id]) # assign values to the organisms for organism in organisms_list: organism.value = values[organism.id] return compound_pd
def is_in_composition_space_pd(self, organism, composition_space, constraints, pool): """ Returns a boolean indicating whether the organism is in the composition space. Whether composition space endpoints are allowed is determined by the value of constraints.allow_endpoints. Args: organism: the Organism to check composition_space: the CompositionSpace of the search constraints: the Constraints of the search pool: the Pool """ # cast the endpoints to PDEntries (just make up some energies) pdentries = [] for endpoint in composition_space.endpoints: pdentries.append(PDEntry(endpoint, -10)) pdentries.append(PDEntry(organism.composition, -10)) # make a CompoundPhaseDiagram and use it to check if the organism # is in the composition space from how many entries it returns composition_checker = CompoundPhaseDiagram(pdentries, composition_space.endpoints) if len( composition_checker.transform_entries( pdentries, composition_space.endpoints)[0]) == len( composition_space.endpoints): print('Organism {} lies outside the composition space '.format( organism.id)) return False # check the composition space endpoints if specified if not constraints.allow_endpoints and len(pool.to_list()) > 0: for endpoint in composition_space.endpoints: if endpoint.almost_equals( organism.composition.reduced_composition): print('Organism {} is at a composition space ' 'endpoint '.format(organism.id)) return False return True
def get_phase_diagram_plot(self): """ Returns a phase diagram plot, as a matplotlib plot object. """ # set the font to Times, rendered with Latex plt.rc('font', **{'family': 'serif', 'serif': ['Times']}) plt.rc('text', usetex=True) # parse the composition space endpoints endpoints_line = self.lines[0].split() endpoints = [] for word in endpoints_line[::-1]: if word == 'endpoints:': break else: endpoints.append(Composition(word)) if len(endpoints) < 2: print('There must be at least 2 endpoint compositions to make a ' 'phase diagram.') quit() # parse the compositions and total energies of all the structures compositions = [] total_energies = [] for i in range(4, len(self.lines)): line = self.lines[i].split() compositions.append(Composition(line[1])) total_energies.append(float(line[2])) # make a list of PDEntries pdentries = [] for i in range(len(compositions)): pdentries.append(PDEntry(compositions[i], total_energies[i])) # make a CompoundPhaseDiagram compound_pd = CompoundPhaseDiagram(pdentries, endpoints) # make a PhaseDiagramPlotter pd_plotter = PDPlotter(compound_pd, show_unstable=50) return pd_plotter.get_plot(label_unstable=False)
def get_convex_hull_area(self, composition_space): """ Returns the area/volume of the current convex hull. Args: composition_space: the CompositionSpace of the search """ # check if the initial population contains organisms at all the # endpoints of the composition space if self.has_endpoints(composition_space) and \ self.has_non_endpoint(composition_space): # compute and print the area or volume of the convex hull pdentries = [] for organism in self.initial_population: pdentries.append( PDEntry(organism.composition, organism.total_energy)) compound_pd = CompoundPhaseDiagram(pdentries, composition_space.endpoints) # get the data for the convex hull qhull_data = compound_pd.qhull_data # for some reason, the last point is positive, so remove it hull_data = np.delete(qhull_data, -1, 0) # make a ConvexHull object from the hull data # Sometime this fails, saying that only two points were given to # construct the convex hull, even though the if statement above # checks that there are enough points. try: convex_hull = ConvexHull(hull_data) except: return None if len(composition_space.endpoints) == 2: return convex_hull.area else: return convex_hull.volume
def setUp(self): self.entries = EntrySet.from_csv(str(module_dir / "pdentries_test.csv")) self.pd = CompoundPhaseDiagram( self.entries, [Composition("Li2O"), Composition("Fe2O3")])
def compute_composition_vector(self, composition_space): """ Returns the composition vector of the organism, as a numpy array. Args: composition_space: the CompositionSpace of the search. """ if composition_space.objective_function == 'epa': return None elif composition_space.objective_function == 'pd': # make CompoundPhaseDiagram and PDAnalyzer objects pdentries = [] for endpoint in composition_space.endpoints: pdentries.append(PDEntry(endpoint, -10)) compound_pd = CompoundPhaseDiagram(pdentries, composition_space.endpoints) # transform the organism's composition transformed_entry = compound_pd.transform_entries( [PDEntry(self.composition, 10)], composition_space.endpoints) # get the transformed species and amounts if len(transformed_entry[0]) == 0: return None transformed_list = str(transformed_entry[0][0]).split() del transformed_list[0] popped = '' while popped != 'with': popped = transformed_list.pop() # separate the dummy species symbols from the amounts symbols = [] amounts = [] for entry in transformed_list: split_entry = entry.split('0+') symbols.append(split_entry[0]) amounts.append(float(split_entry[1])) # make a dictionary mapping dummy species to amounts dummy_species_amounts = {} for i in range(len(symbols)): dummy_species_amounts[DummySpecie(symbol=symbols[i])] = \ amounts[i] # make Composition object with dummy species, get decomposition dummy_comp = Composition(dummy_species_amounts) decomp = compound_pd.get_decomposition(dummy_comp) # get amounts of the decomposition in terms of the (untransformed) # composition space endpoints formatted_decomp = {} for key in decomp: key_dict = key.as_dict() comp = Composition(key_dict['entry']['composition']) formatted_decomp[comp] = decomp[key] # make the composition vector composition_vector = [] # because the random organism creator shuffles the endpoints composition_space.endpoints.sort() for endpoint in composition_space.endpoints: if endpoint in formatted_decomp: composition_vector.append(formatted_decomp[endpoint]) else: composition_vector.append(0.0) return np.array(composition_vector)
def scale_volume_pd(self, organism, composition_space, pool): """ Returns a boolean indicating whether volume scaling did not fail. Args: organism: the Organism whose volume to scale composition_space: the CompositionSpace of the search pool: the Pool Description: 1. Computes the decomposition of the organism - that is, which structures on the convex hull (and their relative amounts) that the organism would decompose to, based on its composition. 2. Computes the weighted average volume per atom of the structures in the decomposition. 3. Scales the volume per atom of the organism to the computed value. """ # make CompoundPhaseDiagram object pdentries = [] for org in pool.promotion_set: pdentries.append(PDEntry(org.composition, org.total_energy)) compound_pd = CompoundPhaseDiagram(pdentries, composition_space.endpoints) # transform the organism's composition transformed_entry = compound_pd.transform_entries( [PDEntry(organism.composition, 10)], composition_space.endpoints) # get the transformed species and amounts transformed_list = str(transformed_entry[0][0]).split() del transformed_list[0] popped = '' while popped != 'with': popped = transformed_list.pop() # separate the dummy species symbols from the amounts symbols = [] amounts = [] for entry in transformed_list: split_entry = entry.split('0+') symbols.append(split_entry[0]) amounts.append(float(split_entry[1])) # make a dictionary mapping dummy species to amounts dummy_species_amounts = {} for i in range(len(symbols)): dummy_species_amounts[DummySpecie(symbol=symbols[i])] = amounts[i] # make Composition object with dummy species, get decomposition dummy_comp = Composition(dummy_species_amounts) decomp = compound_pd.get_decomposition(dummy_comp) # get original compositions and amounts from the decomposition fractions = [] comps = [] for item in decomp: fractions.append(decomp[item]) first_split = str(item).split(',') second_split = first_split[0].split() while second_split[0] != 'composition': del second_split[0] del second_split[0] # build the composition string comp_string = '' for symbol in second_split: comp_string += str(symbol) comps.append(Composition(comp_string)) # get weighted average volume per atom of the organisms in the # decomposition vpa_mean = 0 for i in range(len(comps)): for org in pool.promotion_set: if (comps[i].reduced_composition).almost_equals( org.composition.reduced_composition): vpa_mean += (org.cell.volume / len(org.cell.sites)) * fractions[i] # compute the new volume and scale to it num_atoms = len(organism.cell.sites) new_vol = vpa_mean * num_atoms # this is to suppress the warnings produced if the # scale_lattice method fails with warnings.catch_warnings(): warnings.simplefilter('ignore') organism.cell.scale_lattice(new_vol) if str(organism.cell.lattice.a) == 'nan' or \ organism.cell.lattice.a > 100: print('Volume scaling failed on organism {} during ' 'development '.format(organism.id)) return False return True