Source code for mathematicskit.geometry.systems.convex_hull

r"""Convex hull: via :class:`scipy.spatial.ConvexHull` (Qhull), plus a
hand-rolled Graham scan kept only for pedagogical comparison in 2D.

``scipy.spatial.ConvexHull`` wraps Qhull, a robust, well-tested
computational-geometry library that handles degenerate/collinear input
correctly in any dimension; mathematicskit does not reimplement it as the
primary API. See de Berg et al., *Computational Geometry: Algorithms and
Applications*, 3rd ed., Ch. 1 (for Graham scan) and O'Rourke,
*Computational Geometry in C*, 2nd ed., Ch. 4 (for the general
:math:`d`-dimensional problem Qhull solves).
"""

from __future__ import annotations

import numpy as np
from scipy.spatial import ConvexHull as _ScipyConvexHull

from mathematicskit.geometry.core.base import ConvexHullResult

__all__ = ["convex_hull", "graham_scan"]


[docs] def convex_hull(points: np.ndarray) -> ConvexHullResult: r"""Convex hull of a point set, via :class:`scipy.spatial.ConvexHull` (Qhull). Works in any dimension; the 2D case returns hull vertices in counterclockwise order. See O'Rourke, *Computational Geometry in C*, 2nd ed., Ch. 4. Parameters ---------- points : ndarray, shape (n, d) Returns ------- ConvexHullResult Examples -------- >>> import numpy as np >>> points = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.5, 0.5]]) >>> result = convex_hull(points) >>> sorted(int(v) for v in result.vertices) # the interior point (0.5, 0.5) is excluded [0, 1, 2, 3] >>> round(result.volume, 6) # area of the unit square 1.0 """ hull = _ScipyConvexHull(points) return ConvexHullResult( points=np.asarray(points), vertices=hull.vertices, simplices=hull.simplices, volume=float(hull.volume), area=float(hull.area), method="qhull" )
[docs] def graham_scan(points: np.ndarray) -> ConvexHullResult: r"""Convex hull of a *planar* point set via the Graham scan. Sorts points by angle around the lowest (then leftmost) point, and sweeps them, popping the hull-in-progress whenever the last three points make a clockwise (non-left) turn -- kept hand-rolled specifically to compare against :func:`convex_hull`'s Qhull-based result, not as the primary 2D convex-hull API. See de Berg et al., *Computational Geometry*, 3rd ed., Ch. 1.1. Parameters ---------- points : ndarray, shape (n, 2) Returns ------- ConvexHullResult ``vertices`` holds the hull's point indices in counterclockwise order. ``simplices`` is left empty, since the Graham scan produces only the ordered vertex cycle, not a facet list. ``volume`` is the enclosed area via the shoelace formula on those vertices -- matching :func:`convex_hull`, where Qhull likewise reports the enclosed area as ``volume`` in 2D. ``area`` is left at ``0.0``: Qhull uses it for the hull's perimeter, which this scan does not compute. Examples -------- >>> import numpy as np >>> points = np.array([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.5, 0.5]]) >>> result = graham_scan(points) >>> sorted(int(v) for v in result.vertices) [0, 1, 2, 3] >>> round(result.volume, 6) 1.0 """ points = np.asarray(points, dtype=np.float64) n = points.shape[0] if n < 3: raise ValueError("need at least 3 points") start_idx = int(np.lexsort((points[:, 0], points[:, 1]))[0]) start = points[start_idx] def polar_key(i): dx, dy = points[i, 0] - start[0], points[i, 1] - start[1] angle = np.arctan2(dy, dx) dist = dx * dx + dy * dy return (angle, dist) order = sorted((i for i in range(n) if i != start_idx), key=polar_key) order = [start_idx] + order def cross(o, a, b): return (points[a, 0] - points[o, 0]) * (points[b, 1] - points[o, 1]) - (points[a, 1] - points[o, 1]) * (points[b, 0] - points[o, 0]) stack = [] for i in order: while len(stack) >= 2 and cross(stack[-2], stack[-1], i) <= 0: stack.pop() stack.append(i) from mathematicskit.geometry.systems.polygon import polygon_area hull_points = points[stack] area = polygon_area(hull_points) if len(stack) >= 3 else 0.0 return ConvexHullResult(points=points, vertices=np.array(stack), simplices=np.empty((0, 2), dtype=np.int64), volume=area, area=0.0, method="graham_scan")