diff --git a/clusterview/algorithms.py b/clusterview/algorithms.py index ed7f897..e5730e1 100644 --- a/clusterview/algorithms.py +++ b/clusterview/algorithms.py @@ -8,7 +8,7 @@ class CentroidGrouping: can change). This allows us to do better than just dumping the grouping into a dictionary with a long tuple pointing at an array. """ - def __init__(self, centroid, points=[]): + def __init__(self, centroid, points=None): if not isinstance(centroid, Point): ValueError("Centroid must be a Point.") @@ -16,7 +16,11 @@ class CentroidGrouping: ValueError("Points must be in a list.") self.__centroid = centroid - self.__points = points + + if points is None: + self.__points = [] + else: + self.__points = points @property def centroid(self): @@ -32,15 +36,20 @@ class CentroidGrouping: @param point The point. """ - if not isinstance(point, Point): raise ValueError("Point must be of type Point.") self.__points.append(point) + def __repr__(self): + s = f"CENTROID: {self.__centroid}\n" + s += f"POINTS: {self.__points}" + + return s + def __eq__(self, other): - return (self.centroid == other.centroid and - self.points == other.points) + return (self.__centroid == other.centroid and + self.__points == other.points) class Algorithms: @@ -49,72 +58,48 @@ class Algorithms: geometry algorithms. """ - # Since all algorithms rely on a set of centroids it is stored here - # statically. - __centroids = [] - - @classmethod - def clear_centroids(cls): - cls.__centroids = [] - - @classmethod - def centroids(cls): - return cls.__centroids - - @classmethod - def set_centroids(cls, centroids): - for c in centroids: - if not isinstance(c, Point): - raise ValueError("Centroids must be of type Point.") - - cls.__centroids.append(c) - - @classmethod - def euclidean_grouping(cls, point_set): + @staticmethod + def euclidean_grouping(centroids, point_set): """ Given a point set that EXCLUDES the centroids specified it returns a map from centroid to array of points, where the array of points contains the points with the smallest euclidean distance from that point. - @param cls The class calling the method. - @param point_set The set of points from the UI. + @param centroids The centroids to use. + @param point_set The set of points from the UI excluding centroids. """ if not isinstance(point_set, PointSet): raise ValueError("Euclidean grouping can only be calculated on " + "PointSet types.") - if not cls.__centroids: + if not isinstance(centroids, list): + raise ValueError("Centroids must be of type list.") + + if not centroids: raise ValueError("No centroids specified.") groups = [] - for centroid in cls.__centroids: + for centroid in centroids: groups.append(CentroidGrouping(centroid)) for point in point_set.points: nearest_distance = float("inf") - nearest_centroid = None + nearest_group = None - for centroid in cls.__centroids: - current_distance = Math.euclidean_distance(centroid, point) + for current_group in groups: + current_distance = ( + Math.euclidean_distance(current_group.centroid, point)) if current_distance < nearest_distance: - nearest_centroid = centroid + nearest_group = current_group nearest_distance = current_distance - if nearest_centroid is None: + if nearest_group is None: raise ValueError("Failed to find centroid nearest " + f"to point {point}") - # We successfully found the nearest centroid to the point - # and we can add it to the list. - # TODO: Can CentroidGrouping be made hashable? - # This is relatively slow for large numbers of groups. If - # CentroidGrouping can be made hashable then this becomes O(1). - for group in groups: - if nearest_centroid == group.centroid: - group.add_point(point) - break + nearest_group.add_point(point) return groups diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index 7f90018..64b63a1 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -5,19 +5,9 @@ from clusterview.colors import Color from clusterview.points import Point, PointSet -@pytest.fixture(autouse=True, scope="function") -def teardown(): - """ - Teardown function for after each test. The current pytest best practice - is to run a setup routine, yield, and then run your teardown routine. - """ - yield - Algorithms.clear_centroids() - - def test_empty_centroids(): with pytest.raises(ValueError): - Algorithms.euclidean_grouping(None) + Algorithms.euclidean_grouping([], None) def test_wrong_point_set(): @@ -27,13 +17,22 @@ def test_wrong_point_set(): centroids = [centroid_g1, centroid_g2, centroid_g3] - Algorithms.set_centroids(centroids) + with pytest.raises(ValueError): + Algorithms.euclidean_grouping(centroids, None) + + +def test_empty_point_set(): + centroid_g1 = Point(101, 81, Color.ORANGE, 8, 800, 600) + centroid_g2 = Point(357, 222, Color.RED, 8, 800, 600) + centroid_g3 = Point(728, 47, Color.PURPLE, 8, 800, 600) + + centroids = [centroid_g1, centroid_g2, centroid_g3] with pytest.raises(ValueError): - Algorithms.euclidean_grouping(None) + Algorithms.euclidean_grouping(centroids, []) -def test_euclidean_distance(): +def test_euclidean_grouping(): centroid_g1 = Point(101, 81, Color.ORANGE, 8, 800, 600) centroid_g2 = Point(357, 222, Color.RED, 8, 800, 600) centroid_g3 = Point(728, 47, Color.PURPLE, 8, 800, 600) @@ -77,8 +76,7 @@ def test_euclidean_distance(): expected = [centroid_grouping_1, centroid_grouping_2, centroid_grouping_3] - Algorithms.set_centroids(centroids) - actual = Algorithms.euclidean_grouping(point_set) + actual = Algorithms.euclidean_grouping(centroids, point_set) assert len(actual) == len(expected)