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_3d_domain_simplexes_and_ann_loc( points_3d: np.ndarray, ) -> Tuple[List[Simplex], np.ndarray]: """Returns a list of Simplex objects and coordinates of annotation for one domain in a 3-d chemical potential diagram. Uses PCA to project domain into 2-dimensional space so that ConvexHull can be used to identify the bounding polygon""" points_2d, v, w = simple_pca(points_3d, k=2) domain = ConvexHull(points_2d) centroid_2d = get_centroid_2d(points_2d[domain.vertices]) ann_loc = centroid_2d @ w.T + np.mean(points_3d.T, axis=1) simplexes = [ Simplex(points_3d[indices]) for indices in domain.simplices ] return simplexes, ann_loc
def _get_3d_formula_lines( draw_domains: Dict[str, np.ndarray], formula_colors: Optional[List[str]], ) -> List[Scatter3d]: """Returns a list of Scatter3d objects defining the bounding polyhedra""" if formula_colors is None: formula_colors = px.colors.qualitative.Dark2 lines = [] for idx, (formula, coords) in enumerate(draw_domains.items()): points_3d = coords[:, :3] domain = ConvexHull(points_3d[:, :-1]) simplexes = [ Simplex(points_3d[indices]) for indices in domain.simplices ] x, y, z = [], [], [] for s in simplexes: x.extend(s.coords[:, 0].tolist() + [None]) y.extend(s.coords[:, 1].tolist() + [None]) z.extend(s.coords[:, 2].tolist() + [None]) line = Scatter3d( x=x, y=y, z=z, mode="lines", line={ "width": 8, "color": formula_colors[idx] }, opacity=1.0, name=f"{formula} (lines)", ) lines.append(line) return lines
def get_pourbaix_domains(pourbaix_entries, limits=None): """ Returns a set of pourbaix stable domains (i. e. polygons) in pH-V space from a list of pourbaix_entries This function works by using scipy's HalfspaceIntersection function to construct all of the 2-D polygons that form the boundaries of the planes corresponding to individual entry gibbs free energies as a function of pH and V. Hyperplanes of the form a*pH + b*V + 1 - g(0, 0) are constructed and supplied to HalfspaceIntersection, which then finds the boundaries of each pourbaix region using the intersection points. Args: pourbaix_entries ([PourbaixEntry]): Pourbaix entries with which to construct stable pourbaix domains limits ([[float]]): limits in which to do the pourbaix analysis Returns: Returns a dict of the form {entry: [boundary_points]}. The list of boundary points are the sides of the N-1 dim polytope bounding the allowable ph-V range of each entry. """ if limits is None: limits = [[-2, 16], [-4, 4]] # Get hyperplanes hyperplanes = [ np.array([-PREFAC * entry.npH, -entry.nPhi, 0, -entry.energy]) * entry.normalization_factor for entry in pourbaix_entries ] hyperplanes = np.array(hyperplanes) hyperplanes[:, 2] = 1 max_contribs = np.max(np.abs(hyperplanes), axis=0) g_max = np.dot(-max_contribs, [limits[0][1], limits[1][1], 0, 1]) # Add border hyperplanes and generate HalfspaceIntersection border_hyperplanes = [ [-1, 0, 0, limits[0][0]], [1, 0, 0, -limits[0][1]], [0, -1, 0, limits[1][0]], [0, 1, 0, -limits[1][1]], [0, 0, -1, 2 * g_max], ] hs_hyperplanes = np.vstack([hyperplanes, border_hyperplanes]) interior_point = np.average(limits, axis=1).tolist() + [g_max] hs_int = HalfspaceIntersection(hs_hyperplanes, np.array(interior_point)) # organize the boundary points by entry pourbaix_domains = {entry: [] for entry in pourbaix_entries} for intersection, facet in zip(hs_int.intersections, hs_int.dual_facets): for v in facet: if v < len(pourbaix_entries): this_entry = pourbaix_entries[v] pourbaix_domains[this_entry].append(intersection) # Remove entries with no pourbaix region pourbaix_domains = {k: v for k, v in pourbaix_domains.items() if v} pourbaix_domain_vertices = {} for entry, points in pourbaix_domains.items(): points = np.array(points)[:, :2] # Initial sort to ensure consistency points = points[np.lexsort(np.transpose(points))] center = np.average(points, axis=0) points_centered = points - center # Sort points by cross product of centered points, # isn't strictly necessary but useful for plotting tools points_centered = sorted( points_centered, key=cmp_to_key(lambda x, y: x[0] * y[1] - x[1] * y[0])) points = points_centered + center # Create simplices corresponding to pourbaix boundary simplices = [ Simplex(points[indices]) for indices in ConvexHull(points).simplices ] pourbaix_domains[entry] = simplices pourbaix_domain_vertices[entry] = points return pourbaix_domains, pourbaix_domain_vertices
def get_chempot_range_map(self, limits=[[-2, 16], [-4, 4]]): """ Returns a chemical potential range map for each stable entry. This function works by using scipy's HalfspaceIntersection function to construct all of the 2-D polygons that form the boundaries of the planes corresponding to individual entry gibbs free energies as a function of pH and V. Hyperplanes of the form a*pH + b*V + 1 - g(0, 0) are constructed and supplied to HalfspaceIntersection, which then finds the boundaries of each pourbaix region using the intersection points. Args: limits ([[float]]): limits in which to do the pourbaix analysis Returns: Returns a dict of the form {entry: [boundary_points]}. The list of boundary points are the sides of the N-1 dim polytope bounding the allowable ph-V range of each entry. """ tol = PourbaixAnalyzer.numerical_tol all_chempots = [] facets = self._pd.facets for facet in facets: chempots = self.get_facet_chempots(facet) chempots["H+"] /= -0.0591 chempots["V"] = -chempots["V"] chempots["1"] = chempots["1"] all_chempots.append([chempots[el] for el in self._keys]) # Get hyperplanes corresponding to G as function of pH and V halfspaces = [] qhull_data = np.array(self._pd._qhull_data) stable_entries = self._pd.stable_entries stable_indices = [ self._pd.qhull_entries.index(e) for e in stable_entries ] qhull_data = np.array(self._pd._qhull_data) hyperplanes = np.vstack([ -0.0591 * qhull_data[:, 0], -qhull_data[:, 1], np.ones(len(qhull_data)), -qhull_data[:, 2] ]) hyperplanes = np.transpose(hyperplanes) max_contribs = np.max(np.abs(hyperplanes), axis=0) g_max = np.dot(-max_contribs, [limits[0][1], limits[1][1], 0, 1]) # Add border hyperplanes and generate HalfspaceIntersection border_hyperplanes = [[-1, 0, 0, limits[0][0]], [1, 0, 0, -limits[0][1]], [0, -1, 0, limits[1][0]], [0, 1, 0, -limits[1][1]], [0, 0, -1, 2 * g_max]] hs_hyperplanes = np.vstack( [hyperplanes[stable_indices], border_hyperplanes]) interior_point = np.average(limits, axis=1).tolist() + [g_max] hs_int = HalfspaceIntersection(hs_hyperplanes, np.array(interior_point)) # organize the boundary points by entry pourbaix_domains = {entry: [] for entry in stable_entries} for intersection, facet in zip(hs_int.intersections, hs_int.dual_facets): for v in facet: if v < len(stable_entries): pourbaix_domains[stable_entries[v]].append(intersection) # Remove entries with no pourbaix region pourbaix_domains = {k: v for k, v in pourbaix_domains.items() if v} pourbaix_domain_vertices = {} # Post-process boundary points, sorting isn't strictly necessary # but useful for some plotting tools (e.g. highcharts) for entry, points in pourbaix_domains.items(): points = np.array(points)[:, :2] center = np.average(points, axis=0) points_centered = points - center # Sort points by cross product of centered points, # then roll by min sum to to ensure consistency point_comparator = lambda x, y: x[0] * y[1] - x[1] * y[0] points_centered = sorted(points_centered, key=cmp_to_key(point_comparator)) points = points_centered + center shift = -np.lexsort(np.transpose(points))[0] points = np.roll(points, shift, axis=0) # Create simplices corresponding to pourbaix boundary simplices = [ Simplex(points[indices]) for indices in ConvexHull(points).simplices ] pourbaix_domains[entry] = simplices pourbaix_domain_vertices[entry] = points self.pourbaix_domains = pourbaix_domains self.pourbaix_domain_vertices = pourbaix_domain_vertices return pourbaix_domains