diff --git a/curvepy/__init__.py b/curvepy/__init__.py index 6f12c9058587148a0c9d485beaabf419b3219385..e2a8efe69a94cddbb02ef9a9a8f36fb45eeef567 100644 --- a/curvepy/__init__.py +++ b/curvepy/__init__.py @@ -1,3 +1,13 @@ """ -Welcome to curvepy! More documentation will be written soon. +Welcome to curvepy! + +curvepy is an computational geometric library. It's main focus lies on + +- Different Bézier curve implementations and their viability +- A Green-Sibson/Bowyer/Watson based Delaunay triangulation, with translation in it's Voronoi dual graph + +The technical information can be found in the single modules, the mathematical explanations and design decisions +can be found in the accompanying paper. + +Please read the paper for more information. """ diff --git a/curvepy/types.py b/curvepy/types.py index 2cf83572983a62fe3ce7b456271b2094c36f69fb..8db3d2a512d5fa97d8b459f5b034367a2c432286 100644 --- a/curvepy/types.py +++ b/curvepy/types.py @@ -5,7 +5,6 @@ import sys from abc import ABC, abstractmethod from collections.abc import Sequence from dataclasses import dataclass -from enum import Enum from functools import cached_property, lru_cache, partial from typing import (Any, Callable, Deque, Dict, List, NamedTuple, Optional, Tuple, Union) @@ -15,18 +14,25 @@ import scipy.special as scs from curvepy.utilities import create_straight_line_function - -class CurveTypes(Enum): - bezier_curve = 0 - bezier_curve_threaded = 1 - bezier_curve_blossoms = 2 - - Point2D = Tuple[float, float] Edge2D = Tuple[Point2D, Point2D] class TriangleNode(NamedTuple): + """ + Auxiliary datastructure for easier triangle graph traversal. + Views the triangle from one specific point. + + Attributes + ------- + ccw: Point2D + The point counterclockwise to our pt. + cw: Point2D + The point clockwise to our pt. + pt: Point2D + The third point, from which cw and ccw are semantically defined. + ccc: The circumcircle center. + """ ccw: Point2D cw: Point2D pt: Point2D @@ -34,27 +40,100 @@ class TriangleNode(NamedTuple): class Circle: + """ + A computational representation of a geometric circle. + + + Parameters + ---------- + center: Point2D + The circle center + radius: float + The circle radius + + Attributes + ------- + _center: np.ndarray + The circle center + radius: float + The radius + """ + def __init__(self, center: Point2D, radius: float): self._center = np.array(center) self.radius = radius @property def center(self) -> Point2D: + """ + Property that converts and returns the circle center. + + Returns + ------- + Point2D + The circle center + """ return tuple(self._center) def __contains__(self, pt: Point2D) -> bool: + """ + Checks whether a point lies within the convex set spanned by the circle. + + Parameters + ---------- + pt: Point2D + The point which needs to be checked. + + Returns + ------- + Whether pt lies in the circle. + """ return np.linalg.norm(np.array(pt) - self._center) <= self.radius def __str__(self) -> str: + """ + Generating human readable string representation of circles. + + Returns + ------- + Human readable string representation of circles. + """ return f"(CENTER: {self.center}, RADIUS: {self.radius})" def __repr__(self) -> str: + """ + Generating machine readable string representation of circles. + + Returns + ------- + Machine readable string representation of circles. + """ return f"<CIRCLE: {str(self)}>" def __eq__(self, other: Any) -> bool: + """ + Checks whether 2 objects are the same. + An object is the same iff it is a circle with the same center and radius. + + Parameters + ---------- + other: Any + The compared object. + + Returns + ------- + Whether they are the same circle. + """ return isinstance(other, Circle) and self.center == other.center and self.radius == other.radius def __hash__(self) -> int: + """ + Hashing function, hashes the 3 numbers (x, y, r). + + Returns + ------- + The hash generated by a unique seed. + """ return hash((*self.center, self.radius)) @@ -62,6 +141,13 @@ class Polygon(Sequence): """ Class for creating a 2D or 3D Polygon. + Parameters + ---------- + points: List[np.ndarray] + The points the Polygon is made of + make_copy: bool + whether the points should be deep copied + Attributes ---------- _points: np.ndArray @@ -70,7 +156,6 @@ class Polygon(Sequence): dimension of the polygon _piece_funcs: list list containing all between each points, _points[i] and _points[i+1] - """ def __init__(self, points: List[np.ndarray], make_copy: bool = True) -> None: @@ -97,13 +182,27 @@ class Polygon(Sequence): def __getitem__(self, item: Any) -> Callable[[float], np.ndarray]: """ - Throws ValueError when casting to int. - Throws IndexError when out of bounds. - And probably something else. + Allows us to use the index operator on the underlying vertex array. + + Parameters + ---------- + item: Any + The index + + Returns + ------- + The vertex at the provided index. """ return self._piece_funcs[int(item)] def __len__(self) -> int: + """ + Returns the number of points that make up the polygon. + + Returns + ------- + The number of points that make up the polygon. + """ return len(self._points) def blossom(self, ts: List[float]) -> np.ndarray: @@ -128,6 +227,13 @@ class Polygon(Sequence): class AbstractTriangle(ABC): + """ + Abstract class representing a Triangle. + + Attributes + ---------- + _points: The 3 points representing a triangle. + """ @abstractmethod def __init__(self): @@ -136,16 +242,37 @@ class AbstractTriangle(ABC): @cached_property def lines(self) -> Union[List[Edge2D], List[Tuple[np.ndarray]]]: + """ + Returns the 3 lines of a triangle. + + Returns + ------- + The 3 lines of a triangle. + """ a, b, c = self.points return [(a, b), (b, c), (a, c)] @cached_property def points(self) -> Union[Tuple[Point2D, Point2D, Point2D], List[np.ndarray]]: + """ + Returns the 3 points of a triangle. + + Returns + ------- + The 3 points of a triangle. + """ # If it was mutable caching would break return self._points @cached_property def area(self) -> float: + """ + Returns the area of the triangle. + + Returns + ------- + The area of the triangle. + """ a, b, c = self.points # print(f"{a.shape}, {b.shape}, {c.shape}") return self.calc_area(np.array(a), np.array(b), np.array(c)) @@ -177,9 +304,11 @@ class AbstractTriangle(ABC): @cached_property def circumcircle(self) -> Circle: """ - see: https://www.ics.uci.edu/~eppstein/junkyard/circumcenter.html - :return: + Computes the circumcircle of a triangle. + Returns + ------- + The circumcircle of a triangle. """ a, b, c = self.points @@ -192,27 +321,89 @@ class AbstractTriangle(ABC): return Circle(center=center, radius=radius) def __str__(self) -> str: + """ + Generating human readable string representation of triangles. + + Returns + ------- + Human readable string representation of triangles. + """ return str(self.points) def __repr__(self) -> str: + """ + Generating machine readable string representation of triangles. + + Returns + ------- + Machine readable string representation of triangles. + """ return f"<TRIANLGE: {str(self)}>" class TupleTriangle(AbstractTriangle): + """ + Class for creating a tuple based triangle. + + Parameters + ---------- + a: Point2D + The first point of a triangle. + b: Point2D + The second point of a triangle. + c: Point2D + The third point of a triangle. + + Attributes + ---------- + _points: Tuple[Point2D, Point2D, Point2D] + tuple containing copy of points that create the polygon + """ + def __init__(self, a: Point2D, b: Point2D, c: Point2D): self._points: Tuple[Point2D, Point2D, Point2D] = (a, b, c) def __eq__(self, other: Any) -> bool: + """ + Checks whether 2 objects are the same. + An object is the same iff it is a triangle with the same points. + + Parameters + ---------- + other: Any + The compared object. + + Returns + ------- + Whether they are the same triangle. + """ return isinstance(other, TupleTriangle) and sorted(self.points) == sorted(other.points) def __hash__(self) -> int: + """ + Hashing function, hashes the 3 points. + It is sorted such that ordering does not matter. + + Returns + ------- + The hash generated by a unique seed. + """ return hash(tuple(sorted(self.points))) class PolygonTriangle(Polygon, AbstractTriangle): + """ + Class for creating a polygon based triangle. + + Parameters + ---------- + points: List[np.ndarray] + The points the Polygon is made of + make_copy: bool + whether the points should be deep copied + """ def __init__(self, points: List[np.ndarray], make_copy: bool = True) -> None: - # points.append(points[0]) # https://stackoverflow.com/a/26927718 Polygon.__init__(self, points, make_copy) @@ -241,8 +432,7 @@ class PolygonTriangle(Polygon, AbstractTriangle): for all points of the TupleTriangle to the plane, so that cramer's rule can easily be applied to them in order to calculate the calc_area of the TupleTriangle corresponding to every 3 out of the 4 points. But this method does not overwrite the self._points. - - TODO this is allowed since we use a parallel projection, which is a proper affine transformation. + This is allowed since we use a parallel projection, which is a proper affine transformation. Parameters ---------- @@ -296,8 +486,8 @@ class PolygonTriangle(Polygon, AbstractTriangle): def get_bary_coords(self, p: np.ndarray) -> np.ndarray: """ Calculates the barycentric coordinates of p with respect to the points defining the TupleTriangle. - TODO: Explizit dazuschreiben, dass wir bei rg(A) = 1 trotz Punkt auf der Hyperline verweigern weil degenerate. - TODO: write that p needs to have same dim. + Note that the rg(A) can't be 1 because this would be a degenerated case. + Futher note that p needs to have the same dimension. Parameters ---------- @@ -322,7 +512,32 @@ class PolygonTriangle(Polygon, AbstractTriangle): class Triangle: + """ + Helper "Factory" that decides which Triangle to instanciate + """ + def __call__(self, a, b, c): + """ + Helper method that decides which Triangle to instanciate. + Think of it as a constructor with less overhead. + Pythons call order is + - `__call__` + - `__new__` + - `__init__` + + Parameters + ---------- + a + The first point + b + The second point + c + The third point + + Returns + ------- + The appropriate triangle. + """ if isinstance(a, tuple): return TupleTriangle(a, b, c) elif isinstance(a, np.ndarray): @@ -336,10 +551,25 @@ VoronoiRegions2D = Dict[Point2D, Deque[TriangleNode]] @dataclass(frozen=True) class MinMaxBox: + """ + Dataclass to represent a MinMaxBox, which is an axis parallel rectangle. + + Attributes + ---------- + min_maxs: List[float] + the xmin, xmax, ymin, ymax, ... (for more dimensions) + """ min_maxs: List[float] # [x_min, x_max, y_min, y_max, ...] @cached_property def area(self) -> float: + """ + Calculates the area of the MinMaxBox. + + Returns + ------- + The area of the MinMaxBox. + """ return math.prod([d_max - d_min for d_min, d_max in zip(self[::2], self[1::2])]) @classmethod @@ -350,28 +580,101 @@ class MinMaxBox: return cls(sum([[m[i, :].min(), m[i, :].max()] for i in range(m.shape[0])], [])) def __getitem__(self, item) -> Union[float, List[float]]: + """ + Overrides the [] operator to access the values of the underlying list. + + Parameters + ---------- + item: index + Index or slice + + Returns + ------- + The sliced value of the underlying list. + """ return self.min_maxs.__getitem__(item) def __and__(self, other: MinMaxBox) -> Optional[MinMaxBox]: + """ + Returns the intersection area of two MinMaxBoxes. + + Parameters + ---------- + other: MinMaxBox + The other MinMaxBox from which to generate the intersection. + + Returns + ------- + The intersection area of two MinMaxBoxes. + """ return self.intersect(other) __rand__ = __and__ # For iterators def __len__(self): + """ + Returns the length of the underlying list to make iterators works. + + Returns + ------- + The length of the underlying list to make iterators works. + """ return len(self.min_maxs) def dim(self): + """ + Returns the number of box dimensions. + + Returns + ------- + The number of box dimensions. + """ return len(self) // 2 def __contains__(self, point: Tuple[float, ...]) -> bool: - return self.dim() == len(point) \ - and all(self[2 * i] <= point[i] <= self[(2 * i) + 1] for i in range(len(point))) + """ + Returns whether a point is contained in the MinMaxBox - def same_dimension(self, other: MinMaxBox): + Parameters + ---------- + point: Tuple[float, ...] + The point. Has to be the same dimension to be contained. + + Returns + ------- + Whether a point is contained in the MinMaxBox + """ + return self.dim() == len(point) and all(self[2 * i] <= point[i] <= self[(2 * i) + 1] for i in range(len(point))) + + def same_dimension(self, other: MinMaxBox) -> bool: + """ + Checks whether 2 MinMaxBoxes have the same dimension. + + Parameters + ---------- + other: MinMaxBox + The other MinMaxBox to compare to. + + Returns + ------- + Whether 2 MinMaxBoxes have the same dimension. + """ return len(self) == len(other) def intersect(self, other: MinMaxBox) -> Optional[MinMaxBox]: + """ + Returns the intersection of two MinMaxBoxes, if they intersect, otherwise None. + + Parameters + ---------- + other: MinMaxBox + The other MinMaxBox to check the intersection with. + + Returns + ------- + The intersection of two MinMaxBoxes, if they intersect, otherwise None. + """ if not self.same_dimension(other): return None res = np.zeros((len(self),)) diff --git a/curvepy/utilities.py b/curvepy/utilities.py index 053e903534846d5aecbbddb63aaef30ef1aeb9aa..74b9bb904b1469a36eac32445eced217cadd36e2 100644 --- a/curvepy/utilities.py +++ b/curvepy/utilities.py @@ -110,15 +110,12 @@ a + x(b + x(c+x*(d))) def horner(m: np.ndarray, t: float = 0.5) -> Tuple[Union[float, Any], ...]: """ - TODO show which problem this is - TODO besserer Name sowie auch BezierCurveHorner mit horner-bez - TODO First coeff == Highest Degree - Method using horner's method to calculate point with given t + Method using horner schema of the De Casteljau algorithm to calculate a single point. Parameters ---------- m: np.ndarray: - array containing coefficients + array containing coefficients. Note that the first coefficient is the highest degree. t: float: value for which point is calculated @@ -233,8 +230,32 @@ def intersect_lines(p1: np.ndarray, p2: np.ndarray, p3: np.ndarray, p4: np.ndarr def flatten_list_of_lists(xss: List[List[Any]]) -> List[Any]: + """ + Reduces one list dimension by using using it's `__add__`. + + Parameters + ---------- + xss: List[List[Any]] + Any list that needs a reduced version. + + Returns + ------- + The same list, flattened by one dimension + """ return sum(xss, []) def prod(xs: Iterable[Number]): + """ + The multiplical product of a bunch of numbers. + + Parameters + ---------- + xs: Iterable[Number] + The numbers to multiply + + Returns + ------- + The product, 1 if empty. + """ return functools.reduce(operator.mul, xs, 1)