Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

generalize ConvexHullART to allow alpha-shapes #118

Merged
merged 1 commit into from
Oct 22, 2024
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
163 changes: 120 additions & 43 deletions artlib/experimental/ConvexHullART.py → artlib/experimental/HullART.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from copy import deepcopy
from typing import Optional, Iterable, List, Tuple, Union, Dict
from scipy.spatial import ConvexHull
from shapely import Polygon
from alphashape import alphashape

from artlib.common.BaseART import BaseART
from artlib.experimental.merging import merge_objects


def plot_convex_polygon(
def plot_polygon(
vertices: np.ndarray,
ax: Axes,
line_color: str = "b",
Expand Down Expand Up @@ -91,17 +93,13 @@ def area(self):
else:
return 2*np.linalg.norm(self.points[0,:]-self.points[1,:],ord=2)


HullTypes = Union[ConvexHull, PseudoConvexHull]


def centroid_of_convex_hull(hull: HullTypes):
def centroid_of_convex_hull(hull: Union[PseudoConvexHull, ConvexHull]):
"""
Finds the centroid of the volume of a convex hull in n-dimensional space.

Parameters
----------
hull : HullTypes
hull : Union[PseudoConvexHull, ConvexHull]
A ConvexHull or PseudoConvexHull object.

Returns
Expand Down Expand Up @@ -129,25 +127,110 @@ def centroid_of_convex_hull(hull: HullTypes):
centroid /= total_volume
return centroid

class GeneralHull:
def __init__(self, points: np.ndarray, alpha: float = 0.0):
self.dim = points.shape[1]
self.alpha = alpha
if points.shape[0] <= 2:
self.hull = PseudoConvexHull(points)
elif points.shape[0] == 3 or alpha == 0.0:
self.hull = ConvexHull(points, incremental=True)
else:
self.hull = alphashape(points, alpha=self.alpha)

def add_points(self, points: np.ndarray):
if isinstance(self.hull, PseudoConvexHull):
if self.hull.points.shape[0] == 1:
self.hull.add_points(points.reshape((-1, self.dim)))
else:
new_points = np.vstack(
[
self.hull.points[self.hull.vertices, :],
points.reshape((-1, self.dim))
]
)
self.hull = ConvexHull(new_points, incremental=True)
elif isinstance(self.hull, ConvexHull) and self.alpha == 0.0:
self.hull.add_points(i.reshape((-1, self.dim)))
else:
if isinstance(self.hull, ConvexHull):
new_points = np.vstack(
[
self.hull.points[self.hull.vertices, :],
points.reshape((-1, self.dim))
]
)
self.hull = alphashape(new_points, alpha=self.alpha)
else:
new_points = np.vstack(
[
np.asarray(self.hull.exterior.coords),
points.reshape((-1, self.dim))
]
)
self.hull = alphashape(new_points, alpha=self.alpha)

@property
def area(self):
if isinstance(self.hull, (PseudoConvexHull, ConvexHull)) or self.dim > 2:
return self.hull.area
else:
return self.hull.length

@property
def centroid(self):
if isinstance(self.hull, (PseudoConvexHull, ConvexHull)):
return centroid_of_convex_hull(self.hull)
else:
return self.hull.centroid

@property
def is_empty(self):
if isinstance(self.hull, (PseudoConvexHull, ConvexHull)):
return False
else:
return self.hull.is_empty

@property
def vertices(self):
if isinstance(self.hull, ConvexHull):
return self.hull.points[self.hull.vertices, :]
elif isinstance(self.hull, Polygon):
return np.asarray(self.hull.exterior.coords)
else:
return self.hull.points

def deepcopy(self):
if isinstance(self.hull, Polygon):
points = np.asarray(self.hull.exterior.coords)
return GeneralHull(points, alpha=float(self.alpha))
elif isinstance(self.hull, ConvexHull):
points = self.hull.points[self.hull.vertices, :]
return GeneralHull(points, alpha=float(self.alpha))
else:
return deepcopy(self)

class ConvexHullART(BaseART):

class HullART(BaseART):
"""
ConvexHull ART for Clustering
Hull ART for Clustering
"""

def __init__(self, rho: float, alpha: float):
def __init__(self, rho: float, alpha: float, alpha_hat: float):
"""
Initializes the ConvexHullART object.
Initializes the HullART object.

Parameters
----------
rho : float
Vigilance parameter.
alpha : float
Choice parameter.
alpha_hat : float
alpha shape parameter.

"""
params = {"rho": rho, "alpha": alpha}
params = {"rho": rho, "alpha": alpha, "alpha_hat": alpha_hat}
super().__init__(params)

@staticmethod
Expand All @@ -167,9 +250,12 @@ def validate_params(params: dict):
assert "alpha" in params
assert params["alpha"] >= 0.0
assert isinstance(params["alpha"], float)
assert "alpha_hat" in params
assert params["alpha_hat"] >= 0.0
assert isinstance(params["alpha_hat"], float)

def category_choice(
self, i: np.ndarray, w: HullTypes, params: dict
self, i: np.ndarray, w: GeneralHull, params: dict
) -> tuple[float, Optional[dict]]:
"""
Get the activation of the cluster.
Expand All @@ -178,7 +264,7 @@ def category_choice(
----------
i : np.ndarray
Data sample.
w : HullTypes
w : GeneralHull
Cluster weight or information.
params : dict
Dictionary containing parameters for the algorithm.
Expand All @@ -192,32 +278,25 @@ def category_choice(

"""

if isinstance(w, PseudoConvexHull):

if w.points.shape[0] == 1:
new_w = deepcopy(w)
new_w.add_points(i.reshape((1, -1)))
else:
new_points = np.vstack(
[w.points[w.vertices, :], i.reshape((1, -1))]
)
new_w = ConvexHull(new_points, incremental=True)
else:
new_w = ConvexHull(w.points[w.vertices, :], incremental=True)
new_w.add_points(i.reshape((1, -1)))
new_w = w.deepcopy()
new_w.add_points(i.reshape((1,-1)))
if new_w.is_empty:
raise RuntimeError(
f"alpha_hat={params['alpha_hat']} results in invalid geometry"
)

a_max = float(2*len(i))
new_area = a_max - new_w.area
activation = new_area / (a_max-w.area + params["alpha"])

cache = {"new_w": new_w, "new_area": new_area}
cache = {"new_w": new_w, "new_area": new_area, "activation": activation}

return activation, cache

def match_criterion(
self,
i: np.ndarray,
w: HullTypes,
w: GeneralHull,
params: dict,
cache: Optional[dict] = None,
) -> Tuple[float, Optional[Dict]]:
Expand All @@ -228,7 +307,7 @@ def match_criterion(
----------
i : np.ndarray
Data sample.
w : HullTypes
w : GeneralHull
Cluster weight or information.
params : dict
Dictionary containing parameters for the algorithm.
Expand All @@ -252,18 +331,18 @@ def match_criterion(
def update(
self,
i: np.ndarray,
w: HullTypes,
w: GeneralHull,
params: dict,
cache: Optional[dict] = None,
) -> HullTypes:
) -> GeneralHull:
"""
Get the updated cluster weight.

Parameters
----------
i : np.ndarray
Data sample.
w : HullTypes
w : GeneralHull
Cluster weight or information.
params : dict
Dictionary containing parameters for the algorithm.
Expand All @@ -272,13 +351,13 @@ def update(

Returns
-------
HullTypes
GeneralHull
Updated cluster weight.

"""
return cache["new_w"]

def new_weight(self, i: np.ndarray, params: dict) -> HullTypes:
def new_weight(self, i: np.ndarray, params: dict) -> GeneralHull:
"""
Generate a new cluster weight.

Expand All @@ -291,11 +370,11 @@ def new_weight(self, i: np.ndarray, params: dict) -> HullTypes:

Returns
-------
HullTypes
GeneralHull
New cluster weight.

"""
new_w = PseudoConvexHull(i.reshape((1, -1)))
new_w = GeneralHull(i.reshape((1, -1)), alpha=params["alpha_hat"])
return new_w

def get_cluster_centers(self) -> List[np.ndarray]:
Expand All @@ -310,7 +389,7 @@ def get_cluster_centers(self) -> List[np.ndarray]:
"""
centers = []
for w in self.W:
centers.append(centroid_of_convex_hull(w))
centers.append(w.centroid)
return centers

def plot_cluster_bounds(
Expand All @@ -330,10 +409,8 @@ def plot_cluster_bounds(

"""
for c, w in zip(colors, self.W):
if isinstance(w, ConvexHull):
vertices = w.points[w.vertices, :2]
else:
vertices = w.points[:, :2]
plot_convex_polygon(
vertices = w.vertices[:,:2]

plot_polygon(
vertices, ax, line_width=linewidth, line_color=c
)
4 changes: 2 additions & 2 deletions docs/source/artlib.experimental.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ Module contents
Submodules
----------

artlib.experimental.ConvexHullART module
artlib.experimental.HullART module
----------------------------------------

.. automodule:: artlib.experimental.ConvexHullART
.. automodule:: artlib.experimental.HullART
:members:
:undoc-members:
:show-inheritance:
Expand Down
15 changes: 11 additions & 4 deletions examples/demo_convex_hull_art.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import matplotlib.pyplot as plt
import numpy as np

from artlib.experimental.ConvexHullART import ConvexHullART, plot_convex_polygon
from scipy.spatial import ConvexHull
from artlib.experimental.HullART import HullART, plot_polygon
from alphashape import alphashape


def cluster_blobs():
Expand All @@ -16,8 +16,8 @@ def cluster_blobs():
)
print("Data has shape:", data.shape)

params = {"rho": 0.6, "alpha": 1e-3}
cls = ConvexHullART(**params)
params = {"rho": 0.6, "alpha": 1e-3, "alpha_hat": 1.0}
cls = HullART(**params)

X = cls.prepare_data(data)
print("Prepared data has shape:", X.shape)
Expand All @@ -29,6 +29,13 @@ def cluster_blobs():
cls.visualize(X, y)
plt.show()

def test():
points = np.array(
[(0.0, 0.0), (0.0, 1.0), (1.0,1.0), (1.0, 0.0)]
)
x = alphashape(points, alpha=1.0)
print(x.length)

if __name__ == "__main__":
cluster_blobs()
# test()
2 changes: 1 addition & 1 deletion templates/ART_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class MyART(BaseART):
"""Generic Template for custom ART modules."""

def __init__(self, rho: float):
"""Initializes the ConvexHullART object.
"""Initializes the ART object.

Parameters
----------
Expand Down
Loading