Taylor Bockman
5 years ago
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