class SimplexTest(PymatgenTest): def setUp(self): coords = [] coords.append([0, 0, 0]) coords.append([0, 1, 0]) coords.append([0, 0, 1]) coords.append([1, 0, 0]) self.simplex = Simplex(coords) def test_in_simplex(self): self.assertTrue(self.simplex.in_simplex([0.1, 0.1, 0.1])) self.assertFalse(self.simplex.in_simplex([0.6, 0.6, 0.6])) for i in range(10): coord = np.random.random_sample(size=3) / 3 self.assertTrue(self.simplex.in_simplex(coord)) def test_2dtriangle(self): s = Simplex([[0, 1], [1, 1], [1, 0]]) self.assertArrayAlmostEqual(s.bary_coords([0.5, 0.5]), [0.5, 0, 0.5]) self.assertArrayAlmostEqual(s.bary_coords([0.5, 1]), [0.5, 0.5, 0]) self.assertArrayAlmostEqual(s.bary_coords([0.5, 0.75]), [0.5, 0.25, 0.25]) self.assertArrayAlmostEqual(s.bary_coords([0.75, 0.75]), [0.25, 0.5, 0.25]) s = Simplex([[1, 1], [1, 0]]) self.assertRaises(ValueError, s.bary_coords, [0.5, 0.5]) def test_volume(self): # Should be value of a right tetrahedron. self.assertAlmostEqual(self.simplex.volume, 1/6)
def setUp(self): coords = [] coords.append([0, 0, 0]) coords.append([0, 1, 0]) coords.append([0, 0, 1]) coords.append([1, 0, 0]) self.simplex = Simplex(coords)
def test_2dtriangle(self): s = Simplex([[0, 1], [1, 1], [1, 0]]) self.assertArrayAlmostEqual(s.bary_coords([0.5, 0.5]), [0.5, 0, 0.5]) self.assertArrayAlmostEqual(s.bary_coords([0.5, 1]), [0.5, 0.5, 0]) self.assertArrayAlmostEqual(s.bary_coords([0.5, 0.75]), [0.5, 0.25, 0.25]) self.assertArrayAlmostEqual(s.bary_coords([0.75, 0.75]), [0.25, 0.5, 0.25]) s = Simplex([[1, 1], [1, 0]]) self.assertRaises(ValueError, s.bary_coords, [0.5, 0.5])
def _in_facet(self, facet, entry): """ Checks if a Pourbaix Entry is in a facet. Args: facet: facet to test. entry: Pourbaix Entry to test. """ dim = len(self._keys) if dim > 1: coords = [np.array(self._pd.qhull_data[facet[i]][0:dim - 1]) for i in range(len(facet))] simplex = Simplex(coords) comp_point = [entry.npH, entry.nPhi] return simplex.in_simplex(comp_point, PourbaixAnalyzer.numerical_tol) else: return True
def get_chempot_range_map(self, elements, referenced=True, joggle=True, force_use_pyhull=False): """ Returns a chemical potential range map for each stable entry. Args: elements: Sequence of elements 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 and uO, you will supply [Element("Li"), Element("O")] referenced: If True, gives the results with a reference being the energy of the elemental phase. If False, gives absolute values. joggle (boolean): Whether to joggle the input to avoid precision errors. force_use_pyhull (boolean): Whether the pyhull algorithm is always used, even when scipy is present. Returns: Returns a dict of the form {entry: [simplices]}. The list of simplices are the sides of the N-1 dim polytope bounding the allowable chemical potential range of each entry. """ all_chempots = [] pd = self._pd facets = pd.facets for facet in facets: chempots = self.get_facet_chempots(facet) all_chempots.append([chempots[el] for el in pd.elements]) inds = [pd.elements.index(el) for el in elements] el_energies = {el: 0.0 for el in elements} if referenced: el_energies = {el: pd.el_refs[el].energy_per_atom for el in elements} chempot_ranges = collections.defaultdict(list) vertices = [list(range(len(self._pd.elements)))] if len(all_chempots) > len(self._pd.elements): vertices = get_facets(all_chempots, joggle=joggle, force_use_pyhull=force_use_pyhull) for ufacet in vertices: for combi in itertools.combinations(ufacet, 2): data1 = facets[combi[0]] data2 = facets[combi[1]] common_ent_ind = set(data1).intersection(set(data2)) if len(common_ent_ind) == len(elements): common_entries = [pd.qhull_entries[i] for i in common_ent_ind] data = np.array([[all_chempots[i][j] - el_energies[pd.elements[j]] for j in inds] for i in combi]) sim = Simplex(data) for entry in common_entries: chempot_ranges[entry].append(sim) return chempot_ranges
def _in_facet(self, facet, entry): """ Checks if a Pourbaix Entry is in a facet. Args: facet: facet to test. entry: Pourbaix Entry to test. """ dim = len(self._keys) if dim > 1: coords = [ np.array(self._pd.qhull_data[facet[i]][0:dim - 1]) for i in range(len(facet)) ] simplex = Simplex(coords) comp_point = [entry.npH, entry.nPhi] return simplex.in_simplex(comp_point, PourbaixAnalyzer.numerical_tol) else: return True
def _get_facet(self, comp): """ Get any facet that a composition falls into. Cached so successive calls at same composition are fast. """ if set(comp.elements).difference(self._pd.elements): raise ValueError('{} has elements not in the phase diagram {}' ''.format(comp, self._pd.elements)) c = [comp.get_atomic_fraction(e) for e in self._pd.elements[1:]] for f, s in zip(self._pd.facets, self._pd.simplices): if Simplex(s).in_simplex(c, PDAnalyzer.numerical_tol / 10): return f raise RuntimeError("No facet found for comp = {}".format(comp))
def __init__(self, entries, elements=None): """ Standard constructor for phase diagram. Args: entries ([PDEntry]): A list of PDEntry-like objects having an energy, energy_per_atom and composition. elements ([Element]): Optional list of elements in the phase diagram. If set to None, the elements are determined from the the entries themselves. """ if elements is None: elements = set() for entry in entries: elements.update(entry.composition.elements) elements = list(elements) dim = len(elements) get_reduced_comp = lambda e: e.composition.reduced_composition entries = sorted(entries, key=get_reduced_comp) el_refs = {} min_entries = [] all_entries = [] for c, g in itertools.groupby(entries, key=get_reduced_comp): g = list(g) min_entry = min(g, key=lambda e: e.energy_per_atom) if c.is_element: el_refs[c.elements[0]] = min_entry min_entries.append(min_entry) all_entries.extend(g) if len(el_refs) != dim: raise PhaseDiagramError( "There are no entries associated with a terminal element!.") data = np.array( [[e.composition.get_atomic_fraction(el) for el in elements] + [e.energy_per_atom] for e in min_entries]) # Use only entries with negative formation energy vec = [el_refs[el].energy_per_atom for el in elements] + [-1] form_e = -np.dot(data, vec) inds = np.where(form_e < -self.formation_energy_tol)[0].tolist() # Add the elemental references inds.extend([min_entries.index(el) for el in el_refs.values()]) qhull_entries = [min_entries[i] for i in inds] qhull_data = data[inds][:, 1:] # Add an extra point to enforce full dimensionality. # This point will be present in all upper hull facets. extra_point = np.zeros(dim) + 1 / dim extra_point[-1] = np.max(qhull_data) + 1 qhull_data = np.concatenate([qhull_data, [extra_point]], axis=0) if dim == 1: self.facets = [qhull_data.argmin(axis=0)] else: facets = get_facets(qhull_data) finalfacets = [] for facet in facets: # Skip facets that include the extra point if max(facet) == len(qhull_data) - 1: continue m = qhull_data[facet] m[:, -1] = 1 if abs(np.linalg.det(m)) > 1e-14: finalfacets.append(facet) self.facets = finalfacets self.simplexes = [Simplex(qhull_data[f, :-1]) for f in self.facets] self.all_entries = all_entries self.qhull_data = qhull_data self.dim = dim self.el_refs = el_refs self.elements = elements self.qhull_entries = qhull_entries self._stable_entries = set(self.qhull_entries[i] for i in set(itertools.chain(*self.facets)))