From 3205913933a613fe7bdd15f340dbcaf00e426ffc Mon Sep 17 00:00:00 2001 From: a2misic Date: Fri, 29 Nov 2024 17:22:53 -0500 Subject: [PATCH 01/23] implemented label in cluster estimation --- .../cluster_estimation/cluster_estimation.py | 6 +- modules/object_in_world.py | 10 ++- tests/unit/test_decision.py | 14 ++-- tests/unit/test_landing_pad_tracking.py | 72 +++++++++---------- 4 files changed, 55 insertions(+), 47 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index 10c7bb91..a4c67f11 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -210,17 +210,17 @@ def run( # Remove clusters with covariances too large model_output = self.__filter_by_covariances(model_output) + label = 0 # Create output list of remaining valid clusters detections_in_world = [] for cluster in model_output: result, landing_pad = object_in_world.ObjectInWorld.create( - cluster[0][0], - cluster[0][1], - cluster[2], + cluster[0][0], cluster[0][1], cluster[2], label ) if result: detections_in_world.append(landing_pad) + label += 1 else: self.__logger.warning("Failed to create ObjectInWorld object") diff --git a/modules/object_in_world.py b/modules/object_in_world.py index 83922253..759e47c2 100644 --- a/modules/object_in_world.py +++ b/modules/object_in_world.py @@ -12,7 +12,7 @@ class ObjectInWorld: @classmethod def create( - cls, location_x: float, location_y: float, spherical_variance: float + cls, location_x: float, location_y: float, spherical_variance: float, label: int ) -> "tuple[bool, ObjectInWorld | None]": """ location_x, location_y: Location of the object. @@ -21,7 +21,9 @@ def create( if spherical_variance < 0.0: return False, None - return True, ObjectInWorld(cls.__create_key, location_x, location_y, spherical_variance) + return True, ObjectInWorld( + cls.__create_key, location_x, location_y, spherical_variance, label + ) def __init__( self, @@ -29,6 +31,7 @@ def __init__( location_x: float, location_y: float, spherical_variance: float, + label: int, ) -> None: """ Private constructor, use create() method. @@ -38,12 +41,13 @@ def __init__( self.location_x = location_x self.location_y = location_y self.spherical_variance = spherical_variance + self.label = label def __str__(self) -> str: """ To string. """ - return f"{self.__class__}, location_x: {self.location_x}, location_y: {self.location_y}, spherical_variance: {self.spherical_variance}" + return f"{self.__class__}, location_x: {self.location_x}, location_y: {self.location_y}, spherical_variance: {self.spherical_variance}, label: {self.label}" def __repr__(self) -> str: """ diff --git a/tests/unit/test_decision.py b/tests/unit/test_decision.py index 3b3cf68f..ce9192ea 100644 --- a/tests/unit/test_decision.py +++ b/tests/unit/test_decision.py @@ -43,7 +43,9 @@ def best_pad_within_tolerance() -> object_in_world.ObjectInWorld: # type: ignor location_x = BEST_PAD_LOCATION_X location_y = BEST_PAD_LOCATION_Y spherical_variance = 1.0 - result, pad = object_in_world.ObjectInWorld.create(location_x, location_y, spherical_variance) + result, pad = object_in_world.ObjectInWorld.create( + location_x, location_y, spherical_variance, 0 + ) assert result assert pad is not None @@ -58,7 +60,9 @@ def best_pad_outside_tolerance() -> object_in_world.ObjectInWorld: # type: igno location_x = 100.0 location_y = 200.0 spherical_variance = 5.0 # variance outside tolerance - result, pad = object_in_world.ObjectInWorld.create(location_x, location_y, spherical_variance) + result, pad = object_in_world.ObjectInWorld.create( + location_x, location_y, spherical_variance, 0 + ) assert result assert pad is not None @@ -70,15 +74,15 @@ def pads() -> "list[object_in_world.ObjectInWorld]": # type: ignore """ Create a list of ObjectInWorld instances for the landing pads. """ - result, pad_1 = object_in_world.ObjectInWorld.create(30.0, 40.0, 2.0) + result, pad_1 = object_in_world.ObjectInWorld.create(30.0, 40.0, 2.0, 0) assert result assert pad_1 is not None - result, pad_2 = object_in_world.ObjectInWorld.create(50.0, 60.0, 3.0) + result, pad_2 = object_in_world.ObjectInWorld.create(50.0, 60.0, 3.0, 0) assert result assert pad_2 is not None - result, pad_3 = object_in_world.ObjectInWorld.create(70.0, 80.0, 4.0) + result, pad_3 = object_in_world.ObjectInWorld.create(70.0, 80.0, 4.0, 0) assert result assert pad_3 is not None diff --git a/tests/unit/test_landing_pad_tracking.py b/tests/unit/test_landing_pad_tracking.py index fd6fce60..5faa1f8f 100644 --- a/tests/unit/test_landing_pad_tracking.py +++ b/tests/unit/test_landing_pad_tracking.py @@ -30,23 +30,23 @@ def detections_1() -> "list[object_in_world.ObjectInWorld]": # type: ignore """ Sample instances of ObjectInWorld for testing. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 8) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 8, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(2, 2, 4) + result, obj_2 = object_in_world.ObjectInWorld.create(2, 2, 4, 0) assert result assert obj_2 is not None - result, obj_3 = object_in_world.ObjectInWorld.create(-2, -2, 2) + result, obj_3 = object_in_world.ObjectInWorld.create(-2, -2, 2, 0) assert result assert obj_3 is not None - result, obj_4 = object_in_world.ObjectInWorld.create(3, 3, 10) + result, obj_4 = object_in_world.ObjectInWorld.create(3, 3, 10, 0) assert result assert obj_4 is not None - result, obj_5 = object_in_world.ObjectInWorld.create(-3, -3, 6) + result, obj_5 = object_in_world.ObjectInWorld.create(-3, -3, 6, 0) assert result assert obj_5 is not None @@ -59,23 +59,23 @@ def detections_2() -> "list[object_in_world.ObjectInWorld]": # type: ignore """ Sample instances of ObjectInWorld for testing. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0.5, 0.5, 1) + result, obj_1 = object_in_world.ObjectInWorld.create(0.5, 0.5, 1, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(1.5, 1.5, 3) + result, obj_2 = object_in_world.ObjectInWorld.create(1.5, 1.5, 3, 0) assert result assert obj_2 is not None - result, obj_3 = object_in_world.ObjectInWorld.create(4, 4, 7) + result, obj_3 = object_in_world.ObjectInWorld.create(4, 4, 7, 0) assert result assert obj_3 is not None - result, obj_4 = object_in_world.ObjectInWorld.create(-4, -4, 5) + result, obj_4 = object_in_world.ObjectInWorld.create(-4, -4, 5, 0) assert result assert obj_4 is not None - result, obj_5 = object_in_world.ObjectInWorld.create(5, 5, 9) + result, obj_5 = object_in_world.ObjectInWorld.create(5, 5, 9, 0) assert result assert obj_5 is not None @@ -88,23 +88,23 @@ def detections_3() -> "list[object_in_world.ObjectInWorld]": # type: ignore """ Sample instances of ObjectInWorld for testing. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 8) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 8, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(0.5, 0.5, 4) + result, obj_2 = object_in_world.ObjectInWorld.create(0.5, 0.5, 4, 0) assert result assert obj_2 is not None - result, obj_3 = object_in_world.ObjectInWorld.create(-2, -2, 2) + result, obj_3 = object_in_world.ObjectInWorld.create(-2, -2, 2, 0) assert result assert obj_3 is not None - result, obj_4 = object_in_world.ObjectInWorld.create(3, 3, 10) + result, obj_4 = object_in_world.ObjectInWorld.create(3, 3, 10, 0) assert result assert obj_4 is not None - result, obj_5 = object_in_world.ObjectInWorld.create(-3, -3, 6) + result, obj_5 = object_in_world.ObjectInWorld.create(-3, -3, 6, 0) assert result assert obj_5 is not None @@ -123,11 +123,11 @@ def test_is_similar_positive_equal_to_threshold(self) -> None: Test case where the second landing pad has positive coordinates and the distance between them is equal to the distance threshold. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(1, 1, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(1, 1, 0, 0) assert result assert obj_2 is not None @@ -145,11 +145,11 @@ def test_is_similar_negative_equal_to_threshold(self) -> None: Test case where the second landing pad has negative coordinates and the distance between them is equal to the distance threshold. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(-1, -1, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(-1, -1, 0, 0) assert result assert obj_2 is not None @@ -168,11 +168,11 @@ def test_is_similar_positive_less_than_threshold(self) -> None: Test case where the second landing pad has positive coordinates and the distance between them is less than the distance threshold. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(0.5, 0.5, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(0.5, 0.5, 0, 0) assert result assert obj_2 is not None @@ -191,11 +191,11 @@ def test_is_similar_negative_less_than_threshold(self) -> None: Test case where the second landing pad has negative coordinates and the distance between them is less than the distance threshold. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(-0.5, -0.5, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(-0.5, -0.5, 0, 0) assert result assert obj_2 is not None @@ -214,11 +214,11 @@ def test_is_similar_positive_more_than_threshold(self) -> None: Test case where the second landing pad has positive coordinates and the distance between them is more than the distance threshold. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(2, 2, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(2, 2, 0, 0) assert result assert obj_2 is not None @@ -237,11 +237,11 @@ def test_is_similar_negative_more_than_threshold(self) -> None: Test case where the second landing pad has negative coordinates and the distance between them is more than the distance threshold. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(-2, -2, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(-2, -2, 0, 0) assert result assert obj_2 is not None @@ -269,7 +269,7 @@ def test_mark_false_positive_no_similar( """ Test if marking false positive adds detection to list of false positives. """ - _, false_positive = object_in_world.ObjectInWorld.create(20, 20, 20) + _, false_positive = object_in_world.ObjectInWorld.create(20, 20, 20, 0) assert false_positive is not None tracker._LandingPadTracking__unconfirmed_positives = detections_1 # type: ignore @@ -296,7 +296,7 @@ def test_mark_false_positive_with_similar( Test if marking false positive adds detection to list of false positives and removes. similar landing pads """ - _, false_positive = object_in_world.ObjectInWorld.create(1, 1, 1) + _, false_positive = object_in_world.ObjectInWorld.create(1, 1, 1, 0) assert false_positive is not None tracker._LandingPadTracking__unconfirmed_positives = detections_2 # type: ignore @@ -316,10 +316,10 @@ def test_mark_multiple_false_positive( """ Test if marking false positive adds detection to list of false positives. """ - _, false_positive_1 = object_in_world.ObjectInWorld.create(0, 0, 1) + _, false_positive_1 = object_in_world.ObjectInWorld.create(0, 0, 1, 0) assert false_positive_1 is not None - _, false_positive_2 = object_in_world.ObjectInWorld.create(2, 2, 1) + _, false_positive_2 = object_in_world.ObjectInWorld.create(2, 2, 1, 0) assert false_positive_2 is not None tracker._LandingPadTracking__unconfirmed_positives = detections_1 # type: ignore @@ -344,7 +344,7 @@ def test_mark_confirmed_positive( """ Test if marking confirmed positive adds detection to list of confirmed positives. """ - _, confirmed_positive = object_in_world.ObjectInWorld.create(1, 1, 1) + _, confirmed_positive = object_in_world.ObjectInWorld.create(1, 1, 1, 0) assert confirmed_positive is not None expected = [confirmed_positive] @@ -359,10 +359,10 @@ def test_mark_multiple_confirmed_positives( """ Test if marking confirmed positive adds detection to list of confirmed positives. """ - _, confirmed_positive_1 = object_in_world.ObjectInWorld.create(1, 1, 1) + _, confirmed_positive_1 = object_in_world.ObjectInWorld.create(1, 1, 1, 0) assert confirmed_positive_1 is not None - _, confirmed_positive_2 = object_in_world.ObjectInWorld.create(2, 2, 1) + _, confirmed_positive_2 = object_in_world.ObjectInWorld.create(2, 2, 1, 0) assert confirmed_positive_2 is not None expected = [confirmed_positive_1, confirmed_positive_2] @@ -478,7 +478,7 @@ def test_run_with_confirmed_positive( Test run when there is a confirmed positive. """ # Setup - result, confirmed_positive = object_in_world.ObjectInWorld.create(1, 1, 1) + result, confirmed_positive = object_in_world.ObjectInWorld.create(1, 1, 1, 0) assert result assert confirmed_positive is not None @@ -501,7 +501,7 @@ def test_run_with_false_positive( Test to see if run function doesn't add landing pads that are similar to false positives. """ # Setup - result, false_positive = object_in_world.ObjectInWorld.create(1, 1, 1) + result, false_positive = object_in_world.ObjectInWorld.create(1, 1, 1, 0) assert result assert false_positive is not None From dabc8cb569223f2fe1d61dd7cb8422d5da38ae86 Mon Sep 17 00:00:00 2001 From: a2misic Date: Fri, 29 Nov 2024 19:22:55 -0500 Subject: [PATCH 02/23] implemented cluster estimation label by detection label --- .../cluster_estimation/cluster_estimation.py | 128 +++++++++++++++--- 1 file changed, 112 insertions(+), 16 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index a4c67f11..dc47c266 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -23,15 +23,22 @@ class ClusterEstimation: METHODS ------- run() - Take in list of landing pad detections and return list of estimated landing pad locations + Take in list of object detections and return list of estimated object locations if number of detections is sufficient, or if manually forced to run. + cluster_by_label() + Take in list of detections of the same label and return list of estimated object locations + of the same label. + __decide_to_run() Decide when to run cluster estimation model. __sort_by_weights() Sort input model output list by weights in descending order. + __sort_by_labels() + Sort input detection list by labels in descending order. + __convert_detections_to_point() Convert DetectionInWorld input object to a [x,y] position to store. @@ -40,6 +47,7 @@ class ClusterEstimation: __filter_by_covariances() Removes any cluster with covariances much higher than the lowest covariance value. + """ __create_key = object() @@ -142,10 +150,12 @@ def __init__( self.__logger = local_logger def run( - self, detections: "list[detection_in_world.DetectionInWorld]", run_override: bool + self, + detections: "list[detection_in_world.DetectionInWorld]", + run_override: bool, ) -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": """ - Take in list of landing pad detections and return list of estimated landing pad locations + Take in list of detections and return list of estimated object locations if number of detections is sufficient, or if manually forced to run. PARAMETERS @@ -170,24 +180,92 @@ def run( # Store new input data self.__current_bucket += self.__convert_detections_to_point(detections) + # Decide to run + if not self.__decide_to_run(run_override): + return False, None + + # sort bucket by label in descending order + self.__current_bucket = self.__sort_by_labels(self.__current_bucket) + detections_in_world = [] + + # init search parameters + ptr = 0 + max_label = self.__current_bucket[0][2] + + # itterates through all possible labels + for label in reversed(range(max_label)): + + # creates bucket of labels + bucket_labelled = [] + while ptr < len(self.__current_bucket) and self.__current_bucket[ptr][2] == label: + bucket_labelled += [self.__current_bucket[ptr]] + ptr += 1 + + # skip if no objects have label=label + if len(bucket_labelled) == 0: + continue + + check, labelled_detections_in_world = self.cluster_by_label( + bucket_labelled, run_override, label + ) + + # checks if cluster_by_label ran succssfully + if not check: + self.__logger(f"did not add objects of label={label} to total object detections") + continue + + detections_in_world += labelled_detections_in_world + + return True, detections_in_world + + def cluster_by_label( + self, + points: "list[tuple[float, float, int]]", + run_override: bool, + label: int, + ) -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": + """ + Take in list of detections of the same label and return list of estimated object locations + of the same label. + + PARAMETERS + ---------- + points: list[tuple[float, float, int]] + List containing tuple objects which holds real-world positioning data to run + clustering on and their labels + + run_override: bool + Forces ClusterEstimation to predict if data is available, regardless of any other + requirements. + + RETURNS + ------- + model_ran: bool + True if ClusterEstimation object successfully ran its estimation model, False otherwise. + + objects_in_world: list[ObjectInWorld] or None. + List containing ObjectInWorld objects, containing position and covariance value. + None if conditions not met and model not ran or model failed to converge. + """ + # Decide to run if not self.__decide_to_run(run_override): return False, None # Fit points and get cluster data - self.__vgmm = self.__vgmm.fit(self.__all_points) # type: ignore + __vgmm_label = self.__vgmm.fit(points) # type: ignore # Check convergence - if not self.__vgmm.converged_: - self.__logger.warning("Model failed to converge") + if not __vgmm_label.converged_: + self.__logger.warning(f"Model for label={label} failed to converge") return False, None # Get predictions from cluster model model_output: "list[tuple[np.ndarray, float, float]]" = list( zip( - self.__vgmm.means_, # type: ignore - self.__vgmm.weights_, # type: ignore - self.__vgmm.covariances_, # type: ignore + __vgmm_label.means_, # type: ignore + __vgmm_label.weights_, # type: ignore + __vgmm_label.covariances_, # type: ignore ) ) @@ -210,7 +288,6 @@ def run( # Remove clusters with covariances too large model_output = self.__filter_by_covariances(model_output) - label = 0 # Create output list of remaining valid clusters detections_in_world = [] for cluster in model_output: @@ -220,7 +297,6 @@ def run( if result: detections_in_world.append(landing_pad) - label += 1 else: self.__logger.warning("Failed to create ObjectInWorld object") @@ -285,12 +361,32 @@ def __sort_by_weights( """ return sorted(model_output, key=lambda x: x[1], reverse=True) + @staticmethod + def __sort_by_labels( + points: "list[tuple[float, float, int]]", + ) -> "list[tuple[float, float, int]]": + """ + Sort input detection list by labels in descending order. + + PARAMETERS + ---------- + detections: list[tuple[float, float, int]] + List containing detections, with each element having the format + [x_position, y_position, label]. + + RETURNS + ------- + list[tuple[np.ndarray, float, float]] + List containing detection points sorted in descending order by label + """ + return sorted(points, key=lambda x: x.label, reverse=True) + @staticmethod def __convert_detections_to_point( detections: "list[detection_in_world.DetectionInWorld]", - ) -> "list[tuple[float, float]]": + ) -> "list[tuple[float, float, int]]": """ - Convert DetectionInWorld input object to a list of points- (x,y) positions, to store. + Convert DetectionInWorld input object to a list of points- (x,y) positions with label, to store. PARAMETERS ---------- @@ -300,8 +396,8 @@ def __convert_detections_to_point( RETURNS ------- - points: list[tuple[float, float]] - List of points (x,y). + points: list[tuple[float, float, int]] + List of points (x,y) and their label ------- """ points = [] @@ -313,7 +409,7 @@ def __convert_detections_to_point( # Convert DetectionInWorld objects for detection in detections: # `centre` attribute holds positioning data - points.append(tuple([detection.centre[0], detection.centre[1]])) + points.append(tuple([detection.centre[0], detection.centre[1], detection.label])) return points From a101d17c9d47955e6fb653b3319378db2878ab4b Mon Sep 17 00:00:00 2001 From: a2misic Date: Wed, 18 Dec 2024 21:20:23 -0500 Subject: [PATCH 03/23] fixed implementation --- modules/cluster_estimation/cluster_estimation.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index dc47c266..3fcb2c7a 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -47,7 +47,6 @@ class ClusterEstimation: __filter_by_covariances() Removes any cluster with covariances much higher than the lowest covariance value. - """ __create_key = object() @@ -190,12 +189,13 @@ def run( # init search parameters ptr = 0 - max_label = self.__current_bucket[0][2] - # itterates through all possible labels - for label in reversed(range(max_label)): + # itterates through all points + while ptr <= len(self.__current_bucket): + # reference label + label = self.__current_bucket[ptr][2] - # creates bucket of labels + # creates bucket of points with the same label bucket_labelled = [] while ptr < len(self.__current_bucket) and self.__current_bucket[ptr][2] == label: bucket_labelled += [self.__current_bucket[ptr]] @@ -211,7 +211,9 @@ def run( # checks if cluster_by_label ran succssfully if not check: - self.__logger(f"did not add objects of label={label} to total object detections") + self.__logger.warning( + f"did not add objects of label={label} to total object detections" + ) continue detections_in_world += labelled_detections_in_world From 571cc13d3c2f1bff90dff34bb950801878d3e4fd Mon Sep 17 00:00:00 2001 From: a2misic Date: Mon, 6 Jan 2025 21:32:26 -0500 Subject: [PATCH 04/23] implemented fixes --- modules/cluster_estimation/cluster_estimation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index 3fcb2c7a..cca59beb 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -184,7 +184,7 @@ def run( return False, None # sort bucket by label in descending order - self.__current_bucket = self.__sort_by_labels(self.__current_bucket) + self.__all_points = self.__sort_by_labels(self.__current_bucket) detections_in_world = [] # init search parameters @@ -195,22 +195,22 @@ def run( # reference label label = self.__current_bucket[ptr][2] - # creates bucket of points with the same label + # creates bucket of points with the same label since bucket is sorted by label bucket_labelled = [] - while ptr < len(self.__current_bucket) and self.__current_bucket[ptr][2] == label: - bucket_labelled += [self.__current_bucket[ptr]] + while ptr < len(self.__current_bucket) and self.__all_points[ptr][2] == label: + bucket_labelled.append([self.__all_points[ptr]]) ptr += 1 # skip if no objects have label=label if len(bucket_labelled) == 0: continue - check, labelled_detections_in_world = self.cluster_by_label( + result, labelled_detections_in_world = self.cluster_by_label( bucket_labelled, run_override, label ) # checks if cluster_by_label ran succssfully - if not check: + if not result: self.__logger.warning( f"did not add objects of label={label} to total object detections" ) From 61dcfa1de587deb74ccb147ed3928427b90d39f2 Mon Sep 17 00:00:00 2001 From: a2misic Date: Thu, 16 Jan 2025 15:20:31 -0500 Subject: [PATCH 05/23] implemented changes, made it pass all tests --- .../cluster_estimation/cluster_estimation.py | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index cca59beb..bcb9c7c3 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -178,36 +178,35 @@ def run( """ # Store new input data self.__current_bucket += self.__convert_detections_to_point(detections) + self.__all_points = [] # Decide to run if not self.__decide_to_run(run_override): return False, None # sort bucket by label in descending order - self.__all_points = self.__sort_by_labels(self.__current_bucket) + self.__all_points = self.__sort_by_labels(self.__all_points) detections_in_world = [] # init search parameters ptr = 0 # itterates through all points - while ptr <= len(self.__current_bucket): + while ptr < len(self.__all_points): # reference label - label = self.__current_bucket[ptr][2] + label = self.__all_points[ptr][2] # creates bucket of points with the same label since bucket is sorted by label bucket_labelled = [] - while ptr < len(self.__current_bucket) and self.__all_points[ptr][2] == label: - bucket_labelled.append([self.__all_points[ptr]]) + while ptr < len(self.__all_points) and self.__all_points[ptr][2] == label: + bucket_labelled.append(self.__all_points[ptr]) ptr += 1 # skip if no objects have label=label if len(bucket_labelled) == 0: continue - result, labelled_detections_in_world = self.cluster_by_label( - bucket_labelled, run_override, label - ) + result, labelled_detections_in_world = self.cluster_by_label(bucket_labelled, label) # checks if cluster_by_label ran succssfully if not result: @@ -223,7 +222,6 @@ def run( def cluster_by_label( self, points: "list[tuple[float, float, int]]", - run_override: bool, label: int, ) -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": """ @@ -249,13 +247,8 @@ def cluster_by_label( List containing ObjectInWorld objects, containing position and covariance value. None if conditions not met and model not ran or model failed to converge. """ - - # Decide to run - if not self.__decide_to_run(run_override): - return False, None - # Fit points and get cluster data - __vgmm_label = self.__vgmm.fit(points) # type: ignore + __vgmm_label = self.__vgmm.fit([[point[0], point[1]] for point in points]) # type: ignore # Check convergence if not __vgmm_label.converged_: @@ -302,7 +295,7 @@ def cluster_by_label( else: self.__logger.warning("Failed to create ObjectInWorld object") - self.__logger.info(detections_in_world) + # self.__logger.info(detections_in_world) return True, detections_in_world def __decide_to_run(self, run_override: bool) -> bool: @@ -322,14 +315,13 @@ def __decide_to_run(self, run_override: bool) -> bool: """ count_all = len(self.__all_points) count_current = len(self.__current_bucket) - if not run_override: # Don't run if total points under minimum requirement if count_all + count_current < self.__min_activation_threshold: return False # Don't run if not enough new points - if count_current < self.__min_new_points_to_run and self.__has_ran_once: + if count_current < self.__min_new_points_to_run and not self.__has_ran_once: return False # No data can not run @@ -381,7 +373,9 @@ def __sort_by_labels( list[tuple[np.ndarray, float, float]] List containing detection points sorted in descending order by label """ - return sorted(points, key=lambda x: x.label, reverse=True) + return sorted( + points, key=lambda x: x[2], reverse=True + ) # the label is stored at index 2 of object @staticmethod def __convert_detections_to_point( @@ -433,7 +427,7 @@ def __filter_by_points_ownership( List containing predicted cluster centres after filtering. """ # List of each point's cluster index - cluster_assignment = self.__vgmm.predict(self.__all_points) # type: ignore + cluster_assignment = self.__vgmm.predict([[point[0], point[1]] for point in self.__all_points]) # type: ignore # Find which cluster indices have points clusters_with_points = np.unique(cluster_assignment) From 9c70b0007195abe127cf0ca213a1ae7d1b71caa0 Mon Sep 17 00:00:00 2001 From: a2misic Date: Sun, 26 Jan 2025 16:20:00 -0500 Subject: [PATCH 06/23] work in progress commit --- .../cluster_estimation/cluster_estimation.py | 134 ++------- .../cluster_estimation_by_label.py | 274 ++++++++++++++++++ .../cluster_estimation_worker.py | 28 +- tests/unit/test_cluster_detection.py | 194 ++++++++++++- 4 files changed, 510 insertions(+), 120 deletions(-) create mode 100644 modules/cluster_estimation/cluster_estimation_by_label.py diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index bcb9c7c3..8795958f 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -23,22 +23,15 @@ class ClusterEstimation: METHODS ------- run() - Take in list of object detections and return list of estimated object locations + Take in list of landing pad detections and return list of estimated landing pad locations if number of detections is sufficient, or if manually forced to run. - cluster_by_label() - Take in list of detections of the same label and return list of estimated object locations - of the same label. - __decide_to_run() Decide when to run cluster estimation model. __sort_by_weights() Sort input model output list by weights in descending order. - __sort_by_labels() - Sort input detection list by labels in descending order. - __convert_detections_to_point() Convert DetectionInWorld input object to a [x,y] position to store. @@ -149,12 +142,10 @@ def __init__( self.__logger = local_logger def run( - self, - detections: "list[detection_in_world.DetectionInWorld]", - run_override: bool, + self, detections: "list[detection_in_world.DetectionInWorld]", run_override: bool ) -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": """ - Take in list of detections and return list of estimated object locations + Take in list of landing pad detections and return list of estimated landing pad locations if number of detections is sufficient, or if manually forced to run. PARAMETERS @@ -178,92 +169,31 @@ def run( """ # Store new input data self.__current_bucket += self.__convert_detections_to_point(detections) - self.__all_points = [] + print("len of current bucket = "+str(len(self.__current_bucket))) # Decide to run if not self.__decide_to_run(run_override): return False, None - # sort bucket by label in descending order - self.__all_points = self.__sort_by_labels(self.__all_points) - detections_in_world = [] - - # init search parameters - ptr = 0 - - # itterates through all points - while ptr < len(self.__all_points): - # reference label - label = self.__all_points[ptr][2] - - # creates bucket of points with the same label since bucket is sorted by label - bucket_labelled = [] - while ptr < len(self.__all_points) and self.__all_points[ptr][2] == label: - bucket_labelled.append(self.__all_points[ptr]) - ptr += 1 - - # skip if no objects have label=label - if len(bucket_labelled) == 0: - continue - - result, labelled_detections_in_world = self.cluster_by_label(bucket_labelled, label) - - # checks if cluster_by_label ran succssfully - if not result: - self.__logger.warning( - f"did not add objects of label={label} to total object detections" - ) - continue - - detections_in_world += labelled_detections_in_world - - return True, detections_in_world - - def cluster_by_label( - self, - points: "list[tuple[float, float, int]]", - label: int, - ) -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": - """ - Take in list of detections of the same label and return list of estimated object locations - of the same label. - - PARAMETERS - ---------- - points: list[tuple[float, float, int]] - List containing tuple objects which holds real-world positioning data to run - clustering on and their labels - - run_override: bool - Forces ClusterEstimation to predict if data is available, regardless of any other - requirements. - - RETURNS - ------- - model_ran: bool - True if ClusterEstimation object successfully ran its estimation model, False otherwise. - - objects_in_world: list[ObjectInWorld] or None. - List containing ObjectInWorld objects, containing position and covariance value. - None if conditions not met and model not ran or model failed to converge. - """ # Fit points and get cluster data - __vgmm_label = self.__vgmm.fit([[point[0], point[1]] for point in points]) # type: ignore + self.__vgmm = self.__vgmm.fit(self.__all_points) # type: ignore # Check convergence - if not __vgmm_label.converged_: - self.__logger.warning(f"Model for label={label} failed to converge") + if not self.__vgmm.converged_: + self.__logger.warning("Model failed to converge") return False, None # Get predictions from cluster model model_output: "list[tuple[np.ndarray, float, float]]" = list( zip( - __vgmm_label.means_, # type: ignore - __vgmm_label.weights_, # type: ignore - __vgmm_label.covariances_, # type: ignore + self.__vgmm.means_, # type: ignore + self.__vgmm.weights_, # type: ignore + self.__vgmm.covariances_, # type: ignore ) ) + print("output = "+str(model_output)) + # Empty cluster removal model_output = self.__filter_by_points_ownership(model_output) @@ -279,6 +209,7 @@ def cluster_by_label( viable_clusters.append(model_output[i]) model_output = viable_clusters + print("len model output: "+str(len(model_output))) # Remove clusters with covariances too large model_output = self.__filter_by_covariances(model_output) @@ -287,7 +218,7 @@ def cluster_by_label( detections_in_world = [] for cluster in model_output: result, landing_pad = object_in_world.ObjectInWorld.create( - cluster[0][0], cluster[0][1], cluster[2], label + cluster[0][0], cluster[0][1], cluster[2], 0 ) if result: @@ -295,7 +226,7 @@ def cluster_by_label( else: self.__logger.warning("Failed to create ObjectInWorld object") - # self.__logger.info(detections_in_world) + #self.__logger.info(detections_in_world) return True, detections_in_world def __decide_to_run(self, run_override: bool) -> bool: @@ -315,13 +246,14 @@ def __decide_to_run(self, run_override: bool) -> bool: """ count_all = len(self.__all_points) count_current = len(self.__current_bucket) + if not run_override: # Don't run if total points under minimum requirement if count_all + count_current < self.__min_activation_threshold: return False # Don't run if not enough new points - if count_current < self.__min_new_points_to_run and not self.__has_ran_once: + if count_current < self.__min_new_points_to_run and self.__has_ran_once: return False # No data can not run @@ -355,34 +287,12 @@ def __sort_by_weights( """ return sorted(model_output, key=lambda x: x[1], reverse=True) - @staticmethod - def __sort_by_labels( - points: "list[tuple[float, float, int]]", - ) -> "list[tuple[float, float, int]]": - """ - Sort input detection list by labels in descending order. - - PARAMETERS - ---------- - detections: list[tuple[float, float, int]] - List containing detections, with each element having the format - [x_position, y_position, label]. - - RETURNS - ------- - list[tuple[np.ndarray, float, float]] - List containing detection points sorted in descending order by label - """ - return sorted( - points, key=lambda x: x[2], reverse=True - ) # the label is stored at index 2 of object - @staticmethod def __convert_detections_to_point( detections: "list[detection_in_world.DetectionInWorld]", ) -> "list[tuple[float, float, int]]": """ - Convert DetectionInWorld input object to a list of points- (x,y) positions with label, to store. + Convert DetectionInWorld input object to a list of points- (x,y) positions, to store. PARAMETERS ---------- @@ -392,8 +302,8 @@ def __convert_detections_to_point( RETURNS ------- - points: list[tuple[float, float, int]] - List of points (x,y) and their label + points: list[tuple[float, float]] + List of points (x,y). ------- """ points = [] @@ -405,7 +315,7 @@ def __convert_detections_to_point( # Convert DetectionInWorld objects for detection in detections: # `centre` attribute holds positioning data - points.append(tuple([detection.centre[0], detection.centre[1], detection.label])) + points.append(tuple([detection.centre[0], detection.centre[1]])) return points @@ -427,7 +337,7 @@ def __filter_by_points_ownership( List containing predicted cluster centres after filtering. """ # List of each point's cluster index - cluster_assignment = self.__vgmm.predict([[point[0], point[1]] for point in self.__all_points]) # type: ignore + cluster_assignment = self.__vgmm.predict(self.__all_points) # type: ignore # Find which cluster indices have points clusters_with_points = np.unique(cluster_assignment) diff --git a/modules/cluster_estimation/cluster_estimation_by_label.py b/modules/cluster_estimation/cluster_estimation_by_label.py new file mode 100644 index 00000000..d56fb3b3 --- /dev/null +++ b/modules/cluster_estimation/cluster_estimation_by_label.py @@ -0,0 +1,274 @@ +""" +Take in bounding box coordinates from Geolocation and use to estimate landing pad locations. +Returns an array of classes, each containing the x coordinate, y coordinate, and spherical +covariance of each landing pad estimation. +""" + +import numpy as np +import sklearn +import sklearn.datasets +import sklearn.mixture + +from .. import object_in_world +from .. import detection_in_world +from ..cluster_estimation import cluster_estimation +from ..common.modules.logger import logger + + +class ClusterEstimationByLabel: + """ + Estimate landing pad locations based on landing pad ground detection. Estimation + works by predicting 'cluster centres' from groups of closely placed landing pad + detections. + + ATTRIBUTES + ---------- + min_activation_threshold: int + Minimum total data points before model runs. + + min_new_points_to_run: int + Minimum number of new data points that must be collected before running model. + + METHODS + ------- + run() + Take in list of object detections and return list of estimated object locations + if number of detections is sufficient, or if manually forced to run. + + cluster_by_label() + Take in list of detections of the same label and return list of estimated object locations + of the same label. + + __decide_to_run() + Decide when to run cluster estimation model. + + __sort_by_weights() + Sort input model output list by weights in descending order. + + __sort_by_labels() + Sort input detection list by labels in descending order. + + __convert_detections_to_point() + Convert DetectionInWorld input object to a [x,y] position to store. + + """ + + __create_key = object() + + @classmethod + def create( + cls, + min_activation_threshold: int, + min_new_points_to_run: int, + cluster_model: cluster_estimation.ClusterEstimation, + local_logger: logger.Logger, + ) -> "tuple[bool, ClusterEstimationByLabel | None]": + """ + Data requirement conditions for estimation model to run. + """ + + # At least 1 point for model to fit + if min_activation_threshold < 1: + return False, None + + return True, ClusterEstimationByLabel( + cls.__create_key, + min_activation_threshold, + min_new_points_to_run, + cluster_model, + local_logger, + ) + + def __init__( + self, + class_private_create_key: object, + min_activation_threshold: int, + min_new_points_to_run: int, + cluster_model: cluster_estimation.ClusterEstimation, + local_logger: logger.Logger, + ) -> None: + """ + Private constructor, use create() method. + """ + assert ( + class_private_create_key is ClusterEstimationByLabel.__create_key + ), "Use create() method" + + # Points storage + self.__all_points: "list[tuple[float, float]]" = [] + self.__current_bucket: "list[tuple[float, float]]" = [] + + # Requirements to decide to run + self.__min_activation_threshold = min_activation_threshold + self.__min_new_points_to_run = min_new_points_to_run + self.__logger = local_logger + + # cluster_model + self.__cluster_model = cluster_model + + def run( + self, + detections: "list[detection_in_world.DetectionInWorld]", + run_override: bool, + ) -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": + """ + Take in list of detections and return list of estimated object locations + if number of detections is sufficient, or if manually forced to run. + + PARAMETERS + ---------- + detections: list[DetectionInWorld] + List containing DetectionInWorld objects which holds real-world positioning data to run + clustering on. + + run_override: bool + Forces ClusterEstimation to predict if data is available, regardless of any other + requirements. + + RETURNS + ------- + model_ran: bool + True if ClusterEstimation object successfully ran its estimation model, False otherwise. + + objects_in_world: list[ObjectInWorld] or None. + List containing ObjectInWorld objects, containing position and covariance value. + None if conditions not met and model not ran or model failed to converge. + """ + # Store new input data + self.__current_bucket += detections + self.__all_points = [] + + # Decide to run + if not self.__decide_to_run(run_override): + return False, None + + # sort bucket by label in descending order + self.__all_points = self.__sort_by_labels(self.__all_points) + detections_in_world = [] + + # init search parameters + ptr = 0 + + # itterates through all points + while ptr < len(self.__all_points): + # reference label + label = (self.__all_points[ptr]).label + + # creates bucket of points with the same label since bucket is sorted by label + bucket_labelled = [] + while ptr < len(self.__all_points) and (self.__all_points[ptr]).label == label: + bucket_labelled.append(self.__all_points[ptr]) + ptr += 1 + + # skip if no other objects have the same label + if len(bucket_labelled) == 1: + continue + + print("len bucket = "+str(len(bucket_labelled))) + + result, labelled_detections_in_world = self.__cluster_model.run(bucket_labelled, run_override) + + print("labelled detections = "+str(len(labelled_detections_in_world))) + + for object in labelled_detections_in_world: + object.label = label + + # checks if cluster_by_label ran succssfully + if not result: + self.__logger.warning( + f"did not add objects of label={label} to total object detections" + ) + continue + + detections_in_world += labelled_detections_in_world + + return True, detections_in_world + + def __decide_to_run(self, run_override: bool) -> bool: + """ + Decide when to run cluster estimation model. + + PARAMETERS + ---------- + run_override: bool + Forces ClusterEstimation to predict if data is available, regardless of any other + requirements. + + RETURNS + ------- + bool + True if estimation model will be run, False otherwise. + """ + count_all = len(self.__all_points) + count_current = len(self.__current_bucket) + if not run_override: + # Don't run if total points under minimum requirement + if count_all + count_current < self.__min_activation_threshold: + return False + + # Don't run if not enough new points + if count_current < self.__min_new_points_to_run: + return False + + # No data can not run + if count_all + count_current == 0: + return False + + # Requirements met, empty bucket and run + self.__all_points += self.__current_bucket + self.__current_bucket = [] + + return True + + @staticmethod + def __sort_by_labels( + points: "list[detection_in_world.DetectionInWorld]", + ) -> "list[detection_in_world.DetectionInWorld]": + """ + Sort input detection list by labels in descending order. + + PARAMETERS + ---------- + detections: list[detection_in_world.DetectionInWorld] + List containing detections. + + RETURNS + ------- + list[tuple[np.ndarray, float, float]] + List containing detection points sorted in descending order by label + """ + return sorted( + points, key=lambda x: x.label, reverse=True + ) # the label is stored at index 2 of object + + @staticmethod + def __convert_detections_to_point( + detections: "list[detection_in_world.DetectionInWorld]", + ) -> "list[tuple[float, float, int]]": + """ + Convert DetectionInWorld input object to a list of points- (x,y) positions with label, to store. + + PARAMETERS + ---------- + detections: list[DetectionInWorld] + List of DetectionInWorld intermediate objects, the data structure that is passed to the + worker. + + RETURNS + ------- + points: list[tuple[float, float, int]] + List of points (x,y) and their label + ------- + """ + points = [] + + # Input detections list is empty + if len(detections) == 0: + return points + + # Convert DetectionInWorld objects + for detection in detections: + # `centre` attribute holds positioning data + points.append(tuple([detection.centre[0], detection.centre[1], detection.label])) + + return points diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index 0f378625..62b664f6 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -1,5 +1,7 @@ """ -Gets detections in world space and outputs estimations of objects. +Take in bounding box coordinates from Geolocation and use to estimate landing pad locations. +Returns an array of classes, each containing the x coordinate, y coordinate, and spherical +covariance of each landing pad estimation. """ import os @@ -38,14 +40,26 @@ def cluster_estimation_worker( random_state: int Seed for randomizer, to get consistent results. - input_queue: queue_proxy_wrapper.QueuePRoxyWrapper - Data queue. + METHODS + ------- + run() + Take in list of landing pad detections and return list of estimated landing pad locations + if number of detections is sufficient, or if manually forced to run. - output_queue: queue_proxy_wrapper.QueuePRoxyWrapper - Data queue. + __decide_to_run() + Decide when to run cluster estimation model. - worker_controller: worker_controller.WorkerController - How the main process communicates to this worker process. + __sort_by_weights() + Sort input model output list by weights in descending order. + + __convert_detections_to_point() + Convert DetectionInWorld input object to a [x,y] position to store. + + __filter_by_points_ownership() + Removes any clusters that don't have any points belonging to it. + + __filter_by_covariances() + Removes any cluster with covariances much higher than the lowest covariance value. """ worker_name = pathlib.Path(__file__).stem process_id = os.getpid() diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 155cca06..fd8c95fa 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -7,6 +7,7 @@ import sklearn.datasets from modules.cluster_estimation import cluster_estimation +from modules.cluster_estimation import cluster_estimation_by_label from modules.common.modules.logger import logger from modules import detection_in_world @@ -44,8 +45,30 @@ def cluster_model() -> cluster_estimation.ClusterEstimation: # type: ignore yield model # type: ignore +@pytest.fixture() +def cluster_model_by_label(cluster_model: cluster_estimation.ClusterEstimation) -> cluster_estimation_by_label.ClusterEstimationByLabel: # type: ignore + """ + Cluster estimation by label object. + """ + result, test_logger = logger.Logger.create("test_logger", False) + assert result + assert test_logger is not None + + result, model = cluster_estimation_by_label.ClusterEstimationByLabel.create( + MIN_TOTAL_POINTS_THRESHOLD, + MIN_NEW_POINTS_TO_RUN, + cluster_model, + test_logger, + ) + assert result + assert model is not None + + yield model # type: ignore + + def generate_cluster_data( - n_samples_per_cluster: "list[int]", cluster_standard_deviation: int + n_samples_per_cluster: "list[int]", + cluster_standard_deviation: int, ) -> "tuple[list[detection_in_world.DetectionInWorld], list[np.ndarray]]": """ Returns a list of points (DetectionInWorld objects) with specified points per cluster @@ -107,6 +130,53 @@ def generate_cluster_data( return detections, cluster_positions.tolist() +def generate_cluster_data_by_label( + corresponding_labels: "list[int]", + n_samples_per_cluster: "list[int]", + cluster_standard_deviation: int, +) -> "tuple[list[detection_in_world.DetectionInWorld], list[np.ndarray]]": + """ + Returns a list of labeled points (DetectionInWorld objects) with specified points per cluster + and standard deviation. + + PARAMETERS + ---------- + correspong_labels: "list[int]" + Each entry represents the label to the corresponding cluster given in the + n_samples_per_cluster list. + + n_samples_per_cluster: list[int] + List corresponding to how many points to generate for each generated cluster + ex: [10 20 30] will generate 10 points for one cluster, 20 points for the next, + and 30 points for the final cluster. + + cluster_standard_deviation: int + The standard deviation of the generated points, bigger + standard deviation == more spread out points. + + RETURNS + ------- + detections: list[detection_in_world.DetectionInWorld] + List of points (DetectionInWorld objects). + + cluster_positions: list[np.ndarray] + Coordinate positions of each cluster centre with their label. + ------- + """ + + detections = [] + cluster_positions = [] + + for i in range(len(n_samples_per_cluster)): + temp_detections, cluster_position = generate_cluster_data([n_samples_per_cluster[i]], cluster_standard_deviation) + for detection in temp_detections: + detection.label = corresponding_labels[i] + detections += temp_detections + cluster_positions.append([cluster_position[0], corresponding_labels[i]]) + + return detections, cluster_positions + + def generate_points_away_from_cluster( num_points_to_generate: int, minimum_distance_from_cluster: float, @@ -198,6 +268,24 @@ def test_under_min_total_threshold( assert not result assert detections_in_world is None + def test_under_min_total_threshold_by_label( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + As above, but with labels. + """ + # Setup + original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Less than min threshold (100) + generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert not result + assert detections_in_world is None + + def test_at_min_total_threshold( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -222,6 +310,30 @@ def test_at_min_total_threshold( assert result_2 assert detections_in_world_2 is not None + def test_at_min_total_threshold_by_label( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + As above, but with labels. + """ + # Setup + original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Should not run the first time + new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points + + generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) + generated_detections_2, _ = generate_cluster_data_by_label([1], [new_count], self.__STD_DEV_REG) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + result_2, detections_in_world_2 = cluster_model_by_label.run(generated_detections_2, False) + + # Test + assert not result + assert detections_in_world is None + assert result_2 + assert detections_in_world_2 is not None + + def test_under_min_bucket_size( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -245,6 +357,30 @@ def test_under_min_bucket_size( assert not result_2 assert detections_in_world_2 is None + def test_under_min_bucket_size_by_label( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + As above, but with labels. + """ + # Setup + original_count = MIN_TOTAL_POINTS_THRESHOLD + 10 # Should run the first time + new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points, shouldn't run + + generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) + generated_detections_2, _ = generate_cluster_data_by_label([1], [new_count], self.__STD_DEV_REG) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + result_2, detections_in_world_2 = cluster_model_by_label.run(generated_detections_2, False) + + # Test + assert result + assert detections_in_world is not None + assert not result_2 + assert detections_in_world_2 is None + + def test_good_data(self, cluster_model: cluster_estimation.ClusterEstimation) -> None: """ All conditions met should run. @@ -258,6 +394,20 @@ def test_good_data(self, cluster_model: cluster_estimation.ClusterEstimation) -> # Test assert result assert detections_in_world is not None + + def test_good_data_by_label(self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel) -> None: + """ + As above, but with labels. + """ + original_count = MIN_TOTAL_POINTS_THRESHOLD + 1 # More than min total threshold should run + generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert result + assert detections_in_world is not None class TestCorrectNumberClusterOutputs: @@ -286,6 +436,24 @@ def test_detect_normal_data_single_cluster( assert result assert detections_in_world is not None + def test_detect_normal_data_single_cluster_by_label( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + As above, but with labels. + """ + # Setup + points_per_cluster = [100] + generated_detections, _ = generate_cluster_data_by_label([1], points_per_cluster, self.__STD_DEV_REGULAR) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert result + assert detections_in_world is not None + + def test_detect_normal_data_five_clusters( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -305,6 +473,30 @@ def test_detect_normal_data_five_clusters( assert detections_in_world is not None assert len(detections_in_world) == expected_cluster_count + def test_detect_normal_data_five_clusters_by_label_all_different( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + As above, but with labels. Every cluster has a different label. + """ + # Setup + points_per_cluster = [100, 100, 100, 100, 100] + labels_of_clusters = [1, 1, 1, 1, 1] + expected_cluster_count = len(points_per_cluster) + generated_detections, clusters = generate_cluster_data_by_label(labels_of_clusters, points_per_cluster, self.__STD_DEV_REGULAR) + assert len(generated_detections) == 500 + assert len(clusters) == 5 + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert detections_in_world[0].label == 1 + assert result + assert detections_in_world is not None + assert len(detections_in_world) == expected_cluster_count + + def test_detect_large_std_dev_single_cluster( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: From 0b8970629482c59ae207625d05e1db7d0e365047 Mon Sep 17 00:00:00 2001 From: a2misic Date: Wed, 29 Jan 2025 23:36:25 -0500 Subject: [PATCH 07/23] tests working for cluster by label --- .../cluster_estimation/cluster_estimation.py | 40 ++- .../cluster_estimation_by_label.py | 233 ++++----------- .../cluster_estimation_worker.py | 3 - tests/unit/test_cluster_detection.py | 277 ++++++++---------- 4 files changed, 202 insertions(+), 351 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index 8795958f..a1012491 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -3,6 +3,7 @@ Returns an array of classes, each containing the x coordinate, y coordinate, and spherical covariance of each landing pad estimation. """ +# pylint: disable=duplicate-code import numpy as np import sklearn @@ -20,6 +21,23 @@ class ClusterEstimation: works by predicting 'cluster centres' from groups of closely placed landing pad detections. + ATTRIBUTES + ---------- + min_activation_threshold: int + Minimum total data points before model runs. + + min_new_points_to_run: int + Minimum number of new data points that must be collected before running model. + + random_state: int + Seed for randomizer, to get consistent results. + + local_logger: Logger + For logging error and debug messages. + + label: int + Every cluster generated by this model will have this label + METHODS ------- run() @@ -41,6 +59,7 @@ class ClusterEstimation: __filter_by_covariances() Removes any cluster with covariances much higher than the lowest covariance value. """ + # pylint: disable=too-many-instance-attributes __create_key = object() @@ -63,6 +82,7 @@ def create( max_num_components: int, random_state: int, local_logger: logger.Logger, + label: int, ) -> "tuple[bool, ClusterEstimation | None]": """ Data requirement conditions for estimation model to run. @@ -104,6 +124,7 @@ def create( max_num_components, random_state, local_logger, + label, ) def __init__( @@ -114,6 +135,7 @@ def __init__( max_num_components: int, random_state: int, local_logger: logger.Logger, + label: int, ) -> None: """ Private constructor, use create() method. @@ -140,6 +162,7 @@ def __init__( self.__min_new_points_to_run = min_new_points_to_run self.__has_ran_once = False self.__logger = local_logger + self.__label = label def run( self, detections: "list[detection_in_world.DetectionInWorld]", run_override: bool @@ -169,7 +192,6 @@ def run( """ # Store new input data self.__current_bucket += self.__convert_detections_to_point(detections) - print("len of current bucket = "+str(len(self.__current_bucket))) # Decide to run if not self.__decide_to_run(run_override): @@ -192,8 +214,6 @@ def run( ) ) - print("output = "+str(model_output)) - # Empty cluster removal model_output = self.__filter_by_points_ownership(model_output) @@ -209,25 +229,23 @@ def run( viable_clusters.append(model_output[i]) model_output = viable_clusters - print("len model output: "+str(len(model_output))) # Remove clusters with covariances too large model_output = self.__filter_by_covariances(model_output) # Create output list of remaining valid clusters - detections_in_world = [] + objects_in_world = [] for cluster in model_output: result, landing_pad = object_in_world.ObjectInWorld.create( - cluster[0][0], cluster[0][1], cluster[2], 0 + cluster[0][0], cluster[0][1], cluster[2], self.__label ) if result: - detections_in_world.append(landing_pad) + objects_in_world.append(landing_pad) else: - self.__logger.warning("Failed to create ObjectInWorld object") - - #self.__logger.info(detections_in_world) - return True, detections_in_world + self.__logger.error("Failed to create ObjectInWorld object") + return False, None + return True, objects_in_world def __decide_to_run(self, run_override: bool) -> bool: """ diff --git a/modules/cluster_estimation/cluster_estimation_by_label.py b/modules/cluster_estimation/cluster_estimation_by_label.py index d56fb3b3..3d65f5e8 100644 --- a/modules/cluster_estimation/cluster_estimation_by_label.py +++ b/modules/cluster_estimation/cluster_estimation_by_label.py @@ -3,14 +3,8 @@ Returns an array of classes, each containing the x coordinate, y coordinate, and spherical covariance of each landing pad estimation. """ - -import numpy as np -import sklearn -import sklearn.datasets -import sklearn.mixture - -from .. import object_in_world from .. import detection_in_world +from .. import object_in_world from ..cluster_estimation import cluster_estimation from ..common.modules.logger import logger @@ -29,29 +23,20 @@ class ClusterEstimationByLabel: min_new_points_to_run: int Minimum number of new data points that must be collected before running model. + random_state: int + Seed for randomizer, to get consistent results. + + local_logger: Logger + For logging error and debug messages. + METHODS ------- run() - Take in list of object detections and return list of estimated object locations - if number of detections is sufficient, or if manually forced to run. - - cluster_by_label() - Take in list of detections of the same label and return list of estimated object locations - of the same label. - - __decide_to_run() - Decide when to run cluster estimation model. - - __sort_by_weights() - Sort input model output list by weights in descending order. - - __sort_by_labels() - Sort input detection list by labels in descending order. - - __convert_detections_to_point() - Convert DetectionInWorld input object to a [x,y] position to store. - + Take in list of object detections and return dictionary of labels to + to corresponging clusters of estimated object locations if number of + detections is sufficient, or if manually forced to run. """ + # pylint: disable=too-many-instance-attributes __create_key = object() @@ -60,7 +45,7 @@ def create( cls, min_activation_threshold: int, min_new_points_to_run: int, - cluster_model: cluster_estimation.ClusterEstimation, + random_state: int, local_logger: logger.Logger, ) -> "tuple[bool, ClusterEstimationByLabel | None]": """ @@ -75,7 +60,7 @@ def create( cls.__create_key, min_activation_threshold, min_new_points_to_run, - cluster_model, + random_state, local_logger, ) @@ -84,7 +69,7 @@ def __init__( class_private_create_key: object, min_activation_threshold: int, min_new_points_to_run: int, - cluster_model: cluster_estimation.ClusterEstimation, + random_state: int, local_logger: logger.Logger, ) -> None: """ @@ -94,30 +79,27 @@ def __init__( class_private_create_key is ClusterEstimationByLabel.__create_key ), "Use create() method" - # Points storage - self.__all_points: "list[tuple[float, float]]" = [] - self.__current_bucket: "list[tuple[float, float]]" = [] - # Requirements to decide to run self.__min_activation_threshold = min_activation_threshold self.__min_new_points_to_run = min_new_points_to_run - self.__logger = local_logger + self.__random_state = random_state + self.__local_logger = local_logger - # cluster_model - self.__cluster_model = cluster_model + # cluster model corresponding to each label + self.__label_to_cluster_estimation_model: dict[int, cluster_estimation.ClusterEstimation] = {} def run( self, - detections: "list[detection_in_world.DetectionInWorld]", + input_detections: "list[detection_in_world.DetectionInWorld]", run_override: bool, - ) -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": + ) -> "tuple[True, dict[int, list[object_in_world.ObjectInWorld]]] | tuple[False, None]": """ Take in list of detections and return list of estimated object locations if number of detections is sufficient, or if manually forced to run. PARAMETERS ---------- - detections: list[DetectionInWorld] + input_detections: list[DetectionInWorld] List containing DetectionInWorld objects which holds real-world positioning data to run clustering on. @@ -130,145 +112,38 @@ def run( model_ran: bool True if ClusterEstimation object successfully ran its estimation model, False otherwise. - objects_in_world: list[ObjectInWorld] or None. - List containing ObjectInWorld objects, containing position and covariance value. - None if conditions not met and model not ran or model failed to converge. + labels_to_object_clusters: dict[int, list[object_in_world.ObjectInWorld] or None. + Dictionary where the key is a label and the value is a list of all cluster detections with that label """ - # Store new input data - self.__current_bucket += detections - self.__all_points = [] - - # Decide to run - if not self.__decide_to_run(run_override): - return False, None - - # sort bucket by label in descending order - self.__all_points = self.__sort_by_labels(self.__all_points) - detections_in_world = [] - - # init search parameters - ptr = 0 - - # itterates through all points - while ptr < len(self.__all_points): - # reference label - label = (self.__all_points[ptr]).label - - # creates bucket of points with the same label since bucket is sorted by label - bucket_labelled = [] - while ptr < len(self.__all_points) and (self.__all_points[ptr]).label == label: - bucket_labelled.append(self.__all_points[ptr]) - ptr += 1 - - # skip if no other objects have the same label - if len(bucket_labelled) == 1: - continue - - print("len bucket = "+str(len(bucket_labelled))) - - result, labelled_detections_in_world = self.__cluster_model.run(bucket_labelled, run_override) - - print("labelled detections = "+str(len(labelled_detections_in_world))) - - for object in labelled_detections_in_world: - object.label = label - - # checks if cluster_by_label ran succssfully - if not result: - self.__logger.warning( - f"did not add objects of label={label} to total object detections" + label_to_detections: dict[int, list[detection_in_world.DetectionInWorld]] = {} + for detection in input_detections: + if not (detection.label in label_to_detections): + label_to_detections[detection.label] = [] + label_to_detections[detection.label].append(detection) + + labels_to_object_clusters: dict[int, list[object_in_world.ObjectInWorld]] = {} + for label, detections in label_to_detections.items(): + if not (label in self.__label_to_cluster_estimation_model): + result, cluster_model = cluster_estimation.ClusterEstimation.create( + self.__min_activation_threshold, + self.__min_new_points_to_run, + self.__random_state, + self.__local_logger, + label ) - continue - - detections_in_world += labelled_detections_in_world - - return True, detections_in_world - - def __decide_to_run(self, run_override: bool) -> bool: - """ - Decide when to run cluster estimation model. - - PARAMETERS - ---------- - run_override: bool - Forces ClusterEstimation to predict if data is available, regardless of any other - requirements. - - RETURNS - ------- - bool - True if estimation model will be run, False otherwise. - """ - count_all = len(self.__all_points) - count_current = len(self.__current_bucket) - if not run_override: - # Don't run if total points under minimum requirement - if count_all + count_current < self.__min_activation_threshold: - return False - - # Don't run if not enough new points - if count_current < self.__min_new_points_to_run: - return False - - # No data can not run - if count_all + count_current == 0: - return False - - # Requirements met, empty bucket and run - self.__all_points += self.__current_bucket - self.__current_bucket = [] - - return True - - @staticmethod - def __sort_by_labels( - points: "list[detection_in_world.DetectionInWorld]", - ) -> "list[detection_in_world.DetectionInWorld]": - """ - Sort input detection list by labels in descending order. - - PARAMETERS - ---------- - detections: list[detection_in_world.DetectionInWorld] - List containing detections. - - RETURNS - ------- - list[tuple[np.ndarray, float, float]] - List containing detection points sorted in descending order by label - """ - return sorted( - points, key=lambda x: x.label, reverse=True - ) # the label is stored at index 2 of object - - @staticmethod - def __convert_detections_to_point( - detections: "list[detection_in_world.DetectionInWorld]", - ) -> "list[tuple[float, float, int]]": - """ - Convert DetectionInWorld input object to a list of points- (x,y) positions with label, to store. - - PARAMETERS - ---------- - detections: list[DetectionInWorld] - List of DetectionInWorld intermediate objects, the data structure that is passed to the - worker. - - RETURNS - ------- - points: list[tuple[float, float, int]] - List of points (x,y) and their label - ------- - """ - points = [] - - # Input detections list is empty - if len(detections) == 0: - return points - - # Convert DetectionInWorld objects - for detection in detections: - # `centre` attribute holds positioning data - points.append(tuple([detection.centre[0], detection.centre[1], detection.label])) - - return points + if not result: + self.__local_logger.error(f"Failed to create cluster estimation for label {label}") + return False, None + self.__label_to_cluster_estimation_model[label] = cluster_model + result, clusters = self.__label_to_cluster_estimation_model[label].run( + detections, + run_override, + ) + if not result: + self.__local_logger.error(f"Failed to run cluster estimation model for label {label}") + return False, None + if not (label in labels_to_object_clusters): + labels_to_object_clusters[label] = [] + labels_to_object_clusters[label] += clusters + + return True, labels_to_object_clusters diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index 62b664f6..acd90cae 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -34,9 +34,6 @@ def cluster_estimation_worker( min_new_points_to_run: int Minimum number of new data points that must be collected before running model. - max_num_components: int - Max number of real landing pads. - random_state: int Seed for randomizer, to get consistent results. diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index fd8c95fa..bd738380 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -2,6 +2,7 @@ Testing ClusterEstimation. """ +import random import numpy as np import pytest import sklearn.datasets @@ -38,6 +39,7 @@ def cluster_model() -> cluster_estimation.ClusterEstimation: # type: ignore MAX_NUM_COMPONENTS, RNG_SEED, test_logger, + 0 ) assert result assert model is not None @@ -57,7 +59,7 @@ def cluster_model_by_label(cluster_model: cluster_estimation.ClusterEstimation) result, model = cluster_estimation_by_label.ClusterEstimationByLabel.create( MIN_TOTAL_POINTS_THRESHOLD, MIN_NEW_POINTS_TO_RUN, - cluster_model, + RNG_SEED, test_logger, ) assert result @@ -69,6 +71,7 @@ def cluster_model_by_label(cluster_model: cluster_estimation.ClusterEstimation) def generate_cluster_data( n_samples_per_cluster: "list[int]", cluster_standard_deviation: int, + label: int, ) -> "tuple[list[detection_in_world.DetectionInWorld], list[np.ndarray]]": """ Returns a list of points (DetectionInWorld objects) with specified points per cluster @@ -85,6 +88,9 @@ def generate_cluster_data( The standard deviation of the generated points, bigger standard deviation == more spread out points. + label: int + The label that every generated detection gets assigned + RETURNS ------- detections: list[detection_in_world.DetectionInWorld] @@ -113,13 +119,12 @@ def generate_cluster_data( for point in generated_points: # Placeholder variables to create DetectionInWorld objects placeholder_vertices = np.array([[0, 0], [0, 0], [0, 0], [0, 0]]) - placeholder_label = 1 placeholder_confidence = 0.5 result, detection_to_add = detection_in_world.DetectionInWorld.create( placeholder_vertices, point, - placeholder_label, + label, placeholder_confidence, ) @@ -131,24 +136,18 @@ def generate_cluster_data( def generate_cluster_data_by_label( - corresponding_labels: "list[int]", - n_samples_per_cluster: "list[int]", + labels_to_n_samples_per_cluster: "dict[int, list[int]]", cluster_standard_deviation: int, -) -> "tuple[list[detection_in_world.DetectionInWorld], list[np.ndarray]]": +) -> "tuple[list[detection_in_world.DetectionInWorld], dict[int, list[np.ndarray]]]": """ Returns a list of labeled points (DetectionInWorld objects) with specified points per cluster and standard deviation. PARAMETERS ---------- - correspong_labels: "list[int]" - Each entry represents the label to the corresponding cluster given in the - n_samples_per_cluster list. - - n_samples_per_cluster: list[int] - List corresponding to how many points to generate for each generated cluster - ex: [10 20 30] will generate 10 points for one cluster, 20 points for the next, - and 30 points for the final cluster. + labels_to_cluster_samples: "dict[int, list[int]]" + Dictionary where the key is a label and the value is a + list of integers the represent the number of samples a cluster has. cluster_standard_deviation: int The standard deviation of the generated points, bigger @@ -159,22 +158,21 @@ def generate_cluster_data_by_label( detections: list[detection_in_world.DetectionInWorld] List of points (DetectionInWorld objects). - cluster_positions: list[np.ndarray] - Coordinate positions of each cluster centre with their label. + labels_to_cluster_positions: dict[int, list[np.ndarray]] + Dictionary where the key is a label and the value is a + list of coordinate positions of each cluster centre with that label. ------- """ detections = [] - cluster_positions = [] + labels_to_cluster_positions: dict[int, list[np.ndarray]] = {} - for i in range(len(n_samples_per_cluster)): - temp_detections, cluster_position = generate_cluster_data([n_samples_per_cluster[i]], cluster_standard_deviation) - for detection in temp_detections: - detection.label = corresponding_labels[i] + for label, n_samples_list in labels_to_n_samples_per_cluster.items(): + temp_detections, cluster_positions = generate_cluster_data(n_samples_list, cluster_standard_deviation, label) detections += temp_detections - cluster_positions.append([cluster_position[0], corresponding_labels[i]]) + labels_to_cluster_positions[label] = cluster_positions - return detections, cluster_positions + return detections, labels_to_cluster_positions def generate_points_away_from_cluster( @@ -259,7 +257,7 @@ def test_under_min_total_threshold( """ # Setup original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Less than min threshold (100) - generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG) + generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG, 0) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -268,24 +266,6 @@ def test_under_min_total_threshold( assert not result assert detections_in_world is None - def test_under_min_total_threshold_by_label( - self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel - ) -> None: - """ - As above, but with labels. - """ - # Setup - original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Less than min threshold (100) - generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - - # Test - assert not result - assert detections_in_world is None - - def test_at_min_total_threshold( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -297,8 +277,8 @@ def test_at_min_total_threshold( original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Should not run the first time new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points - generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG) - generated_detections_2, _ = generate_cluster_data([new_count], self.__STD_DEV_REG) + generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG, 0) + generated_detections_2, _ = generate_cluster_data([new_count], self.__STD_DEV_REG, 0) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -310,30 +290,6 @@ def test_at_min_total_threshold( assert result_2 assert detections_in_world_2 is not None - def test_at_min_total_threshold_by_label( - self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel - ) -> None: - """ - As above, but with labels. - """ - # Setup - original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Should not run the first time - new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points - - generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) - generated_detections_2, _ = generate_cluster_data_by_label([1], [new_count], self.__STD_DEV_REG) - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - result_2, detections_in_world_2 = cluster_model_by_label.run(generated_detections_2, False) - - # Test - assert not result - assert detections_in_world is None - assert result_2 - assert detections_in_world_2 is not None - - def test_under_min_bucket_size( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -344,8 +300,8 @@ def test_under_min_bucket_size( original_count = MIN_TOTAL_POINTS_THRESHOLD + 10 # Should run the first time new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points, shouldn't run - generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG) - generated_detections_2, _ = generate_cluster_data([new_count], self.__STD_DEV_REG) + generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG, 0) + generated_detections_2, _ = generate_cluster_data([new_count], self.__STD_DEV_REG, 0) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -357,36 +313,12 @@ def test_under_min_bucket_size( assert not result_2 assert detections_in_world_2 is None - def test_under_min_bucket_size_by_label( - self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel - ) -> None: - """ - As above, but with labels. - """ - # Setup - original_count = MIN_TOTAL_POINTS_THRESHOLD + 10 # Should run the first time - new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points, shouldn't run - - generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) - generated_detections_2, _ = generate_cluster_data_by_label([1], [new_count], self.__STD_DEV_REG) - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - result_2, detections_in_world_2 = cluster_model_by_label.run(generated_detections_2, False) - - # Test - assert result - assert detections_in_world is not None - assert not result_2 - assert detections_in_world_2 is None - - def test_good_data(self, cluster_model: cluster_estimation.ClusterEstimation) -> None: """ All conditions met should run. """ original_count = MIN_TOTAL_POINTS_THRESHOLD + 1 # More than min total threshold should run - generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG) + generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG, 0) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -394,20 +326,6 @@ def test_good_data(self, cluster_model: cluster_estimation.ClusterEstimation) -> # Test assert result assert detections_in_world is not None - - def test_good_data_by_label(self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel) -> None: - """ - As above, but with labels. - """ - original_count = MIN_TOTAL_POINTS_THRESHOLD + 1 # More than min total threshold should run - generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - - # Test - assert result - assert detections_in_world is not None class TestCorrectNumberClusterOutputs: @@ -427,7 +345,7 @@ def test_detect_normal_data_single_cluster( """ # Setup points_per_cluster = [100] - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR, 0) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -436,24 +354,6 @@ def test_detect_normal_data_single_cluster( assert result assert detections_in_world is not None - def test_detect_normal_data_single_cluster_by_label( - self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel - ) -> None: - """ - As above, but with labels. - """ - # Setup - points_per_cluster = [100] - generated_detections, _ = generate_cluster_data_by_label([1], points_per_cluster, self.__STD_DEV_REGULAR) - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - - # Test - assert result - assert detections_in_world is not None - - def test_detect_normal_data_five_clusters( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -463,7 +363,7 @@ def test_detect_normal_data_five_clusters( # Setup points_per_cluster = [100, 100, 100, 100, 100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR, 0) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -473,30 +373,6 @@ def test_detect_normal_data_five_clusters( assert detections_in_world is not None assert len(detections_in_world) == expected_cluster_count - def test_detect_normal_data_five_clusters_by_label_all_different( - self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel - ) -> None: - """ - As above, but with labels. Every cluster has a different label. - """ - # Setup - points_per_cluster = [100, 100, 100, 100, 100] - labels_of_clusters = [1, 1, 1, 1, 1] - expected_cluster_count = len(points_per_cluster) - generated_detections, clusters = generate_cluster_data_by_label(labels_of_clusters, points_per_cluster, self.__STD_DEV_REGULAR) - assert len(generated_detections) == 500 - assert len(clusters) == 5 - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - - # Test - assert detections_in_world[0].label == 1 - assert result - assert detections_in_world is not None - assert len(detections_in_world) == expected_cluster_count - - def test_detect_large_std_dev_single_cluster( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -506,7 +382,7 @@ def test_detect_large_std_dev_single_cluster( # Setup points_per_cluster = [100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_LARGE) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_LARGE, 0) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -525,7 +401,7 @@ def test_detect_large_std_dev_five_clusters( # Setup points_per_cluster = [100, 100, 100, 100, 100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_LARGE) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_LARGE, 0) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -545,7 +421,7 @@ def test_detect_skewed_data_single_cluster( # Setup points_per_cluster = [10, 100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR, 0) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -568,6 +444,7 @@ def test_detect_skewed_data_five_clusters( generated_detections, cluster_positions = generate_cluster_data( points_per_cluster, self.__STD_DEV_REGULAR, + 0, ) # Add 5 random points to dataset, each being at least 20m away from cluster centres @@ -599,7 +476,7 @@ def test_detect_consecutive_inputs_single_cluster( # Setup points_per_cluster = [100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR, 0) # Run result_latest = False @@ -623,7 +500,7 @@ def test_detect_consecutive_inputs_five_clusters( # Setup points_per_cluster = [100, 100, 100, 100, 100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR, 0) # Run result_latest = False @@ -657,6 +534,7 @@ def test_position_regular_data( generated_detections, cluster_positions = generate_cluster_data( points_per_cluster, self.__STD_DEV_REG, + 0, ) # Run @@ -681,3 +559,86 @@ def test_position_regular_data( break assert is_match + + +class TestCorrectClusterEstimationByLabel: + """ + Tests if cluster estimation by label properly sorts labels. + """ + + __STD_DEV_REG = 1 # Regular standard deviation is 1m + __MAX_POSITION_TOLERANCE = 1 + + def test_one_label( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + Five clusters with small standard devition that all have the same label + """ + # Setup + labels_to_n_samples_per_cluster = {1: [100, 100, 100, 100, 100]} + generated_detections, labels_to_generated_cluster_positions = generate_cluster_data_by_label( + labels_to_n_samples_per_cluster, + self.__STD_DEV_REG + ) + random.shuffle(generated_detections) # so all abojects with the same label are not arranged all in a row + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert result + assert detections_in_world is not None + assert len(detections_in_world[1]) == 5 + for cluster in detections_in_world[1]: + assert cluster.label == 1 + is_match = False + for generated_cluster in labels_to_generated_cluster_positions[1]: + # Check if coordinates are equal + distance = np.linalg.norm( + [cluster.location_x - generated_cluster[0], cluster.location_y - generated_cluster[1]] + ) + if distance < self.__MAX_POSITION_TOLERANCE: + is_match = True + break + + assert is_match + + def test_multiple_labels( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + Five clusters with small standard devition that have different labels + """ + # Setup + labels_to_n_samples_per_cluster = {1: [100, 100, 100], 2: [100, 100, 100], 3: [100, 100, 100]} + generated_detections, labels_to_generated_cluster_positions = generate_cluster_data_by_label( + labels_to_n_samples_per_cluster, + self.__STD_DEV_REG + ) + random.shuffle(generated_detections) # so all abojects with the same label are not arranged all in a row + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert result + assert detections_in_world is not None + assert len(detections_in_world[1]) == 3 + assert len(detections_in_world[2]) == 3 + assert len(detections_in_world[3]) == 3 + for label in range (1, 4): + for cluster in detections_in_world[label]: + assert cluster.label == label + is_match = False + for generated_cluster in labels_to_generated_cluster_positions[label]: + # Check if coordinates are equal + distance = np.linalg.norm( + [cluster.location_x - generated_cluster[0], cluster.location_y - generated_cluster[1]] + ) + if distance < self.__MAX_POSITION_TOLERANCE: + is_match = True + break + + assert is_match + \ No newline at end of file From f5c4b9080bafe50ae00540e0528aac15506df60c Mon Sep 17 00:00:00 2001 From: a2misic Date: Wed, 29 Jan 2025 23:40:27 -0500 Subject: [PATCH 08/23] reformated --- .../cluster_estimation/cluster_estimation.py | 2 + .../cluster_estimation_by_label.py | 24 ++++--- tests/unit/test_cluster_detection.py | 67 +++++++++++++------ 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index a1012491..fb1d851c 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -3,6 +3,7 @@ Returns an array of classes, each containing the x coordinate, y coordinate, and spherical covariance of each landing pad estimation. """ + # pylint: disable=duplicate-code import numpy as np @@ -59,6 +60,7 @@ class ClusterEstimation: __filter_by_covariances() Removes any cluster with covariances much higher than the lowest covariance value. """ + # pylint: disable=too-many-instance-attributes __create_key = object() diff --git a/modules/cluster_estimation/cluster_estimation_by_label.py b/modules/cluster_estimation/cluster_estimation_by_label.py index 3d65f5e8..32ff5287 100644 --- a/modules/cluster_estimation/cluster_estimation_by_label.py +++ b/modules/cluster_estimation/cluster_estimation_by_label.py @@ -3,6 +3,7 @@ Returns an array of classes, each containing the x coordinate, y coordinate, and spherical covariance of each landing pad estimation. """ + from .. import detection_in_world from .. import object_in_world from ..cluster_estimation import cluster_estimation @@ -36,6 +37,7 @@ class ClusterEstimationByLabel: to corresponging clusters of estimated object locations if number of detections is sufficient, or if manually forced to run. """ + # pylint: disable=too-many-instance-attributes __create_key = object() @@ -86,7 +88,9 @@ def __init__( self.__local_logger = local_logger # cluster model corresponding to each label - self.__label_to_cluster_estimation_model: dict[int, cluster_estimation.ClusterEstimation] = {} + self.__label_to_cluster_estimation_model: dict[ + int, cluster_estimation.ClusterEstimation + ] = {} def run( self, @@ -117,22 +121,24 @@ def run( """ label_to_detections: dict[int, list[detection_in_world.DetectionInWorld]] = {} for detection in input_detections: - if not (detection.label in label_to_detections): + if not detection.label in label_to_detections: label_to_detections[detection.label] = [] label_to_detections[detection.label].append(detection) labels_to_object_clusters: dict[int, list[object_in_world.ObjectInWorld]] = {} for label, detections in label_to_detections.items(): - if not (label in self.__label_to_cluster_estimation_model): + if not label in self.__label_to_cluster_estimation_model: result, cluster_model = cluster_estimation.ClusterEstimation.create( self.__min_activation_threshold, self.__min_new_points_to_run, self.__random_state, self.__local_logger, - label + label, ) if not result: - self.__local_logger.error(f"Failed to create cluster estimation for label {label}") + self.__local_logger.error( + f"Failed to create cluster estimation for label {label}" + ) return False, None self.__label_to_cluster_estimation_model[label] = cluster_model result, clusters = self.__label_to_cluster_estimation_model[label].run( @@ -140,10 +146,12 @@ def run( run_override, ) if not result: - self.__local_logger.error(f"Failed to run cluster estimation model for label {label}") + self.__local_logger.error( + f"Failed to run cluster estimation model for label {label}" + ) return False, None - if not (label in labels_to_object_clusters): + if not label in labels_to_object_clusters: labels_to_object_clusters[label] = [] labels_to_object_clusters[label] += clusters - + return True, labels_to_object_clusters diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index bd738380..05bd10af 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -48,7 +48,7 @@ def cluster_model() -> cluster_estimation.ClusterEstimation: # type: ignore @pytest.fixture() -def cluster_model_by_label(cluster_model: cluster_estimation.ClusterEstimation) -> cluster_estimation_by_label.ClusterEstimationByLabel: # type: ignore +def cluster_model_by_label() -> cluster_estimation_by_label.ClusterEstimationByLabel: # type: ignore """ Cluster estimation by label object. """ @@ -146,7 +146,7 @@ def generate_cluster_data_by_label( PARAMETERS ---------- labels_to_cluster_samples: "dict[int, list[int]]" - Dictionary where the key is a label and the value is a + Dictionary where the key is a label and the value is a list of integers the represent the number of samples a cluster has. cluster_standard_deviation: int @@ -159,7 +159,7 @@ def generate_cluster_data_by_label( List of points (DetectionInWorld objects). labels_to_cluster_positions: dict[int, list[np.ndarray]] - Dictionary where the key is a label and the value is a + Dictionary where the key is a label and the value is a list of coordinate positions of each cluster centre with that label. ------- """ @@ -168,7 +168,9 @@ def generate_cluster_data_by_label( labels_to_cluster_positions: dict[int, list[np.ndarray]] = {} for label, n_samples_list in labels_to_n_samples_per_cluster.items(): - temp_detections, cluster_positions = generate_cluster_data(n_samples_list, cluster_standard_deviation, label) + temp_detections, cluster_positions = generate_cluster_data( + n_samples_list, cluster_standard_deviation, label + ) detections += temp_detections labels_to_cluster_positions[label] = cluster_positions @@ -345,7 +347,9 @@ def test_detect_normal_data_single_cluster( """ # Setup points_per_cluster = [100] - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR, 0) + generated_detections, _ = generate_cluster_data( + points_per_cluster, self.__STD_DEV_REGULAR, 0 + ) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -363,7 +367,9 @@ def test_detect_normal_data_five_clusters( # Setup points_per_cluster = [100, 100, 100, 100, 100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR, 0) + generated_detections, _ = generate_cluster_data( + points_per_cluster, self.__STD_DEV_REGULAR, 0 + ) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -421,7 +427,9 @@ def test_detect_skewed_data_single_cluster( # Setup points_per_cluster = [10, 100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR, 0) + generated_detections, _ = generate_cluster_data( + points_per_cluster, self.__STD_DEV_REGULAR, 0 + ) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -476,7 +484,9 @@ def test_detect_consecutive_inputs_single_cluster( # Setup points_per_cluster = [100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR, 0) + generated_detections, _ = generate_cluster_data( + points_per_cluster, self.__STD_DEV_REGULAR, 0 + ) # Run result_latest = False @@ -500,7 +510,9 @@ def test_detect_consecutive_inputs_five_clusters( # Setup points_per_cluster = [100, 100, 100, 100, 100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR, 0) + generated_detections, _ = generate_cluster_data( + points_per_cluster, self.__STD_DEV_REGULAR, 0 + ) # Run result_latest = False @@ -577,11 +589,12 @@ def test_one_label( """ # Setup labels_to_n_samples_per_cluster = {1: [100, 100, 100, 100, 100]} - generated_detections, labels_to_generated_cluster_positions = generate_cluster_data_by_label( - labels_to_n_samples_per_cluster, - self.__STD_DEV_REG + generated_detections, labels_to_generated_cluster_positions = ( + generate_cluster_data_by_label(labels_to_n_samples_per_cluster, self.__STD_DEV_REG) ) - random.shuffle(generated_detections) # so all abojects with the same label are not arranged all in a row + random.shuffle( + generated_detections + ) # so all abojects with the same label are not arranged all in a row # Run result, detections_in_world = cluster_model_by_label.run(generated_detections, False) @@ -596,7 +609,10 @@ def test_one_label( for generated_cluster in labels_to_generated_cluster_positions[1]: # Check if coordinates are equal distance = np.linalg.norm( - [cluster.location_x - generated_cluster[0], cluster.location_y - generated_cluster[1]] + [ + cluster.location_x - generated_cluster[0], + cluster.location_y - generated_cluster[1], + ] ) if distance < self.__MAX_POSITION_TOLERANCE: is_match = True @@ -611,12 +627,17 @@ def test_multiple_labels( Five clusters with small standard devition that have different labels """ # Setup - labels_to_n_samples_per_cluster = {1: [100, 100, 100], 2: [100, 100, 100], 3: [100, 100, 100]} - generated_detections, labels_to_generated_cluster_positions = generate_cluster_data_by_label( - labels_to_n_samples_per_cluster, - self.__STD_DEV_REG + labels_to_n_samples_per_cluster = { + 1: [100, 100, 100], + 2: [100, 100, 100], + 3: [100, 100, 100], + } + generated_detections, labels_to_generated_cluster_positions = ( + generate_cluster_data_by_label(labels_to_n_samples_per_cluster, self.__STD_DEV_REG) ) - random.shuffle(generated_detections) # so all abojects with the same label are not arranged all in a row + random.shuffle( + generated_detections + ) # so all abojects with the same label are not arranged all in a row # Run result, detections_in_world = cluster_model_by_label.run(generated_detections, False) @@ -627,18 +648,20 @@ def test_multiple_labels( assert len(detections_in_world[1]) == 3 assert len(detections_in_world[2]) == 3 assert len(detections_in_world[3]) == 3 - for label in range (1, 4): + for label in range(1, 4): for cluster in detections_in_world[label]: assert cluster.label == label is_match = False for generated_cluster in labels_to_generated_cluster_positions[label]: # Check if coordinates are equal distance = np.linalg.norm( - [cluster.location_x - generated_cluster[0], cluster.location_y - generated_cluster[1]] + [ + cluster.location_x - generated_cluster[0], + cluster.location_y - generated_cluster[1], + ] ) if distance < self.__MAX_POSITION_TOLERANCE: is_match = True break assert is_match - \ No newline at end of file From f6870634344a3cf0df14159b2c927af93a334e59 Mon Sep 17 00:00:00 2001 From: a2misic Date: Fri, 29 Nov 2024 17:22:53 -0500 Subject: [PATCH 09/23] implemented label in cluster estimation --- modules/cluster_estimation/cluster_estimation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index fb1d851c..da273677 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -235,6 +235,7 @@ def run( # Remove clusters with covariances too large model_output = self.__filter_by_covariances(model_output) + label = 0 # Create output list of remaining valid clusters objects_in_world = [] for cluster in model_output: From c0ea3e0f924fe077329b6a9bb01ebaf9c4fcc597 Mon Sep 17 00:00:00 2001 From: a2misic Date: Fri, 29 Nov 2024 19:22:55 -0500 Subject: [PATCH 10/23] implemented cluster estimation label by detection label --- .../cluster_estimation/cluster_estimation.py | 125 ++++++++++++++++-- 1 file changed, 111 insertions(+), 14 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index da273677..16265132 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -42,15 +42,22 @@ class ClusterEstimation: METHODS ------- run() - Take in list of landing pad detections and return list of estimated landing pad locations + Take in list of object detections and return list of estimated object locations if number of detections is sufficient, or if manually forced to run. + cluster_by_label() + Take in list of detections of the same label and return list of estimated object locations + of the same label. + __decide_to_run() Decide when to run cluster estimation model. __sort_by_weights() Sort input model output list by weights in descending order. + __sort_by_labels() + Sort input detection list by labels in descending order. + __convert_detections_to_point() Convert DetectionInWorld input object to a [x,y] position to store. @@ -59,6 +66,7 @@ class ClusterEstimation: __filter_by_covariances() Removes any cluster with covariances much higher than the lowest covariance value. + """ # pylint: disable=too-many-instance-attributes @@ -167,10 +175,12 @@ def __init__( self.__label = label def run( - self, detections: "list[detection_in_world.DetectionInWorld]", run_override: bool + self, + detections: "list[detection_in_world.DetectionInWorld]", + run_override: bool, ) -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": """ - Take in list of landing pad detections and return list of estimated landing pad locations + Take in list of detections and return list of estimated object locations if number of detections is sufficient, or if manually forced to run. PARAMETERS @@ -195,24 +205,92 @@ def run( # Store new input data self.__current_bucket += self.__convert_detections_to_point(detections) + # Decide to run + if not self.__decide_to_run(run_override): + return False, None + + # sort bucket by label in descending order + self.__current_bucket = self.__sort_by_labels(self.__current_bucket) + detections_in_world = [] + + # init search parameters + ptr = 0 + max_label = self.__current_bucket[0][2] + + # itterates through all possible labels + for label in reversed(range(max_label)): + + # creates bucket of labels + bucket_labelled = [] + while ptr < len(self.__current_bucket) and self.__current_bucket[ptr][2] == label: + bucket_labelled += [self.__current_bucket[ptr]] + ptr += 1 + + # skip if no objects have label=label + if len(bucket_labelled) == 0: + continue + + check, labelled_detections_in_world = self.cluster_by_label( + bucket_labelled, run_override, label + ) + + # checks if cluster_by_label ran succssfully + if not check: + self.__logger(f"did not add objects of label={label} to total object detections") + continue + + detections_in_world += labelled_detections_in_world + + return True, detections_in_world + + def cluster_by_label( + self, + points: "list[tuple[float, float, int]]", + run_override: bool, + label: int, + ) -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": + """ + Take in list of detections of the same label and return list of estimated object locations + of the same label. + + PARAMETERS + ---------- + points: list[tuple[float, float, int]] + List containing tuple objects which holds real-world positioning data to run + clustering on and their labels + + run_override: bool + Forces ClusterEstimation to predict if data is available, regardless of any other + requirements. + + RETURNS + ------- + model_ran: bool + True if ClusterEstimation object successfully ran its estimation model, False otherwise. + + objects_in_world: list[ObjectInWorld] or None. + List containing ObjectInWorld objects, containing position and covariance value. + None if conditions not met and model not ran or model failed to converge. + """ + # Decide to run if not self.__decide_to_run(run_override): return False, None # Fit points and get cluster data - self.__vgmm = self.__vgmm.fit(self.__all_points) # type: ignore + __vgmm_label = self.__vgmm.fit(points) # type: ignore # Check convergence - if not self.__vgmm.converged_: - self.__logger.warning("Model failed to converge") + if not __vgmm_label.converged_: + self.__logger.warning(f"Model for label={label} failed to converge") return False, None # Get predictions from cluster model model_output: "list[tuple[np.ndarray, float, float]]" = list( zip( - self.__vgmm.means_, # type: ignore - self.__vgmm.weights_, # type: ignore - self.__vgmm.covariances_, # type: ignore + __vgmm_label.means_, # type: ignore + __vgmm_label.weights_, # type: ignore + __vgmm_label.covariances_, # type: ignore ) ) @@ -235,7 +313,6 @@ def run( # Remove clusters with covariances too large model_output = self.__filter_by_covariances(model_output) - label = 0 # Create output list of remaining valid clusters objects_in_world = [] for cluster in model_output: @@ -308,12 +385,32 @@ def __sort_by_weights( """ return sorted(model_output, key=lambda x: x[1], reverse=True) + @staticmethod + def __sort_by_labels( + points: "list[tuple[float, float, int]]", + ) -> "list[tuple[float, float, int]]": + """ + Sort input detection list by labels in descending order. + + PARAMETERS + ---------- + detections: list[tuple[float, float, int]] + List containing detections, with each element having the format + [x_position, y_position, label]. + + RETURNS + ------- + list[tuple[np.ndarray, float, float]] + List containing detection points sorted in descending order by label + """ + return sorted(points, key=lambda x: x.label, reverse=True) + @staticmethod def __convert_detections_to_point( detections: "list[detection_in_world.DetectionInWorld]", ) -> "list[tuple[float, float, int]]": """ - Convert DetectionInWorld input object to a list of points- (x,y) positions, to store. + Convert DetectionInWorld input object to a list of points- (x,y) positions with label, to store. PARAMETERS ---------- @@ -323,8 +420,8 @@ def __convert_detections_to_point( RETURNS ------- - points: list[tuple[float, float]] - List of points (x,y). + points: list[tuple[float, float, int]] + List of points (x,y) and their label ------- """ points = [] @@ -336,7 +433,7 @@ def __convert_detections_to_point( # Convert DetectionInWorld objects for detection in detections: # `centre` attribute holds positioning data - points.append(tuple([detection.centre[0], detection.centre[1]])) + points.append(tuple([detection.centre[0], detection.centre[1], detection.label])) return points From 50ef6ffe6cd907fed42c6ca6832fcc05a462be7f Mon Sep 17 00:00:00 2001 From: a2misic Date: Wed, 18 Dec 2024 21:20:23 -0500 Subject: [PATCH 11/23] fixed implementation --- modules/cluster_estimation/cluster_estimation.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index 16265132..1fb7a5b8 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -66,7 +66,6 @@ class ClusterEstimation: __filter_by_covariances() Removes any cluster with covariances much higher than the lowest covariance value. - """ # pylint: disable=too-many-instance-attributes @@ -215,12 +214,13 @@ def run( # init search parameters ptr = 0 - max_label = self.__current_bucket[0][2] - # itterates through all possible labels - for label in reversed(range(max_label)): + # itterates through all points + while ptr <= len(self.__current_bucket): + # reference label + label = self.__current_bucket[ptr][2] - # creates bucket of labels + # creates bucket of points with the same label bucket_labelled = [] while ptr < len(self.__current_bucket) and self.__current_bucket[ptr][2] == label: bucket_labelled += [self.__current_bucket[ptr]] @@ -236,7 +236,9 @@ def run( # checks if cluster_by_label ran succssfully if not check: - self.__logger(f"did not add objects of label={label} to total object detections") + self.__logger.warning( + f"did not add objects of label={label} to total object detections" + ) continue detections_in_world += labelled_detections_in_world From 42c9fbe2646b015e66d21f7111662a745ca72ec9 Mon Sep 17 00:00:00 2001 From: a2misic Date: Mon, 6 Jan 2025 21:32:26 -0500 Subject: [PATCH 12/23] implemented fixes --- modules/cluster_estimation/cluster_estimation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index 1fb7a5b8..7b8fab0a 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -209,7 +209,7 @@ def run( return False, None # sort bucket by label in descending order - self.__current_bucket = self.__sort_by_labels(self.__current_bucket) + self.__all_points = self.__sort_by_labels(self.__current_bucket) detections_in_world = [] # init search parameters @@ -220,22 +220,22 @@ def run( # reference label label = self.__current_bucket[ptr][2] - # creates bucket of points with the same label + # creates bucket of points with the same label since bucket is sorted by label bucket_labelled = [] - while ptr < len(self.__current_bucket) and self.__current_bucket[ptr][2] == label: - bucket_labelled += [self.__current_bucket[ptr]] + while ptr < len(self.__current_bucket) and self.__all_points[ptr][2] == label: + bucket_labelled.append([self.__all_points[ptr]]) ptr += 1 # skip if no objects have label=label if len(bucket_labelled) == 0: continue - check, labelled_detections_in_world = self.cluster_by_label( + result, labelled_detections_in_world = self.cluster_by_label( bucket_labelled, run_override, label ) # checks if cluster_by_label ran succssfully - if not check: + if not result: self.__logger.warning( f"did not add objects of label={label} to total object detections" ) From 6c8e7242d0f2cbdeeaf0db4c6ddc1cbec181c20a Mon Sep 17 00:00:00 2001 From: a2misic Date: Thu, 16 Jan 2025 15:20:31 -0500 Subject: [PATCH 13/23] implemented changes, made it pass all tests --- .../cluster_estimation/cluster_estimation.py | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index 7b8fab0a..c30bbc86 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -203,36 +203,35 @@ def run( """ # Store new input data self.__current_bucket += self.__convert_detections_to_point(detections) + self.__all_points = [] # Decide to run if not self.__decide_to_run(run_override): return False, None # sort bucket by label in descending order - self.__all_points = self.__sort_by_labels(self.__current_bucket) + self.__all_points = self.__sort_by_labels(self.__all_points) detections_in_world = [] # init search parameters ptr = 0 # itterates through all points - while ptr <= len(self.__current_bucket): + while ptr < len(self.__all_points): # reference label - label = self.__current_bucket[ptr][2] + label = self.__all_points[ptr][2] # creates bucket of points with the same label since bucket is sorted by label bucket_labelled = [] - while ptr < len(self.__current_bucket) and self.__all_points[ptr][2] == label: - bucket_labelled.append([self.__all_points[ptr]]) + while ptr < len(self.__all_points) and self.__all_points[ptr][2] == label: + bucket_labelled.append(self.__all_points[ptr]) ptr += 1 # skip if no objects have label=label if len(bucket_labelled) == 0: continue - result, labelled_detections_in_world = self.cluster_by_label( - bucket_labelled, run_override, label - ) + result, labelled_detections_in_world = self.cluster_by_label(bucket_labelled, label) # checks if cluster_by_label ran succssfully if not result: @@ -248,7 +247,6 @@ def run( def cluster_by_label( self, points: "list[tuple[float, float, int]]", - run_override: bool, label: int, ) -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": """ @@ -274,13 +272,8 @@ def cluster_by_label( List containing ObjectInWorld objects, containing position and covariance value. None if conditions not met and model not ran or model failed to converge. """ - - # Decide to run - if not self.__decide_to_run(run_override): - return False, None - # Fit points and get cluster data - __vgmm_label = self.__vgmm.fit(points) # type: ignore + __vgmm_label = self.__vgmm.fit([[point[0], point[1]] for point in points]) # type: ignore # Check convergence if not __vgmm_label.converged_: @@ -346,14 +339,13 @@ def __decide_to_run(self, run_override: bool) -> bool: """ count_all = len(self.__all_points) count_current = len(self.__current_bucket) - if not run_override: # Don't run if total points under minimum requirement if count_all + count_current < self.__min_activation_threshold: return False # Don't run if not enough new points - if count_current < self.__min_new_points_to_run and self.__has_ran_once: + if count_current < self.__min_new_points_to_run and not self.__has_ran_once: return False # No data can not run @@ -405,7 +397,9 @@ def __sort_by_labels( list[tuple[np.ndarray, float, float]] List containing detection points sorted in descending order by label """ - return sorted(points, key=lambda x: x.label, reverse=True) + return sorted( + points, key=lambda x: x[2], reverse=True + ) # the label is stored at index 2 of object @staticmethod def __convert_detections_to_point( @@ -457,7 +451,7 @@ def __filter_by_points_ownership( List containing predicted cluster centres after filtering. """ # List of each point's cluster index - cluster_assignment = self.__vgmm.predict(self.__all_points) # type: ignore + cluster_assignment = self.__vgmm.predict([[point[0], point[1]] for point in self.__all_points]) # type: ignore # Find which cluster indices have points clusters_with_points = np.unique(cluster_assignment) From b702a76240cb632f19ddb88ec005b4514326cfdd Mon Sep 17 00:00:00 2001 From: a2misic Date: Sun, 26 Jan 2025 16:20:00 -0500 Subject: [PATCH 14/23] work in progress commit --- .../cluster_estimation/cluster_estimation.py | 130 +++------------- .../cluster_estimation_worker.py | 3 + tests/unit/test_cluster_detection.py | 143 ++++++++++++++++++ 3 files changed, 166 insertions(+), 110 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index c30bbc86..ba40f3d9 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -42,22 +42,15 @@ class ClusterEstimation: METHODS ------- run() - Take in list of object detections and return list of estimated object locations + Take in list of landing pad detections and return list of estimated landing pad locations if number of detections is sufficient, or if manually forced to run. - cluster_by_label() - Take in list of detections of the same label and return list of estimated object locations - of the same label. - __decide_to_run() Decide when to run cluster estimation model. __sort_by_weights() Sort input model output list by weights in descending order. - __sort_by_labels() - Sort input detection list by labels in descending order. - __convert_detections_to_point() Convert DetectionInWorld input object to a [x,y] position to store. @@ -174,12 +167,10 @@ def __init__( self.__label = label def run( - self, - detections: "list[detection_in_world.DetectionInWorld]", - run_override: bool, + self, detections: "list[detection_in_world.DetectionInWorld]", run_override: bool ) -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": """ - Take in list of detections and return list of estimated object locations + Take in list of landing pad detections and return list of estimated landing pad locations if number of detections is sufficient, or if manually forced to run. PARAMETERS @@ -203,92 +194,31 @@ def run( """ # Store new input data self.__current_bucket += self.__convert_detections_to_point(detections) - self.__all_points = [] + print("len of current bucket = "+str(len(self.__current_bucket))) # Decide to run if not self.__decide_to_run(run_override): return False, None - # sort bucket by label in descending order - self.__all_points = self.__sort_by_labels(self.__all_points) - detections_in_world = [] - - # init search parameters - ptr = 0 - - # itterates through all points - while ptr < len(self.__all_points): - # reference label - label = self.__all_points[ptr][2] - - # creates bucket of points with the same label since bucket is sorted by label - bucket_labelled = [] - while ptr < len(self.__all_points) and self.__all_points[ptr][2] == label: - bucket_labelled.append(self.__all_points[ptr]) - ptr += 1 - - # skip if no objects have label=label - if len(bucket_labelled) == 0: - continue - - result, labelled_detections_in_world = self.cluster_by_label(bucket_labelled, label) - - # checks if cluster_by_label ran succssfully - if not result: - self.__logger.warning( - f"did not add objects of label={label} to total object detections" - ) - continue - - detections_in_world += labelled_detections_in_world - - return True, detections_in_world - - def cluster_by_label( - self, - points: "list[tuple[float, float, int]]", - label: int, - ) -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": - """ - Take in list of detections of the same label and return list of estimated object locations - of the same label. - - PARAMETERS - ---------- - points: list[tuple[float, float, int]] - List containing tuple objects which holds real-world positioning data to run - clustering on and their labels - - run_override: bool - Forces ClusterEstimation to predict if data is available, regardless of any other - requirements. - - RETURNS - ------- - model_ran: bool - True if ClusterEstimation object successfully ran its estimation model, False otherwise. - - objects_in_world: list[ObjectInWorld] or None. - List containing ObjectInWorld objects, containing position and covariance value. - None if conditions not met and model not ran or model failed to converge. - """ # Fit points and get cluster data - __vgmm_label = self.__vgmm.fit([[point[0], point[1]] for point in points]) # type: ignore + self.__vgmm = self.__vgmm.fit(self.__all_points) # type: ignore # Check convergence - if not __vgmm_label.converged_: - self.__logger.warning(f"Model for label={label} failed to converge") + if not self.__vgmm.converged_: + self.__logger.warning("Model failed to converge") return False, None # Get predictions from cluster model model_output: "list[tuple[np.ndarray, float, float]]" = list( zip( - __vgmm_label.means_, # type: ignore - __vgmm_label.weights_, # type: ignore - __vgmm_label.covariances_, # type: ignore + self.__vgmm.means_, # type: ignore + self.__vgmm.weights_, # type: ignore + self.__vgmm.covariances_, # type: ignore ) ) + print("output = "+str(model_output)) + # Empty cluster removal model_output = self.__filter_by_points_ownership(model_output) @@ -304,6 +234,7 @@ def cluster_by_label( viable_clusters.append(model_output[i]) model_output = viable_clusters + print("len model output: "+str(len(model_output))) # Remove clusters with covariances too large model_output = self.__filter_by_covariances(model_output) @@ -339,13 +270,14 @@ def __decide_to_run(self, run_override: bool) -> bool: """ count_all = len(self.__all_points) count_current = len(self.__current_bucket) + if not run_override: # Don't run if total points under minimum requirement if count_all + count_current < self.__min_activation_threshold: return False # Don't run if not enough new points - if count_current < self.__min_new_points_to_run and not self.__has_ran_once: + if count_current < self.__min_new_points_to_run and self.__has_ran_once: return False # No data can not run @@ -379,34 +311,12 @@ def __sort_by_weights( """ return sorted(model_output, key=lambda x: x[1], reverse=True) - @staticmethod - def __sort_by_labels( - points: "list[tuple[float, float, int]]", - ) -> "list[tuple[float, float, int]]": - """ - Sort input detection list by labels in descending order. - - PARAMETERS - ---------- - detections: list[tuple[float, float, int]] - List containing detections, with each element having the format - [x_position, y_position, label]. - - RETURNS - ------- - list[tuple[np.ndarray, float, float]] - List containing detection points sorted in descending order by label - """ - return sorted( - points, key=lambda x: x[2], reverse=True - ) # the label is stored at index 2 of object - @staticmethod def __convert_detections_to_point( detections: "list[detection_in_world.DetectionInWorld]", ) -> "list[tuple[float, float, int]]": """ - Convert DetectionInWorld input object to a list of points- (x,y) positions with label, to store. + Convert DetectionInWorld input object to a list of points- (x,y) positions, to store. PARAMETERS ---------- @@ -416,8 +326,8 @@ def __convert_detections_to_point( RETURNS ------- - points: list[tuple[float, float, int]] - List of points (x,y) and their label + points: list[tuple[float, float]] + List of points (x,y). ------- """ points = [] @@ -429,7 +339,7 @@ def __convert_detections_to_point( # Convert DetectionInWorld objects for detection in detections: # `centre` attribute holds positioning data - points.append(tuple([detection.centre[0], detection.centre[1], detection.label])) + points.append(tuple([detection.centre[0], detection.centre[1]])) return points @@ -451,7 +361,7 @@ def __filter_by_points_ownership( List containing predicted cluster centres after filtering. """ # List of each point's cluster index - cluster_assignment = self.__vgmm.predict([[point[0], point[1]] for point in self.__all_points]) # type: ignore + cluster_assignment = self.__vgmm.predict(self.__all_points) # type: ignore # Find which cluster indices have points clusters_with_points = np.unique(cluster_assignment) diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index acd90cae..62b664f6 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -34,6 +34,9 @@ def cluster_estimation_worker( min_new_points_to_run: int Minimum number of new data points that must be collected before running model. + max_num_components: int + Max number of real landing pads. + random_state: int Seed for randomizer, to get consistent results. diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 05bd10af..93986063 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -68,6 +68,27 @@ def cluster_model_by_label() -> cluster_estimation_by_label.ClusterEstimationByL yield model # type: ignore +@pytest.fixture() +def cluster_model_by_label(cluster_model: cluster_estimation.ClusterEstimation) -> cluster_estimation_by_label.ClusterEstimationByLabel: # type: ignore + """ + Cluster estimation by label object. + """ + result, test_logger = logger.Logger.create("test_logger", False) + assert result + assert test_logger is not None + + result, model = cluster_estimation_by_label.ClusterEstimationByLabel.create( + MIN_TOTAL_POINTS_THRESHOLD, + MIN_NEW_POINTS_TO_RUN, + cluster_model, + test_logger, + ) + assert result + assert model is not None + + yield model # type: ignore + + def generate_cluster_data( n_samples_per_cluster: "list[int]", cluster_standard_deviation: int, @@ -268,6 +289,24 @@ def test_under_min_total_threshold( assert not result assert detections_in_world is None + def test_under_min_total_threshold_by_label( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + As above, but with labels. + """ + # Setup + original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Less than min threshold (100) + generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert not result + assert detections_in_world is None + + def test_at_min_total_threshold( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -292,6 +331,30 @@ def test_at_min_total_threshold( assert result_2 assert detections_in_world_2 is not None + def test_at_min_total_threshold_by_label( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + As above, but with labels. + """ + # Setup + original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Should not run the first time + new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points + + generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) + generated_detections_2, _ = generate_cluster_data_by_label([1], [new_count], self.__STD_DEV_REG) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + result_2, detections_in_world_2 = cluster_model_by_label.run(generated_detections_2, False) + + # Test + assert not result + assert detections_in_world is None + assert result_2 + assert detections_in_world_2 is not None + + def test_under_min_bucket_size( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -315,6 +378,30 @@ def test_under_min_bucket_size( assert not result_2 assert detections_in_world_2 is None + def test_under_min_bucket_size_by_label( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + As above, but with labels. + """ + # Setup + original_count = MIN_TOTAL_POINTS_THRESHOLD + 10 # Should run the first time + new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points, shouldn't run + + generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) + generated_detections_2, _ = generate_cluster_data_by_label([1], [new_count], self.__STD_DEV_REG) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + result_2, detections_in_world_2 = cluster_model_by_label.run(generated_detections_2, False) + + # Test + assert result + assert detections_in_world is not None + assert not result_2 + assert detections_in_world_2 is None + + def test_good_data(self, cluster_model: cluster_estimation.ClusterEstimation) -> None: """ All conditions met should run. @@ -328,6 +415,20 @@ def test_good_data(self, cluster_model: cluster_estimation.ClusterEstimation) -> # Test assert result assert detections_in_world is not None + + def test_good_data_by_label(self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel) -> None: + """ + As above, but with labels. + """ + original_count = MIN_TOTAL_POINTS_THRESHOLD + 1 # More than min total threshold should run + generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert result + assert detections_in_world is not None class TestCorrectNumberClusterOutputs: @@ -358,6 +459,24 @@ def test_detect_normal_data_single_cluster( assert result assert detections_in_world is not None + def test_detect_normal_data_single_cluster_by_label( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + As above, but with labels. + """ + # Setup + points_per_cluster = [100] + generated_detections, _ = generate_cluster_data_by_label([1], points_per_cluster, self.__STD_DEV_REGULAR) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert result + assert detections_in_world is not None + + def test_detect_normal_data_five_clusters( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -379,6 +498,30 @@ def test_detect_normal_data_five_clusters( assert detections_in_world is not None assert len(detections_in_world) == expected_cluster_count + def test_detect_normal_data_five_clusters_by_label_all_different( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + As above, but with labels. Every cluster has a different label. + """ + # Setup + points_per_cluster = [100, 100, 100, 100, 100] + labels_of_clusters = [1, 1, 1, 1, 1] + expected_cluster_count = len(points_per_cluster) + generated_detections, clusters = generate_cluster_data_by_label(labels_of_clusters, points_per_cluster, self.__STD_DEV_REGULAR) + assert len(generated_detections) == 500 + assert len(clusters) == 5 + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert detections_in_world[0].label == 1 + assert result + assert detections_in_world is not None + assert len(detections_in_world) == expected_cluster_count + + def test_detect_large_std_dev_single_cluster( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: From 34224fe1e6ea726900a21151d3aaa9cd226cbfb4 Mon Sep 17 00:00:00 2001 From: a2misic Date: Wed, 29 Jan 2025 23:36:25 -0500 Subject: [PATCH 15/23] tests working for cluster by label --- .../cluster_estimation/cluster_estimation.py | 6 +- .../cluster_estimation_worker.py | 3 - tests/unit/test_cluster_detection.py | 125 +----------------- 3 files changed, 4 insertions(+), 130 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index ba40f3d9..a0cf3634 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -3,6 +3,7 @@ Returns an array of classes, each containing the x coordinate, y coordinate, and spherical covariance of each landing pad estimation. """ +# pylint: disable=duplicate-code # pylint: disable=duplicate-code @@ -60,6 +61,7 @@ class ClusterEstimation: __filter_by_covariances() Removes any cluster with covariances much higher than the lowest covariance value. """ + # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes @@ -194,7 +196,6 @@ def run( """ # Store new input data self.__current_bucket += self.__convert_detections_to_point(detections) - print("len of current bucket = "+str(len(self.__current_bucket))) # Decide to run if not self.__decide_to_run(run_override): @@ -217,8 +218,6 @@ def run( ) ) - print("output = "+str(model_output)) - # Empty cluster removal model_output = self.__filter_by_points_ownership(model_output) @@ -234,7 +233,6 @@ def run( viable_clusters.append(model_output[i]) model_output = viable_clusters - print("len model output: "+str(len(model_output))) # Remove clusters with covariances too large model_output = self.__filter_by_covariances(model_output) diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index 62b664f6..acd90cae 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -34,9 +34,6 @@ def cluster_estimation_worker( min_new_points_to_run: int Minimum number of new data points that must be collected before running model. - max_num_components: int - Max number of real landing pads. - random_state: int Seed for randomizer, to get consistent results. diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 93986063..69cff04c 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -61,6 +61,7 @@ def cluster_model_by_label() -> cluster_estimation_by_label.ClusterEstimationByL MIN_NEW_POINTS_TO_RUN, RNG_SEED, test_logger, + 0 ) assert result assert model is not None @@ -80,7 +81,7 @@ def cluster_model_by_label(cluster_model: cluster_estimation.ClusterEstimation) result, model = cluster_estimation_by_label.ClusterEstimationByLabel.create( MIN_TOTAL_POINTS_THRESHOLD, MIN_NEW_POINTS_TO_RUN, - cluster_model, + RNG_SEED, test_logger, ) assert result @@ -289,24 +290,6 @@ def test_under_min_total_threshold( assert not result assert detections_in_world is None - def test_under_min_total_threshold_by_label( - self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel - ) -> None: - """ - As above, but with labels. - """ - # Setup - original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Less than min threshold (100) - generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - - # Test - assert not result - assert detections_in_world is None - - def test_at_min_total_threshold( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -331,30 +314,6 @@ def test_at_min_total_threshold( assert result_2 assert detections_in_world_2 is not None - def test_at_min_total_threshold_by_label( - self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel - ) -> None: - """ - As above, but with labels. - """ - # Setup - original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Should not run the first time - new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points - - generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) - generated_detections_2, _ = generate_cluster_data_by_label([1], [new_count], self.__STD_DEV_REG) - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - result_2, detections_in_world_2 = cluster_model_by_label.run(generated_detections_2, False) - - # Test - assert not result - assert detections_in_world is None - assert result_2 - assert detections_in_world_2 is not None - - def test_under_min_bucket_size( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -378,30 +337,6 @@ def test_under_min_bucket_size( assert not result_2 assert detections_in_world_2 is None - def test_under_min_bucket_size_by_label( - self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel - ) -> None: - """ - As above, but with labels. - """ - # Setup - original_count = MIN_TOTAL_POINTS_THRESHOLD + 10 # Should run the first time - new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points, shouldn't run - - generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) - generated_detections_2, _ = generate_cluster_data_by_label([1], [new_count], self.__STD_DEV_REG) - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - result_2, detections_in_world_2 = cluster_model_by_label.run(generated_detections_2, False) - - # Test - assert result - assert detections_in_world is not None - assert not result_2 - assert detections_in_world_2 is None - - def test_good_data(self, cluster_model: cluster_estimation.ClusterEstimation) -> None: """ All conditions met should run. @@ -415,20 +350,6 @@ def test_good_data(self, cluster_model: cluster_estimation.ClusterEstimation) -> # Test assert result assert detections_in_world is not None - - def test_good_data_by_label(self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel) -> None: - """ - As above, but with labels. - """ - original_count = MIN_TOTAL_POINTS_THRESHOLD + 1 # More than min total threshold should run - generated_detections, _ = generate_cluster_data_by_label([1], [original_count], self.__STD_DEV_REG) - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - - # Test - assert result - assert detections_in_world is not None class TestCorrectNumberClusterOutputs: @@ -459,24 +380,6 @@ def test_detect_normal_data_single_cluster( assert result assert detections_in_world is not None - def test_detect_normal_data_single_cluster_by_label( - self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel - ) -> None: - """ - As above, but with labels. - """ - # Setup - points_per_cluster = [100] - generated_detections, _ = generate_cluster_data_by_label([1], points_per_cluster, self.__STD_DEV_REGULAR) - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - - # Test - assert result - assert detections_in_world is not None - - def test_detect_normal_data_five_clusters( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: @@ -498,30 +401,6 @@ def test_detect_normal_data_five_clusters( assert detections_in_world is not None assert len(detections_in_world) == expected_cluster_count - def test_detect_normal_data_five_clusters_by_label_all_different( - self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel - ) -> None: - """ - As above, but with labels. Every cluster has a different label. - """ - # Setup - points_per_cluster = [100, 100, 100, 100, 100] - labels_of_clusters = [1, 1, 1, 1, 1] - expected_cluster_count = len(points_per_cluster) - generated_detections, clusters = generate_cluster_data_by_label(labels_of_clusters, points_per_cluster, self.__STD_DEV_REGULAR) - assert len(generated_detections) == 500 - assert len(clusters) == 5 - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - - # Test - assert detections_in_world[0].label == 1 - assert result - assert detections_in_world is not None - assert len(detections_in_world) == expected_cluster_count - - def test_detect_large_std_dev_single_cluster( self, cluster_model: cluster_estimation.ClusterEstimation ) -> None: From 6c02dc848b3fa5cd16aba074e6b04644415d8d92 Mon Sep 17 00:00:00 2001 From: a2misic Date: Wed, 29 Jan 2025 23:40:27 -0500 Subject: [PATCH 16/23] reformated --- modules/cluster_estimation/cluster_estimation.py | 2 ++ tests/unit/test_cluster_detection.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index a0cf3634..86b99866 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -3,6 +3,7 @@ Returns an array of classes, each containing the x coordinate, y coordinate, and spherical covariance of each landing pad estimation. """ + # pylint: disable=duplicate-code # pylint: disable=duplicate-code @@ -61,6 +62,7 @@ class ClusterEstimation: __filter_by_covariances() Removes any cluster with covariances much higher than the lowest covariance value. """ + # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 69cff04c..bd26aa29 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -70,7 +70,7 @@ def cluster_model_by_label() -> cluster_estimation_by_label.ClusterEstimationByL @pytest.fixture() -def cluster_model_by_label(cluster_model: cluster_estimation.ClusterEstimation) -> cluster_estimation_by_label.ClusterEstimationByLabel: # type: ignore +def cluster_model_by_label() -> cluster_estimation_by_label.ClusterEstimationByLabel: # type: ignore """ Cluster estimation by label object. """ From 2106bbedfa8bb3626e673e894eb0006dd3ccbcde Mon Sep 17 00:00:00 2001 From: a2misic Date: Mon, 3 Feb 2025 13:36:26 -0500 Subject: [PATCH 17/23] integrated review changes --- main_2024.py | 1 + .../cluster_estimation_by_label.py | 29 ++++++++++++++++--- .../cluster_estimation_worker.py | 2 ++ tests/unit/test_cluster_detection.py | 1 + 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/main_2024.py b/main_2024.py index 25f68a72..e7c8ac74 100644 --- a/main_2024.py +++ b/main_2024.py @@ -346,6 +346,7 @@ def main() -> int: MIN_NEW_POINTS_TO_RUN, MAX_NUM_COMPONENTS, RANDOM_STATE, + 0, ), input_queues=[geolocation_to_cluster_estimation_queue], output_queues=[cluster_estimation_to_communications_queue], diff --git a/modules/cluster_estimation/cluster_estimation_by_label.py b/modules/cluster_estimation/cluster_estimation_by_label.py index 32ff5287..7f88d996 100644 --- a/modules/cluster_estimation/cluster_estimation_by_label.py +++ b/modules/cluster_estimation/cluster_estimation_by_label.py @@ -6,8 +6,8 @@ from .. import detection_in_world from .. import object_in_world -from ..cluster_estimation import cluster_estimation from ..common.modules.logger import logger +from . import cluster_estimation class ClusterEstimationByLabel: @@ -19,11 +19,14 @@ class ClusterEstimationByLabel: ATTRIBUTES ---------- min_activation_threshold: int - Minimum total data points before model runs. + Minimum total data points before model runs. Must be at least max_num_components. min_new_points_to_run: int Minimum number of new data points that must be collected before running model. + max_num_components: int + Max number of real landing pads. Must be at least 1. + random_state: int Seed for randomizer, to get consistent results. @@ -47,6 +50,7 @@ def create( cls, min_activation_threshold: int, min_new_points_to_run: int, + max_num_components: int, random_state: int, local_logger: logger.Logger, ) -> "tuple[bool, ClusterEstimationByLabel | None]": @@ -55,13 +59,23 @@ def create( """ # At least 1 point for model to fit - if min_activation_threshold < 1: + if min_activation_threshold < max_num_components: + return False, None + + if min_new_points_to_run < 0: + return False, None + + if max_num_components < 1: + return False, None + + if random_state < 0: return False, None return True, ClusterEstimationByLabel( cls.__create_key, min_activation_threshold, min_new_points_to_run, + max_num_components, random_state, local_logger, ) @@ -71,6 +85,7 @@ def __init__( class_private_create_key: object, min_activation_threshold: int, min_new_points_to_run: int, + max_num_components: int, random_state: int, local_logger: logger.Logger, ) -> None: @@ -84,10 +99,12 @@ def __init__( # Requirements to decide to run self.__min_activation_threshold = min_activation_threshold self.__min_new_points_to_run = min_new_points_to_run + self.__max_num_components = max_num_components self.__random_state = random_state self.__local_logger = local_logger - # cluster model corresponding to each label + # Cluster model corresponding to each label + # Each cluster estimation object stores the detections given to in its __all_points bucket across runs self.__label_to_cluster_estimation_model: dict[ int, cluster_estimation.ClusterEstimation ] = {} @@ -120,6 +137,7 @@ def run( Dictionary where the key is a label and the value is a list of all cluster detections with that label """ label_to_detections: dict[int, list[detection_in_world.DetectionInWorld]] = {} + # Sorting detections by label for detection in input_detections: if not detection.label in label_to_detections: label_to_detections[detection.label] = [] @@ -127,10 +145,12 @@ def run( labels_to_object_clusters: dict[int, list[object_in_world.ObjectInWorld]] = {} for label, detections in label_to_detections.items(): + # create cluster estimation for label if it doesn't exist if not label in self.__label_to_cluster_estimation_model: result, cluster_model = cluster_estimation.ClusterEstimation.create( self.__min_activation_threshold, self.__min_new_points_to_run, + self.__max_num_components, self.__random_state, self.__local_logger, label, @@ -141,6 +161,7 @@ def run( ) return False, None self.__label_to_cluster_estimation_model[label] = cluster_model + # runs cluster estimation for specific label result, clusters = self.__label_to_cluster_estimation_model[label].run( detections, run_override, diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index acd90cae..19f0b96f 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -19,6 +19,7 @@ def cluster_estimation_worker( min_new_points_to_run: int, max_num_components: int, random_state: int, + label: int, input_queue: queue_proxy_wrapper.QueueProxyWrapper, output_queue: queue_proxy_wrapper.QueueProxyWrapper, controller: worker_controller.WorkerController, @@ -75,6 +76,7 @@ def cluster_estimation_worker( max_num_components, random_state, local_logger, + label, ) if not result: local_logger.error("Worker failed to create class object", True) diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index bd26aa29..4224e715 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -59,6 +59,7 @@ def cluster_model_by_label() -> cluster_estimation_by_label.ClusterEstimationByL result, model = cluster_estimation_by_label.ClusterEstimationByLabel.create( MIN_TOTAL_POINTS_THRESHOLD, MIN_NEW_POINTS_TO_RUN, + MAX_NUM_COMPONENTS, RNG_SEED, test_logger, 0 From 1692e169c09a0152cb1325af4ee2d6df75aa0b3a Mon Sep 17 00:00:00 2001 From: a2misic Date: Thu, 6 Feb 2025 14:36:25 -0500 Subject: [PATCH 18/23] removed label parameter from default cluster estimation --- main_2024.py | 1 - .../cluster_estimation/cluster_estimation.py | 16 +++++------- .../cluster_estimation_by_label.py | 1 - .../cluster_estimation_worker.py | 2 -- tests/unit/test_cluster_detection.py | 25 +------------------ 5 files changed, 7 insertions(+), 38 deletions(-) diff --git a/main_2024.py b/main_2024.py index e7c8ac74..25f68a72 100644 --- a/main_2024.py +++ b/main_2024.py @@ -346,7 +346,6 @@ def main() -> int: MIN_NEW_POINTS_TO_RUN, MAX_NUM_COMPONENTS, RANDOM_STATE, - 0, ), input_queues=[geolocation_to_cluster_estimation_queue], output_queues=[cluster_estimation_to_communications_queue], diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index 86b99866..2e82609f 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -6,8 +6,6 @@ # pylint: disable=duplicate-code -# pylint: disable=duplicate-code - import numpy as np import sklearn import sklearn.datasets @@ -38,9 +36,6 @@ class ClusterEstimation: local_logger: Logger For logging error and debug messages. - label: int - Every cluster generated by this model will have this label - METHODS ------- run() @@ -88,7 +83,6 @@ def create( max_num_components: int, random_state: int, local_logger: logger.Logger, - label: int, ) -> "tuple[bool, ClusterEstimation | None]": """ Data requirement conditions for estimation model to run. @@ -130,7 +124,6 @@ def create( max_num_components, random_state, local_logger, - label, ) def __init__( @@ -141,7 +134,6 @@ def __init__( max_num_components: int, random_state: int, local_logger: logger.Logger, - label: int, ) -> None: """ Private constructor, use create() method. @@ -168,7 +160,6 @@ def __init__( self.__min_new_points_to_run = min_new_points_to_run self.__has_ran_once = False self.__logger = local_logger - self.__label = label def run( self, detections: "list[detection_in_world.DetectionInWorld]", run_override: bool @@ -196,6 +187,10 @@ def run( List containing ObjectInWorld objects, containing position and covariance value. None if conditions not met and model not ran or model failed to converge. """ + # in use, all detections will have the same label, so the + # first element's label was arbitrarily selected + label = detections[0].label + # Store new input data self.__current_bucket += self.__convert_detections_to_point(detections) @@ -228,6 +223,7 @@ def run( # Filter out all clusters after __WEIGHT_DROP_THRESHOLD weight drop occurs viable_clusters = [model_output[0]] + print(f"len(model_output) = {len(model_output)}") for i in range(1, len(model_output)): if model_output[i][1] / model_output[i - 1][1] < self.__WEIGHT_DROP_THRESHOLD: break @@ -243,7 +239,7 @@ def run( objects_in_world = [] for cluster in model_output: result, landing_pad = object_in_world.ObjectInWorld.create( - cluster[0][0], cluster[0][1], cluster[2], self.__label + cluster[0][0], cluster[0][1], cluster[2], label ) if result: diff --git a/modules/cluster_estimation/cluster_estimation_by_label.py b/modules/cluster_estimation/cluster_estimation_by_label.py index 7f88d996..c2e10143 100644 --- a/modules/cluster_estimation/cluster_estimation_by_label.py +++ b/modules/cluster_estimation/cluster_estimation_by_label.py @@ -153,7 +153,6 @@ def run( self.__max_num_components, self.__random_state, self.__local_logger, - label, ) if not result: self.__local_logger.error( diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index 19f0b96f..acd90cae 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -19,7 +19,6 @@ def cluster_estimation_worker( min_new_points_to_run: int, max_num_components: int, random_state: int, - label: int, input_queue: queue_proxy_wrapper.QueueProxyWrapper, output_queue: queue_proxy_wrapper.QueueProxyWrapper, controller: worker_controller.WorkerController, @@ -76,7 +75,6 @@ def cluster_estimation_worker( max_num_components, random_state, local_logger, - label, ) if not result: local_logger.error("Worker failed to create class object", True) diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 4224e715..2597e78c 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -21,7 +21,7 @@ # Test functions use test fixture signature names and access class privates # No enable -# pylint: disable=protected-access,redefined-outer-name +# pylint: disable=protected-access,redefined-outer-name,too-many-instance-attributes @pytest.fixture() @@ -39,7 +39,6 @@ def cluster_model() -> cluster_estimation.ClusterEstimation: # type: ignore MAX_NUM_COMPONENTS, RNG_SEED, test_logger, - 0 ) assert result assert model is not None @@ -62,28 +61,6 @@ def cluster_model_by_label() -> cluster_estimation_by_label.ClusterEstimationByL MAX_NUM_COMPONENTS, RNG_SEED, test_logger, - 0 - ) - assert result - assert model is not None - - yield model # type: ignore - - -@pytest.fixture() -def cluster_model_by_label() -> cluster_estimation_by_label.ClusterEstimationByLabel: # type: ignore - """ - Cluster estimation by label object. - """ - result, test_logger = logger.Logger.create("test_logger", False) - assert result - assert test_logger is not None - - result, model = cluster_estimation_by_label.ClusterEstimationByLabel.create( - MIN_TOTAL_POINTS_THRESHOLD, - MIN_NEW_POINTS_TO_RUN, - RNG_SEED, - test_logger, ) assert result assert model is not None From 769ba6e95a1514d9a47529e22d48ea01fa4289ab Mon Sep 17 00:00:00 2001 From: Cyuber Date: Tue, 11 Feb 2025 01:08:43 -0500 Subject: [PATCH 19/23] implemented review changes --- .../cluster_estimation/cluster_estimation.py | 59 ++-- .../cluster_estimation_by_label.py | 79 ++--- .../cluster_estimation_worker.py | 4 +- modules/common | 2 +- modules/object_in_world.py | 8 +- tests/unit/test_cluster_detection.py | 139 +------- .../unit/test_cluster_estimation_by_label.py | 324 ++++++++++++++++++ tests/unit/test_decision.py | 10 +- tests/unit/test_landing_pad_tracking.py | 72 ++-- 9 files changed, 439 insertions(+), 258 deletions(-) create mode 100644 tests/unit/test_cluster_estimation_by_label.py diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index 2e82609f..b53690cf 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -4,8 +4,6 @@ covariance of each landing pad estimation. """ -# pylint: disable=duplicate-code - import numpy as np import sklearn import sklearn.datasets @@ -58,10 +56,6 @@ class ClusterEstimation: Removes any cluster with covariances much higher than the lowest covariance value. """ - # pylint: disable=too-many-instance-attributes - - # pylint: disable=too-many-instance-attributes - __create_key = object() # VGMM Hyperparameters @@ -105,16 +99,14 @@ def create( RETURNS: The ClusterEstimation object if all conditions pass, otherwise False, None """ - if min_activation_threshold < max_num_components: - return False, None - - if min_new_points_to_run < 0: - return False, None - - if max_num_components < 1: - return False, None + is_valid_arguments = ClusterEstimation.check_create_arguments( + min_activation_threshold, + min_new_points_to_run, + max_num_components, + random_state + ) - if random_state < 0: + if not is_valid_arguments: return False, None return True, ClusterEstimation( @@ -187,10 +179,6 @@ def run( List containing ObjectInWorld objects, containing position and covariance value. None if conditions not met and model not ran or model failed to converge. """ - # in use, all detections will have the same label, so the - # first element's label was arbitrarily selected - label = detections[0].label - # Store new input data self.__current_bucket += self.__convert_detections_to_point(detections) @@ -223,7 +211,6 @@ def run( # Filter out all clusters after __WEIGHT_DROP_THRESHOLD weight drop occurs viable_clusters = [model_output[0]] - print(f"len(model_output) = {len(model_output)}") for i in range(1, len(model_output)): if model_output[i][1] / model_output[i - 1][1] < self.__WEIGHT_DROP_THRESHOLD: break @@ -239,7 +226,7 @@ def run( objects_in_world = [] for cluster in model_output: result, landing_pad = object_in_world.ObjectInWorld.create( - cluster[0][0], cluster[0][1], cluster[2], label + cluster[0][0], cluster[0][1], cluster[2] ) if result: @@ -247,8 +234,36 @@ def run( else: self.__logger.error("Failed to create ObjectInWorld object") return False, None + + self.__logger.info(objects_in_world) return True, objects_in_world + @staticmethod + def check_create_arguments( + min_activation_threshold: int, + min_new_points_to_run: int, + max_num_components: int, + random_state: int, + ): + """ + Checks if a valid cluster estimation object can be constructed. + + See `ClusterEstimation` for parameter descriptions. + """ + if min_activation_threshold < max_num_components: + return False + + if min_new_points_to_run < 0: + return False + + if max_num_components < 1: + return False + + if random_state < 0: + return False + + return True + def __decide_to_run(self, run_override: bool) -> bool: """ Decide when to run cluster estimation model. @@ -310,7 +325,7 @@ def __sort_by_weights( @staticmethod def __convert_detections_to_point( detections: "list[detection_in_world.DetectionInWorld]", - ) -> "list[tuple[float, float, int]]": + ) -> "list[tuple[float, float]]": """ Convert DetectionInWorld input object to a list of points- (x,y) positions, to store. diff --git a/modules/cluster_estimation/cluster_estimation_by_label.py b/modules/cluster_estimation/cluster_estimation_by_label.py index c2e10143..af37c370 100644 --- a/modules/cluster_estimation/cluster_estimation_by_label.py +++ b/modules/cluster_estimation/cluster_estimation_by_label.py @@ -1,20 +1,16 @@ """ -Take in bounding box coordinates from Geolocation and use to estimate landing pad locations. -Returns an array of classes, each containing the x coordinate, y coordinate, and spherical -covariance of each landing pad estimation. +Cluster estimation by label. """ +from . import cluster_estimation from .. import detection_in_world from .. import object_in_world from ..common.modules.logger import logger -from . import cluster_estimation class ClusterEstimationByLabel: """ - Estimate landing pad locations based on landing pad ground detection. Estimation - works by predicting 'cluster centres' from groups of closely placed landing pad - detections. + Cluster estimation filtered on label. ATTRIBUTES ---------- @@ -36,13 +32,8 @@ class ClusterEstimationByLabel: METHODS ------- run() - Take in list of object detections and return dictionary of labels to - to corresponging clusters of estimated object locations if number of - detections is sufficient, or if manually forced to run. + Cluster estimation filtered by label. """ - - # pylint: disable=too-many-instance-attributes - __create_key = object() @classmethod @@ -53,22 +44,19 @@ def create( max_num_components: int, random_state: int, local_logger: logger.Logger, - ) -> "tuple[bool, ClusterEstimationByLabel | None]": + ) -> "tuple[True, ClusterEstimationByLabel] | tuple[False, None]": """ - Data requirement conditions for estimation model to run. + See `ClusterEstimation` for parameter descriptions. """ - # At least 1 point for model to fit - if min_activation_threshold < max_num_components: - return False, None - - if min_new_points_to_run < 0: - return False, None - - if max_num_components < 1: - return False, None + is_valid_arguments = cluster_estimation.ClusterEstimation.check_create_arguments( + min_activation_threshold, + min_new_points_to_run, + max_num_components, + random_state + ) - if random_state < 0: + if not is_valid_arguments: return False, None return True, ClusterEstimationByLabel( @@ -96,7 +84,7 @@ def __init__( class_private_create_key is ClusterEstimationByLabel.__create_key ), "Use create() method" - # Requirements to decide to run + # Construction arguments for `ClusterEstimation` self.__min_activation_threshold = min_activation_threshold self.__min_new_points_to_run = min_new_points_to_run self.__max_num_components = max_num_components @@ -111,41 +99,33 @@ def __init__( def run( self, - input_detections: "list[detection_in_world.DetectionInWorld]", + input_detections: list[detection_in_world.DetectionInWorld], run_override: bool, - ) -> "tuple[True, dict[int, list[object_in_world.ObjectInWorld]]] | tuple[False, None]": + ) -> tuple[True, dict[int, list[object_in_world.ObjectInWorld]]] | tuple[False, None]: """ - Take in list of detections and return list of estimated object locations - if number of detections is sufficient, or if manually forced to run. - - PARAMETERS - ---------- - input_detections: list[DetectionInWorld] - List containing DetectionInWorld objects which holds real-world positioning data to run - clustering on. - - run_override: bool - Forces ClusterEstimation to predict if data is available, regardless of any other - requirements. + See `ClusterEstimation` for parameter descriptions. RETURNS ------- model_ran: bool True if ClusterEstimation object successfully ran its estimation model, False otherwise. - labels_to_object_clusters: dict[int, list[object_in_world.ObjectInWorld] or None. - Dictionary where the key is a label and the value is a list of all cluster detections with that label + labels_to_objects: dict[int, list[object_in_world.ObjectInWorld] or None. + Dictionary where the key is a label and the value is a list of all cluster detections with that label. + ObjectInWorld objects don't have a label property, but they are sorted into label categories in the dictionary. """ label_to_detections: dict[int, list[detection_in_world.DetectionInWorld]] = {} + # Sorting detections by label for detection in input_detections: if not detection.label in label_to_detections: label_to_detections[detection.label] = [] label_to_detections[detection.label].append(detection) - labels_to_object_clusters: dict[int, list[object_in_world.ObjectInWorld]] = {} + labels_to_objects: dict[int, list[object_in_world.ObjectInWorld]] = {} + for label, detections in label_to_detections.items(): - # create cluster estimation for label if it doesn't exist + # Create cluster estimation for label if it doesn't exist if not label in self.__label_to_cluster_estimation_model: result, cluster_model = cluster_estimation.ClusterEstimation.create( self.__min_activation_threshold, @@ -160,18 +140,21 @@ def run( ) return False, None self.__label_to_cluster_estimation_model[label] = cluster_model + # runs cluster estimation for specific label result, clusters = self.__label_to_cluster_estimation_model[label].run( detections, run_override, ) + if not result: self.__local_logger.error( f"Failed to run cluster estimation model for label {label}" ) return False, None - if not label in labels_to_object_clusters: - labels_to_object_clusters[label] = [] - labels_to_object_clusters[label] += clusters - return True, labels_to_object_clusters + if not label in labels_to_objects: + labels_to_objects[label] = [] + labels_to_objects[label] += clusters + + return True, labels_to_objects diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index acd90cae..fefd0930 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -1,7 +1,5 @@ """ -Take in bounding box coordinates from Geolocation and use to estimate landing pad locations. -Returns an array of classes, each containing the x coordinate, y coordinate, and spherical -covariance of each landing pad estimation. +Gets detections in world space and outputs estimations of objects. """ import os diff --git a/modules/common b/modules/common index 9acf88b4..9b10a334 160000 --- a/modules/common +++ b/modules/common @@ -1 +1 @@ -Subproject commit 9acf88b42dfdb145e7eabb1b09a55df102ee00ad +Subproject commit 9b10a334651b7cca5d014d4640e42d3a55d128f8 diff --git a/modules/object_in_world.py b/modules/object_in_world.py index 759e47c2..943fe2ff 100644 --- a/modules/object_in_world.py +++ b/modules/object_in_world.py @@ -12,7 +12,7 @@ class ObjectInWorld: @classmethod def create( - cls, location_x: float, location_y: float, spherical_variance: float, label: int + cls, location_x: float, location_y: float, spherical_variance: float ) -> "tuple[bool, ObjectInWorld | None]": """ location_x, location_y: Location of the object. @@ -22,7 +22,7 @@ def create( return False, None return True, ObjectInWorld( - cls.__create_key, location_x, location_y, spherical_variance, label + cls.__create_key, location_x, location_y, spherical_variance, ) def __init__( @@ -31,7 +31,6 @@ def __init__( location_x: float, location_y: float, spherical_variance: float, - label: int, ) -> None: """ Private constructor, use create() method. @@ -41,13 +40,12 @@ def __init__( self.location_x = location_x self.location_y = location_y self.spherical_variance = spherical_variance - self.label = label def __str__(self) -> str: """ To string. """ - return f"{self.__class__}, location_x: {self.location_x}, location_y: {self.location_y}, spherical_variance: {self.spherical_variance}, label: {self.label}" + return f"{self.__class__}, location_x: {self.location_x}, location_y: {self.location_y}, spherical_variance: {self.spherical_variance}" def __repr__(self) -> str: """ diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 2597e78c..85dd22c1 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -134,49 +134,6 @@ def generate_cluster_data( return detections, cluster_positions.tolist() - -def generate_cluster_data_by_label( - labels_to_n_samples_per_cluster: "dict[int, list[int]]", - cluster_standard_deviation: int, -) -> "tuple[list[detection_in_world.DetectionInWorld], dict[int, list[np.ndarray]]]": - """ - Returns a list of labeled points (DetectionInWorld objects) with specified points per cluster - and standard deviation. - - PARAMETERS - ---------- - labels_to_cluster_samples: "dict[int, list[int]]" - Dictionary where the key is a label and the value is a - list of integers the represent the number of samples a cluster has. - - cluster_standard_deviation: int - The standard deviation of the generated points, bigger - standard deviation == more spread out points. - - RETURNS - ------- - detections: list[detection_in_world.DetectionInWorld] - List of points (DetectionInWorld objects). - - labels_to_cluster_positions: dict[int, list[np.ndarray]] - Dictionary where the key is a label and the value is a - list of coordinate positions of each cluster centre with that label. - ------- - """ - - detections = [] - labels_to_cluster_positions: dict[int, list[np.ndarray]] = {} - - for label, n_samples_list in labels_to_n_samples_per_cluster.items(): - temp_detections, cluster_positions = generate_cluster_data( - n_samples_list, cluster_standard_deviation, label - ) - detections += temp_detections - labels_to_cluster_positions[label] = cluster_positions - - return detections, labels_to_cluster_positions - - def generate_points_away_from_cluster( num_points_to_generate: int, minimum_distance_from_cluster: float, @@ -570,98 +527,4 @@ def test_position_regular_data( is_match = True break - assert is_match - - -class TestCorrectClusterEstimationByLabel: - """ - Tests if cluster estimation by label properly sorts labels. - """ - - __STD_DEV_REG = 1 # Regular standard deviation is 1m - __MAX_POSITION_TOLERANCE = 1 - - def test_one_label( - self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel - ) -> None: - """ - Five clusters with small standard devition that all have the same label - """ - # Setup - labels_to_n_samples_per_cluster = {1: [100, 100, 100, 100, 100]} - generated_detections, labels_to_generated_cluster_positions = ( - generate_cluster_data_by_label(labels_to_n_samples_per_cluster, self.__STD_DEV_REG) - ) - random.shuffle( - generated_detections - ) # so all abojects with the same label are not arranged all in a row - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - - # Test - assert result - assert detections_in_world is not None - assert len(detections_in_world[1]) == 5 - for cluster in detections_in_world[1]: - assert cluster.label == 1 - is_match = False - for generated_cluster in labels_to_generated_cluster_positions[1]: - # Check if coordinates are equal - distance = np.linalg.norm( - [ - cluster.location_x - generated_cluster[0], - cluster.location_y - generated_cluster[1], - ] - ) - if distance < self.__MAX_POSITION_TOLERANCE: - is_match = True - break - - assert is_match - - def test_multiple_labels( - self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel - ) -> None: - """ - Five clusters with small standard devition that have different labels - """ - # Setup - labels_to_n_samples_per_cluster = { - 1: [100, 100, 100], - 2: [100, 100, 100], - 3: [100, 100, 100], - } - generated_detections, labels_to_generated_cluster_positions = ( - generate_cluster_data_by_label(labels_to_n_samples_per_cluster, self.__STD_DEV_REG) - ) - random.shuffle( - generated_detections - ) # so all abojects with the same label are not arranged all in a row - - # Run - result, detections_in_world = cluster_model_by_label.run(generated_detections, False) - - # Test - assert result - assert detections_in_world is not None - assert len(detections_in_world[1]) == 3 - assert len(detections_in_world[2]) == 3 - assert len(detections_in_world[3]) == 3 - for label in range(1, 4): - for cluster in detections_in_world[label]: - assert cluster.label == label - is_match = False - for generated_cluster in labels_to_generated_cluster_positions[label]: - # Check if coordinates are equal - distance = np.linalg.norm( - [ - cluster.location_x - generated_cluster[0], - cluster.location_y - generated_cluster[1], - ] - ) - if distance < self.__MAX_POSITION_TOLERANCE: - is_match = True - break - - assert is_match + assert is_match \ No newline at end of file diff --git a/tests/unit/test_cluster_estimation_by_label.py b/tests/unit/test_cluster_estimation_by_label.py new file mode 100644 index 00000000..47e19ff1 --- /dev/null +++ b/tests/unit/test_cluster_estimation_by_label.py @@ -0,0 +1,324 @@ +""" +Testing ClusterEstimationByLabel. +""" + +import random +import numpy as np +import pytest +import sklearn.datasets + +from modules.cluster_estimation import cluster_estimation_by_label +from modules.common.modules.logger import logger +from modules import detection_in_world + +MIN_TOTAL_POINTS_THRESHOLD = 100 +MIN_NEW_POINTS_TO_RUN = 10 +MAX_NUM_COMPONENTS = 10 +RNG_SEED = 0 +CENTRE_BOX_SIZE = 500 + +@pytest.fixture() +def cluster_model_by_label() -> cluster_estimation_by_label.ClusterEstimationByLabel: # type: ignore + """ + Cluster estimation by label object. + """ + result, test_logger = logger.Logger.create("test_logger", False) + assert result + assert test_logger is not None + + result, model = cluster_estimation_by_label.ClusterEstimationByLabel.create( + MIN_TOTAL_POINTS_THRESHOLD, + MIN_NEW_POINTS_TO_RUN, + MAX_NUM_COMPONENTS, + RNG_SEED, + test_logger, + ) + assert result + assert model is not None + + yield model # type: ignore + +def generate_cluster_data( + n_samples_per_cluster: "list[int]", + cluster_standard_deviation: int, + label: int, +) -> "tuple[list[detection_in_world.DetectionInWorld], list[np.ndarray]]": + """ + Returns a list of points (DetectionInWorld objects) with specified points per cluster + and standard deviation. + + PARAMETERS + ---------- + n_samples_per_cluster: list[int] + List corresponding to how many points to generate for each generated cluster + ex: [10 20 30] will generate 10 points for one cluster, 20 points for the next, + and 30 points for the final cluster. + + cluster_standard_deviation: int + The standard deviation of the generated points, bigger + standard deviation == more spread out points. + + label: int + The label that every generated detection gets assigned + + RETURNS + ------- + detections: list[detection_in_world.DetectionInWorld] + List of points (DetectionInWorld objects). + + cluster_positions: list[np.ndarray] + Coordinate positions of each cluster centre. + ------- + """ + # .make_blobs() is a sklearn library function that returns a tuple of two values + # First value is ndarray of shape (2, total # of samples) that gives the (x,y) + # coordinate of generated data points. + # Second value is the integer labels for cluster membership of each generated point (unused). + # Third value is the (x,y) coordinates for each of the cluster centres. + + generated_points, _, cluster_positions = sklearn.datasets.make_blobs( # type: ignore + n_samples=n_samples_per_cluster, + n_features=2, + cluster_std=cluster_standard_deviation, + center_box=(0, CENTRE_BOX_SIZE), + random_state=RNG_SEED, + return_centers=True, + ) + + detections = [] + for point in generated_points: + # Placeholder variables to create DetectionInWorld objects + placeholder_vertices = np.array([[0, 0], [0, 0], [0, 0], [0, 0]]) + placeholder_confidence = 0.5 + + result, detection_to_add = detection_in_world.DetectionInWorld.create( + placeholder_vertices, + point, + label, + placeholder_confidence, + ) + + assert result + assert detection_to_add is not None + detections.append(detection_to_add) + + return detections, cluster_positions.tolist() + +def generate_cluster_data_by_label( + labels_to_n_samples_per_cluster: "dict[int, list[int]]", + cluster_standard_deviation: int, +) -> "tuple[list[detection_in_world.DetectionInWorld], dict[int, list[np.ndarray]]]": + """ + Returns a list of labeled points (DetectionInWorld objects) with specified points per cluster + and standard deviation. + + PARAMETERS + ---------- + labels_to_cluster_samples: "dict[int, list[int]]" + Dictionary where the key is a label and the value is a + list of integers the represent the number of samples a cluster has. + + cluster_standard_deviation: int + The standard deviation of the generated points, bigger + standard deviation == more spread out points. + + RETURNS + ------- + detections: list[detection_in_world.DetectionInWorld] + List of points (DetectionInWorld objects). + + labels_to_cluster_positions: dict[int, list[np.ndarray]] + Dictionary where the key is a label and the value is a + list of coordinate positions of each cluster centre with that label. + ------- + """ + + detections = [] + labels_to_cluster_positions: dict[int, list[np.ndarray]] = {} + + for label, n_samples_list in labels_to_n_samples_per_cluster.items(): + temp_detections, cluster_positions = generate_cluster_data( + n_samples_list, cluster_standard_deviation, label + ) + detections += temp_detections + labels_to_cluster_positions[label] = cluster_positions + + return detections, labels_to_cluster_positions + +class TestModelExecutionCondition: + """ + Tests execution condition for estimation worker at different amount of total and new data + points. + """ + + __STD_DEV_REG = 1 # Regular standard deviation is 1m + + def test_under_min_total_threshold( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + Total data under threshold should not run. + """ + # Setup + original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Less than min threshold (100) + + generated_detections, _ = generate_cluster_data_by_label({0: [original_count]}, self.__STD_DEV_REG) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert not result + assert detections_in_world is None + + def test_at_min_total_threshold( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + Should run once total threshold reached regardless of + current bucket size. + """ + # Setup + original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Should not run the first time + new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points + + generated_detections, _ = generate_cluster_data_by_label({0: [original_count]}, self.__STD_DEV_REG) + generated_detections_2, _ = generate_cluster_data_by_label({0: [new_count]}, self.__STD_DEV_REG) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + result_2, detections_in_world_2 = cluster_model_by_label.run(generated_detections_2, False) + + # Test + assert not result + assert detections_in_world is None + assert result_2 + assert detections_in_world_2 is not None + + def test_under_min_bucket_size( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + New data under threshold should not run. + """ + # Setup + original_count = MIN_TOTAL_POINTS_THRESHOLD + 10 # Should run the first time + new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points, shouldn't run + + generated_detections, _ = generate_cluster_data_by_label({0: [original_count]}, self.__STD_DEV_REG) + generated_detections_2, _ = generate_cluster_data_by_label({0: [new_count]}, self.__STD_DEV_REG) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + result_2, detections_in_world_2 = cluster_model_by_label.run(generated_detections_2, False) + + # Test + assert result + assert detections_in_world is not None + assert not result_2 + assert detections_in_world_2 is None + + def test_good_data(self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel) -> None: + """ + All conditions met should run. + """ + original_count = MIN_TOTAL_POINTS_THRESHOLD + 1 # More than min total threshold should run + generated_detections, _ = generate_cluster_data_by_label({0: [original_count]}, self.__STD_DEV_REG) + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert result + assert detections_in_world is not None + +class TestCorrectClusterPositionOutput: + """ + Tests if cluster estimation by label properly sorts labels. + """ + + __STD_DEV_REG = 1 # Regular standard deviation is 1m + __MAX_POSITION_TOLERANCE = 1 + + def test_one_label( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + Five clusters with small standard devition that all have the same label + """ + # Setup + labels_to_n_samples_per_cluster = {1: [50, 100, 150, 200, 250]} + generated_detections, labels_to_generated_cluster_positions = ( + generate_cluster_data_by_label(labels_to_n_samples_per_cluster, self.__STD_DEV_REG) + ) + random.shuffle( + generated_detections + ) # so all abojects with the same label are not arranged all in a row + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert result + assert detections_in_world is not None + assert len(detections_in_world[1]) == 5 + for cluster in detections_in_world[1]: + is_match = False + for generated_cluster in labels_to_generated_cluster_positions[1]: + # Check if coordinates are equal + distance = np.linalg.norm( + [ + cluster.location_x - generated_cluster[0], + cluster.location_y - generated_cluster[1], + ] + ) + if distance < self.__MAX_POSITION_TOLERANCE: + is_match = True + break + + assert is_match + + def test_multiple_labels( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: + """ + Five clusters with small standard devition each belonging to one of three labels, with large points per cluster + """ + # Setup + labels_to_n_samples_per_cluster = { + 1: [70, 100, 130], + 2: [60, 90, 120], + 3: [50, 80, 110], + } + generated_detections, labels_to_generated_cluster_positions = ( + generate_cluster_data_by_label(labels_to_n_samples_per_cluster, self.__STD_DEV_REG) + ) + random.shuffle( + generated_detections + ) # so all abojects with the same label are not arranged all in a row + + # Run + result, detections_in_world = cluster_model_by_label.run(generated_detections, False) + + # Test + assert result + assert detections_in_world is not None + assert len(detections_in_world[1]) == 3 + assert len(detections_in_world[2]) == 3 + assert len(detections_in_world[3]) == 3 + for label in range(1, 4): + for cluster in detections_in_world[label]: + is_match = False + for generated_cluster in labels_to_generated_cluster_positions[label]: + # Check if coordinates are equal + distance = np.linalg.norm( + [ + cluster.location_x - generated_cluster[0], + cluster.location_y - generated_cluster[1], + ] + ) + if distance < self.__MAX_POSITION_TOLERANCE: + is_match = True + break + + assert is_match \ No newline at end of file diff --git a/tests/unit/test_decision.py b/tests/unit/test_decision.py index ce9192ea..b82d98d0 100644 --- a/tests/unit/test_decision.py +++ b/tests/unit/test_decision.py @@ -44,7 +44,7 @@ def best_pad_within_tolerance() -> object_in_world.ObjectInWorld: # type: ignor location_y = BEST_PAD_LOCATION_Y spherical_variance = 1.0 result, pad = object_in_world.ObjectInWorld.create( - location_x, location_y, spherical_variance, 0 + location_x, location_y, spherical_variance ) assert result assert pad is not None @@ -61,7 +61,7 @@ def best_pad_outside_tolerance() -> object_in_world.ObjectInWorld: # type: igno location_y = 200.0 spherical_variance = 5.0 # variance outside tolerance result, pad = object_in_world.ObjectInWorld.create( - location_x, location_y, spherical_variance, 0 + location_x, location_y, spherical_variance ) assert result assert pad is not None @@ -74,15 +74,15 @@ def pads() -> "list[object_in_world.ObjectInWorld]": # type: ignore """ Create a list of ObjectInWorld instances for the landing pads. """ - result, pad_1 = object_in_world.ObjectInWorld.create(30.0, 40.0, 2.0, 0) + result, pad_1 = object_in_world.ObjectInWorld.create(30.0, 40.0, 2.0) assert result assert pad_1 is not None - result, pad_2 = object_in_world.ObjectInWorld.create(50.0, 60.0, 3.0, 0) + result, pad_2 = object_in_world.ObjectInWorld.create(50.0, 60.0, 3.0) assert result assert pad_2 is not None - result, pad_3 = object_in_world.ObjectInWorld.create(70.0, 80.0, 4.0, 0) + result, pad_3 = object_in_world.ObjectInWorld.create(70.0, 80.0, 4.0) assert result assert pad_3 is not None diff --git a/tests/unit/test_landing_pad_tracking.py b/tests/unit/test_landing_pad_tracking.py index 5faa1f8f..fd6fce60 100644 --- a/tests/unit/test_landing_pad_tracking.py +++ b/tests/unit/test_landing_pad_tracking.py @@ -30,23 +30,23 @@ def detections_1() -> "list[object_in_world.ObjectInWorld]": # type: ignore """ Sample instances of ObjectInWorld for testing. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 8, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 8) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(2, 2, 4, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(2, 2, 4) assert result assert obj_2 is not None - result, obj_3 = object_in_world.ObjectInWorld.create(-2, -2, 2, 0) + result, obj_3 = object_in_world.ObjectInWorld.create(-2, -2, 2) assert result assert obj_3 is not None - result, obj_4 = object_in_world.ObjectInWorld.create(3, 3, 10, 0) + result, obj_4 = object_in_world.ObjectInWorld.create(3, 3, 10) assert result assert obj_4 is not None - result, obj_5 = object_in_world.ObjectInWorld.create(-3, -3, 6, 0) + result, obj_5 = object_in_world.ObjectInWorld.create(-3, -3, 6) assert result assert obj_5 is not None @@ -59,23 +59,23 @@ def detections_2() -> "list[object_in_world.ObjectInWorld]": # type: ignore """ Sample instances of ObjectInWorld for testing. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0.5, 0.5, 1, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0.5, 0.5, 1) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(1.5, 1.5, 3, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(1.5, 1.5, 3) assert result assert obj_2 is not None - result, obj_3 = object_in_world.ObjectInWorld.create(4, 4, 7, 0) + result, obj_3 = object_in_world.ObjectInWorld.create(4, 4, 7) assert result assert obj_3 is not None - result, obj_4 = object_in_world.ObjectInWorld.create(-4, -4, 5, 0) + result, obj_4 = object_in_world.ObjectInWorld.create(-4, -4, 5) assert result assert obj_4 is not None - result, obj_5 = object_in_world.ObjectInWorld.create(5, 5, 9, 0) + result, obj_5 = object_in_world.ObjectInWorld.create(5, 5, 9) assert result assert obj_5 is not None @@ -88,23 +88,23 @@ def detections_3() -> "list[object_in_world.ObjectInWorld]": # type: ignore """ Sample instances of ObjectInWorld for testing. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 8, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 8) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(0.5, 0.5, 4, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(0.5, 0.5, 4) assert result assert obj_2 is not None - result, obj_3 = object_in_world.ObjectInWorld.create(-2, -2, 2, 0) + result, obj_3 = object_in_world.ObjectInWorld.create(-2, -2, 2) assert result assert obj_3 is not None - result, obj_4 = object_in_world.ObjectInWorld.create(3, 3, 10, 0) + result, obj_4 = object_in_world.ObjectInWorld.create(3, 3, 10) assert result assert obj_4 is not None - result, obj_5 = object_in_world.ObjectInWorld.create(-3, -3, 6, 0) + result, obj_5 = object_in_world.ObjectInWorld.create(-3, -3, 6) assert result assert obj_5 is not None @@ -123,11 +123,11 @@ def test_is_similar_positive_equal_to_threshold(self) -> None: Test case where the second landing pad has positive coordinates and the distance between them is equal to the distance threshold. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(1, 1, 0, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(1, 1, 0) assert result assert obj_2 is not None @@ -145,11 +145,11 @@ def test_is_similar_negative_equal_to_threshold(self) -> None: Test case where the second landing pad has negative coordinates and the distance between them is equal to the distance threshold. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(-1, -1, 0, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(-1, -1, 0) assert result assert obj_2 is not None @@ -168,11 +168,11 @@ def test_is_similar_positive_less_than_threshold(self) -> None: Test case where the second landing pad has positive coordinates and the distance between them is less than the distance threshold. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(0.5, 0.5, 0, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(0.5, 0.5, 0) assert result assert obj_2 is not None @@ -191,11 +191,11 @@ def test_is_similar_negative_less_than_threshold(self) -> None: Test case where the second landing pad has negative coordinates and the distance between them is less than the distance threshold. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(-0.5, -0.5, 0, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(-0.5, -0.5, 0) assert result assert obj_2 is not None @@ -214,11 +214,11 @@ def test_is_similar_positive_more_than_threshold(self) -> None: Test case where the second landing pad has positive coordinates and the distance between them is more than the distance threshold. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(2, 2, 0, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(2, 2, 0) assert result assert obj_2 is not None @@ -237,11 +237,11 @@ def test_is_similar_negative_more_than_threshold(self) -> None: Test case where the second landing pad has negative coordinates and the distance between them is more than the distance threshold. """ - result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0, 0) + result, obj_1 = object_in_world.ObjectInWorld.create(0, 0, 0) assert result assert obj_1 is not None - result, obj_2 = object_in_world.ObjectInWorld.create(-2, -2, 0, 0) + result, obj_2 = object_in_world.ObjectInWorld.create(-2, -2, 0) assert result assert obj_2 is not None @@ -269,7 +269,7 @@ def test_mark_false_positive_no_similar( """ Test if marking false positive adds detection to list of false positives. """ - _, false_positive = object_in_world.ObjectInWorld.create(20, 20, 20, 0) + _, false_positive = object_in_world.ObjectInWorld.create(20, 20, 20) assert false_positive is not None tracker._LandingPadTracking__unconfirmed_positives = detections_1 # type: ignore @@ -296,7 +296,7 @@ def test_mark_false_positive_with_similar( Test if marking false positive adds detection to list of false positives and removes. similar landing pads """ - _, false_positive = object_in_world.ObjectInWorld.create(1, 1, 1, 0) + _, false_positive = object_in_world.ObjectInWorld.create(1, 1, 1) assert false_positive is not None tracker._LandingPadTracking__unconfirmed_positives = detections_2 # type: ignore @@ -316,10 +316,10 @@ def test_mark_multiple_false_positive( """ Test if marking false positive adds detection to list of false positives. """ - _, false_positive_1 = object_in_world.ObjectInWorld.create(0, 0, 1, 0) + _, false_positive_1 = object_in_world.ObjectInWorld.create(0, 0, 1) assert false_positive_1 is not None - _, false_positive_2 = object_in_world.ObjectInWorld.create(2, 2, 1, 0) + _, false_positive_2 = object_in_world.ObjectInWorld.create(2, 2, 1) assert false_positive_2 is not None tracker._LandingPadTracking__unconfirmed_positives = detections_1 # type: ignore @@ -344,7 +344,7 @@ def test_mark_confirmed_positive( """ Test if marking confirmed positive adds detection to list of confirmed positives. """ - _, confirmed_positive = object_in_world.ObjectInWorld.create(1, 1, 1, 0) + _, confirmed_positive = object_in_world.ObjectInWorld.create(1, 1, 1) assert confirmed_positive is not None expected = [confirmed_positive] @@ -359,10 +359,10 @@ def test_mark_multiple_confirmed_positives( """ Test if marking confirmed positive adds detection to list of confirmed positives. """ - _, confirmed_positive_1 = object_in_world.ObjectInWorld.create(1, 1, 1, 0) + _, confirmed_positive_1 = object_in_world.ObjectInWorld.create(1, 1, 1) assert confirmed_positive_1 is not None - _, confirmed_positive_2 = object_in_world.ObjectInWorld.create(2, 2, 1, 0) + _, confirmed_positive_2 = object_in_world.ObjectInWorld.create(2, 2, 1) assert confirmed_positive_2 is not None expected = [confirmed_positive_1, confirmed_positive_2] @@ -478,7 +478,7 @@ def test_run_with_confirmed_positive( Test run when there is a confirmed positive. """ # Setup - result, confirmed_positive = object_in_world.ObjectInWorld.create(1, 1, 1, 0) + result, confirmed_positive = object_in_world.ObjectInWorld.create(1, 1, 1) assert result assert confirmed_positive is not None @@ -501,7 +501,7 @@ def test_run_with_false_positive( Test to see if run function doesn't add landing pads that are similar to false positives. """ # Setup - result, false_positive = object_in_world.ObjectInWorld.create(1, 1, 1, 0) + result, false_positive = object_in_world.ObjectInWorld.create(1, 1, 1) assert result assert false_positive is not None From 8618361270516bef921e794e944a60820c1657bf Mon Sep 17 00:00:00 2001 From: Cyuber Date: Tue, 11 Feb 2025 01:14:37 -0500 Subject: [PATCH 20/23] formatting changes --- .../cluster_estimation/cluster_estimation.py | 5 +-- .../cluster_estimation_by_label.py | 8 ++-- modules/object_in_world.py | 5 ++- tests/unit/test_cluster_detection.py | 6 +-- .../unit/test_cluster_estimation_by_label.py | 39 +++++++++++++++---- tests/unit/test_decision.py | 8 +--- 6 files changed, 44 insertions(+), 27 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index b53690cf..a14867ba 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -100,10 +100,7 @@ def create( RETURNS: The ClusterEstimation object if all conditions pass, otherwise False, None """ is_valid_arguments = ClusterEstimation.check_create_arguments( - min_activation_threshold, - min_new_points_to_run, - max_num_components, - random_state + min_activation_threshold, min_new_points_to_run, max_num_components, random_state ) if not is_valid_arguments: diff --git a/modules/cluster_estimation/cluster_estimation_by_label.py b/modules/cluster_estimation/cluster_estimation_by_label.py index af37c370..3421ae3b 100644 --- a/modules/cluster_estimation/cluster_estimation_by_label.py +++ b/modules/cluster_estimation/cluster_estimation_by_label.py @@ -34,6 +34,7 @@ class ClusterEstimationByLabel: run() Cluster estimation filtered by label. """ + __create_key = object() @classmethod @@ -50,10 +51,7 @@ def create( """ is_valid_arguments = cluster_estimation.ClusterEstimation.check_create_arguments( - min_activation_threshold, - min_new_points_to_run, - max_num_components, - random_state + min_activation_threshold, min_new_points_to_run, max_num_components, random_state ) if not is_valid_arguments: @@ -103,7 +101,7 @@ def run( run_override: bool, ) -> tuple[True, dict[int, list[object_in_world.ObjectInWorld]]] | tuple[False, None]: """ - See `ClusterEstimation` for parameter descriptions. + See `ClusterEstimation` for parameter descriptions. RETURNS ------- diff --git a/modules/object_in_world.py b/modules/object_in_world.py index 943fe2ff..6d8ef38d 100644 --- a/modules/object_in_world.py +++ b/modules/object_in_world.py @@ -22,7 +22,10 @@ def create( return False, None return True, ObjectInWorld( - cls.__create_key, location_x, location_y, spherical_variance, + cls.__create_key, + location_x, + location_y, + spherical_variance, ) def __init__( diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 85dd22c1..51d3882d 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -2,7 +2,6 @@ Testing ClusterEstimation. """ -import random import numpy as np import pytest import sklearn.datasets @@ -21,7 +20,7 @@ # Test functions use test fixture signature names and access class privates # No enable -# pylint: disable=protected-access,redefined-outer-name,too-many-instance-attributes +# pylint: disable=protected-access,redefined-outer-name,too-many-instance-attributes,duplicate-code @pytest.fixture() @@ -134,6 +133,7 @@ def generate_cluster_data( return detections, cluster_positions.tolist() + def generate_points_away_from_cluster( num_points_to_generate: int, minimum_distance_from_cluster: float, @@ -527,4 +527,4 @@ def test_position_regular_data( is_match = True break - assert is_match \ No newline at end of file + assert is_match diff --git a/tests/unit/test_cluster_estimation_by_label.py b/tests/unit/test_cluster_estimation_by_label.py index 47e19ff1..ff2c32ee 100644 --- a/tests/unit/test_cluster_estimation_by_label.py +++ b/tests/unit/test_cluster_estimation_by_label.py @@ -17,6 +17,11 @@ RNG_SEED = 0 CENTRE_BOX_SIZE = 500 +# Test functions use test fixture signature names and access class privates +# No enable +# pylint: disable=protected-access,redefined-outer-name,too-many-instance-attributes,duplicate-code + + @pytest.fixture() def cluster_model_by_label() -> cluster_estimation_by_label.ClusterEstimationByLabel: # type: ignore """ @@ -38,6 +43,7 @@ def cluster_model_by_label() -> cluster_estimation_by_label.ClusterEstimationByL yield model # type: ignore + def generate_cluster_data( n_samples_per_cluster: "list[int]", cluster_standard_deviation: int, @@ -104,6 +110,7 @@ def generate_cluster_data( return detections, cluster_positions.tolist() + def generate_cluster_data_by_label( labels_to_n_samples_per_cluster: "dict[int, list[int]]", cluster_standard_deviation: int, @@ -145,6 +152,7 @@ def generate_cluster_data_by_label( return detections, labels_to_cluster_positions + class TestModelExecutionCondition: """ Tests execution condition for estimation worker at different amount of total and new data @@ -162,7 +170,9 @@ def test_under_min_total_threshold( # Setup original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Less than min threshold (100) - generated_detections, _ = generate_cluster_data_by_label({0: [original_count]}, self.__STD_DEV_REG) + generated_detections, _ = generate_cluster_data_by_label( + {0: [original_count]}, self.__STD_DEV_REG + ) # Run result, detections_in_world = cluster_model_by_label.run(generated_detections, False) @@ -182,8 +192,12 @@ def test_at_min_total_threshold( original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Should not run the first time new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points - generated_detections, _ = generate_cluster_data_by_label({0: [original_count]}, self.__STD_DEV_REG) - generated_detections_2, _ = generate_cluster_data_by_label({0: [new_count]}, self.__STD_DEV_REG) + generated_detections, _ = generate_cluster_data_by_label( + {0: [original_count]}, self.__STD_DEV_REG + ) + generated_detections_2, _ = generate_cluster_data_by_label( + {0: [new_count]}, self.__STD_DEV_REG + ) # Run result, detections_in_world = cluster_model_by_label.run(generated_detections, False) @@ -205,8 +219,12 @@ def test_under_min_bucket_size( original_count = MIN_TOTAL_POINTS_THRESHOLD + 10 # Should run the first time new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points, shouldn't run - generated_detections, _ = generate_cluster_data_by_label({0: [original_count]}, self.__STD_DEV_REG) - generated_detections_2, _ = generate_cluster_data_by_label({0: [new_count]}, self.__STD_DEV_REG) + generated_detections, _ = generate_cluster_data_by_label( + {0: [original_count]}, self.__STD_DEV_REG + ) + generated_detections_2, _ = generate_cluster_data_by_label( + {0: [new_count]}, self.__STD_DEV_REG + ) # Run result, detections_in_world = cluster_model_by_label.run(generated_detections, False) @@ -218,12 +236,16 @@ def test_under_min_bucket_size( assert not result_2 assert detections_in_world_2 is None - def test_good_data(self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel) -> None: + def test_good_data( + self, cluster_model_by_label: cluster_estimation_by_label.ClusterEstimationByLabel + ) -> None: """ All conditions met should run. """ original_count = MIN_TOTAL_POINTS_THRESHOLD + 1 # More than min total threshold should run - generated_detections, _ = generate_cluster_data_by_label({0: [original_count]}, self.__STD_DEV_REG) + generated_detections, _ = generate_cluster_data_by_label( + {0: [original_count]}, self.__STD_DEV_REG + ) # Run result, detections_in_world = cluster_model_by_label.run(generated_detections, False) @@ -232,6 +254,7 @@ def test_good_data(self, cluster_model_by_label: cluster_estimation_by_label.Clu assert result assert detections_in_world is not None + class TestCorrectClusterPositionOutput: """ Tests if cluster estimation by label properly sorts labels. @@ -321,4 +344,4 @@ def test_multiple_labels( is_match = True break - assert is_match \ No newline at end of file + assert is_match diff --git a/tests/unit/test_decision.py b/tests/unit/test_decision.py index b82d98d0..3b3cf68f 100644 --- a/tests/unit/test_decision.py +++ b/tests/unit/test_decision.py @@ -43,9 +43,7 @@ def best_pad_within_tolerance() -> object_in_world.ObjectInWorld: # type: ignor location_x = BEST_PAD_LOCATION_X location_y = BEST_PAD_LOCATION_Y spherical_variance = 1.0 - result, pad = object_in_world.ObjectInWorld.create( - location_x, location_y, spherical_variance - ) + result, pad = object_in_world.ObjectInWorld.create(location_x, location_y, spherical_variance) assert result assert pad is not None @@ -60,9 +58,7 @@ def best_pad_outside_tolerance() -> object_in_world.ObjectInWorld: # type: igno location_x = 100.0 location_y = 200.0 spherical_variance = 5.0 # variance outside tolerance - result, pad = object_in_world.ObjectInWorld.create( - location_x, location_y, spherical_variance - ) + result, pad = object_in_world.ObjectInWorld.create(location_x, location_y, spherical_variance) assert result assert pad is not None From d3c0c894d07fedfbc501c509ee197f71ae8e748e Mon Sep 17 00:00:00 2001 From: Cyuber Date: Wed, 5 Mar 2025 23:20:47 -0500 Subject: [PATCH 21/23] implemented reviewed changes --- .../cluster_estimation/cluster_estimation.py | 72 +++++++----------- .../cluster_estimation_by_label.py | 4 +- .../cluster_estimation_worker.py | 30 +------- tests/unit/test_cluster_detection.py | 75 +++++-------------- 4 files changed, 52 insertions(+), 129 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index a14867ba..e66fcf67 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -20,20 +20,6 @@ class ClusterEstimation: works by predicting 'cluster centres' from groups of closely placed landing pad detections. - ATTRIBUTES - ---------- - min_activation_threshold: int - Minimum total data points before model runs. - - min_new_points_to_run: int - Minimum number of new data points that must be collected before running model. - - random_state: int - Seed for randomizer, to get consistent results. - - local_logger: Logger - For logging error and debug messages. - METHODS ------- run() @@ -69,6 +55,32 @@ class ClusterEstimation: __WEIGHT_DROP_THRESHOLD = 0.1 __MAX_COVARIANCE_THRESHOLD = 10 + @staticmethod + def check_create_arguments( + min_activation_threshold: int, + min_new_points_to_run: int, + max_num_components: int, + random_state: int, + ) -> bool: + """ + Checks if a valid cluster estimation object can be constructed. + + See `ClusterEstimation` for parameter descriptions. + """ + if min_activation_threshold < max_num_components: + return False + + if min_new_points_to_run < 0: + return False + + if max_num_components < 1: + return False + + if random_state < 0: + return False + + return True + @classmethod def create( cls, @@ -226,41 +238,15 @@ def run( cluster[0][0], cluster[0][1], cluster[2] ) - if result: - objects_in_world.append(landing_pad) - else: + if not result: self.__logger.error("Failed to create ObjectInWorld object") return False, None + objects_in_world.append(landing_pad) + self.__logger.info(objects_in_world) return True, objects_in_world - @staticmethod - def check_create_arguments( - min_activation_threshold: int, - min_new_points_to_run: int, - max_num_components: int, - random_state: int, - ): - """ - Checks if a valid cluster estimation object can be constructed. - - See `ClusterEstimation` for parameter descriptions. - """ - if min_activation_threshold < max_num_components: - return False - - if min_new_points_to_run < 0: - return False - - if max_num_components < 1: - return False - - if random_state < 0: - return False - - return True - def __decide_to_run(self, run_override: bool) -> bool: """ Decide when to run cluster estimation model. diff --git a/modules/cluster_estimation/cluster_estimation_by_label.py b/modules/cluster_estimation/cluster_estimation_by_label.py index 3421ae3b..ba195525 100644 --- a/modules/cluster_estimation/cluster_estimation_by_label.py +++ b/modules/cluster_estimation/cluster_estimation_by_label.py @@ -118,6 +118,7 @@ def run( for detection in input_detections: if not detection.label in label_to_detections: label_to_detections[detection.label] = [] + label_to_detections[detection.label].append(detection) labels_to_objects: dict[int, list[object_in_world.ObjectInWorld]] = {} @@ -137,9 +138,10 @@ def run( f"Failed to create cluster estimation for label {label}" ) return False, None + self.__label_to_cluster_estimation_model[label] = cluster_model - # runs cluster estimation for specific label + # Runs cluster estimation for specific label result, clusters = self.__label_to_cluster_estimation_model[label].run( detections, run_override, diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index fefd0930..d04bdd61 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -26,35 +26,9 @@ def cluster_estimation_worker( PARAMETERS ---------- - min_activation_threshold: int - Minimum total data points before model runs. + + See `ClusterEstimation` for parameter descriptions. - min_new_points_to_run: int - Minimum number of new data points that must be collected before running model. - - random_state: int - Seed for randomizer, to get consistent results. - - METHODS - ------- - run() - Take in list of landing pad detections and return list of estimated landing pad locations - if number of detections is sufficient, or if manually forced to run. - - __decide_to_run() - Decide when to run cluster estimation model. - - __sort_by_weights() - Sort input model output list by weights in descending order. - - __convert_detections_to_point() - Convert DetectionInWorld input object to a [x,y] position to store. - - __filter_by_points_ownership() - Removes any clusters that don't have any points belonging to it. - - __filter_by_covariances() - Removes any cluster with covariances much higher than the lowest covariance value. """ worker_name = pathlib.Path(__file__).stem process_id = os.getpid() diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 51d3882d..1a846c03 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -7,7 +7,6 @@ import sklearn.datasets from modules.cluster_estimation import cluster_estimation -from modules.cluster_estimation import cluster_estimation_by_label from modules.common.modules.logger import logger from modules import detection_in_world @@ -20,7 +19,7 @@ # Test functions use test fixture signature names and access class privates # No enable -# pylint: disable=protected-access,redefined-outer-name,too-many-instance-attributes,duplicate-code +# pylint: disable=protected-access,redefined-outer-name @pytest.fixture() @@ -45,32 +44,8 @@ def cluster_model() -> cluster_estimation.ClusterEstimation: # type: ignore yield model # type: ignore -@pytest.fixture() -def cluster_model_by_label() -> cluster_estimation_by_label.ClusterEstimationByLabel: # type: ignore - """ - Cluster estimation by label object. - """ - result, test_logger = logger.Logger.create("test_logger", False) - assert result - assert test_logger is not None - - result, model = cluster_estimation_by_label.ClusterEstimationByLabel.create( - MIN_TOTAL_POINTS_THRESHOLD, - MIN_NEW_POINTS_TO_RUN, - MAX_NUM_COMPONENTS, - RNG_SEED, - test_logger, - ) - assert result - assert model is not None - - yield model # type: ignore - - def generate_cluster_data( - n_samples_per_cluster: "list[int]", - cluster_standard_deviation: int, - label: int, + n_samples_per_cluster: "list[int]", cluster_standard_deviation: int ) -> "tuple[list[detection_in_world.DetectionInWorld], list[np.ndarray]]": """ Returns a list of points (DetectionInWorld objects) with specified points per cluster @@ -87,9 +62,6 @@ def generate_cluster_data( The standard deviation of the generated points, bigger standard deviation == more spread out points. - label: int - The label that every generated detection gets assigned - RETURNS ------- detections: list[detection_in_world.DetectionInWorld] @@ -118,12 +90,13 @@ def generate_cluster_data( for point in generated_points: # Placeholder variables to create DetectionInWorld objects placeholder_vertices = np.array([[0, 0], [0, 0], [0, 0], [0, 0]]) + placeholder_label = 1 placeholder_confidence = 0.5 result, detection_to_add = detection_in_world.DetectionInWorld.create( placeholder_vertices, point, - label, + placeholder_label, placeholder_confidence, ) @@ -216,7 +189,7 @@ def test_under_min_total_threshold( """ # Setup original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Less than min threshold (100) - generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG, 0) + generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -236,8 +209,8 @@ def test_at_min_total_threshold( original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Should not run the first time new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points - generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG, 0) - generated_detections_2, _ = generate_cluster_data([new_count], self.__STD_DEV_REG, 0) + generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG) + generated_detections_2, _ = generate_cluster_data([new_count], self.__STD_DEV_REG) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -259,8 +232,8 @@ def test_under_min_bucket_size( original_count = MIN_TOTAL_POINTS_THRESHOLD + 10 # Should run the first time new_count = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points, shouldn't run - generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG, 0) - generated_detections_2, _ = generate_cluster_data([new_count], self.__STD_DEV_REG, 0) + generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG) + generated_detections_2, _ = generate_cluster_data([new_count], self.__STD_DEV_REG) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -277,7 +250,7 @@ def test_good_data(self, cluster_model: cluster_estimation.ClusterEstimation) -> All conditions met should run. """ original_count = MIN_TOTAL_POINTS_THRESHOLD + 1 # More than min total threshold should run - generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG, 0) + generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -304,9 +277,7 @@ def test_detect_normal_data_single_cluster( """ # Setup points_per_cluster = [100] - generated_detections, _ = generate_cluster_data( - points_per_cluster, self.__STD_DEV_REGULAR, 0 - ) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -324,9 +295,7 @@ def test_detect_normal_data_five_clusters( # Setup points_per_cluster = [100, 100, 100, 100, 100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data( - points_per_cluster, self.__STD_DEV_REGULAR, 0 - ) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -345,7 +314,7 @@ def test_detect_large_std_dev_single_cluster( # Setup points_per_cluster = [100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_LARGE, 0) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_LARGE) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -364,7 +333,7 @@ def test_detect_large_std_dev_five_clusters( # Setup points_per_cluster = [100, 100, 100, 100, 100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_LARGE, 0) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_LARGE) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -384,9 +353,7 @@ def test_detect_skewed_data_single_cluster( # Setup points_per_cluster = [10, 100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data( - points_per_cluster, self.__STD_DEV_REGULAR, 0 - ) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) # Run result, detections_in_world = cluster_model.run(generated_detections, False) @@ -409,7 +376,6 @@ def test_detect_skewed_data_five_clusters( generated_detections, cluster_positions = generate_cluster_data( points_per_cluster, self.__STD_DEV_REGULAR, - 0, ) # Add 5 random points to dataset, each being at least 20m away from cluster centres @@ -441,9 +407,7 @@ def test_detect_consecutive_inputs_single_cluster( # Setup points_per_cluster = [100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data( - points_per_cluster, self.__STD_DEV_REGULAR, 0 - ) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) # Run result_latest = False @@ -467,9 +431,7 @@ def test_detect_consecutive_inputs_five_clusters( # Setup points_per_cluster = [100, 100, 100, 100, 100] expected_cluster_count = len(points_per_cluster) - generated_detections, _ = generate_cluster_data( - points_per_cluster, self.__STD_DEV_REGULAR, 0 - ) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) # Run result_latest = False @@ -503,7 +465,6 @@ def test_position_regular_data( generated_detections, cluster_positions = generate_cluster_data( points_per_cluster, self.__STD_DEV_REG, - 0, ) # Run @@ -527,4 +488,4 @@ def test_position_regular_data( is_match = True break - assert is_match + assert is_match \ No newline at end of file From d12aa3409e165e60fd05ca1e8d60d0285d773612 Mon Sep 17 00:00:00 2001 From: Cyuber Date: Wed, 5 Mar 2025 23:24:51 -0500 Subject: [PATCH 22/23] fixed formatting --- modules/cluster_estimation/cluster_estimation_by_label.py | 8 +++++--- modules/cluster_estimation/cluster_estimation_worker.py | 4 ++-- tests/unit/test_cluster_detection.py | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation_by_label.py b/modules/cluster_estimation/cluster_estimation_by_label.py index ba195525..bbd8150c 100644 --- a/modules/cluster_estimation/cluster_estimation_by_label.py +++ b/modules/cluster_estimation/cluster_estimation_by_label.py @@ -48,6 +48,8 @@ def create( ) -> "tuple[True, ClusterEstimationByLabel] | tuple[False, None]": """ See `ClusterEstimation` for parameter descriptions. + + Return: Success, cluster estimation by label object. """ is_valid_arguments = cluster_estimation.ClusterEstimation.check_create_arguments( @@ -114,11 +116,11 @@ def run( """ label_to_detections: dict[int, list[detection_in_world.DetectionInWorld]] = {} - # Sorting detections by label + # Filtering detections by label for detection in input_detections: if not detection.label in label_to_detections: label_to_detections[detection.label] = [] - + label_to_detections[detection.label].append(detection) labels_to_objects: dict[int, list[object_in_world.ObjectInWorld]] = {} @@ -138,7 +140,7 @@ def run( f"Failed to create cluster estimation for label {label}" ) return False, None - + self.__label_to_cluster_estimation_model[label] = cluster_model # Runs cluster estimation for specific label diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index d04bdd61..3b2ac686 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -26,8 +26,8 @@ def cluster_estimation_worker( PARAMETERS ---------- - - See `ClusterEstimation` for parameter descriptions. + + See `ClusterEstimation` for parameter descriptions. """ worker_name = pathlib.Path(__file__).stem diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 1a846c03..155cca06 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -488,4 +488,4 @@ def test_position_regular_data( is_match = True break - assert is_match \ No newline at end of file + assert is_match From b8155cb999df519c4942b291260bef33fbeff8df Mon Sep 17 00:00:00 2001 From: Aleksa-M Date: Wed, 5 Mar 2025 23:42:24 -0500 Subject: [PATCH 23/23] empty commit to fix contributing account