From 866d061755f87af022e250115d0f166b2b6b7614 Mon Sep 17 00:00:00 2001 From: Lars Quentin <lars.quentin@stud.uni-goettingen.de> Date: Thu, 7 Jul 2022 17:42:43 +0200 Subject: [PATCH 1/6] :memo: __init__.py docstring --- curvepy/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/curvepy/__init__.py b/curvepy/__init__.py index 6f12c90..e2a8efe 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. """ -- GitLab From e3118270c34a412d5791cf05d421c3880738f366 Mon Sep 17 00:00:00 2001 From: Lars Quentin <lars.quentin@stud.uni-goettingen.de> Date: Fri, 8 Jul 2022 09:38:33 +0200 Subject: [PATCH 2/6] :memo: utilities.py docstrings --- curvepy/utilities.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/curvepy/utilities.py b/curvepy/utilities.py index 053e903..74b9bb9 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) -- GitLab From 009de4e2f67760f47a1343eb8c9545a9c36c08c8 Mon Sep 17 00:00:00 2001 From: Lars Quentin <lars.quentin@stud.uni-goettingen.de> Date: Fri, 8 Jul 2022 09:42:55 +0200 Subject: [PATCH 3/6] :coffin: removed dead enum --- curvepy/types.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/curvepy/types.py b/curvepy/types.py index 2cf8357..4cc526f 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,13 +14,6 @@ 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] @@ -366,7 +358,7 @@ class MinMaxBox: 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))) + and all(self[2 * i] <= point[i] <= self[(2 * i) + 1] for i in range(len(point))) def same_dimension(self, other: MinMaxBox): return len(self) == len(other) -- GitLab From 39cdcf86e5cba2ba0bb9bda5a466ebb14667737f Mon Sep 17 00:00:00 2001 From: Lars Quentin <lars.quentin@stud.uni-goettingen.de> Date: Fri, 8 Jul 2022 10:40:45 +0200 Subject: [PATCH 4/6] :memo: types --- curvepy/types.py | 331 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 319 insertions(+), 12 deletions(-) diff --git a/curvepy/types.py b/curvepy/types.py index 4cc526f..0cf47bc 100644 --- a/curvepy/types.py +++ b/curvepy/types.py @@ -19,6 +19,20 @@ 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 @@ -26,27 +40,99 @@ 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)) @@ -54,6 +140,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 @@ -62,7 +155,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: @@ -89,13 +181,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: @@ -120,7 +226,13 @@ class Polygon(Sequence): class AbstractTriangle(ABC): + """ + Abstract class representing a Triangle. + Attributes + ---------- + _points: The 3 points representing a triangle. + """ @abstractmethod def __init__(self): # In order to not have unresolved references to _points @@ -128,16 +240,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)) @@ -169,9 +302,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 @@ -184,27 +319,87 @@ 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) @@ -233,8 +428,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 ---------- @@ -288,8 +482,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 ---------- @@ -314,7 +508,31 @@ 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): @@ -328,10 +546,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 @@ -342,28 +575,102 @@ 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: + """ + Returns whether a point is contained in the 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): + 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),)) -- GitLab From 2fea993c4b7bc45adccc427172898eb99fae8740 Mon Sep 17 00:00:00 2001 From: Lars Quentin <lars.quentin@stud.uni-goettingen.de> Date: Fri, 8 Jul 2022 10:41:34 +0200 Subject: [PATCH 5/6] :memo: formatter --- curvepy/types.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/curvepy/types.py b/curvepy/types.py index 0cf47bc..d3a2eaa 100644 --- a/curvepy/types.py +++ b/curvepy/types.py @@ -58,6 +58,7 @@ class Circle: radius: float The radius """ + def __init__(self, center: Point2D, radius: float): self._center = np.array(center) self.radius = radius @@ -233,6 +234,7 @@ class AbstractTriangle(ABC): ---------- _points: The 3 points representing a triangle. """ + @abstractmethod def __init__(self): # In order to not have unresolved references to _points @@ -357,6 +359,7 @@ class TupleTriangle(AbstractTriangle): _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) @@ -399,6 +402,7 @@ class PolygonTriangle(Polygon, AbstractTriangle): make_copy: bool whether the points should be deep copied """ + def __init__(self, points: List[np.ndarray], make_copy: bool = True) -> None: # https://stackoverflow.com/a/26927718 Polygon.__init__(self, points, make_copy) @@ -511,6 +515,7 @@ class Triangle: """ Helper "Factory" that decides which Triangle to instanciate """ + def __call__(self, a, b, c): """ Helper method that decides which Triangle to instanciate. -- GitLab From ca26eee8076a3d6a526efeacab002a8b7c1e3703 Mon Sep 17 00:00:00 2001 From: Lars Quentin <lars.quentin@stud.uni-goettingen.de> Date: Fri, 8 Jul 2022 10:52:48 +0200 Subject: [PATCH 6/6] :green_heart: make CI happy? --- curvepy/types.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/curvepy/types.py b/curvepy/types.py index d3a2eaa..8db3d2a 100644 --- a/curvepy/types.py +++ b/curvepy/types.py @@ -645,8 +645,7 @@ class MinMaxBox: ------- 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))) + 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: """ -- GitLab