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)
class SimplexTest(unittest.TestCase): 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]]) np.testing.assert_almost_equal(s.bary_coords([0.5, 0.5]), [0.5, 0, 0.5]) np.testing.assert_almost_equal(s.bary_coords([0.5, 1]), [0.5, 0.5, 0]) np.testing.assert_almost_equal(s.bary_coords([0.5, 0.75]), [0.5, 0.25, 0.25]) np.testing.assert_almost_equal(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)
class SimplexTest(unittest.TestCase): 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]]) np.testing.assert_almost_equal(s.bary_coords([0.5, 0.5]), [0.5, 0, 0.5]) np.testing.assert_almost_equal(s.bary_coords([0.5, 1]), [0.5, 0.5, 0]) np.testing.assert_almost_equal(s.bary_coords([0.5, 0.75]), [0.5, 0.25, 0.25]) np.testing.assert_almost_equal(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)
class SimplexTest(unittest.TestCase): 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 xrange(10): coord = np.random.random_sample(size=(3)) / 3 self.assertTrue(self.simplex.in_simplex(coord))
def _in_facet(self, facet, comp): """ Checks if a composition is in a facet. Args: facet: facet to test. comp: Composition to test. """ els = self._pd.elements dim = len(els) if dim > 1: coords = self._pd.qhull_data[facet, :-1] simplex = Simplex(coords) comp_point = [comp.get_atomic_fraction(e) for e in els[1:]] return simplex.in_simplex(comp_point, PDAnalyzer.numerical_tol) else: return True
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 _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 xrange(len(facet))] simplex = Simplex(coords) comp_point = [entry.npH, entry.nPhi] return simplex.in_simplex(comp_point, PourbaixAnalyzer.numerical_tol) else: return True
def test_2dtriangle(self): s = Simplex([[0, 1], [1, 1], [1, 0]]) np.testing.assert_almost_equal(s.bary_coords([0.5, 0.5]), [0.5, 0, 0.5]) np.testing.assert_almost_equal(s.bary_coords([0.5, 1]), [0.5, 0.5, 0]) np.testing.assert_almost_equal(s.bary_coords([0.5, 0.75]), [0.5, 0.25, 0.25]) np.testing.assert_almost_equal(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 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, comp): """ Checks if a composition is in a facet. Args: facet: facet to test. comp: Composition to test. """ dim = len(self._pd.elements) if dim > 1: coords = [np.array(self._pd.qhull_data[facet[i]][0:dim - 1]) for i in xrange(len(facet))] simplex = Simplex(coords) comp_point = [comp.get_atomic_fraction(self._pd.elements[i]) for i in xrange(1, len(self._pd.elements))] return simplex.in_simplex(comp_point, PDAnalyzer.numerical_tol) else: return True
def _in_facet(self, facet, comp): """ Checks if a composition is in a facet. Args: facet: facet to test. comp: Composition to test. """ els = self._pd.elements dim = len(els) if dim > 1: coords = [ np.array(self._pd.qhull_data[facet[i]][0:dim - 1]) for i in xrange(len(facet)) ] simplex = Simplex(coords) comp_point = [ comp.get_atomic_fraction(els[i]) for i in xrange(1, len(els)) ] return simplex.in_simplex(comp_point, PDAnalyzer.numerical_tol) else: return True
def get_chempot_range_map(self, elements): """ 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")] 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: pd.el_refs[el].energy_per_atom for el in elements} chempot_ranges = collections.defaultdict(list) for ufacet in ConvexHull(all_chempots).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 simplices(self): """ Returns the simplices of the convex hull. """ return [Simplex([self.points[i] for i in v]) for v in self.vertices]
def simplices(self): """ Returns the simplices of the triangulation. """ return [Simplex([self.points[i] for i in v]) for v in self.vertices]
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) el_refs = {} for el in elements: el_entries = list( filter( lambda e: e.composition.is_element and e.composition. elements[0] == el, entries)) if len(el_entries) == 0: raise PhaseDiagramError( "There are no entries associated with terminal {}.".format( el)) el_refs[el] = min(el_entries, key=lambda e: e.energy_per_atom) data = [] for entry in entries: comp = entry.composition row = [comp.get_atomic_fraction(el) for el in elements] row.append(entry.energy_per_atom) data.append(row) data = np.array(data) self.all_entries_hulldata = data[:, 1:] #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) #make sure that if there are multiple entries at the same composition #within 1e-4 eV/atom of each other, only use the lower energy one. #This fixes the precision errors in the convex hull. #This is significantly faster than grouping by composition and then #taking the lowest energy of each group ind = [] prev_c = [] # compositions within 1e-4 of current entry prev_e = [] # energies of those compositions for i in np.argsort([e.energy_per_atom for e in entries]): if form_e[i] > -self.formation_energy_tol: continue epa = entries[i].energy_per_atom #trim the front of the lists while prev_e and epa > 1e-4 + prev_e[0]: prev_c.pop(0) prev_e.pop(0) frac_comp = entries[i].composition.fractional_composition if frac_comp not in prev_c: ind.append(i) prev_e.append(epa) prev_c.append(frac_comp) #add the elemental references ind.extend([entries.index(el) for el in el_refs.values()]) qhull_entries = [entries[i] for i in ind] qhull_data = data[ind][:, 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.simplices = [Simplex(qhull_data[f, :-1]) for f in self.facets] self.all_entries = entries self.qhull_data = qhull_data self.dim = dim self.el_refs = el_refs self.elements = elements self.qhull_entries = qhull_entries