From 49d88909c2790dd7f245b2afd5639f0f517a2395 Mon Sep 17 00:00:00 2001 From: Rhobar9 <86721432+Rhobar9@users.noreply.github.com> Date: Mon, 29 May 2023 13:40:37 -0700 Subject: [PATCH 1/7] added_docs_for_exceptions_and_Delaunay_class --- .idea/.gitignore | 3 +++ .idea/alpha_shapes.iml | 17 ++++++++++++++++ .../inspectionProfiles/profiles_settings.xml | 6 ++++++ .idea/misc.xml | 7 +++++++ .idea/modules.xml | 8 ++++++++ .idea/vcs.xml | 6 ++++++ alpha_shapes/alpha_shapes.py | 20 +++++++++++++++---- 7 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/alpha_shapes.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/alpha_shapes.iml b/.idea/alpha_shapes.iml new file mode 100644 index 0000000..5195124 --- /dev/null +++ b/.idea/alpha_shapes.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..971acd1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..871fc4d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/alpha_shapes/alpha_shapes.py b/alpha_shapes/alpha_shapes.py index ea78528..d0bcdc7 100644 --- a/alpha_shapes/alpha_shapes.py +++ b/alpha_shapes/alpha_shapes.py @@ -1,5 +1,6 @@ """ -Utility module for the calculation of alpha shapes +This is a core module of package, which contains exceptions, functions +and classes essential to printing figures. """ import numpy as np @@ -10,28 +11,37 @@ class AlphaException(Exception): + """Abstract class for all exceptions which will be raised within Alpha_Shaper class + directly or through Delaunay class""" pass class NotEnoughPoints(AlphaException): + """If instance of class Delaunay has less than 3 points, this exception will be raised""" pass class OptimizationFailure(AlphaException): + """If Alpha_Shaper instance can't cover all vertices, this exception will be raised""" pass -class OptimizationWarnging(UserWarning): +class OptimizationWarning(UserWarning): + """Warns user without interrupting the program""" pass class Delaunay(Triangulation): """ - Visitor sublclass of matplotlib.tri.Triangulation. - Mimics scipy.spatial.Delaunay interface. + Delaunay is abstract class, which derives from matplotlib.tri.Triangulation. + It adds set of coordinates and essential methods. """ def __init__(self, coords: NDArray): + """In try block function invokes __init__ method of class + Triangulation from matplotlib package and sends + to it default set of coordinates. If ValueError occurs, + function will tackle it.""" try: super().__init__(x=coords[:, 0], y=coords[:, 1]) except ValueError as e: @@ -42,9 +52,11 @@ def __init__(self, coords: NDArray): @property def simplices(self): + """Creates simplices property essential to further operations""" return self.triangles def __len__(self): + """Returns amount of object's edges""" return self.simplices.shape[0] From a99fd0a534263bff1d7c5588b256e8de21911de3 Mon Sep 17 00:00:00 2001 From: Aleksander Koprowski Date: Sat, 3 Jun 2023 13:04:47 -0700 Subject: [PATCH 2/7] added_docs_and_type_hints_for_Alpha_Shaper_class --- alpha_shapes/alpha_shapes.py | 89 ++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/alpha_shapes/alpha_shapes.py b/alpha_shapes/alpha_shapes.py index d0bcdc7..5608053 100644 --- a/alpha_shapes/alpha_shapes.py +++ b/alpha_shapes/alpha_shapes.py @@ -61,7 +61,9 @@ def __len__(self): class Alpha_Shaper(Delaunay): + """Crucial class to creating alpha-shapes""" def __init__(self, points: ArrayLike, normalize=True): + """Assings points to instance, creates basic properties and denormalizes points""" self.normalized = normalize points = np.array(points) @@ -77,37 +79,50 @@ def __init__(self, points: ArrayLike, normalize=True): if self.normalized: self._denormalize(center, scale) - def _denormalize(self, center, scale): + def _denormalize(self, center: ArrayLike, scale: ArrayLike): + """Transforms back points into their orginal scale""" self.x = self.x * scale[0] + center[0] self.y = self.y * scale[1] + center[1] - def _calculate_cirumradii_sq_of_internal_triangles(self): + def _calculate_cirumradii_sq_of_internal_triangles(self) -> NDArray: + """Main-line method, which calculates circumradius squares of all internal triangles + and saves them into numpy.array. It's important during initialization of object.""" circumradii_sq = [ self._get_circumradius_sq_of_internal_simplex(smpl) for smpl in self.simplices ] return np.array(circumradii_sq) - def _get_circumradius_sq_of_internal_simplex(self, smpl): + def _get_circumradius_sq_of_internal_simplex(self, smpl: ArrayLike) -> NDArray: + """Reads values of simplices and then + sends it to _calculate_cirumradius_sq_of_triangle function.""" x = self.x[smpl] y = self.y[smpl] return _calculate_cirumradius_sq_of_triangle(x, y) - def _sorted_simplices(self): + def _sorted_simplices(self) -> NDArray: + """Returns all simplices of instance, sorted before by given axis""" return self.simplices[self.argsort] - def _sorted_circumradii_sw(self): + def _sorted_circumradii_sw(self) -> NDArray: + """Returns sorted values of squares circumraiduses of internal triangles""" return self.circumradii_sq[self.argsort] - def _shape_from_simplices(self, simplices): + def _shape_from_simplices(self, simplices: ArrayLike) -> ArrayLike: + """Sends values of simplices to _simplex_to_triangle + function and saves triangles. Function shapely.ops.unary_union can receive and return many types of objects, + its output depends on input types. In this case function will return array of ints.""" triangles = [_simplex_to_triangle(smpl, self) for smpl in simplices] return unary_union(triangles) - def get_mask(self, alpha): + def get_mask(self, alpha: float) -> NDArray: + """Creates mask, based on squares circumradiuses of internal triangles""" return self.circumradii_sq > 1 / alpha**2 - def get_shape(self, alpha): + def get_shape(self, alpha: float) -> ArrayLike: + """Returns shape, constrained by aplha in form of array. + If aplha is less than 0, function will use original array of simplices""" if alpha > 0: select = self.circumradii_sq <= 1 / alpha**2 simplices = self.simplices[select] @@ -116,29 +131,23 @@ def get_shape(self, alpha): return self._shape_from_simplices(simplices) - def _nth_shape(self, n): - """ - return the shape formed by the n smallest simplices - """ + def _nth_shape(self, n: int) -> ArrayLike: + """Returns the shape formed by the amount of n-smallest simplices""" simplices = self._sorted_simplices()[:n] return self._shape_from_simplices(simplices) - def all_vertices(self): + def all_vertices(self) -> set: + """Returns all vertices of object by set""" return set(np.ravel(self.simplices)) - def _uncovered_vertices(self, simplices): - """ - Return a set of vertices that is not covered by the - specified simplices. - """ + def _uncovered_vertices(self, simplices: ArrayLike) -> set: + """Returns a set of vertices, which is not covered by the specified simplices.""" return self.all_vertices() - set(np.ravel(simplices)) - def _get_minimum_fully_covering_index_of_simplices(self): - """ - Return the minimum index of simplices needed to cover all vertices. - The set of all simplices up to this index is fully covering. - """ - # At least N//3 triangles are needed to connect N points. + def _get_minimum_fully_covering_index_of_simplices(self) -> ArrayLike: + """Returns the minimum amount of simplices essential to cover all vertices. + If function face problem with vertices, it will raise OptimizationFailure exception.""" + # We have to use at least N//3 triangles to connect N points. simplices = self._sorted_simplices() n_start = len(self) // 3 n_finish = len(self) @@ -156,8 +165,14 @@ def _get_minimum_fully_covering_index_of_simplices(self): if uncovered_vertices: raise OptimizationFailure("Maybe there are duplicate points?") - def optimize(self): - # At least N//3 triangles are needed to connect N points. + def optimize(self) -> (NDArray, ArrayLike): + """Eliminates redundant simplices and then sets appropriate mask. + + Returns: + alpha_opt: themost accurate alpha value based on minimal amount of simplices + shape: shape after optimization + """ + # We have to use at least N//3 triangles to connect N points n_min = self._get_minimum_fully_covering_index_of_simplices() alpha_opt = 1 / np.sqrt(self._sorted_circumradii_sw()[n_min]) - 1e-10 simplices = self._sorted_simplices() @@ -166,9 +181,7 @@ def optimize(self): return alpha_opt, shape def set_mask_at_alpha(self, alpha: float): - """ - Set the mask for the alpha shape at the specified alpha value. - """ + """Sets the mask for the alpha shape based on the appropriate alpha value.""" mask = self.get_mask(alpha) self.set_mask(mask) return self @@ -221,15 +234,8 @@ def _circumradius_sq(lengths): return num / denom -def _calculate_cirumradius_sq_of_triangle(x: ArrayLike, y: ArrayLike): - """ - calculates the squared circumradius of a triangle with coordinates x, y - - Parameters: - ----------- - x, y: array-like, shape(3,) - coordinates of the triangle - """ +def _calculate_cirumradius_sq_of_triangle(x: ArrayLike, y: ArrayLike) -> NDArray: + """Calculates the squared circumradius of a triangle with coordinates x, y""" dx = x - np.roll(x, shift=-1) dy = y - np.roll(y, shift=-1) @@ -237,7 +243,12 @@ def _calculate_cirumradius_sq_of_triangle(x: ArrayLike, y: ArrayLike): return _circumradius_sq(lengths) -def _simplex_to_triangle(smpl, tri): +def _simplex_to_triangle(smpl, tri) -> Polygon: + """Creates internal triangles from given simplices. + + Returns: + + """ x = tri.x[smpl] y = tri.y[smpl] From 1f20e0456ee59eae83597a1731ec7f8d1ae9e107 Mon Sep 17 00:00:00 2001 From: Aleksander Koprowski Date: Mon, 5 Jun 2023 11:13:56 -0700 Subject: [PATCH 3/7] added_docs_for_all_objects_in_plotting.py_file --- .idea/.name | 1 + alpha_shapes/alpha_shapes.py | 2 +- alpha_shapes/plotting.py | 19 ++++++++++++++++--- 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 .idea/.name diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..4ecca9f --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +plotting.py \ No newline at end of file diff --git a/alpha_shapes/alpha_shapes.py b/alpha_shapes/alpha_shapes.py index 5608053..8f17721 100644 --- a/alpha_shapes/alpha_shapes.py +++ b/alpha_shapes/alpha_shapes.py @@ -169,7 +169,7 @@ def optimize(self) -> (NDArray, ArrayLike): """Eliminates redundant simplices and then sets appropriate mask. Returns: - alpha_opt: themost accurate alpha value based on minimal amount of simplices + alpha_opt: the most accurate alpha value based on minimal amount of simplices shape: shape after optimization """ # We have to use at least N//3 triangles to connect N points diff --git a/alpha_shapes/plotting.py b/alpha_shapes/plotting.py index 9f9698d..9d3cee8 100644 --- a/alpha_shapes/plotting.py +++ b/alpha_shapes/plotting.py @@ -1,9 +1,19 @@ +"""This module contains mechanisms essential to printing figures. +Functions receive sets of points from alpha_shapes and then plot appropriate shapes. +This pattern is used in examples directory. +""" import numpy as np from matplotlib.path import Path from matplotlib.patches import PathPatch def plot_alpha_shape(ax, alpha_shape): + """Main-line function, which plots all sets of figure's points. + + Args: + ax: Axes object received from matplotlib.subplots function + alpha_shape: set of points given by Alpha_Shaper._get_shape method + """ try: geoms = alpha_shape.geoms except AttributeError: @@ -14,9 +24,12 @@ def plot_alpha_shape(ax, alpha_shape): def _plot_polygon(ax, polygon): - """ - Plot a polygon using matplotlib's PathPatch. - see https://stackoverflow.com/a/70533052/6060982 + """Plots a polygon using matplotlib's PathPatch. + This thread on stackoverflow may be helpful https://stackoverflow.com/a/70533052/6060982. + + Args: + ax: Axes object received from matplotlib.subplots function. + polygon: Polygon object (from shapely.geometry) nested in object returned by Alpha_Shaper._get_shape method. """ xe, ye = polygon.exterior.xy exterior = Path(np.column_stack([xe, ye])) From c2a54f455fbbe22c4366e7a4913e3f94b04855e8 Mon Sep 17 00:00:00 2001 From: Aleksander Koprowski Date: Fri, 9 Jun 2023 03:36:02 -0700 Subject: [PATCH 4/7] last_amendments --- .idea/.name | 2 +- alpha_shapes/alpha_shapes.py | 56 +++++++++++++++++++++++------------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/.idea/.name b/.idea/.name index 4ecca9f..5954dd7 100644 --- a/.idea/.name +++ b/.idea/.name @@ -1 +1 @@ -plotting.py \ No newline at end of file +alpha_shapes.py \ No newline at end of file diff --git a/alpha_shapes/alpha_shapes.py b/alpha_shapes/alpha_shapes.py index 8f17721..7248d9b 100644 --- a/alpha_shapes/alpha_shapes.py +++ b/alpha_shapes/alpha_shapes.py @@ -187,26 +187,24 @@ def set_mask_at_alpha(self, alpha: float): return self -def _normalize_points(points: NDArray): - """ - Normalize points to the unit square, centered at the origin. +def _normalize_points(points: ArrayLike): + """Normalizes points to the unit square, centered at the origin. - Parameters: - ----------- + Args: points: array-like, shape(N,2) coordinates of the points Returns: - -------- - points: array, shape(N,2) - normalized coordinates of the points + points: array, shape(N,2) + normalized coordinates of the points - center: array, shape(2,) - coordinates of the center of the points + center: array, shape(2,) + coordinates of the center of the points - scale: array, shape(2,) - scale factors for the normalization + scale: array, shape(2,) + scale factors for the normalization """ + center = points.mean(axis=0) scale = np.ptp(points, axis=0) # peak to peak distance normalized_points = (points - center) / scale @@ -214,13 +212,17 @@ def _normalize_points(points: NDArray): return normalized_points, center, scale -def _circumradius_sq(lengths): - r""" - Calculate the squared circumradius `r_c^2`, - where - r_c = \frac {abc}{4{\sqrt {s(s-a)(s-b)(s-c)}}} - See: `https://en.wikipedia.org/wiki/Circumscribed_circle` +def _circumradius_sq(lengths: NDArray) -> NDArray: + """ Calculates the squared circumradius of triangle. + See more about it on: `https://en.wikipedia.org/wiki/Circumscribed_circle`. + + Args: + lengths: contains lengths of triangle's sides. + + Returns: + Contains values of squared circumradiuses. """ + lengths = np.asarray(lengths) s = np.sum(lengths) / 2 @@ -235,7 +237,16 @@ def _circumradius_sq(lengths): def _calculate_cirumradius_sq_of_triangle(x: ArrayLike, y: ArrayLike) -> NDArray: - """Calculates the squared circumradius of a triangle with coordinates x, y""" + """Calculates the squared circumradius of a triangle with coordinates x, y. + + Args: + x: Contains all x values of triangle's points. + y: Contains all y values of triangle's points. + + Returns: + NDArray with outcome from _circumradius_sq. It contains squared circumradius of internal triangle. + """ + dx = x - np.roll(x, shift=-1) dy = y - np.roll(y, shift=-1) @@ -246,9 +257,14 @@ def _calculate_cirumradius_sq_of_triangle(x: ArrayLike, y: ArrayLike) -> NDArray def _simplex_to_triangle(smpl, tri) -> Polygon: """Creates internal triangles from given simplices. - Returns: + Args: + smpl: values of simplex. + tri: particular triangle. + Returns: + Polygon(shapely.geometry): contains points values of internal triangle. """ + x = tri.x[smpl] y = tri.y[smpl] From d273e36e922cd07005e43f1da96aa9cb5333e397 Mon Sep 17 00:00:00 2001 From: Aleksander Koprowski Date: Tue, 20 Jun 2023 17:29:19 +0200 Subject: [PATCH 5/7] Remove .idea directory from version control --- .idea/.gitignore | 3 --- .idea/.name | 1 - .idea/alpha_shapes.iml | 17 ----------------- .idea/inspectionProfiles/profiles_settings.xml | 6 ------ .idea/misc.xml | 7 ------- .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ 7 files changed, 48 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/.name delete mode 100644 .idea/alpha_shapes.iml delete mode 100644 .idea/inspectionProfiles/profiles_settings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 5954dd7..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -alpha_shapes.py \ No newline at end of file diff --git a/.idea/alpha_shapes.iml b/.idea/alpha_shapes.iml deleted file mode 100644 index 5195124..0000000 --- a/.idea/alpha_shapes.iml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 971acd1..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 871fc4d..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 933618090b9b0514f32a9457be98173c349904f3 Mon Sep 17 00:00:00 2001 From: Aleksander Koprowski Date: Fri, 7 Jul 2023 18:32:02 +0200 Subject: [PATCH 6/7] Amendments_after_first_review_ --- alpha_shapes/alpha_shapes.py | 87 +++++++++++++++--------------------- alpha_shapes/plotting.py | 20 +++++---- 2 files changed, 48 insertions(+), 59 deletions(-) diff --git a/alpha_shapes/alpha_shapes.py b/alpha_shapes/alpha_shapes.py index 62cec0e..806dfd0 100644 --- a/alpha_shapes/alpha_shapes.py +++ b/alpha_shapes/alpha_shapes.py @@ -13,18 +13,17 @@ class AlphaException(Exception): - """Abstract class for all exceptions which will be raised within Alpha_Shaper class - directly or through Delaunay class.""" + """Abstract class for exceptions which could be raised during the work of Alpha_Shaper class.""" pass class NotEnoughPoints(AlphaException): - """If instance of class Delaunay has less than 3 points, this exception will be raised.""" + """Raised when an operation requires a certain number of points and that condition is not met.""" pass class OptimizationFailure(AlphaException): - """If Alpha_Shaper instance can't cover all vertices, this exception will be raised.""" + """Raised when the conditions for optimization are not met.""" pass @@ -34,20 +33,14 @@ class OptimizationWarning(UserWarning): class Delaunay(Triangulation): - """ - Delaunay is abstract class, which derives from matplotlib.tri.Triangulation. - It adds set of coordinates and essential methods. + """Abstract class, which provides useful interface. This idea is similar to scipy.spatial.Delaunay solution. + Coordinates of future Alpha_Shaper object will be added via Delaunay class. + If the coordinates do not meet the conditions, the class will raise an appropriate error. + It also adds key methods. """ def __init__(self, coords: NDArray) -> None: - """In try block function invokes __init__ method of class - Triangulation from matplotlib package and sends - to it default set of coordinates. If ValueError occurs, - function will tackle it. - - """ - try: super().__init__(x=coords[:, 0], y=coords[:, 1]) except ValueError as e: @@ -67,10 +60,12 @@ def __len__(self) -> int: class Alpha_Shaper(Delaunay): - """Crucial class to creating alpha-shapes.""" + mask: NDArray # for type hinting + """Crucial class to creating alpha-shapes. + The class handles points, creates internal triangles and generates shapes. + """ def __init__(self, points: ArrayLike, normalize=True) -> None: - """Assign points to instance, create basic properties and denormalize points""" self.normalized = normalize points = np.array(points) @@ -84,9 +79,7 @@ def __init__(self, points: ArrayLike, normalize=True) -> None: self._initialize(points) def _initialize(self, points: NDArray) -> None: - """ - _initialize the alpha shaper. - """ + """_initialize the alpha shaper.""" super().__init__(points) @@ -95,15 +88,13 @@ def _initialize(self, points: NDArray) -> None: default_mask = np.full_like(self.circumradii_sq, False, dtype=bool) self.set_mask(default_mask) - def _denormalize(self, center: ArrayLike, scale: ArrayLike) -> None: - """Transform back points into their orginal scale.""" + def _denormalize(self, center: NDArray, scale: NDArray) -> None: + """Transform back points into their original scale.""" self.x = self.x * scale[0] + center[0] self.y = self.y * scale[1] + center[1] def _calculate_cirumradii_sq_of_internal_triangles(self) -> NDArray: - """Main-line method, which calculates circumradius squares of all internal triangles - and saves them into numpy.array. It's important during initialization of object. - """ + """Method calculates circumradiuses squares of all internal triangles.""" circumradii_sq = [ self._get_circumradius_sq_of_internal_simplex(smpl) @@ -111,16 +102,13 @@ def _calculate_cirumradii_sq_of_internal_triangles(self) -> NDArray: ] return np.array(circumradii_sq) - def _get_circumradius_sq_of_internal_simplex(self, smpl: ArrayLike) -> NDArray: - """Read values of simplices and then - send it to _calculate_cirumradius_sq_of_triangle function. - """ - + def _get_circumradius_sq_of_internal_simplex(self, smpl: slice) -> NDArray: + """Read value of squared circumradius of internal triangle.""" x = self.x[smpl] y = self.y[smpl] return _calculate_cirumradius_sq_of_triangle(x, y) - def _sorted_simplices(self) -> NDArray: + def _sorted_simplices(self) -> NDArray[np.float64]: """Return all simplices of instance, sorted before by given axis.""" return self.simplices[self.argsort] @@ -129,22 +117,19 @@ def _sorted_circumradii_sw(self) -> NDArray[np.float64]: return self.circumradii_sq[self.argsort] def _shape_from_simplices(self, simplices: ArrayLike) -> ArrayLike: - """Send values of simplices to _simplex_to_triangle - function and save triangles. Function shapely.ops.unary_union can receive and return many types of objects, - its output depends on input types. In this case function will return array of ints. - """ + """From given simplices create triangles. Then make union.""" triangles = [_simplex_to_triangle(smpl, self) for smpl in simplices] return unary_union(triangles) def get_mask(self, alpha: float) -> NDArray: - """Create mask, based on squares circumradiuses of internal triangles.""" + """Create mask, based on squares of circumradiuses of internal triangles.""" return self.circumradii_sq > 1 / alpha**2 def get_shape(self, alpha: float) -> ArrayLike: - """Return shape, constrained by aplha in form of array. - If alpha is less than 0, function will use original array of simplices. + """Return shape, constrained by alpha. + If alpha is less or equal to 0, function will use original array of simplices. """ if alpha > 0: @@ -161,17 +146,17 @@ def _nth_shape(self, n: int) -> ArrayLike: return self._shape_from_simplices(simplices) def all_vertices(self) -> set: - """Return all vertices of object by set.""" + """Return all vertices of object.""" return set(np.ravel(self.simplices)) def _uncovered_vertices(self, simplices: ArrayLike) -> set: """Return a set of vertices, which is not covered by the specified simplices.""" return self.all_vertices() - set(np.ravel(simplices)) - def _get_minimum_fully_covering_index_of_simplices(self) -> ArrayLike: + def _get_minimum_fully_covering_index_of_simplices(self) -> int: """Return the minimum amount of simplices essential to cover all vertices. The set of all simplices up to this index is fully covering. - If function face problem with vertices, it will raise OptimizationFailure exception. + If function face problems, it will raise appropriate exceptions. """ # We have to use at least N//3 triangles to connect N points. @@ -191,12 +176,12 @@ def _get_minimum_fully_covering_index_of_simplices(self) -> ArrayLike: raise OptimizationFailure("Maybe there are duplicate points?") - def optimize(self) -> (NDArray, ArrayLike): - """Eliminate redundant simplices and then sets appropriate mask. + def optimize(self) -> Tuple[NDArray, ArrayLike]: + """Eliminate redundant simplices and then set appropriate mask. Returns: - alpha_opt: the most accurate alpha value based on minimal amount of simplices - shape: shape after optimization + alpha_opt: the most appropriate alpha value based on minimal amount of simplices. + shape: shape after optimization. """ # We have to use at least N//3 triangles to connect N points @@ -208,7 +193,7 @@ def optimize(self) -> (NDArray, ArrayLike): return alpha_opt, shape def set_mask_at_alpha(self, alpha: float): - """Set the mask for the alpha shape based on the appropriate alpha value.""" + """Set the mask for the alpha shape based on the given alpha value.""" mask = self.get_mask(alpha) self.set_mask(mask) return self @@ -241,7 +226,7 @@ def _normalize_points(points: NDArray) -> Tuple[NDArray, NDArray, NDArray]: def _circumradius_sq(lengths: NDArray) -> NDArray: - """ Calculate the squared circumradius of triangle. + """Calculate the squared circumradius of triangle. See more about it on: `https://en.wikipedia.org/wiki/Circumscribed_circle`. Args: @@ -273,7 +258,7 @@ def _calculate_cirumradius_sq_of_triangle(x: ArrayLike, y: ArrayLike) -> NDArray y: Contains all y values of triangle's points. Returns: - NDArray with outcome from _circumradius_sq. It contains squared circumradius of internal triangle. + outcome: Contains squared circumradius of internal triangle. """ @@ -284,15 +269,15 @@ def _calculate_cirumradius_sq_of_triangle(x: ArrayLike, y: ArrayLike) -> NDArray return _circumradius_sq(lengths) -def _simplex_to_triangle(smpl, tri) -> Polygon: - """Create internal triangles from given simplices. +def _simplex_to_triangle(smpl: slice, tri) -> Polygon: + """Create internal triangle from given simplices. Args: - smpl: values of simplex. + smpl: value of simplex. tri: particular triangle. Returns: - Polygon(shapely.geometry): contains points values of internal triangle. + Polygon: contains points values of internal triangle. """ diff --git a/alpha_shapes/plotting.py b/alpha_shapes/plotting.py index 9d3cee8..45fc1d8 100644 --- a/alpha_shapes/plotting.py +++ b/alpha_shapes/plotting.py @@ -1,19 +1,21 @@ """This module contains mechanisms essential to printing figures. -Functions receive sets of points from alpha_shapes and then plot appropriate shapes. -This pattern is used in examples directory. +Functions receive sets of points and then plot appropriate shapes. """ + import numpy as np from matplotlib.path import Path from matplotlib.patches import PathPatch def plot_alpha_shape(ax, alpha_shape): - """Main-line function, which plots all sets of figure's points. + """Mainline function, which plots all sets of figure's points. Args: - ax: Axes object received from matplotlib.subplots function - alpha_shape: set of points given by Alpha_Shaper._get_shape method + ax(Axes.matplotlib.subplots): axes of image. + alpha_shape(numpy.ArrayLike): set of points to print. + """ + try: geoms = alpha_shape.geoms except AttributeError: @@ -24,13 +26,15 @@ def plot_alpha_shape(ax, alpha_shape): def _plot_polygon(ax, polygon): - """Plots a polygon using matplotlib's PathPatch. + """Plot a polygon using matplotlib's PathPatch. This thread on stackoverflow may be helpful https://stackoverflow.com/a/70533052/6060982. Args: - ax: Axes object received from matplotlib.subplots function. - polygon: Polygon object (from shapely.geometry) nested in object returned by Alpha_Shaper._get_shape method. + ax(Axes.matplotlib.subplots): axes of image. + polygon(shapely.geometry.Polygon): set of points to print. + """ + xe, ye = polygon.exterior.xy exterior = Path(np.column_stack([xe, ye])) holes = [Path(np.asarray(hole.coords)) for hole in polygon.interiors] From 7b2d80dfd78769afe0d8ba7fdfa9858ba9764c19 Mon Sep 17 00:00:00 2001 From: Aleksander Koprowski Date: Sun, 5 Nov 2023 20:08:24 +0100 Subject: [PATCH 7/7] Amendments_after_second_review --- alpha_shapes/alpha_shapes.py | 98 +++++++++++++++++++++++------------- alpha_shapes/plotting.py | 4 +- 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/alpha_shapes/alpha_shapes.py b/alpha_shapes/alpha_shapes.py index 806dfd0..c691cf8 100644 --- a/alpha_shapes/alpha_shapes.py +++ b/alpha_shapes/alpha_shapes.py @@ -1,9 +1,9 @@ """ -This is a core module of package, which contains exceptions, functions -and classes essential to printing figures. +This is a core module of package which contains exceptions, functions +and classes essential to creating and working with figures. """ -from typing import Tuple +from typing import Tuple, Union import numpy as np from matplotlib.tri import Triangulation @@ -33,14 +33,22 @@ class OptimizationWarning(UserWarning): class Delaunay(Triangulation): - """Abstract class, which provides useful interface. This idea is similar to scipy.spatial.Delaunay solution. - Coordinates of future Alpha_Shaper object will be added via Delaunay class. - If the coordinates do not meet the conditions, the class will raise an appropriate error. - It also adds key methods. - + """Abstract class with useful interface. + Visitor sublclass of matplotlib.tri.Triangulation. + See similar idea on scipy.spatial.Delaunay solution. """ def __init__(self, coords: NDArray) -> None: + """Set the interface object and pass the coords into it. + + Args: + coords(NDArray): raw coords at which preparation process will be performed. + + Raises: + - NotEnoughPoints: If there are fewer than 3 points provided. + - ValueError: For other value-related issues with the coordinates. + """ + try: super().__init__(x=coords[:, 0], y=coords[:, 1]) except ValueError as e: @@ -51,21 +59,30 @@ def __init__(self, coords: NDArray) -> None: @property def simplices(self) -> NDArray: - """Create simplices property essential to further operations.""" + """Return the collection of triangles.""" return self.triangles def __len__(self) -> int: - """Return amount of object's edges.""" + """Return amount of object's simplices.""" return self.simplices.shape[0] class Alpha_Shaper(Delaunay): + """The class enables the creation of shapes and further operations on them.""" + mask: NDArray # for type hinting - """Crucial class to creating alpha-shapes. - The class handles points, creates internal triangles and generates shapes. - """ def __init__(self, points: ArrayLike, normalize=True) -> None: + """Pass points into shaper. Optionally perform normalization. + + Args: + points(ArrayLike): points used to create shapes. + + normalize(bool): The flag determines whether the normalization process will be carried out. + See more about normalization on https://github.com/panosz/alpha_shapes. + + """ + self.normalized = normalize points = np.array(points) @@ -79,7 +96,12 @@ def __init__(self, points: ArrayLike, normalize=True) -> None: self._initialize(points) def _initialize(self, points: NDArray) -> None: - """_initialize the alpha shaper.""" + """_initialize the alpha shaper. + + Args: + points(NDArray): points at which normalization will be performed. + + """ super().__init__(points) @@ -109,7 +131,7 @@ def _get_circumradius_sq_of_internal_simplex(self, smpl: slice) -> NDArray: return _calculate_cirumradius_sq_of_triangle(x, y) def _sorted_simplices(self) -> NDArray[np.float64]: - """Return all simplices of instance, sorted before by given axis.""" + """Return the collection of simplices, sorted by their circumradius.""" return self.simplices[self.argsort] def _sorted_circumradii_sw(self) -> NDArray[np.float64]: @@ -117,19 +139,21 @@ def _sorted_circumradii_sw(self) -> NDArray[np.float64]: return self.circumradii_sq[self.argsort] def _shape_from_simplices(self, simplices: ArrayLike) -> ArrayLike: - """From given simplices create triangles. Then make union.""" + """Return the shape from simplices. + Output is in unary_union form which makes further operations on a shape easier. """ triangles = [_simplex_to_triangle(smpl, self) for smpl in simplices] return unary_union(triangles) def get_mask(self, alpha: float) -> NDArray: - """Create mask, based on squares of circumradiuses of internal triangles.""" + """Return mask, based on squares of circumradiuses of internal triangles. + Mask specifies which elements should be considered for triangulation.""" return self.circumradii_sq > 1 / alpha**2 def get_shape(self, alpha: float) -> ArrayLike: - """Return shape, constrained by alpha. - If alpha is less or equal to 0, function will use original array of simplices. + """Return shape, based on the given alpha. + If alpha is less or equal to 0, the shape creation will be based on external points. """ if alpha > 0: @@ -154,11 +178,15 @@ def _uncovered_vertices(self, simplices: ArrayLike) -> set: return self.all_vertices() - set(np.ravel(simplices)) def _get_minimum_fully_covering_index_of_simplices(self) -> int: - """Return the minimum amount of simplices essential to cover all vertices. + """Return the minimum index of simplices needed to cover all vertices. The set of all simplices up to this index is fully covering. - If function face problems, it will raise appropriate exceptions. + + Raises: + - OptimizationFailure: For issues when the conditions for optimization are not met. + A common issue is duplicate points in the dataset. """ + # We have to use at least N//3 triangles to connect N points. simplices = self._sorted_simplices() n_start = len(self) // 3 @@ -177,13 +205,15 @@ def _get_minimum_fully_covering_index_of_simplices(self) -> int: raise OptimizationFailure("Maybe there are duplicate points?") def optimize(self) -> Tuple[NDArray, ArrayLike]: - """Eliminate redundant simplices and then set appropriate mask. + """Return the alpha value that allows plotting the shape with the minimum number of triangles. + Vertices of initial triangulation aren't left uncovered. Returns: - alpha_opt: the most appropriate alpha value based on minimal amount of simplices. - shape: shape after optimization. + alpha_opt(NDArray): optimized alpha value. + shape(ArrayLike): shape based on the optimized alpha value. """ + # We have to use at least N//3 triangles to connect N points n_min = self._get_minimum_fully_covering_index_of_simplices() alpha_opt = 1 / np.sqrt(self._sorted_circumradii_sw()[n_min]) - 1e-10 @@ -225,15 +255,15 @@ def _normalize_points(points: NDArray) -> Tuple[NDArray, NDArray, NDArray]: return normalized_points, center, scale -def _circumradius_sq(lengths: NDArray) -> NDArray: - """Calculate the squared circumradius of triangle. +def _circumradius_sq(lengths: NDArray) -> Union[float, np.inf]: + """Calculate the squared circumradius `r_c^2` where r_c = \frac {abc}{4{\sqrt {s(s-a)(s-b)(s-c)}}}. See more about it on: `https://en.wikipedia.org/wiki/Circumscribed_circle`. Args: - lengths: contains lengths of triangle's sides. + lengths(NDArray): contains lengths of triangle's sides. Returns: - Contains values of squared circumradiuses. + Union[float, np.inf]: value of squared circumradius. """ @@ -254,11 +284,11 @@ def _calculate_cirumradius_sq_of_triangle(x: ArrayLike, y: ArrayLike) -> NDArray """Calculate the squared circumradius of a triangle with coordinates x, y. Args: - x: Contains all x values of triangle's points. - y: Contains all y values of triangle's points. + x, y: array-like, shape(3,) + coordinates of the triangle Returns: - outcome: Contains squared circumradius of internal triangle. + NDArray: squared circumradius of internal triangle. """ @@ -270,14 +300,14 @@ def _calculate_cirumradius_sq_of_triangle(x: ArrayLike, y: ArrayLike) -> NDArray def _simplex_to_triangle(smpl: slice, tri) -> Polygon: - """Create internal triangle from given simplices. + """Return triangle points. Args: - smpl: value of simplex. + smpl(slice): value of simplex. tri: particular triangle. Returns: - Polygon: contains points values of internal triangle. + Polygon: contains points values of triangle. """ diff --git a/alpha_shapes/plotting.py b/alpha_shapes/plotting.py index 45fc1d8..e158365 100644 --- a/alpha_shapes/plotting.py +++ b/alpha_shapes/plotting.py @@ -1,5 +1,5 @@ """This module contains mechanisms essential to printing figures. -Functions receive sets of points and then plot appropriate shapes. +Functions make necessary operations on points sets and plot appropriate shapes. """ import numpy as np @@ -8,7 +8,7 @@ def plot_alpha_shape(ax, alpha_shape): - """Mainline function, which plots all sets of figure's points. + """Plot final set of figure's points. Args: ax(Axes.matplotlib.subplots): axes of image.