3 changed files with 216 additions and 1 deletions
			
			
		@ -0,0 +1,120 @@ | 
				
			|||||||
 | 
					from .math import Math | 
				
			||||||
 | 
					from .points import Point, PointSet | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CentroidGrouping: | 
				
			||||||
 | 
					    """ | 
				
			||||||
 | 
					    A storage class used because Points are not hashable (since the x and y | 
				
			||||||
 | 
					    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=[]): | 
				
			||||||
 | 
					        if not isinstance(centroid, Point): | 
				
			||||||
 | 
					            ValueError("Centroid must be a Point.") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not isinstance(points, list): | 
				
			||||||
 | 
					            ValueError("Points must be in a list.") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.__centroid = centroid | 
				
			||||||
 | 
					        self.__points = points | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property | 
				
			||||||
 | 
					    def centroid(self): | 
				
			||||||
 | 
					        return self.__centroid | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property | 
				
			||||||
 | 
					    def points(self): | 
				
			||||||
 | 
					        return self.__points | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def add_point(self, point): | 
				
			||||||
 | 
					        """ | 
				
			||||||
 | 
					        Adds a point. | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        @param point The point. | 
				
			||||||
 | 
					        """ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not isinstance(point, Point): | 
				
			||||||
 | 
					            raise ValueError("Point must be of type Point.") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.__points.append(point) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __eq__(self, other): | 
				
			||||||
 | 
					        return (self.centroid == other.centroid and | 
				
			||||||
 | 
					                self.points == other.points) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Algorithms: | 
				
			||||||
 | 
					    """ | 
				
			||||||
 | 
					    A static class for handling and containing various computational | 
				
			||||||
 | 
					    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): | 
				
			||||||
 | 
					        """ | 
				
			||||||
 | 
					        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. | 
				
			||||||
 | 
					        """ | 
				
			||||||
 | 
					        if not isinstance(point_set, PointSet): | 
				
			||||||
 | 
					            raise ValueError("Euclidean grouping can only be calculated on " + | 
				
			||||||
 | 
					                             "PointSet types.") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not cls.__centroids: | 
				
			||||||
 | 
					            raise ValueError("No centroids specified.") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        groups = [] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for centroid in cls.__centroids: | 
				
			||||||
 | 
					            groups.append(CentroidGrouping(centroid)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for point in point_set.points: | 
				
			||||||
 | 
					            nearest_distance = float("inf") | 
				
			||||||
 | 
					            nearest_centroid = None | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for centroid in cls.__centroids: | 
				
			||||||
 | 
					                current_distance = Math.euclidean_distance(centroid, point) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if current_distance < nearest_distance: | 
				
			||||||
 | 
					                    nearest_centroid = centroid | 
				
			||||||
 | 
					                    nearest_distance = current_distance | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if nearest_centroid 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 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return groups | 
				
			||||||
@ -0,0 +1,89 @@ | 
				
			|||||||
 | 
					import pytest | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from clusterview.algorithms import Algorithms, CentroidGrouping | 
				
			||||||
 | 
					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) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_wrong_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] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Algorithms.set_centroids(centroids) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with pytest.raises(ValueError): | 
				
			||||||
 | 
					        Algorithms.euclidean_grouping(None) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_euclidean_distance(): | 
				
			||||||
 | 
					    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] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    point1_g1 = Point(67, 59, Color.GREY, 8, 800, 600) | 
				
			||||||
 | 
					    point2_g1 = Point(116, 53, Color.GREY, 8, 800, 600) | 
				
			||||||
 | 
					    point3_g1 = Point(144, 105, Color.GREY, 8, 800, 600) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    point1_g2 = Point(388, 243, Color.GREY, 8, 800, 600) | 
				
			||||||
 | 
					    point2_g2 = Point(358, 248, Color.GREY, 8, 800, 600) | 
				
			||||||
 | 
					    point3_g2 = Point(426, 202, Color.GREY, 8, 800, 600) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    point1_g3 = Point(750, 47, Color.GREY, 8, 800, 600) | 
				
			||||||
 | 
					    point2_g3 = Point(741, 85, Color.GREY, 8, 800, 600) | 
				
			||||||
 | 
					    point3_g3 = Point(700, 72, Color.GREY, 8, 800, 600) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # This PointSet is the PointSet that excludes the centroids. | 
				
			||||||
 | 
					    point_set = PointSet(8, 800, 600) | 
				
			||||||
 | 
					    point_set.add_point(67, 59, Color.GREY) | 
				
			||||||
 | 
					    point_set.add_point(116, 53, Color.GREY) | 
				
			||||||
 | 
					    point_set.add_point(144, 105, Color.GREY) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    point_set.add_point(388, 243, Color.GREY) | 
				
			||||||
 | 
					    point_set.add_point(358, 248, Color.GREY) | 
				
			||||||
 | 
					    point_set.add_point(426, 202, Color.GREY) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    point_set.add_point(750, 47, Color.GREY) | 
				
			||||||
 | 
					    point_set.add_point(741, 85, Color.GREY) | 
				
			||||||
 | 
					    point_set.add_point(700, 72, Color.GREY) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    centroid_grouping_1 = CentroidGrouping(centroid_g1, | 
				
			||||||
 | 
					                                           [point1_g1, point2_g1, point3_g1]) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    centroid_grouping_2 = CentroidGrouping(centroid_g2, | 
				
			||||||
 | 
					                                           [point1_g2, point2_g2, point3_g2]) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    centroid_grouping_3 = CentroidGrouping(centroid_g3, | 
				
			||||||
 | 
					                                           [point1_g3, point2_g3, point3_g3]) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expected = [centroid_grouping_1, centroid_grouping_2, centroid_grouping_3] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Algorithms.set_centroids(centroids) | 
				
			||||||
 | 
					    actual = Algorithms.euclidean_grouping(point_set) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert len(actual) == len(expected) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Since I don't want to figure out what grouping is where I'll accept | 
				
			||||||
 | 
					    # the linearity of `in`. | 
				
			||||||
 | 
					    assert centroid_grouping_1 in actual | 
				
			||||||
 | 
					    assert centroid_grouping_2 in actual | 
				
			||||||
 | 
					    assert centroid_grouping_3 in actual | 
				
			||||||
					Loading…
					
					
				
		Reference in new issue