From 2bb1da37fca84ed01b5c3ae662358e124908531b Mon Sep 17 00:00:00 2001 From: Kleyt0n Date: Fri, 5 Jun 2026 22:33:57 +0100 Subject: [PATCH] chore: add ci pipleine and format check --- .github/workflows/ci.yaml | 40 +++ .github/workflows/python-package.yaml | 10 +- .gitignore | 2 + cgeom/algorithms/__init__.py | 37 +-- cgeom/algorithms/_convexhull.py | 21 +- cgeom/algorithms/_delaunay.py | 51 ++-- cgeom/algorithms/_intersection.py | 13 +- cgeom/algorithms/_mincircle.py | 87 +++++-- cgeom/algorithms/_triangulation.py | 129 +++++++--- cgeom/algorithms/_voronoi.py | 120 +++++++-- cgeom/elements/elements.py | 10 +- cgeom/elements/models.py | 41 +-- cgeom/visualization/_plotting.py | 345 +++++++++++++++++-------- examples/convex_hull.py | 13 +- examples/delaunay.py | 12 +- examples/elements.py | 356 +++++++++++++++++++------- examples/segment_intersection.py | 8 +- examples/voronoi.py | 3 +- pyproject.toml | 17 +- tests/test_convexhull.py | 11 +- tests/test_delaunay.py | 42 ++- tests/test_intersection.py | 2 + tests/test_mincircle.py | 15 +- tests/test_simple.py | 3 +- tests/test_triangulation.py | 6 +- tests/test_validation.py | 48 ++-- tests/test_voronoi.py | 35 ++- uv.lock | 72 ++---- 28 files changed, 1086 insertions(+), 463 deletions(-) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..4b255d0 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality: + name: Format, lint & test + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@v4 + + - name: Install Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --dev + + - name: Check formatting (ruff) + run: uv run ruff format --check . + + - name: Lint (ruff) + run: uv run ruff check --output-format=github . + + - name: Test (pytest) + run: uv run pytest diff --git a/.github/workflows/python-package.yaml b/.github/workflows/python-package.yaml index bb33dad..b538f93 100644 --- a/.github/workflows/python-package.yaml +++ b/.github/workflows/python-package.yaml @@ -25,12 +25,10 @@ jobs: run: uv python install ${{ matrix.python-version }} - name: Install dependencies run: uv sync --dev - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - uv run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.venv - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - uv run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --exclude=.venv + - name: Check formatting with ruff + run: uv run ruff format --check . + - name: Lint with ruff + run: uv run ruff check . - name: Test with pytest run: uv run pytest diff --git a/.gitignore b/.gitignore index 5ffaa83..ae6f0c1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ __pycache__/ dist/ build/ .venv/ + +docs/_build/ diff --git a/cgeom/algorithms/__init__.py b/cgeom/algorithms/__init__.py index 8c7c43f..3fabee0 100644 --- a/cgeom/algorithms/__init__.py +++ b/cgeom/algorithms/__init__.py @@ -1,34 +1,25 @@ - - -from ._mincircle import ( - MinimumCircle -) - from ._convexhull import ( ConvexHull, ) - -from ._triangulation import ( - PolygonTriangulation, -) - -from ._voronoi import ( - VoronoiDiagram, -) - from ._delaunay import ( DelaunayTriangulation, ) - from ._intersection import ( SegmentIntersection, ) +from ._mincircle import MinimumCircle +from ._triangulation import ( + PolygonTriangulation, +) +from ._voronoi import ( + VoronoiDiagram, +) __all__ = [ - 'MinimumCircle', - 'ConvexHull', - 'PolygonTriangulation', - 'VoronoiDiagram', - 'DelaunayTriangulation', - 'SegmentIntersection', -] \ No newline at end of file + "MinimumCircle", + "ConvexHull", + "PolygonTriangulation", + "VoronoiDiagram", + "DelaunayTriangulation", + "SegmentIntersection", +] diff --git a/cgeom/algorithms/_convexhull.py b/cgeom/algorithms/_convexhull.py index 2827290..ad8d5f4 100644 --- a/cgeom/algorithms/_convexhull.py +++ b/cgeom/algorithms/_convexhull.py @@ -1,6 +1,8 @@ -import numpy as np import math +import numpy as np + + class ConvexHull: """Class to find the convex hull of a set of points using the Gift Wrapping algorithm"" @@ -11,6 +13,7 @@ class ConvexHull: Attributes: points (np.array): Array of points """ + def __init__(self, points): """Initialize the convex hull with a set of 2D points. @@ -23,6 +26,7 @@ def __init__(self, points): non-numeric, or wrong shape. """ from cgeom.elements.models import ConvexHullInput + validated = ConvexHullInput(points=points) self.points = np.array(validated.points) @@ -62,7 +66,14 @@ def low_angle(self, b, c): u = [u[0] / mag_u, u[1] / mag_u] v = [v[0] / mag_v, v[1] / mag_v] angle = math.acos(u[0] * v[0] + u[1] * v[1]) - orient = b[0] * c[1] + b[1] * a[0] + c[0] * a[1] - a[0] * c[1] - a[1] * b[0] - c[0] * b[1] + orient = ( + b[0] * c[1] + + b[1] * a[0] + + c[0] * a[1] + - a[0] * c[1] + - a[1] * b[0] + - c[0] * b[1] + ) if orient < 0: angle = 2 * math.pi - angle if angle < low_angle: @@ -86,9 +97,10 @@ def convex_hull(self): b = self.low_angle(a, ch[-2]) return ch - def plot(self, title='Convex Hull for a set of points'): + def plot(self, title="Convex Hull for a set of points"): """Deprecated: use cgeom.visualization.plot_convex_hull() instead.""" import warnings + warnings.warn( "ConvexHull.plot() is deprecated. " "Use cgeom.visualization.plot_convex_hull(hull_obj, title) instead.", @@ -96,6 +108,7 @@ def plot(self, title='Convex Hull for a set of points'): stacklevel=2, ) from cgeom.visualization import plot_convex_hull + plot_convex_hull(self, title) def get_indexes(self): @@ -110,4 +123,4 @@ def get_indexes(self): while element[0] != self.points[j][0] or element[1] != self.points[j][1]: j += 1 indexes.append(j) - return(indexes) + return indexes diff --git a/cgeom/algorithms/_delaunay.py b/cgeom/algorithms/_delaunay.py index e75b4fb..b0207eb 100644 --- a/cgeom/algorithms/_delaunay.py +++ b/cgeom/algorithms/_delaunay.py @@ -20,6 +20,7 @@ def __init__(self, points): non-numeric, or wrong shape. """ from cgeom.elements.models import DelaunayTriangulationInput + validated = DelaunayTriangulationInput(points=points) self.points = np.array(validated.points) self._triangles = None @@ -50,11 +51,13 @@ def triangulate(self): margin = 20.0 # Super-triangle vertices stored at indices n, n+1, n+2 - super_pts = np.array([ - [mid_x - margin * delta, mid_y - delta], - [mid_x + margin * delta, mid_y - delta], - [mid_x, mid_y + margin * delta], - ]) + super_pts = np.array( + [ + [mid_x - margin * delta, mid_y - delta], + [mid_x + margin * delta, mid_y - delta], + [mid_x, mid_y + margin * delta], + ] + ) all_pts = np.vstack([pts, super_pts]) # Each triangle is a frozenset of 3 indices @@ -65,7 +68,9 @@ def triangulate(self): bad = set() for tri in triangles: a, b, c = tri - if self._in_circumcircle(all_pts[i], all_pts[a], all_pts[b], all_pts[c]): + if self._in_circumcircle( + all_pts[i], all_pts[a], all_pts[b], all_pts[c] + ): bad.add(tri) # Find boundary polygon: edges belonging to exactly one bad triangle @@ -118,8 +123,9 @@ def get_edges(self): key = (min(edge), max(edge)) if key not in seen: seen.add(key) - edges.append([self.points[key[0]].tolist(), - self.points[key[1]].tolist()]) + edges.append( + [self.points[key[0]].tolist(), self.points[key[1]].tolist()] + ) return edges def get_circumcircles(self): @@ -140,6 +146,7 @@ def get_circumcircles(self): def plot(self, title="Delaunay Triangulation"): """Deprecated: use cgeom.visualization.plot_delaunay() instead.""" import warnings + warnings.warn( "DelaunayTriangulation.plot() is deprecated. " "Use cgeom.visualization.plot_delaunay(dt_obj, title) instead.", @@ -147,6 +154,7 @@ def plot(self, title="Delaunay Triangulation"): stacklevel=2, ) from cgeom.visualization import plot_delaunay + plot_delaunay(self, title) @staticmethod @@ -161,12 +169,16 @@ def _circumcircle(a, b, c): cx, cy = float(c[0]), float(c[1]) D = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)) - ux = ((ax * ax + ay * ay) * (by - cy) - + (bx * bx + by * by) * (cy - ay) - + (cx * cx + cy * cy) * (ay - by)) / D - uy = ((ax * ax + ay * ay) * (cx - bx) - + (bx * bx + by * by) * (ax - cx) - + (cx * cx + cy * cy) * (bx - ax)) / D + ux = ( + (ax * ax + ay * ay) * (by - cy) + + (bx * bx + by * by) * (cy - ay) + + (cx * cx + cy * cy) * (ay - by) + ) / D + uy = ( + (ax * ax + ay * ay) * (cx - bx) + + (bx * bx + by * by) * (ax - cx) + + (cx * cx + cy * cy) * (bx - ax) + ) / D r = np.sqrt((ax - ux) ** 2 + (ay - uy) ** 2) return ux, uy, r @@ -181,14 +193,17 @@ def _in_circumcircle(p, a, b, c): bx, by = float(b[0]) - float(p[0]), float(b[1]) - float(p[1]) cx, cy = float(c[0]) - float(p[0]), float(c[1]) - float(p[1]) - det = (ax * ax + ay * ay) * (bx * cy - cx * by) \ - - (bx * bx + by * by) * (ax * cy - cx * ay) \ + det = ( + (ax * ax + ay * ay) * (bx * cy - cx * by) + - (bx * bx + by * by) * (ax * cy - cx * ay) + (cx * cx + cy * cy) * (ax * by - bx * ay) + ) # The sign of det depends on triangle orientation (CCW vs CW). # Compute orientation and flip if clockwise. - orient = (float(b[0]) - float(a[0])) * (float(c[1]) - float(a[1])) \ - - (float(b[1]) - float(a[1])) * (float(c[0]) - float(a[0])) + orient = (float(b[0]) - float(a[0])) * (float(c[1]) - float(a[1])) - ( + float(b[1]) - float(a[1]) + ) * (float(c[0]) - float(a[0])) if orient < 0: det = -det diff --git a/cgeom/algorithms/_intersection.py b/cgeom/algorithms/_intersection.py index f821feb..896129c 100644 --- a/cgeom/algorithms/_intersection.py +++ b/cgeom/algorithms/_intersection.py @@ -5,11 +5,11 @@ import numpy as np - # --------------------------------------------------------------------------- # Event types — ordering matters for the heap (left < intersection < right) # --------------------------------------------------------------------------- + class _EventType(IntEnum): LEFT = 0 INTERSECTION = 1 @@ -97,6 +97,7 @@ def _y_at_x(seg, x): # Sweep-line status structure # --------------------------------------------------------------------------- + class _SweepLineStatus: """Maintains sorted list of active segments ordered by y at current sweep x.""" @@ -159,6 +160,7 @@ def neighbors(self, seg_idx): # Main class # --------------------------------------------------------------------------- + class SegmentIntersection: """Find intersection points among a set of 2D line segments. @@ -181,6 +183,7 @@ def __init__(self, segments): segment, non-numeric, or wrong shape. """ from cgeom.elements.models import SegmentIntersectionInput + validated = SegmentIntersectionInput(segments=segments) self.segments = np.array(validated.segments) @@ -222,9 +225,9 @@ def _check_and_add(seg_a, seg_b): if rkey not in found_points: found_points[rkey] = pt found_pairs.add(pair) - heapq.heappush(events, ( - pt[0], _EventType.INTERSECTION, pt[1], seg_a, seg_b - )) + heapq.heappush( + events, (pt[0], _EventType.INTERSECTION, pt[1], seg_a, seg_b) + ) while events: x, etype, y, s1, s2 = heapq.heappop(events) @@ -303,6 +306,7 @@ def get_segments(self): def plot(self, title="Segment Intersections"): """Deprecated: use cgeom.visualization.plot_intersections() instead.""" import warnings + warnings.warn( "SegmentIntersection.plot() is deprecated. " "Use cgeom.visualization.plot_intersections(si_obj, title) instead.", @@ -310,6 +314,7 @@ def plot(self, title="Segment Intersections"): stacklevel=2, ) from cgeom.visualization import plot_intersections + plot_intersections(self, title) def _normalize_segments(self): diff --git a/cgeom/algorithms/_mincircle.py b/cgeom/algorithms/_mincircle.py index 5e3e7e2..fc76882 100644 --- a/cgeom/algorithms/_mincircle.py +++ b/cgeom/algorithms/_mincircle.py @@ -1,7 +1,7 @@ import random -import numpy as np from math import sqrt + class MinimumCircle: """Find the minimum enclosing circle for a set of 2D points. @@ -20,6 +20,7 @@ def minimum_circle_heuristic(num_points): :return: circle """ from cgeom.elements.models import MinimumCircleInput + validated = MinimumCircleInput(points=num_points) num_points = [p[:] for p in validated.points] X_max = max([abs(x) for x, y in num_points]) @@ -35,7 +36,10 @@ def minimum_circle_heuristic(num_points): d_max = 0 for i in range(len(points)): for j in range(i + 1, len(points)): - d = sqrt((points[i][0] - points[j][0]) ** 2 + (points[i][1] - points[j][1]) ** 2) + d = sqrt( + (points[i][0] - points[j][0]) ** 2 + + (points[i][1] - points[j][1]) ** 2 + ) if d > d_max: d_max = d Pi = points[i] @@ -45,9 +49,14 @@ def minimum_circle_heuristic(num_points): circle[0] = [(Pi[0] + Pj[0]) / 2, (Pi[1] + Pj[1]) / 2] circle[1] = d_max / 2 for point in num_points: - d_mod = sqrt((point[0] - circle[0][0]) ** 2 + (point[1] - circle[0][1]) ** 2) + d_mod = sqrt( + (point[0] - circle[0][0]) ** 2 + (point[1] - circle[0][1]) ** 2 + ) if d_mod > circle[1]: - d_norm = [(point[0] - circle[0][0]) / d_mod, (point[1] - circle[0][1]) / d_mod] + d_norm = [ + (point[0] - circle[0][0]) / d_mod, + (point[1] - circle[0][1]) / d_mod, + ] circle[0][0] = circle[0][0] + ((d_mod - circle[1]) / 2) * d_norm[0] circle[0][1] = circle[0][1] + ((d_mod - circle[1]) / 2) * d_norm[1] circle[1] = (d_mod + circle[1]) / 2 @@ -82,15 +91,41 @@ def minimum_circle_points(num_points, q1, q2): circle[0][1] = (q1[1] + q2[1]) / 2 circle[1] = (sqrt((q2[0] - q1[0]) ** 2 + (q2[1] - q1[1]) ** 2)) / 2 for i in range(0, len(num_points)): - d = sqrt((circle[0][0] - num_points[i][0]) ** 2 + (circle[0][1] - num_points[i][1]) ** 2) + d = sqrt( + (circle[0][0] - num_points[i][0]) ** 2 + + (circle[0][1] - num_points[i][1]) ** 2 + ) if d > circle[1]: - D = 2 * ((q2[0] - q1[0]) * (num_points[i][1] - q1[1]) - (num_points[i][0] - q1[0]) * (q2[1] - q1[1])) - circleX = ((num_points[i][1] - q1[1]) * (q2[0] ** 2 + q2[1] ** 2 - q1[0] ** 2 - q1[1] ** 2) - (q2[1] - q1[1]) * ( - num_points[i][0] ** 2 + num_points[i][1] ** 2 - q1[0] ** 2 - q1[1] ** 2)) / D - circleY = ((q2[0] - q1[0]) * (num_points[i][0] ** 2 + num_points[i][1] ** 2 - q1[0] ** 2 - q1[1] ** 2) - (num_points[i][0] - q1[0]) * ( - q2[0] ** 2 + q2[1] ** 2 - q1[0] ** 2 - q1[1] ** 2)) / D + D = 2 * ( + (q2[0] - q1[0]) * (num_points[i][1] - q1[1]) + - (num_points[i][0] - q1[0]) * (q2[1] - q1[1]) + ) + circleX = ( + (num_points[i][1] - q1[1]) + * (q2[0] ** 2 + q2[1] ** 2 - q1[0] ** 2 - q1[1] ** 2) + - (q2[1] - q1[1]) + * ( + num_points[i][0] ** 2 + + num_points[i][1] ** 2 + - q1[0] ** 2 + - q1[1] ** 2 + ) + ) / D + circleY = ( + (q2[0] - q1[0]) + * ( + num_points[i][0] ** 2 + + num_points[i][1] ** 2 + - q1[0] ** 2 + - q1[1] ** 2 + ) + - (num_points[i][0] - q1[0]) + * (q2[0] ** 2 + q2[1] ** 2 - q1[0] ** 2 - q1[1] ** 2) + ) / D circle[0] = [circleX, circleY] - circle[1] = sqrt((q1[0] - circle[0][0]) ** 2 + (q1[1] - circle[0][1]) ** 2) + circle[1] = sqrt( + (q1[0] - circle[0][0]) ** 2 + (q1[1] - circle[0][1]) ** 2 + ) return circle @staticmethod @@ -104,11 +139,18 @@ def min_circle_with_point(num_points, q): circle = [[0.0, 0.0], 0.0] circle[0][0] = (num_points[0][0] + q[0]) / 2 circle[0][1] = (num_points[0][1] + q[1]) / 2 - circle[1] = (sqrt((num_points[0][0] - q[0]) ** 2 + (num_points[0][1] - q[1]) ** 2)) / 2 + circle[1] = ( + sqrt((num_points[0][0] - q[0]) ** 2 + (num_points[0][1] - q[1]) ** 2) + ) / 2 for i in range(1, len(num_points)): - d = sqrt((circle[0][0] - num_points[i][0]) ** 2 + (circle[0][1] - num_points[i][1]) ** 2) + d = sqrt( + (circle[0][0] - num_points[i][0]) ** 2 + + (circle[0][1] - num_points[i][1]) ** 2 + ) if d > circle[1]: - circle = MinimumCircle.minimum_circle_points(num_points[0:i], num_points[i], q) + circle = MinimumCircle.minimum_circle_points( + num_points[0:i], num_points[i], q + ) return circle def minimum_circle(self, num_points): @@ -118,15 +160,24 @@ def minimum_circle(self, num_points): :return: circle """ from cgeom.elements.models import MinimumCircleInput + validated = MinimumCircleInput(points=num_points) num_points = [p[:] for p in validated.points] random_perm = self.randomized_permutation(num_points) circle = [[0.0, 0.0], 0.0] circle[0][0] = (random_perm[0][0] + random_perm[1][0]) / 2 circle[0][1] = (random_perm[0][1] + random_perm[1][1]) / 2 - circle[1] = (sqrt((random_perm[1][0] - random_perm[0][0]) ** 2 + (random_perm[1][1] - random_perm[0][1]) ** 2)) / 2 + circle[1] = ( + sqrt( + (random_perm[1][0] - random_perm[0][0]) ** 2 + + (random_perm[1][1] - random_perm[0][1]) ** 2 + ) + ) / 2 for i in range(2, len(random_perm)): - d = sqrt((circle[0][0] - random_perm[i][0]) ** 2 + (circle[0][1] - random_perm[i][1]) ** 2) + d = sqrt( + (circle[0][0] - random_perm[i][0]) ** 2 + + (circle[0][1] - random_perm[i][1]) ** 2 + ) if d > circle[1]: circle = self.min_circle_with_point(random_perm[0:i], random_perm[i]) return circle @@ -134,6 +185,7 @@ def minimum_circle(self, num_points): def plot_min_circle_random(self, sizes, path=None, show=False): """Deprecated: use cgeom.visualization.plot_min_circle_random() instead.""" import warnings + warnings.warn( "MinimumCircle.plot_min_circle_random() is deprecated. " "Use cgeom.visualization.plot_min_circle_random(mc_obj, sizes, path, show) instead.", @@ -141,11 +193,13 @@ def plot_min_circle_random(self, sizes, path=None, show=False): stacklevel=2, ) from cgeom.visualization import plot_min_circle_random + plot_min_circle_random(self, sizes, path, show) def plot_min_circle(self, data, path=None, show=False): """Deprecated: use cgeom.visualization.plot_min_circle() instead.""" import warnings + warnings.warn( "MinimumCircle.plot_min_circle() is deprecated. " "Use cgeom.visualization.plot_min_circle(mc_obj, data, path, show) instead.", @@ -153,4 +207,5 @@ def plot_min_circle(self, data, path=None, show=False): stacklevel=2, ) from cgeom.visualization import plot_min_circle + plot_min_circle(self, data, path, show) diff --git a/cgeom/algorithms/_triangulation.py b/cgeom/algorithms/_triangulation.py index 4ec413c..808da5b 100644 --- a/cgeom/algorithms/_triangulation.py +++ b/cgeom/algorithms/_triangulation.py @@ -1,7 +1,9 @@ -import numpy as np import math import random +import numpy as np + + class PolygonTriangulation: """Triangulate a simple polygon using the ear-clipping algorithm. @@ -24,6 +26,7 @@ def __init__(self, poly, poly_name="Polygon"): non-numeric, or wrong shape. """ from cgeom.elements.models import PolygonTriangulationInput + validated = PolygonTriangulationInput(poly=poly, poly_name=poly_name) self.poly = np.array(validated.poly) self.poly_name = validated.poly_name @@ -45,7 +48,9 @@ def is_ear(self, poly, vertex): # First, we start testing if the line lays inside the polygon by analysing the triangle made by vertex, vertex+1, vertex-1 p1 = poly[vertex] - if vertex == len(poly) - 1: # If the vertex is in the last position, vertex + 1 is the first vertex + if ( + vertex == len(poly) - 1 + ): # If the vertex is in the last position, vertex + 1 is the first vertex p2 = poly[0] else: p2 = poly[vertex + 1] @@ -57,23 +62,58 @@ def is_ear(self, poly, vertex): # Now we need to find the triangle's orientation - orient = v1[0] * v2[1] + v3[0] * v1[1] + v2[0] * v3[1] - v3[0] * v2[1] - v3[1] * v1[0] - v2[0] * v1[1] + orient = ( + v1[0] * v2[1] + + v3[0] * v1[1] + + v2[0] * v3[1] + - v3[0] * v2[1] + - v3[1] * v1[0] + - v2[0] * v1[1] + ) if orient <= 0: return False else: - # Finally, we need to check if any other vertex is inside the triangle made by vertex-1, vertex and vertex+1 for i, p in enumerate(poly): - D = p1[0] * p2[1] + p2[0] * p3[1] + p3[0] * p1[1] - p2[1] * p3[0] - p1[0] * p3[1] - p1[1] * p2[0] - D1 = p[0] * p2[1] + p2[0] * p3[1] + p3[0] * p[1] - p2[1] * p3[0] - p3[1] * p[0] - p[1] * p2[0] - D2 = p1[0] * p[1] + p[0] * p3[1] + p3[0] * p1[1] - p[1] * p3[0] - p3[1] * p1[0] - p1[1] * p[0] - D3 = p1[0] * p2[1] + p2[0] * p[1] + p[0] * p1[1] - p2[1] * p[0] - p[1] * p1[0] - p1[1] * p2[0] + D = ( + p1[0] * p2[1] + + p2[0] * p3[1] + + p3[0] * p1[1] + - p2[1] * p3[0] + - p1[0] * p3[1] + - p1[1] * p2[0] + ) + D1 = ( + p[0] * p2[1] + + p2[0] * p3[1] + + p3[0] * p[1] + - p2[1] * p3[0] + - p3[1] * p[0] + - p[1] * p2[0] + ) + D2 = ( + p1[0] * p[1] + + p[0] * p3[1] + + p3[0] * p1[1] + - p[1] * p3[0] + - p3[1] * p1[0] + - p1[1] * p[0] + ) + D3 = ( + p1[0] * p2[1] + + p2[0] * p[1] + + p[0] * p1[1] + - p2[1] * p[0] + - p[1] * p1[0] + - p1[1] * p2[0] + ) lambda1 = D1 / D lambda2 = D2 / D lambda3 = D3 / D - if lambda1 > 0 and lambda2 > 0 and lambda3 > 0: return False + if lambda1 > 0 and lambda2 > 0 and lambda3 > 0: + return False return True def Triangulation(self): @@ -83,17 +123,19 @@ def Triangulation(self): list: List of diagonals, where each diagonal is a pair of [x, y] points. """ poly = self.poly - diag = [] # List of diagonals + diag = [] # List of diagonals vertex = 0 - while len(poly) > 3: # The loop runs until only one triangle is left - if vertex >= len(poly): # When the loop reaches the last vertex + while len(poly) > 3: # The loop runs until only one triangle is left + if vertex >= len(poly): # When the loop reaches the last vertex vertex = 0 - if self.is_ear(poly, vertex): # Test if the vertex is an ear - if vertex == len(poly) - 1: # If the vertex is in the last position, vertex + 1 is the first vertex + if self.is_ear(poly, vertex): # Test if the vertex is an ear + if ( + vertex == len(poly) - 1 + ): # If the vertex is in the last position, vertex + 1 is the first vertex diag.append([list(poly[vertex - 1]), list(poly[0])]) else: diag.append([list(poly[vertex - 1]), list(poly[vertex + 1])]) - poly = np.delete(poly, vertex, 0) # Remove the vertex + poly = np.delete(poly, vertex, 0) # Remove the vertex vertex += 1 return diag @@ -107,16 +149,19 @@ def get_diag_vertexes(self): for i, diagonal in enumerate(self.diagonals): for vertex in diagonal: index = 0 - while vertex[0] != self.poly[index][0] or vertex[1] != self.poly[index][1]: + while ( + vertex[0] != self.poly[index][0] or vertex[1] != self.poly[index][1] + ): index += 1 diag_vertexes.append(index) - diag_vertexes[i] = [diag_vertexes[-2],diag_vertexes[-1]] + diag_vertexes[i] = [diag_vertexes[-2], diag_vertexes[-1]] del diag_vertexes[-1] - return(diag_vertexes) + return diag_vertexes def plot_triangulation(self): """Deprecated: use cgeom.visualization.plot_triangulation() instead.""" import warnings + warnings.warn( "PolygonTriangulation.plot_triangulation() is deprecated. " "Use cgeom.visualization.plot_triangulation(tri_obj) instead.", @@ -124,11 +169,14 @@ def plot_triangulation(self): stacklevel=2, ) from cgeom.visualization import plot_triangulation + plot_triangulation(self) + # Function to generate random polygons -def generatePolygon( ctrX, ctrY, aveRadius, irregularity, spikeyness, numVerts ): + +def generatePolygon(ctrX, ctrY, aveRadius, irregularity, spikeyness, numVerts): """Generate a random simple polygon. Args: @@ -142,38 +190,39 @@ def generatePolygon( ctrX, ctrY, aveRadius, irregularity, spikeyness, numVerts ) Returns: list[tuple[int, int]]: Vertices of the generated polygon. """ - irregularity = clip( irregularity, 0,1 ) * 2*math.pi / numVerts - spikeyness = clip( spikeyness, 0,1 ) * aveRadius + irregularity = clip(irregularity, 0, 1) * 2 * math.pi / numVerts + spikeyness = clip(spikeyness, 0, 1) * aveRadius # generate n angle steps angleSteps = [] - lower = (2*math.pi / numVerts) - irregularity - upper = (2*math.pi / numVerts) + irregularity + lower = (2 * math.pi / numVerts) - irregularity + upper = (2 * math.pi / numVerts) + irregularity sum = 0 - for i in range(numVerts) : + for i in range(numVerts): tmp = random.uniform(lower, upper) - angleSteps.append( tmp ) + angleSteps.append(tmp) sum = sum + tmp # normalize the steps so that point 0 and point n+1 are the same - k = sum / (2*math.pi) - for i in range(numVerts) : + k = sum / (2 * math.pi) + for i in range(numVerts): angleSteps[i] = angleSteps[i] / k # now generate the points points = [] - angle = random.uniform(0, 2*math.pi) - for i in range(numVerts) : - r_i = clip( random.gauss(aveRadius, spikeyness), 0, 2*aveRadius ) - x = ctrX + r_i*math.cos(angle) - y = ctrY + r_i*math.sin(angle) - points.append( (int(x),int(y)) ) + angle = random.uniform(0, 2 * math.pi) + for i in range(numVerts): + r_i = clip(random.gauss(aveRadius, spikeyness), 0, 2 * aveRadius) + x = ctrX + r_i * math.cos(angle) + y = ctrY + r_i * math.sin(angle) + points.append((int(x), int(y))) angle = angle + angleSteps[i] return points -def clip(x, min, max) : + +def clip(x, min, max): """Clamp *x* to the range [min, max]. Args: @@ -184,7 +233,11 @@ def clip(x, min, max) : Returns: The clamped value. """ - if( min > max ) : return x - elif( x < min ) : return min - elif( x > max ) : return max - else : return x + if min > max: + return x + elif x < min: + return min + elif x > max: + return max + else: + return x diff --git a/cgeom/algorithms/_voronoi.py b/cgeom/algorithms/_voronoi.py index 77241fb..83dc224 100644 --- a/cgeom/algorithms/_voronoi.py +++ b/cgeom/algorithms/_voronoi.py @@ -1,5 +1,6 @@ import numpy as np + class VoronoiDiagram: """Construct a Voronoi diagram for a set of 2D sites using incremental insertion. @@ -21,6 +22,7 @@ def __init__(self, data): a y-coordinate, non-numeric, or wrong shape. """ from cgeom.elements.models import VoronoiDiagramInput + validated = VoronoiDiagramInput(points=data) self.data = np.array(validated.points) self.cells = [] @@ -36,7 +38,7 @@ def search_for_cell(self, point): """ dist = float("inf") for i, cell in enumerate(self.cells): - aux = np.sqrt((point[0] - cell[0][0])**2 + (point[1] - cell[0][1])**2) + aux = np.sqrt((point[0] - cell[0][0]) ** 2 + (point[1] - cell[0][1]) ** 2) if aux < dist: dist = aux pos = i @@ -89,9 +91,25 @@ def adapt_cell(self, visited_cell, edge_added, point_added): A = edge[0] B = edge[1] C = edge_added[0] - D_0 = round(A[0] * B[1] + A[1] * C[0] + B[0] * C[1] - C[0] * B[1] - A[0] * C[1] - A[1] * B[0], 4) + D_0 = round( + A[0] * B[1] + + A[1] * C[0] + + B[0] * C[1] + - C[0] * B[1] + - A[0] * C[1] + - A[1] * B[0], + 4, + ) C = edge_added[1] - D_1 = round(A[0] * B[1] + A[1] * C[0] + B[0] * C[1] - C[0] * B[1] - A[0] * C[1] - A[1] * B[0], 4) + D_1 = round( + A[0] * B[1] + + A[1] * C[0] + + B[0] * C[1] + - C[0] * B[1] + - A[0] * C[1] + - A[1] * B[0], + 4, + ) # Determine precision based on the magnitude of D if max(abs(D_0), abs(D_1)) >= 2000000: @@ -109,8 +127,13 @@ def adapt_cell(self, visited_cell, edge_added, point_added): else: p = edge_added[1] - dist_1 = np.sqrt((point_added[0] - A[0]) ** 2 + (point_added[1] - A[1]) ** 2) - dist_2 = np.sqrt((self.cells[visited_cell][0][0] - A[0]) ** 2 + (self.cells[visited_cell][0][1] - A[1]) ** 2) + dist_1 = np.sqrt( + (point_added[0] - A[0]) ** 2 + (point_added[1] - A[1]) ** 2 + ) + dist_2 = np.sqrt( + (self.cells[visited_cell][0][0] - A[0]) ** 2 + + (self.cells[visited_cell][0][1] - A[1]) ** 2 + ) if dist_1 < dist_2: self.cells[visited_cell][1][i][0] = [p[0], p[1], 1][:] @@ -118,8 +141,13 @@ def adapt_cell(self, visited_cell, edge_added, point_added): self.cells[visited_cell][1][i][1] = [p[0], p[1], 1][:] if abs(D_0) > precision and abs(D_1) > precision: - dist_1 = np.sqrt((point_added[0] - A[0]) ** 2 + (point_added[1] - A[1]) ** 2) - dist_2 = np.sqrt((self.cells[visited_cell][0][0] - A[0]) ** 2 + (self.cells[visited_cell][0][1] - A[1]) ** 2) + dist_1 = np.sqrt( + (point_added[0] - A[0]) ** 2 + (point_added[1] - A[1]) ** 2 + ) + dist_2 = np.sqrt( + (self.cells[visited_cell][0][0] - A[0]) ** 2 + + (self.cells[visited_cell][0][1] - A[1]) ** 2 + ) if dist_1 < dist_2: drop.append(edge) @@ -171,11 +199,17 @@ def make_edge(self, cell, p): if abs(D) > 0.1: D_t = (M[1] - A[1]) * V_3[0] - (M[0] - A[0]) * V_3[1] t = D_t / D - INTERSECT = [round(M[0] + t * V_2[0], 4), round(M[1] + t * V_2[1], 4), 1] + INTERSECT = [ + round(M[0] + t * V_2[0], 4), + round(M[1] + t * V_2[1], 4), + 1, + ] IS_VALID = False if A[2] == 1 and B[2] == 1: - if INTERSECT[0] < max(A[0], B[0]) and INTERSECT[0] > min(A[0], B[0]): + if INTERSECT[0] < max(A[0], B[0]) and INTERSECT[0] > min( + A[0], B[0] + ): IS_VALID = True if A[2] == 0 and B[2] == 1: @@ -200,21 +234,46 @@ def make_edge(self, cell, p): if len(output) == 1: if self.search_for_cell(M) == cell: - inf_point = [round(M[0] - 10 * T * V_2[0], 4), round(M[1] - 10 * T * V_2[1], 4), 0] + inf_point = [ + round(M[0] - 10 * T * V_2[0], 4), + round(M[1] - 10 * T * V_2[1], 4), + 0, + ] i = 2 - while np.sqrt((self.cells[cell][0][0] - inf_point[0]) ** 2 + (self.cells[cell][0][1] - inf_point[0]) ** 2) < 1000: - inf_point = [round(M[0] - i * 10 * T * V_2[0], 4), round(M[1] - i * 10 * T * V_2[1], 4), 0] + while ( + np.sqrt( + (self.cells[cell][0][0] - inf_point[0]) ** 2 + + (self.cells[cell][0][1] - inf_point[0]) ** 2 + ) + < 1000 + ): + inf_point = [ + round(M[0] - i * 10 * T * V_2[0], 4), + round(M[1] - i * 10 * T * V_2[1], 4), + 0, + ] i += 1 else: - inf_point = [round(M[0] + 10 * T * V_2[0], 4), round(M[1] + 10 * T * V_2[1], 4), 0] + inf_point = [ + round(M[0] + 10 * T * V_2[0], 4), + round(M[1] + 10 * T * V_2[1], 4), + 0, + ] i = 2 while ( - np.sqrt((self.cells[cell][0][0] - inf_point[0]) ** 2 + (self.cells[cell][0][1] - inf_point[0]) ** 2) + np.sqrt( + (self.cells[cell][0][0] - inf_point[0]) ** 2 + + (self.cells[cell][0][1] - inf_point[0]) ** 2 + ) < 1000 or self.search_for_cell(inf_point) != cell ): - inf_point = [round(M[0] + i * 10 * T * V_2[0], 4), round(M[1] + i * 10 * T * V_2[1], 4), 0] + inf_point = [ + round(M[0] + i * 10 * T * V_2[0], 4), + round(M[1] + i * 10 * T * V_2[1], 4), + 0, + ] i += 1 output.append(inf_point) @@ -262,15 +321,26 @@ def build_voronoi_diagram(self): Returns: list: The list of Voronoi cells (also stored in ``self.cells``). """ - mid = [(self.data[1][0] + self.data[0][0]) / 2, (self.data[1][1] + self.data[0][1]) / 2] + mid = [ + (self.data[1][0] + self.data[0][0]) / 2, + (self.data[1][1] + self.data[0][1]) / 2, + ] V = [self.data[0][0] - mid[0], self.data[0][1] - mid[1]] self.cells.append( [ self.data[0], [ [ - [round(mid[0] + 1000, 4), round(mid[1] - (V[0] / V[1]) * 1000, 4), 0], - [round(mid[0] - 1000, 4), round(mid[1] + (V[0] / V[1]) * 1000, 4), 0], + [ + round(mid[0] + 1000, 4), + round(mid[1] - (V[0] / V[1]) * 1000, 4), + 0, + ], + [ + round(mid[0] - 1000, 4), + round(mid[1] + (V[0] / V[1]) * 1000, 4), + 0, + ], ] ], ] @@ -280,8 +350,16 @@ def build_voronoi_diagram(self): self.data[1], [ [ - [round(mid[0] + 1000, 4), round(mid[1] - (V[0] / V[1]) * 1000, 4), 0], - [round(mid[0] - 1000, 4), round(mid[1] + (V[0] / V[1]) * 1000, 4), 0], + [ + round(mid[0] + 1000, 4), + round(mid[1] - (V[0] / V[1]) * 1000, 4), + 0, + ], + [ + round(mid[0] - 1000, 4), + round(mid[1] + (V[0] / V[1]) * 1000, 4), + 0, + ], ] ], ] @@ -296,6 +374,7 @@ def build_voronoi_diagram(self): def plot_voronoi(self, cells): """Deprecated: use cgeom.visualization.plot_voronoi() instead.""" import warnings + warnings.warn( "VoronoiDiagram.plot_voronoi() is deprecated. " "Use cgeom.visualization.plot_voronoi(voronoi_obj, cells) instead.", @@ -303,4 +382,5 @@ def plot_voronoi(self, cells): stacklevel=2, ) from cgeom.visualization import plot_voronoi + plot_voronoi(self, cells) diff --git a/cgeom/elements/elements.py b/cgeom/elements/elements.py index 3ac6268..0edeb12 100644 --- a/cgeom/elements/elements.py +++ b/cgeom/elements/elements.py @@ -170,8 +170,9 @@ def length(self) -> float: @property def midpoint(self) -> Point: """Midpoint of the segment.""" - return Point(x=(self.start.x + self.end.x) / 2, - y=(self.start.y + self.end.y) / 2) + return Point( + x=(self.start.x + self.end.x) / 2, y=(self.start.y + self.end.y) / 2 + ) def to_list(self) -> list[list[float]]: """Return as [[x1, y1], [x2, y2]].""" @@ -217,7 +218,7 @@ def _validate_radius(self): @property def area(self) -> float: """Area of the circle.""" - return math.pi * self.radius ** 2 + return math.pi * self.radius**2 @property def circumference(self) -> float: @@ -287,8 +288,7 @@ def perimeter(self) -> float: """Perimeter of the polygon.""" n = len(self.vertices) return sum( - self.vertices[i].distance_to(self.vertices[(i + 1) % n]) - for i in range(n) + self.vertices[i].distance_to(self.vertices[(i + 1) % n]) for i in range(n) ) def to_numpy(self) -> np.ndarray: diff --git a/cgeom/elements/models.py b/cgeom/elements/models.py index 0253442..8e423a1 100644 --- a/cgeom/elements/models.py +++ b/cgeom/elements/models.py @@ -70,9 +70,7 @@ def validate_input(cls, data): raise ValueError("ConvexHull requires at least 3 points") if _all_collinear(points): - raise ValueError( - "All points are collinear; convex hull is undefined" - ) + raise ValueError("All points are collinear; convex hull is undefined") if _has_duplicates(points): warnings.warn( @@ -129,19 +127,13 @@ def validate_input(cls, data): points = _to_point_list(raw) if len(points) < 3: - raise ValueError( - "Polygon triangulation requires at least 3 vertices" - ) + raise ValueError("Polygon triangulation requires at least 3 vertices") if _all_collinear(points): - raise ValueError( - "All vertices are collinear; polygon is degenerate" - ) + raise ValueError("All vertices are collinear; polygon is degenerate") if _has_duplicates(points): - warnings.warn( - "Duplicate vertices detected in polygon", UserWarning - ) + warnings.warn("Duplicate vertices detected in polygon", UserWarning) return {"poly": points, "poly_name": poly_name} @@ -184,6 +176,7 @@ def _to_segment_list(raw) -> List[List[List[float]]]: # Handle list of Segment objects from cgeom.elements.elements import Segment + if isinstance(raw, (list, tuple)) and len(raw) > 0 and isinstance(raw[0], Segment): raw = [s.to_list() for s in raw] @@ -196,9 +189,7 @@ def _to_segment_list(raw) -> List[List[List[float]]]: raise ValueError("Segments must contain only numeric values") if arr.ndim != 3 or arr.shape[1] != 2 or arr.shape[2] != 2: - raise ValueError( - f"Segments must have shape (n, 2, 2), got shape {arr.shape}" - ) + raise ValueError(f"Segments must have shape (n, 2, 2), got shape {arr.shape}") return arr.tolist() @@ -219,26 +210,20 @@ def validate_input(cls, data): segments = _to_segment_list(raw) if len(segments) < 2: - raise ValueError( - "SegmentIntersection requires at least 2 segments" - ) + raise ValueError("SegmentIntersection requires at least 2 segments") for i, seg in enumerate(segments): x1, y1 = seg[0] x2, y2 = seg[1] if abs(x1 - x2) < 1e-12 and abs(y1 - y2) < 1e-12: - raise ValueError( - f"Segment {i} has zero length (identical endpoints)" - ) + raise ValueError(f"Segment {i} has zero length (identical endpoints)") seen = set() for seg in segments: key = (tuple(seg[0]), tuple(seg[1])) rkey = (tuple(seg[1]), tuple(seg[0])) if key in seen or rkey in seen: - warnings.warn( - "Duplicate segments detected", UserWarning - ) + warnings.warn("Duplicate segments detected", UserWarning) break seen.add(key) @@ -261,14 +246,10 @@ def validate_input(cls, data): points = _to_point_list(raw) if len(points) < 3: - raise ValueError( - "Delaunay triangulation requires at least 3 points" - ) + raise ValueError("Delaunay triangulation requires at least 3 points") if _all_collinear(points): - raise ValueError( - "All points are collinear; triangulation is undefined" - ) + raise ValueError("All points are collinear; triangulation is undefined") if _has_duplicates(points): warnings.warn( diff --git a/cgeom/visualization/_plotting.py b/cgeom/visualization/_plotting.py index ebcb2a9..f9c195e 100644 --- a/cgeom/visualization/_plotting.py +++ b/cgeom/visualization/_plotting.py @@ -8,57 +8,66 @@ # --------------------------------------------------------------------------- # Global rcParams — polished minimalist defaults # --------------------------------------------------------------------------- -mpl.rcParams.update({ - "font.family": "sans-serif", - "font.sans-serif": ["Inter", "Helvetica Neue", "Helvetica", - "Arial", "DejaVu Sans"], - "font.size": 9, - "axes.unicode_minus": False, - "figure.dpi": 150, - "savefig.dpi": 300, - "savefig.bbox": "tight", - "savefig.pad_inches": 0.15, -}) +mpl.rcParams.update( + { + "font.family": "sans-serif", + "font.sans-serif": [ + "Inter", + "Helvetica Neue", + "Helvetica", + "Arial", + "DejaVu Sans", + ], + "font.size": 9, + "axes.unicode_minus": False, + "figure.dpi": 150, + "savefig.dpi": 300, + "savefig.bbox": "tight", + "savefig.pad_inches": 0.15, + } +) # --------------------------------------------------------------------------- # Blue / navy palette — deep navies for structure, Smart Blue for focal data, # slate & lavender greys for labels and annotations. # --------------------------------------------------------------------------- -_INK = "#002855" # Prussian Blue — primary structure, main edges -_ACCENT = "#0466c8" # Smart Blue — focal data points, emphasis -_CHARCOAL = "#023e7d" # Regal Navy — secondary edges, lines -_STEEL = "#5c677d" # Blue Slate — tick labels, axis labels -_SILVER = "#7d8597" # Slate Grey — annotations, dashes, subtle markers -_ASH = "#979dac" # Lavender Grey — light ticks, borders -_MIST = "#e6e9f1" # faint lavender — grid lines, fill base color -_BG = "#fbfcfe" # cool near-white — figure & axes background +_INK = "#002855" # Prussian Blue — primary structure, main edges +_ACCENT = "#0466c8" # Smart Blue — focal data points, emphasis +_CHARCOAL = "#023e7d" # Regal Navy — secondary edges, lines +_STEEL = "#5c677d" # Blue Slate — tick labels, axis labels +_SILVER = "#7d8597" # Slate Grey — annotations, dashes, subtle markers +_ASH = "#979dac" # Lavender Grey — light ticks, borders +_MIST = "#e6e9f1" # faint lavender — grid lines, fill base color +_BG = "#fbfcfe" # cool near-white — figure & axes background # --------------------------------------------------------------------------- # Visual sizing constants # --------------------------------------------------------------------------- -_PT_SIZE = 28 # primary scatter point size -_PT_ACCENT = 40 # emphasized scatter point size -_PT_EDGE_W = 0.8 # white edge width on scatter points -_LINE_W = 1.0 # primary edge/line width -_LINE_W_THIN = 0.7 # secondary/dashed line width -_FILL_ALPHA = 0.06 # polygon/triangle fill opacity -_CIRCLE_ALPHA = 0.08 # circle fill opacity +_PT_SIZE = 28 # primary scatter point size +_PT_ACCENT = 40 # emphasized scatter point size +_PT_EDGE_W = 0.8 # white edge width on scatter points +_LINE_W = 1.0 # primary edge/line width +_LINE_W_THIN = 0.7 # secondary/dashed line width +_FILL_ALPHA = 0.06 # polygon/triangle fill opacity +_CIRCLE_ALPHA = 0.08 # circle fill opacity # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- + def _hex_to_rgb(hex_color): """Convert ``'#rrggbb'`` to an ``(r, g, b)`` float tuple.""" h = hex_color.lstrip("#") - return tuple(int(h[i:i + 2], 16) / 255.0 for i in (0, 2, 4)) + return tuple(int(h[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) def _style_ax(ax, title=None): """Apply a minimal, modern look to an axes object.""" if title: - ax.set_title(title, loc="left", fontsize=12.5, fontweight="500", - color=_INK, pad=12) + ax.set_title( + title, loc="left", fontsize=12.5, fontweight="500", color=_INK, pad=12 + ) ax.set_facecolor(_BG) ax.set_axisbelow(True) ax.grid(True, color=_MIST, linewidth=0.7, zorder=0) @@ -79,6 +88,7 @@ def _new_fig(figsize=(5.5, 5.5)): # Convex Hull # --------------------------------------------------------------------------- + def plot_convex_hull(hull_obj, title="Convex Hull"): """Plot the points and their convex hull. @@ -93,19 +103,32 @@ def plot_convex_hull(hull_obj, title="Convex Hull"): ch = hull_obj.convex_hull() xs = [p[0] for p in ch] + [ch[0][0]] ys = [p[1] for p in ch] + [ch[0][1]] - ax.fill(xs, ys, - facecolor=(*_hex_to_rgb(_MIST), _FILL_ALPHA), - edgecolor=_CHARCOAL, linewidth=_LINE_W, zorder=2) + ax.fill( + xs, + ys, + facecolor=(*_hex_to_rgb(_MIST), _FILL_ALPHA), + edgecolor=_CHARCOAL, + linewidth=_LINE_W, + zorder=2, + ) ax.scatter( - hull_obj.points[:, 0], hull_obj.points[:, 1], - c=_STEEL, s=_PT_SIZE, zorder=3, - edgecolors="white", linewidths=_PT_EDGE_W, + hull_obj.points[:, 0], + hull_obj.points[:, 1], + c=_STEEL, + s=_PT_SIZE, + zorder=3, + edgecolors="white", + linewidths=_PT_EDGE_W, ) ax.scatter( - [p[0] for p in ch], [p[1] for p in ch], - c=_ACCENT, s=_PT_ACCENT, zorder=4, - edgecolors="white", linewidths=_PT_EDGE_W, + [p[0] for p in ch], + [p[1] for p in ch], + c=_ACCENT, + s=_PT_ACCENT, + zorder=4, + edgecolors="white", + linewidths=_PT_EDGE_W, ) plt.tight_layout() @@ -116,6 +139,7 @@ def plot_convex_hull(hull_obj, title="Convex Hull"): # Minimum Circle (random sizes) # --------------------------------------------------------------------------- + def plot_min_circle_random(mc_obj, sizes, path=None, show=False): """Plot minimum circles for randomly generated point sets of varying sizes. @@ -143,45 +167,81 @@ def plot_min_circle_random(mc_obj, sizes, path=None, show=False): time_heuristic.append(time.time() - start_time) print(f"MinCircle Center/Radius: {min_circle[0]} / {min_circle[1]}") - print(f"Heuristic Center/Radius: {min_circle_heuristic[0]} / {min_circle_heuristic[1]}") + print( + f"Heuristic Center/Radius: {min_circle_heuristic[0]} / {min_circle_heuristic[1]}" + ) fig, axes = plt.subplots(1, 2, figsize=(11, 5)) fig.patch.set_facecolor(_BG) - for idx, (circ, label) in enumerate([ - (min_circle, f"Exact (n={num_points})"), - (min_circle_heuristic, f"Heuristic (n={num_points})"), - ]): + for idx, (circ, label) in enumerate( + [ + (min_circle, f"Exact (n={num_points})"), + (min_circle_heuristic, f"Heuristic (n={num_points})"), + ] + ): ax = axes[idx] _style_ax(ax, label) ax.set_aspect("equal") - ax.scatter(x, y, c=_ACCENT, s=16, zorder=3, - edgecolors="white", linewidths=_PT_EDGE_W) + ax.scatter( + x, + y, + c=_ACCENT, + s=16, + zorder=3, + edgecolors="white", + linewidths=_PT_EDGE_W, + ) circle_patch = plt.Circle( - circ[0], circ[1], + circ[0], + circ[1], facecolor=(*_hex_to_rgb(_MIST), _CIRCLE_ALPHA), - edgecolor=_CHARCOAL, linewidth=_LINE_W, zorder=1, + edgecolor=_CHARCOAL, + linewidth=_LINE_W, + zorder=1, ) ax.add_patch(circle_patch) - ax.plot(circ[0][0], circ[0][1], "+", color=_SILVER, - markersize=7, markeredgewidth=1.0, zorder=4) + ax.plot( + circ[0][0], + circ[0][1], + "+", + color=_SILVER, + markersize=7, + markeredgewidth=1.0, + zorder=4, + ) ax.set_xlim(-10, 10) ax.set_ylim(-10, 10) plt.tight_layout() if path is not None: - fig.savefig(path + f"plot_min_circle_{num_points}_points.pdf", - bbox_inches="tight") + fig.savefig( + path + f"plot_min_circle_{num_points}_points.pdf", bbox_inches="tight" + ) if show: plt.show() # Runtime comparison fig, ax = _new_fig(figsize=(5.5, 4)) _style_ax(ax, "Runtime") - ax.plot(sizes, time_min_circle, color=_ACCENT, linewidth=_LINE_W, - marker="o", markersize=4, label="Exact") - ax.plot(sizes, time_heuristic, color=_SILVER, linewidth=_LINE_W, - marker="o", markersize=4, label="Heuristic") + ax.plot( + sizes, + time_min_circle, + color=_ACCENT, + linewidth=_LINE_W, + marker="o", + markersize=4, + label="Exact", + ) + ax.plot( + sizes, + time_heuristic, + color=_SILVER, + linewidth=_LINE_W, + marker="o", + markersize=4, + label="Heuristic", + ) ax.set_xlabel("Input size", fontsize=8.5, color=_STEEL) ax.set_ylabel("Time (s)", fontsize=8.5, color=_STEEL) ax.legend(frameon=False, fontsize=9) @@ -196,6 +256,7 @@ def plot_min_circle_random(mc_obj, sizes, path=None, show=False): # Minimum Circle (given data) # --------------------------------------------------------------------------- + def plot_min_circle(mc_obj, data, path=None, show=False): """Plot minimum circles for a given dataset. @@ -214,28 +275,49 @@ def plot_min_circle(mc_obj, data, path=None, show=False): t_heur = time.time() - start_time print(f"MinCircle Center/Radius: {min_circle[0]} / {min_circle[1]}") - print(f"Heuristic Center/Radius: {min_circle_heuristic[0]} / {min_circle_heuristic[1]}") + print( + f"Heuristic Center/Radius: {min_circle_heuristic[0]} / {min_circle_heuristic[1]}" + ) fig, axes = plt.subplots(1, 2, figsize=(11, 5)) fig.patch.set_facecolor(_BG) - for idx, (circ, label) in enumerate([ - (min_circle, "Exact"), - (min_circle_heuristic, "Heuristic"), - ]): + for idx, (circ, label) in enumerate( + [ + (min_circle, "Exact"), + (min_circle_heuristic, "Heuristic"), + ] + ): ax = axes[idx] _style_ax(ax, label) ax.set_aspect("equal") - ax.scatter(data[:, 0], data[:, 1], c=_ACCENT, s=16, - zorder=3, edgecolors="white", linewidths=_PT_EDGE_W) + ax.scatter( + data[:, 0], + data[:, 1], + c=_ACCENT, + s=16, + zorder=3, + edgecolors="white", + linewidths=_PT_EDGE_W, + ) circle_patch = plt.Circle( - circ[0], circ[1], + circ[0], + circ[1], facecolor=(*_hex_to_rgb(_MIST), _CIRCLE_ALPHA), - edgecolor=_CHARCOAL, linewidth=_LINE_W, zorder=1, + edgecolor=_CHARCOAL, + linewidth=_LINE_W, + zorder=1, ) ax.add_patch(circle_patch) - ax.plot(circ[0][0], circ[0][1], "+", color=_SILVER, - markersize=7, markeredgewidth=1.0, zorder=4) + ax.plot( + circ[0][0], + circ[0][1], + "+", + color=_SILVER, + markersize=7, + markeredgewidth=1.0, + zorder=4, + ) margin = circ[1] * 0.15 ax.set_xlim(circ[0][0] - circ[1] - margin, circ[0][0] + circ[1] + margin) ax.set_ylim(circ[0][1] - circ[1] - margin, circ[0][1] + circ[1] + margin) @@ -249,9 +331,14 @@ def plot_min_circle(mc_obj, data, path=None, show=False): # Runtime bar chart fig, ax = _new_fig(figsize=(4, 3.5)) _style_ax(ax, "Runtime") - ax.bar(["Exact", "Heuristic"], [t_exact, t_heur], - color=[_ACCENT, _SILVER], width=0.40, - edgecolor=_BG, linewidth=0.6) + ax.bar( + ["Exact", "Heuristic"], + [t_exact, t_heur], + color=[_ACCENT, _SILVER], + width=0.40, + edgecolor=_BG, + linewidth=0.6, + ) ax.set_ylabel("Time (s)", fontsize=8.5, color=_STEEL) ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.4f")) plt.tight_layout() @@ -265,6 +352,7 @@ def plot_min_circle(mc_obj, data, path=None, show=False): # Triangulation # --------------------------------------------------------------------------- + def plot_triangulation(tri_obj): """Plot the polygon and its triangulation diagonals. @@ -277,18 +365,34 @@ def plot_triangulation(tri_obj): xs = list(tri_obj.poly[:, 0]) + [tri_obj.poly[0, 0]] ys = list(tri_obj.poly[:, 1]) + [tri_obj.poly[0, 1]] - ax.fill(xs, ys, - facecolor=(*_hex_to_rgb(_MIST), _FILL_ALPHA), - edgecolor=_INK, linewidth=_LINE_W, zorder=2) + ax.fill( + xs, + ys, + facecolor=(*_hex_to_rgb(_MIST), _FILL_ALPHA), + edgecolor=_INK, + linewidth=_LINE_W, + zorder=2, + ) for diag in tri_obj.diagonals: - ax.plot([diag[0][0], diag[1][0]], [diag[0][1], diag[1][1]], - color=_SILVER, linewidth=_LINE_W_THIN, - linestyle=(0, (3, 4)), zorder=3) + ax.plot( + [diag[0][0], diag[1][0]], + [diag[0][1], diag[1][1]], + color=_SILVER, + linewidth=_LINE_W_THIN, + linestyle=(0, (3, 4)), + zorder=3, + ) - ax.scatter(tri_obj.poly[:, 0], tri_obj.poly[:, 1], - c=_ACCENT, s=_PT_SIZE, zorder=4, - edgecolors="white", linewidths=_PT_EDGE_W) + ax.scatter( + tri_obj.poly[:, 0], + tri_obj.poly[:, 1], + c=_ACCENT, + s=_PT_SIZE, + zorder=4, + edgecolors="white", + linewidths=_PT_EDGE_W, + ) plt.tight_layout() plt.show() @@ -298,6 +402,7 @@ def plot_triangulation(tri_obj): # Delaunay Triangulation # --------------------------------------------------------------------------- + def plot_delaunay(dt_obj, title="Delaunay Triangulation", show_circumcircles=False): """Plot the Delaunay triangulation. @@ -317,32 +422,43 @@ def plot_delaunay(dt_obj, title="Delaunay Triangulation", show_circumcircles=Fal for tri in triangles: xs = [p[0] for p in tri] + [tri[0][0]] ys = [p[1] for p in tri] + [tri[0][1]] - ax.fill(xs, ys, - facecolor=(*_hex_to_rgb(_MIST), 0.05), - edgecolor="none", zorder=1) + ax.fill( + xs, ys, facecolor=(*_hex_to_rgb(_MIST), 0.05), edgecolor="none", zorder=1 + ) # Edges for edge in edges: - ax.plot([edge[0][0], edge[1][0]], [edge[0][1], edge[1][1]], - color=_CHARCOAL, linewidth=_LINE_W, zorder=2) + ax.plot( + [edge[0][0], edge[1][0]], + [edge[0][1], edge[1][1]], + color=_CHARCOAL, + linewidth=_LINE_W, + zorder=2, + ) # Circumcircles if show_circumcircles: for circ in dt_obj.get_circumcircles(): circle_patch = plt.Circle( - circ[0], circ[1], + circ[0], + circ[1], facecolor="none", edgecolor=(*_hex_to_rgb(_SILVER), 0.4), linewidth=_LINE_W_THIN, - linestyle=(0, (4, 5)), zorder=3, + linestyle=(0, (4, 5)), + zorder=3, ) ax.add_patch(circle_patch) # Points on top ax.scatter( - dt_obj.points[:, 0], dt_obj.points[:, 1], - c=_ACCENT, s=_PT_SIZE, zorder=4, - edgecolors="white", linewidths=_PT_EDGE_W, + dt_obj.points[:, 0], + dt_obj.points[:, 1], + c=_ACCENT, + s=_PT_SIZE, + zorder=4, + edgecolors="white", + linewidths=_PT_EDGE_W, ) plt.tight_layout() @@ -353,6 +469,7 @@ def plot_delaunay(dt_obj, title="Delaunay Triangulation", show_circumcircles=Fal # Segment Intersections # --------------------------------------------------------------------------- + def plot_intersections(si_obj, title="Segment Intersections"): """Plot segments and their intersection points. @@ -370,24 +487,40 @@ def plot_intersections(si_obj, title="Segment Intersections"): # Draw segments for seg in segs: ax.plot( - [seg[0][0], seg[1][0]], [seg[0][1], seg[1][1]], - color=_CHARCOAL, linewidth=_LINE_W, zorder=2, + [seg[0][0], seg[1][0]], + [seg[0][1], seg[1][1]], + color=_CHARCOAL, + linewidth=_LINE_W, + zorder=2, solid_capstyle="round", ) # Segment endpoints endpoints_x = [p[0] for seg in segs for p in seg] endpoints_y = [p[1] for seg in segs for p in seg] - ax.scatter(endpoints_x, endpoints_y, c=_INK, s=_PT_SIZE, zorder=3, - edgecolors="white", linewidths=_PT_EDGE_W) + ax.scatter( + endpoints_x, + endpoints_y, + c=_INK, + s=_PT_SIZE, + zorder=3, + edgecolors="white", + linewidths=_PT_EDGE_W, + ) # Intersection points if intersections: ix = [p[0] for p in intersections] iy = [p[1] for p in intersections] ax.scatter( - ix, iy, c=_ACCENT, s=50, zorder=5, - marker="o", edgecolors="white", linewidths=1.2, + ix, + iy, + c=_ACCENT, + s=50, + zorder=5, + marker="o", + edgecolors="white", + linewidths=1.2, ) plt.tight_layout() @@ -398,6 +531,7 @@ def plot_intersections(si_obj, title="Segment Intersections"): # Voronoi # --------------------------------------------------------------------------- + def plot_voronoi(voronoi_obj, cells): """Plot the Voronoi diagram showing sites and cell edges. @@ -412,13 +546,24 @@ def plot_voronoi(voronoi_obj, cells): for c in cells: for line in c[1]: - ax.plot([line[0][0], line[1][0]], [line[0][1], line[1][1]], - linewidth=_LINE_W_THIN, - color=(*_hex_to_rgb(_CHARCOAL), 0.45), - solid_capstyle="round", zorder=2) + ax.plot( + [line[0][0], line[1][0]], + [line[0][1], line[1][1]], + linewidth=_LINE_W_THIN, + color=(*_hex_to_rgb(_CHARCOAL), 0.45), + solid_capstyle="round", + zorder=2, + ) - ax.scatter(data[:, 0], data[:, 1], c=_ACCENT, s=_PT_SIZE, zorder=4, - edgecolors="white", linewidths=_PT_EDGE_W) + ax.scatter( + data[:, 0], + data[:, 1], + c=_ACCENT, + s=_PT_SIZE, + zorder=4, + edgecolors="white", + linewidths=_PT_EDGE_W, + ) pad_x = (data[:, 0].max() - data[:, 0].min()) * 0.12 pad_y = (data[:, 1].max() - data[:, 1].min()) * 0.12 diff --git a/examples/convex_hull.py b/examples/convex_hull.py index 95dd9ba..0cc77e5 100644 --- a/examples/convex_hull.py +++ b/examples/convex_hull.py @@ -2,7 +2,16 @@ from cgeom.visualization import plot_convex_hull # create a list of points -points = [(326, 237),(373, 209), (378, 265), (443, 241), (396, 231), (416, 270), (361, 335), (324, 297)] +points = [ + (326, 237), + (373, 209), + (378, 265), + (443, 241), + (396, 231), + (416, 270), + (361, 335), + (324, 297), +] # create a convex hull object with the list of points convex_hull = ConvexHull(points) @@ -11,4 +20,4 @@ plot_convex_hull(convex_hull) # print the indexes of the points that form the convex hull -print("Convex Hull vertex indices:", convex_hull.get_indexes()) \ No newline at end of file +print("Convex Hull vertex indices:", convex_hull.get_indexes()) diff --git a/examples/delaunay.py b/examples/delaunay.py index 907981d..12e2b15 100644 --- a/examples/delaunay.py +++ b/examples/delaunay.py @@ -3,8 +3,16 @@ # create a list of points points = [ - (326, 237), (373, 209), (378, 265), (443, 241), (396, 231), - (416, 270), (361, 335), (324, 297), (400, 306), (454, 315), + (326, 237), + (373, 209), + (378, 265), + (443, 241), + (396, 231), + (416, 270), + (361, 335), + (324, 297), + (400, 306), + (454, 315), ] # create a Delaunay triangulation object diff --git a/examples/elements.py b/examples/elements.py index b0e6471..b445fd2 100644 --- a/examples/elements.py +++ b/examples/elements.py @@ -1,15 +1,16 @@ -import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt -from cgeom import Point, Line, Segment, Circle, Polygon -from cgeom.algorithms import MinimumCircle, ConvexHull +import numpy as np + +from cgeom import Circle, Line, Point, Polygon, Segment +from cgeom.algorithms import ConvexHull, MinimumCircle # --- Point --- # Multiple construction forms -p1 = Point(x=1.0, y=2.0) # keyword -p2 = Point([3, 4]) # from list -p3 = Point((5, 6)) # from tuple -p4 = Point(np.array([7, 8])) # from numpy array +p1 = Point(x=1.0, y=2.0) # keyword +p2 = Point([3, 4]) # from list +p3 = Point((5, 6)) # from tuple +p4 = Point(np.array([7, 8])) # from numpy array # Destructuring and indexing x, y = p1 @@ -31,8 +32,8 @@ # Vertical line vertical = Line([[3, 0], [3, 5]]) -print(f"Vertical line slope: {vertical.slope}") # None -print(f"Vertical y-intercept: {vertical.y_intercept}") # None +print(f"Vertical line slope: {vertical.slope}") # None +print(f"Vertical y-intercept: {vertical.y_intercept}") # None # --- Segment --- seg = Segment([[0, 0], [3, 4]]) @@ -75,45 +76,71 @@ print(f"\nMinimumCircle -> Circle: {circle}") # Polygon from ConvexHull output -hull_points = [(326, 237), (373, 209), (378, 265), (443, 241), - (396, 231), (416, 270), (361, 335), (324, 297)] +hull_points = [ + (326, 237), + (373, 209), + (378, 265), + (443, 241), + (396, 231), + (416, 270), + (361, 335), + (324, 297), +] ch = ConvexHull(hull_points) hull_vertices = ch.convex_hull() # returns [[x, y], ...] hull_polygon = Polygon(hull_vertices) -print(f"ConvexHull -> Polygon: {hull_polygon.num_vertices} vertices, area={hull_polygon.area:.1f}") +print( + f"ConvexHull -> Polygon: {hull_polygon.num_vertices} vertices, area={hull_polygon.area:.1f}" +) # --------------------------------------------------------------------------- # Grid plot of all elements # Mirrors the project palette (cgeom.visualization._plotting) # --------------------------------------------------------------------------- -_INK = "#002855" # Prussian Blue — primary structure, edges -_ACCENT = "#0466c8" # Smart Blue — focal data points, emphasis -_CHARCOAL = "#023e7d" # Regal Navy — secondary edges, lines -_STEEL = "#5c677d" # Blue Slate — labels -_SILVER = "#7d8597" # Slate Grey — annotations, dashes -_ASH = "#979dac" # Lavender Grey — pill borders, ticks -_MIST = "#e6e9f1" # faint lavender — grid lines -_FILL = "#eef1f6" # panel/shape fill -_BG = "#ffffff" # figure background +_INK = "#002855" # Prussian Blue — primary structure, edges +_ACCENT = "#0466c8" # Smart Blue — focal data points, emphasis +_CHARCOAL = "#023e7d" # Regal Navy — secondary edges, lines +_STEEL = "#5c677d" # Blue Slate — labels +_SILVER = "#7d8597" # Slate Grey — annotations, dashes +_ASH = "#979dac" # Lavender Grey — pill borders, ticks +_MIST = "#e6e9f1" # faint lavender — grid lines +_FILL = "#eef1f6" # panel/shape fill +_BG = "#ffffff" # figure background -mpl.rcParams.update({ - "font.family": "sans-serif", - "font.sans-serif": ["Inter", "Helvetica Neue", "Helvetica", - "Arial", "DejaVu Sans"], - "axes.unicode_minus": False, - "figure.dpi": 150, - "savefig.dpi": 220, -}) +mpl.rcParams.update( + { + "font.family": "sans-serif", + "font.sans-serif": [ + "Inter", + "Helvetica Neue", + "Helvetica", + "Arial", + "DejaVu Sans", + ], + "axes.unicode_minus": False, + "figure.dpi": 150, + "savefig.dpi": 220, + } +) def _style_ax(ax, index, title): """Apply a clean, modern panel look to an axes object.""" # small index "eyebrow" above a left-aligned lightweight title - ax.text(0.0, 1.20, f"{index:02d}", transform=ax.transAxes, - fontsize=8, fontweight="700", color=_SILVER, - ha="left", va="bottom") - ax.set_title(f"{title}", loc="left", fontsize=12.5, - fontweight="500", color=_INK, pad=10) + ax.text( + 0.0, + 1.20, + f"{index:02d}", + transform=ax.transAxes, + fontsize=8, + fontweight="700", + color=_SILVER, + ha="left", + va="bottom", + ) + ax.set_title( + f"{title}", loc="left", fontsize=12.5, fontweight="500", color=_INK, pad=10 + ) ax.set_facecolor(_BG) ax.set_axisbelow(True) @@ -123,15 +150,22 @@ def _style_ax(ax, index, title): ax.tick_params(colors=_STEEL, labelsize=7, length=0, width=0, pad=4) -def _pill(ax, x, y, text, dx=8, dy=8, ha="left", va="center", - color=_STEEL, weight="500"): +def _pill( + ax, x, y, text, dx=8, dy=8, ha="left", va="center", color=_STEEL, weight="500" +): """Draw a small rounded label badge anchored to a data point.""" ax.annotate( - text, (x, y), textcoords="offset points", xytext=(dx, dy), - fontsize=7.5, color=color, ha=ha, va=va, fontweight=weight, + text, + (x, y), + textcoords="offset points", + xytext=(dx, dy), + fontsize=7.5, + color=color, + ha=ha, + va=va, + fontweight=weight, zorder=6, - bbox=dict(boxstyle="round,pad=0.32", fc="white", - ec=_ASH, lw=0.7), + bbox=dict(boxstyle="round,pad=0.32", fc="white", ec=_ASH, lw=0.7), ) @@ -142,14 +176,26 @@ def _pill(ax, x, y, text, dx=8, dy=8, ha="left", va="center", ax = axes[0, 0] _style_ax(ax, 1, "Point") # distance connector first (behind points) -ax.plot([p1.x, p2.x], [p1.y, p2.y], color=_SILVER, - linewidth=1.0, linestyle=(0, (3, 3)), zorder=2) +ax.plot( + [p1.x, p2.x], + [p1.y, p2.y], + color=_SILVER, + linewidth=1.0, + linestyle=(0, (3, 3)), + zorder=2, +) for p, label in [(p1, "keyword"), (p2, "list"), (p3, "tuple"), (p4, "numpy")]: - ax.scatter(p.x, p.y, c=_ACCENT, s=46, zorder=4, - edgecolors="white", linewidths=0.9) + ax.scatter(p.x, p.y, c=_ACCENT, s=46, zorder=4, edgecolors="white", linewidths=0.9) _pill(ax, p.x, p.y, label, dx=9, dy=9, ha="left") -_pill(ax, (p1.x + p2.x) / 2, (p1.y + p2.y) / 2, - f"d = {p1.distance_to(p2):.2f}", dx=10, dy=-12, color=_CHARCOAL) +_pill( + ax, + (p1.x + p2.x) / 2, + (p1.y + p2.y) / 2, + f"d = {p1.distance_to(p2):.2f}", + dx=10, + dy=-12, + color=_CHARCOAL, +) ax.margins(0.18) # 2) Line ------------------------------------------------------------------- @@ -158,10 +204,23 @@ def _pill(ax, x, y, text, dx=8, dy=8, ha="left", va="center", ax.set_xlim(-1, 5) ax.set_ylim(-2, 8) lx = np.array([-0.6, 4.2]) -ax.plot(lx, line.slope * lx + line.y_intercept, color=_INK, - linewidth=1.8, zorder=3, solid_capstyle="round") -ax.scatter([line.point1.x, line.point2.x], [line.point1.y, line.point2.y], - c=_ACCENT, s=42, zorder=5, edgecolors="white", linewidths=0.9) +ax.plot( + lx, + line.slope * lx + line.y_intercept, + color=_INK, + linewidth=1.8, + zorder=3, + solid_capstyle="round", +) +ax.scatter( + [line.point1.x, line.point2.x], + [line.point1.y, line.point2.y], + c=_ACCENT, + s=42, + zorder=5, + edgecolors="white", + linewidths=0.9, +) ax.axvline(x=3, color=_SILVER, linewidth=1.6, linestyle=(0, (4, 4)), zorder=2) _pill(ax, 2.0, 4.0, f"slope = {line.slope:.0f}", dx=10, dy=-2, color=_CHARCOAL) _pill(ax, 3.0, 6.6, "slope = None", dx=10, dy=0, color=_STEEL) @@ -170,38 +229,87 @@ def _pill(ax, x, y, text, dx=8, dy=8, ha="left", va="center", ax = axes[0, 2] _style_ax(ax, 3, "Segment") ax.set_aspect("equal") -ax.plot([seg.start.x, seg.end.x], [seg.start.y, seg.end.y], - color=_INK, linewidth=2.0, zorder=3, solid_capstyle="round") -ax.scatter([seg.start.x, seg.end.x], [seg.start.y, seg.end.y], - c=_ACCENT, s=42, zorder=5, edgecolors="white", linewidths=0.9) +ax.plot( + [seg.start.x, seg.end.x], + [seg.start.y, seg.end.y], + color=_INK, + linewidth=2.0, + zorder=3, + solid_capstyle="round", +) +ax.scatter( + [seg.start.x, seg.end.x], + [seg.start.y, seg.end.y], + c=_ACCENT, + s=42, + zorder=5, + edgecolors="white", + linewidths=0.9, +) mid = seg.midpoint -ax.scatter(mid.x, mid.y, c="white", s=46, zorder=5, marker="o", - edgecolors=_INK, linewidths=1.4) +ax.scatter( + mid.x, mid.y, c="white", s=46, zorder=5, marker="o", edgecolors=_INK, linewidths=1.4 +) _pill(ax, mid.x, mid.y, "midpoint", dx=11, dy=10, color=_CHARCOAL) -_pill(ax, (seg.start.x + seg.end.x) / 2, (seg.start.y + seg.end.y) / 2, - f"len = {seg.length:.1f}", dx=12, dy=-14, color=_STEEL) +_pill( + ax, + (seg.start.x + seg.end.x) / 2, + (seg.start.y + seg.end.y) / 2, + f"len = {seg.length:.1f}", + dx=12, + dy=-14, + color=_STEEL, +) ax.margins(0.18) # 4) Circle ----------------------------------------------------------------- ax = axes[1, 0] _style_ax(ax, 4, "Circle") ax.set_aspect("equal") -ax.add_patch(plt.Circle((c1.center.x, c1.center.y), c1.radius, - facecolor=_FILL, edgecolor=_CHARCOAL, - linewidth=1.6, zorder=2)) +ax.add_patch( + plt.Circle( + (c1.center.x, c1.center.y), + c1.radius, + facecolor=_FILL, + edgecolor=_CHARCOAL, + linewidth=1.6, + zorder=2, + ) +) # radius indicator -edge = Point(c1.center.x + c1.radius * np.cos(np.pi / 5), - c1.center.y + c1.radius * np.sin(np.pi / 5)) -ax.plot([c1.center.x, edge.x], [c1.center.y, edge.y], - color=_SILVER, linewidth=1.1, linestyle=(0, (3, 3)), zorder=3) -ax.plot(c1.center.x, c1.center.y, "+", color=_CHARCOAL, - markersize=9, markeredgewidth=1.6, zorder=4) +edge = Point( + c1.center.x + c1.radius * np.cos(np.pi / 5), + c1.center.y + c1.radius * np.sin(np.pi / 5), +) +ax.plot( + [c1.center.x, edge.x], + [c1.center.y, edge.y], + color=_SILVER, + linewidth=1.1, + linestyle=(0, (3, 3)), + zorder=3, +) +ax.plot( + c1.center.x, + c1.center.y, + "+", + color=_CHARCOAL, + markersize=9, + markeredgewidth=1.6, + zorder=4, +) pin = Point(3, 4) -ax.scatter(pin.x, pin.y, c=_ACCENT, s=46, zorder=5, - edgecolors="white", linewidths=0.9) +ax.scatter(pin.x, pin.y, c=_ACCENT, s=46, zorder=5, edgecolors="white", linewidths=0.9) _pill(ax, pin.x, pin.y, "contains", dx=9, dy=9, color=_CHARCOAL) -_pill(ax, (c1.center.x + edge.x) / 2, (c1.center.y + edge.y) / 2, - f"r = {c1.radius:.0f}", dx=6, dy=8, color=_STEEL) +_pill( + ax, + (c1.center.x + edge.x) / 2, + (c1.center.y + edge.y) / 2, + f"r = {c1.radius:.0f}", + dx=6, + dy=8, + color=_STEEL, +) margin = c1.radius * 0.22 ax.set_xlim(c1.center.x - c1.radius - margin, c1.center.x + c1.radius + margin) ax.set_ylim(c1.center.y - c1.radius - margin, c1.center.y + c1.radius + margin) @@ -214,12 +322,20 @@ def _pill(ax, x, y, text, dx=8, dy=8, ha="left", va="center", xs = [v[0] for v in verts] + [verts[0][0]] ys = [v[1] for v in verts] + [verts[0][1]] ax.fill(xs, ys, facecolor=_FILL, edgecolor=_INK, linewidth=1.8, zorder=2) -ax.scatter([v[0] for v in verts], [v[1] for v in verts], - c=_ACCENT, s=42, zorder=5, edgecolors="white", linewidths=0.9) +ax.scatter( + [v[0] for v in verts], + [v[1] for v in verts], + c=_ACCENT, + s=42, + zorder=5, + edgecolors="white", + linewidths=0.9, +) cx = sum(v[0] for v in verts) / len(verts) cy = sum(v[1] for v in verts) / len(verts) -_pill(ax, cx, cy, f"area = {triangle.area:.1f}", dx=0, dy=0, - ha="center", color=_CHARCOAL) +_pill( + ax, cx, cy, f"area = {triangle.area:.1f}", dx=0, dy=0, ha="center", color=_CHARCOAL +) ax.margins(0.18) # 6) Algorithm interop — ConvexHull polygon + MinimumCircle ----------------- @@ -231,33 +347,85 @@ def _pill(ax, x, y, text, dx=8, dy=8, ha="left", va="center", hys = [v[1] for v in hv] + [hv[0][1]] mc2 = MinimumCircle() circ2 = Circle(mc2.minimum_circle(hull_points)) -ax.add_patch(plt.Circle((circ2.center.x, circ2.center.y), circ2.radius, - facecolor="none", edgecolor=_SILVER, - linewidth=1.3, linestyle=(0, (4, 4)), zorder=1)) -ax.fill(hxs, hys, facecolor=_FILL, edgecolor=_CHARCOAL, - linewidth=1.6, zorder=2) +ax.add_patch( + plt.Circle( + (circ2.center.x, circ2.center.y), + circ2.radius, + facecolor="none", + edgecolor=_SILVER, + linewidth=1.3, + linestyle=(0, (4, 4)), + zorder=1, + ) +) +ax.fill(hxs, hys, facecolor=_FILL, edgecolor=_CHARCOAL, linewidth=1.6, zorder=2) all_pts = np.array(hull_points) -ax.scatter(all_pts[:, 0], all_pts[:, 1], c=_STEEL, s=26, zorder=3, - edgecolors="white", linewidths=0.7) -ax.scatter([v[0] for v in hv], [v[1] for v in hv], - c=_ACCENT, s=42, zorder=5, edgecolors="white", linewidths=0.9) -ax.plot(circ2.center.x, circ2.center.y, "+", color=_CHARCOAL, - markersize=9, markeredgewidth=1.6, zorder=4) -_pill(ax, circ2.center.x, circ2.center.y + circ2.radius, - "min circle", dx=0, dy=10, ha="center", color=_STEEL) +ax.scatter( + all_pts[:, 0], + all_pts[:, 1], + c=_STEEL, + s=26, + zorder=3, + edgecolors="white", + linewidths=0.7, +) +ax.scatter( + [v[0] for v in hv], + [v[1] for v in hv], + c=_ACCENT, + s=42, + zorder=5, + edgecolors="white", + linewidths=0.9, +) +ax.plot( + circ2.center.x, + circ2.center.y, + "+", + color=_CHARCOAL, + markersize=9, + markeredgewidth=1.6, + zorder=4, +) +_pill( + ax, + circ2.center.x, + circ2.center.y + circ2.radius, + "min circle", + dx=0, + dy=10, + ha="center", + color=_STEEL, +) _pill(ax, hv[0][0], hv[0][1], "hull", dx=-6, dy=-12, ha="right", color=_CHARCOAL) pad = circ2.radius * 0.18 ax.set_xlim(circ2.center.x - circ2.radius - pad, circ2.center.x + circ2.radius + pad) ax.set_ylim(circ2.center.y - circ2.radius - pad, circ2.center.y + circ2.radius + pad) # Header -------------------------------------------------------------------- -fig.text(0.5, 0.975, "Geometric Primitives", ha="center", va="top", - fontsize=17, fontweight="600", color=_INK) -fig.text(0.5, 0.938, "Composable building blocks of the compute-geometry library", - ha="center", va="top", fontsize=9.5, color=_STEEL) +fig.text( + 0.5, + 0.975, + "Geometric Primitives", + ha="center", + va="top", + fontsize=17, + fontweight="600", + color=_INK, +) +fig.text( + 0.5, + 0.938, + "Composable building blocks of the compute-geometry library", + ha="center", + va="top", + fontsize=9.5, + color=_STEEL, +) plt.tight_layout(rect=[0.015, 0.02, 0.985, 0.90], h_pad=3.2, w_pad=2.6) -fig.savefig("public/elements.png", dpi=220, facecolor=_BG, - bbox_inches="tight", pad_inches=0.25) +fig.savefig( + "public/elements.png", dpi=220, facecolor=_BG, bbox_inches="tight", pad_inches=0.25 +) print("\nSaved public/elements.png") plt.show() diff --git a/examples/segment_intersection.py b/examples/segment_intersection.py index c0032a3..ba8b1c1 100644 --- a/examples/segment_intersection.py +++ b/examples/segment_intersection.py @@ -3,10 +3,10 @@ # create a list of segments: X cross + horizontal + vertical segments = [ - [[0, 0], [4, 4]], # diagonal / - [[0, 4], [4, 0]], # diagonal \ - [[0, 2], [4, 2]], # horizontal - [[2, 0], [2, 4]], # vertical + [[0, 0], [4, 4]], # diagonal / + [[0, 4], [4, 0]], # diagonal \ + [[0, 2], [4, 2]], # horizontal + [[2, 0], [2, 4]], # vertical ] # create a SegmentIntersection object diff --git a/examples/voronoi.py b/examples/voronoi.py index caec0ff..f34f8f7 100644 --- a/examples/voronoi.py +++ b/examples/voronoi.py @@ -1,4 +1,5 @@ import numpy as np + from cgeom.algorithms import VoronoiDiagram from cgeom.visualization import plot_voronoi @@ -12,4 +13,4 @@ cells = voronoi.build_voronoi_diagram() # plot the voronoi diagram -plot_voronoi(voronoi, cells) \ No newline at end of file +plot_voronoi(voronoi, cells) diff --git a/pyproject.toml b/pyproject.toml index a01c5a3..38d37db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ [dependency-groups] dev = [ - "flake8>=5.0.4", + "ruff>=0.6.0", "pre-commit>=2.20.0", "pytest>=7.1.3", "commitizen>=2.40.0", @@ -41,3 +41,18 @@ changelog_incremental = true version_files = [ "pyproject.toml:version" ] + +[tool.ruff] +line-length = 88 +target-version = "py312" +extend-exclude = [".venv"] + +[tool.ruff.lint] +# pyflakes (F), pycodestyle errors/warnings (E/W), import sorting (I) +select = ["E", "F", "W", "I"] +# line length is enforced by the formatter, not the linter +ignore = ["E501"] + +[tool.ruff.lint.per-file-ignores] +# __init__.py re-exports symbols for the public API +"__init__.py" = ["F401"] diff --git a/tests/test_convexhull.py b/tests/test_convexhull.py index 9657b42..d63b7ab 100644 --- a/tests/test_convexhull.py +++ b/tests/test_convexhull.py @@ -1,8 +1,10 @@ import matplotlib + matplotlib.use("Agg") import numpy as np import pytest + from cgeom.algorithms import ConvexHull @@ -49,8 +51,13 @@ class TestConvexHullWithInteriorPoints: def setup_method(self): self.points = [ - [0, 0], [10, 0], [10, 10], [0, 10], # corners - [5, 5], [3, 3], [7, 2], # interior + [0, 0], + [10, 0], + [10, 10], + [0, 10], # corners + [5, 5], + [3, 3], + [7, 2], # interior ] self.ch = ConvexHull(self.points) diff --git a/tests/test_delaunay.py b/tests/test_delaunay.py index c2e2744..489d09c 100644 --- a/tests/test_delaunay.py +++ b/tests/test_delaunay.py @@ -1,10 +1,12 @@ """Tests for Delaunay triangulation (Bowyer-Watson).""" import matplotlib + matplotlib.use("Agg") import numpy as np import pytest + from cgeom.algorithms import ConvexHull, DelaunayTriangulation @@ -78,9 +80,18 @@ class TestDelaunayConvexHullRelation: def test_hull_edges_subset(self): pts = [ - [0, 0], [10, 0], [10, 10], [0, 10], - [5, 5], [3, 3], [7, 2], [2, 8], - [8, 6], [6, 1], [1, 5], [9, 9], + [0, 0], + [10, 0], + [10, 10], + [0, 10], + [5, 5], + [3, 3], + [7, 2], + [2, 8], + [8, 6], + [6, 1], + [1, 5], + [9, 9], ] dt = DelaunayTriangulation(pts) ch = ConvexHull(pts) @@ -152,10 +163,26 @@ class TestDelaunayLargerSet: def setup_method(self): self.data = [ - [326, 237], [373, 209], [378, 265], [443, 241], [396, 231], - [416, 270], [361, 335], [324, 297], [400, 306], [454, 315], - [489, 285], [488, 234], [443, 185], [421, 137], [380, 169], - [315, 160], [297, 204], [267, 248], [265, 344], [342, 263], + [326, 237], + [373, 209], + [378, 265], + [443, 241], + [396, 231], + [416, 270], + [361, 335], + [324, 297], + [400, 306], + [454, 315], + [489, 285], + [488, 234], + [443, 185], + [421, 137], + [380, 169], + [315, 160], + [297, 204], + [267, 248], + [265, 344], + [342, 263], ] self.dt = DelaunayTriangulation(self.data) @@ -179,6 +206,7 @@ def test_delaunay_property(self): # Helper # --------------------------------------------------------------------------- + def _assert_delaunay_property(dt): """Assert no input point lies strictly inside any triangle's circumcircle.""" tris = dt.triangulate() diff --git a/tests/test_intersection.py b/tests/test_intersection.py index 098bf85..2715bd2 100644 --- a/tests/test_intersection.py +++ b/tests/test_intersection.py @@ -1,10 +1,12 @@ """Tests for line segment intersection (Bentley-Ottmann and brute force).""" import matplotlib + matplotlib.use("Agg") import numpy as np import pytest + from cgeom.algorithms import SegmentIntersection diff --git a/tests/test_mincircle.py b/tests/test_mincircle.py index 7f69a62..9b17041 100644 --- a/tests/test_mincircle.py +++ b/tests/test_mincircle.py @@ -1,9 +1,12 @@ import matplotlib + matplotlib.use("Agg") import math import random + import pytest + from cgeom.algorithms import MinimumCircle @@ -62,8 +65,12 @@ class TestMinimumCircleWithInterior: def setup_method(self): self.mc = MinimumCircle() self.points = [ - [0, 0], [10, 0], [10, 10], [0, 10], # square - [5, 5], [3, 7], # interior + [0, 0], + [10, 0], + [10, 10], + [0, 10], # square + [5, 5], + [3, 7], # interior ] def test_all_enclosed(self): @@ -85,7 +92,9 @@ class TestMinimumCircleHeuristic: def setup_method(self): self.mc = MinimumCircle() random.seed(42) - self.points = [[random.uniform(-10, 10), random.uniform(-10, 10)] for _ in range(20)] + self.points = [ + [random.uniform(-10, 10), random.uniform(-10, 10)] for _ in range(20) + ] def test_heuristic_encloses_all(self): circle = MinimumCircle.minimum_circle_heuristic(self.points) diff --git a/tests/test_simple.py b/tests/test_simple.py index 487f4cd..c2cb329 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -1,5 +1,6 @@ # create simple test case # run: python -m unittest tests/test_simple.py + def test_simple(): - assert 1 == 1 \ No newline at end of file + assert 1 == 1 diff --git a/tests/test_triangulation.py b/tests/test_triangulation.py index 18c28e0..92acfe5 100644 --- a/tests/test_triangulation.py +++ b/tests/test_triangulation.py @@ -1,8 +1,10 @@ import matplotlib + matplotlib.use("Agg") import numpy as np import pytest + from cgeom.algorithms import PolygonTriangulation @@ -76,9 +78,7 @@ class TestTriangulationLShaped: def setup_method(self): # L-shape in CCW order - poly = np.array([ - [0, 0], [4, 0], [4, 2], [2, 2], [2, 4], [0, 4] - ], dtype=float) + poly = np.array([[0, 0], [4, 0], [4, 2], [2, 2], [2, 4], [0, 4]], dtype=float) self.tri = PolygonTriangulation(poly) def test_diagonal_count(self): diff --git a/tests/test_validation.py b/tests/test_validation.py index aee9328..745d0ed 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1,21 +1,14 @@ """Tests for pydantic input validation on all algorithm models.""" import matplotlib + matplotlib.use("Agg") + import numpy as np import pytest -import warnings from pydantic import ValidationError -from cgeom.elements.models import ( - ConvexHullInput, - DelaunayTriangulationInput, - MinimumCircleInput, - PolygonTriangulationInput, - SegmentIntersectionInput, - VoronoiDiagramInput, -) from cgeom.algorithms import ( ConvexHull, DelaunayTriangulation, @@ -24,14 +17,21 @@ SegmentIntersection, VoronoiDiagram, ) - +from cgeom.elements.models import ( + ConvexHullInput, + DelaunayTriangulationInput, + MinimumCircleInput, + PolygonTriangulationInput, + SegmentIntersectionInput, + VoronoiDiagramInput, +) # --------------------------------------------------------------------------- # ConvexHullInput # --------------------------------------------------------------------------- -class TestConvexHullValidation: +class TestConvexHullValidation: def test_too_few_points(self): with pytest.raises(ValidationError, match="at least 3 points"): ConvexHullInput(points=[[0, 0], [1, 1]]) @@ -74,8 +74,8 @@ def test_integration_rejects_bad_input(self): # MinimumCircleInput # --------------------------------------------------------------------------- -class TestMinimumCircleValidation: +class TestMinimumCircleValidation: def test_too_few_points(self): with pytest.raises(ValidationError, match="at least 2 points"): MinimumCircleInput(points=[[0, 0]]) @@ -102,8 +102,8 @@ def test_integration_heuristic(self): # PolygonTriangulationInput # --------------------------------------------------------------------------- -class TestPolygonTriangulationValidation: +class TestPolygonTriangulationValidation: def test_too_few_vertices(self): with pytest.raises(ValidationError, match="at least 3 vertices"): PolygonTriangulationInput(poly=[[0, 0], [1, 1]]) @@ -118,9 +118,7 @@ def test_non_numeric(self): def test_duplicate_warning(self): with pytest.warns(UserWarning, match="Duplicate"): - PolygonTriangulationInput( - poly=[[0, 0], [4, 0], [2, 3], [0, 0]] - ) + PolygonTriangulationInput(poly=[[0, 0], [4, 0], [2, 3], [0, 0]]) def test_valid_ndarray(self): arr = np.array([[0, 0], [4, 0], [2, 3]], dtype=float) @@ -136,8 +134,8 @@ def test_integration_rejects_bad_input(self): # VoronoiDiagramInput # --------------------------------------------------------------------------- -class TestVoronoiDiagramValidation: +class TestVoronoiDiagramValidation: def test_too_few_points(self): with pytest.raises(ValidationError, match="at least 2 points"): VoronoiDiagramInput(points=[[0, 0]]) @@ -163,8 +161,8 @@ def test_integration_rejects_same_y(self): # DelaunayTriangulationInput # --------------------------------------------------------------------------- -class TestDelaunayTriangulationValidation: +class TestDelaunayTriangulationValidation: def test_too_few_points(self): with pytest.raises(ValidationError, match="at least 3 points"): DelaunayTriangulationInput(points=[[0, 0], [1, 1]]) @@ -206,17 +204,15 @@ def test_integration_rejects_bad_input(self): # SegmentIntersectionInput # --------------------------------------------------------------------------- -class TestSegmentIntersectionValidation: +class TestSegmentIntersectionValidation: def test_too_few_segments(self): with pytest.raises(ValidationError, match="at least 2 segments"): SegmentIntersectionInput(segments=[[[0, 0], [1, 1]]]) def test_zero_length_segment(self): with pytest.raises(ValidationError, match="zero length"): - SegmentIntersectionInput( - segments=[[[0, 0], [0, 0]], [[1, 0], [2, 0]]] - ) + SegmentIntersectionInput(segments=[[[0, 0], [0, 0]], [[1, 0], [2, 0]]]) def test_non_numeric(self): with pytest.raises(ValidationError, match="numeric"): @@ -240,18 +236,14 @@ def test_duplicate_warning(self): def test_valid_input_formats(self): # list - result = SegmentIntersectionInput( - segments=[[[0, 0], [1, 1]], [[0, 1], [1, 0]]] - ) + result = SegmentIntersectionInput(segments=[[[0, 0], [1, 1]], [[0, 1], [1, 0]]]) assert len(result.segments) == 2 # ndarray arr = np.array([[[0, 0], [1, 1]], [[0, 1], [1, 0]]], dtype=float) result = SegmentIntersectionInput(segments=arr) assert len(result.segments) == 2 # tuples - result = SegmentIntersectionInput( - segments=[((0, 0), (1, 1)), ((0, 1), (1, 0))] - ) + result = SegmentIntersectionInput(segments=[((0, 0), (1, 1)), ((0, 1), (1, 0))]) assert len(result.segments) == 2 def test_integration_rejects_bad_input(self): diff --git a/tests/test_voronoi.py b/tests/test_voronoi.py index f88ed77..aa61d88 100644 --- a/tests/test_voronoi.py +++ b/tests/test_voronoi.py @@ -1,8 +1,9 @@ import matplotlib + matplotlib.use("Agg") -import numpy as np import pytest + from cgeom.algorithms import VoronoiDiagram @@ -52,8 +53,12 @@ class TestVoronoiSixPoints: def setup_method(self): self.data = [ - [326, 237], [373, 209], [378, 265], - [443, 241], [396, 231], [416, 270], + [326, 237], + [373, 209], + [378, 265], + [443, 241], + [396, 231], + [416, 270], ] self.vd = VoronoiDiagram(self.data) self.cells = self.vd.build_voronoi_diagram() @@ -96,10 +101,26 @@ class TestVoronoiFullExample: def setup_method(self): self.data = [ - [326, 237], [373, 209], [378, 265], [443, 241], [396, 231], - [416, 270], [361, 335], [324, 297], [400, 306], [454, 315], - [489, 285], [488, 234], [443, 185], [421, 137], [380, 169], - [315, 160], [297, 204], [267, 248], [265, 344], [342, 263], + [326, 237], + [373, 209], + [378, 265], + [443, 241], + [396, 231], + [416, 270], + [361, 335], + [324, 297], + [400, 306], + [454, 315], + [489, 285], + [488, 234], + [443, 185], + [421, 137], + [380, 169], + [315, 160], + [297, 204], + [267, 248], + [265, 344], + [342, 263], ] self.vd = VoronoiDiagram(self.data) self.cells = self.vd.build_voronoi_diagram() diff --git a/uv.lock b/uv.lock index 13b9ef0..12c400c 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -226,10 +226,10 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "commitizen" }, - { name = "flake8" }, { name = "ipykernel" }, { name = "pre-commit" }, { name = "pytest" }, + { name = "ruff" }, ] [package.metadata] @@ -245,10 +245,10 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "commitizen", specifier = ">=2.40.0" }, - { name = "flake8", specifier = ">=5.0.4" }, { name = "ipykernel", specifier = ">=6.25.2" }, { name = "pre-commit", specifier = ">=2.20.0" }, { name = "pytest", specifier = ">=7.1.3" }, + { name = "ruff", specifier = ">=0.6.0" }, ] [[package]] @@ -404,20 +404,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] -[[package]] -name = "flake8" -version = "7.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mccabe" }, - { name = "pycodestyle" }, - { name = "pyflakes" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, -] - [[package]] name = "fonttools" version = "4.62.1" @@ -814,15 +800,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -1154,15 +1131,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] -[[package]] -name = "pycodestyle" -version = "2.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, -] - [[package]] name = "pycparser" version = "3.0" @@ -1258,15 +1226,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] -[[package]] -name = "pyflakes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, -] - [[package]] name = "pygments" version = "2.19.2" @@ -1440,6 +1399,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] +[[package]] +name = "ruff" +version = "0.15.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" }, + { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" }, + { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" }, + { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" }, +] + [[package]] name = "seaborn" version = "0.13.2"