Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 4 additions & 6 deletions .github/workflows/python-package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ __pycache__/
dist/
build/
.venv/

docs/_build/
37 changes: 14 additions & 23 deletions cgeom/algorithms/__init__.py
Original file line number Diff line number Diff line change
@@ -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',
]
"MinimumCircle",
"ConvexHull",
"PolygonTriangulation",
"VoronoiDiagram",
"DelaunayTriangulation",
"SegmentIntersection",
]
21 changes: 17 additions & 4 deletions cgeom/algorithms/_convexhull.py
Original file line number Diff line number Diff line change
@@ -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""

Expand All @@ -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.

Expand All @@ -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)

Expand Down Expand Up @@ -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:
Expand All @@ -86,16 +97,18 @@ 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.",
DeprecationWarning,
stacklevel=2,
)
from cgeom.visualization import plot_convex_hull

plot_convex_hull(self, title)

def get_indexes(self):
Expand All @@ -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
51 changes: 33 additions & 18 deletions cgeom/algorithms/_delaunay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -140,13 +146,15 @@ 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.",
DeprecationWarning,
stacklevel=2,
)
from cgeom.visualization import plot_delaunay

plot_delaunay(self, title)

@staticmethod
Expand All @@ -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

Expand All @@ -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

Expand Down
13 changes: 9 additions & 4 deletions cgeom/algorithms/_intersection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -159,6 +160,7 @@ def neighbors(self, seg_idx):
# Main class
# ---------------------------------------------------------------------------


class SegmentIntersection:
"""Find intersection points among a set of 2D line segments.

Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -303,13 +306,15 @@ 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.",
DeprecationWarning,
stacklevel=2,
)
from cgeom.visualization import plot_intersections

plot_intersections(self, title)

def _normalize_segments(self):
Expand Down
Loading
Loading