Source code for cgeom.elements.models

"""Pydantic validation models for computational geometry algorithm inputs."""

import warnings
from typing import List

import numpy as np
from pydantic import BaseModel, model_validator


def _to_point_list(raw) -> List[List[float]]:
    """Accept list/tuple/ndarray, validate shape (n, 2), return List[List[float]]."""
    if raw is None:
        raise ValueError("Points data is required")

    if isinstance(raw, np.ndarray):
        arr = raw.astype(float)
    else:
        try:
            arr = np.array(raw, dtype=float)
        except (ValueError, TypeError):
            raise ValueError("Points must contain only numeric values")

    if arr.ndim != 2 or arr.shape[1] != 2:
        raise ValueError(f"Points must have shape (n, 2), got shape {arr.shape}")

    return arr.tolist()


def _has_duplicates(points: List[List[float]]) -> bool:
    """Set-based duplicate check on point coordinates."""
    seen = set()
    for p in points:
        key = (p[0], p[1])
        if key in seen:
            return True
        seen.add(key)
    return False


def _all_collinear(points: List[List[float]]) -> bool:
    """Cross-product test: returns True if all points lie on the same line."""
    if len(points) < 3:
        return True
    x0, y0 = points[0]
    x1, y1 = points[1]
    for i in range(2, len(points)):
        x2, y2 = points[i]
        cross = (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0)
        if abs(cross) > 1e-10:
            return False
    return True


[docs] class ConvexHullInput(BaseModel): """Validation model for ConvexHull inputs.""" points: List[List[float]]
[docs] @model_validator(mode="before") @classmethod def validate_input(cls, data): if isinstance(data, dict): raw = data.get("points") else: raw = data points = _to_point_list(raw) if len(points) < 3: raise ValueError("ConvexHull requires at least 3 points") if _all_collinear(points): raise ValueError("All points are collinear; convex hull is undefined") if _has_duplicates(points): warnings.warn( "Duplicate points detected; they will be included but may " "not affect the hull", UserWarning, ) return {"points": points}
[docs] class MinimumCircleInput(BaseModel): """Validation model for MinimumCircle inputs.""" points: List[List[float]]
[docs] @model_validator(mode="before") @classmethod def validate_input(cls, data): if isinstance(data, dict): raw = data.get("points") else: raw = data points = _to_point_list(raw) if len(points) < 2: raise ValueError("MinimumCircle requires at least 2 points") if _has_duplicates(points): warnings.warn("Duplicate points detected", UserWarning) return {"points": points}
[docs] class PolygonTriangulationInput(BaseModel): """Validation model for PolygonTriangulation inputs.""" poly: List[List[float]] poly_name: str = "Polygon"
[docs] @model_validator(mode="before") @classmethod def validate_input(cls, data): if not isinstance(data, dict): raise ValueError( "PolygonTriangulationInput expects keyword arguments " "(poly=..., poly_name=...)" ) raw = data.get("poly") poly_name = data.get("poly_name", "Polygon") points = _to_point_list(raw) if len(points) < 3: raise ValueError("Polygon triangulation requires at least 3 vertices") if _all_collinear(points): raise ValueError("All vertices are collinear; polygon is degenerate") if _has_duplicates(points): warnings.warn("Duplicate vertices detected in polygon", UserWarning) return {"poly": points, "poly_name": poly_name}
[docs] class VoronoiDiagramInput(BaseModel): """Validation model for VoronoiDiagram inputs.""" points: List[List[float]]
[docs] @model_validator(mode="before") @classmethod def validate_input(cls, data): if isinstance(data, dict): raw = data.get("points") else: raw = data points = _to_point_list(raw) if len(points) < 2: raise ValueError("Voronoi diagram requires at least 2 points") if abs(points[0][1] - points[1][1]) < 1e-10: raise ValueError( "First two points have the same y-coordinate, which causes " "division by zero in the Voronoi construction. " "Reorder points so the first two have different y-coordinates." ) if _has_duplicates(points): warnings.warn("Duplicate points detected", UserWarning) return {"points": points}
def _to_segment_list(raw) -> List[List[List[float]]]: """Accept list/tuple/ndarray of segments, validate shape (n, 2, 2).""" if raw is None: raise ValueError("Segments data is required") # 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] if isinstance(raw, np.ndarray): arr = raw.astype(float) else: try: arr = np.array(raw, dtype=float) except (ValueError, TypeError): 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}") return arr.tolist()
[docs] class SegmentIntersectionInput(BaseModel): """Validation model for SegmentIntersection inputs.""" segments: List[List[List[float]]]
[docs] @model_validator(mode="before") @classmethod def validate_input(cls, data): if isinstance(data, dict): raw = data.get("segments") else: raw = data segments = _to_segment_list(raw) if len(segments) < 2: 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)") 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) break seen.add(key) return {"segments": segments}
[docs] class DelaunayTriangulationInput(BaseModel): """Validation model for DelaunayTriangulation inputs.""" points: List[List[float]]
[docs] @model_validator(mode="before") @classmethod def validate_input(cls, data): if isinstance(data, dict): raw = data.get("points") else: raw = data points = _to_point_list(raw) if len(points) < 3: raise ValueError("Delaunay triangulation requires at least 3 points") if _all_collinear(points): raise ValueError("All points are collinear; triangulation is undefined") if _has_duplicates(points): warnings.warn( "Duplicate points detected; they will be included but may " "not affect the triangulation", UserWarning, ) return {"points": points}