From bf8e3da6316a9fd0957d7eefa14c749e2122ce63 Mon Sep 17 00:00:00 2001 From: Bill Date: Thu, 13 Feb 2025 19:24:11 -0500 Subject: [PATCH 01/14] min_points_per_cluster --- .../cluster_estimation/cluster_estimation.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index 10c7bb91..760a300e 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -63,6 +63,7 @@ def create( max_num_components: int, random_state: int, local_logger: logger.Logger, + min_points_per_cluster: int, ) -> "tuple[bool, ClusterEstimation | None]": """ Data requirement conditions for estimation model to run. @@ -82,6 +83,9 @@ def create( local_logger: logger.Logger The local logger to log this object's information. + + min_points_per_cluster: int + Minimum number of points that must be assigned to a cluster for it to be considered valid. RETURNS: The ClusterEstimation object if all conditions pass, otherwise False, None """ @@ -96,6 +100,9 @@ def create( if random_state < 0: return False, None + + if min_points_per_cluster < 1: + return False, None return True, ClusterEstimation( cls.__create_key, @@ -104,6 +111,7 @@ def create( max_num_components, random_state, local_logger, + min_points_per_cluster, ) def __init__( @@ -114,6 +122,7 @@ def __init__( max_num_components: int, random_state: int, local_logger: logger.Logger, + min_points_per_cluster: int, ) -> None: """ Private constructor, use create() method. @@ -140,6 +149,7 @@ def __init__( self.__min_new_points_to_run = min_new_points_to_run self.__has_ran_once = False self.__logger = local_logger + self.__min_points_per_cluster = min_points_per_cluster def run( self, detections: "list[detection_in_world.DetectionInWorld]", run_override: bool @@ -337,15 +347,16 @@ def __filter_by_points_ownership( # List of each point's cluster index cluster_assignment = self.__vgmm.predict(self.__all_points) # type: ignore - # Find which cluster indices have points - clusters_with_points = np.unique(cluster_assignment) + # Get counts for each cluster index + unique, counts = np.unique(cluster_assignment, return_counts=True) + cluster_counts = dict(zip(unique, counts)) # Remove empty clusters filtered_output: "list[tuple[np.ndarray, float, float]]" = [] # By cluster index # pylint: disable-next=consider-using-enumerate for i in range(len(model_output)): - if i in clusters_with_points: + if cluster_counts.get(i, 0) >= self.__min_points_per_cluster: filtered_output.append(model_output[i]) return filtered_output From b721e380ab9eca5942ba4056a1f49b18f88ead29 Mon Sep 17 00:00:00 2001 From: Bill Date: Thu, 13 Feb 2025 19:30:15 -0500 Subject: [PATCH 02/14] config --- config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config.yaml b/config.yaml index c6d4f59e..6155d651 100644 --- a/config.yaml +++ b/config.yaml @@ -74,6 +74,7 @@ cluster_estimation: min_new_points_to_run: 5 max_num_components: 10 random_state: 0 + min_points_per_cluster: 3 communications: timeout: 30.0 # seconds From f998e6b59f318aaea9881142cdafa4b7db1f2cd3 Mon Sep 17 00:00:00 2001 From: Bill Date: Thu, 13 Feb 2025 20:16:50 -0500 Subject: [PATCH 03/14] format --- modules/cluster_estimation/cluster_estimation.py | 6 +++--- tests/integration/test_communications_to_ground_station.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index 760a300e..b6d0e99f 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -1,6 +1,6 @@ """ 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 +Returns an array of classes, each containing the x coordinate, y coordinate, and spherical covariance of each landing pad estimation. """ @@ -83,7 +83,7 @@ def create( local_logger: logger.Logger The local logger to log this object's information. - + min_points_per_cluster: int Minimum number of points that must be assigned to a cluster for it to be considered valid. @@ -100,7 +100,7 @@ def create( if random_state < 0: return False, None - + if min_points_per_cluster < 1: return False, None diff --git a/tests/integration/test_communications_to_ground_station.py b/tests/integration/test_communications_to_ground_station.py index e8e72a81..8730cca0 100644 --- a/tests/integration/test_communications_to_ground_station.py +++ b/tests/integration/test_communications_to_ground_station.py @@ -1,5 +1,5 @@ """ -Test MAVLink integration test +Test MAVLink integration test """ import multiprocessing as mp From ce18ce1154793f373bae60c8600fdc046915c05c Mon Sep 17 00:00:00 2001 From: Bill Date: Thu, 13 Feb 2025 20:44:14 -0500 Subject: [PATCH 04/14] fix --- modules/cluster_estimation/cluster_estimation.py | 2 +- modules/cluster_estimation/cluster_estimation_worker.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 b6d0e99f..e681dc14 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -13,7 +13,7 @@ from .. import detection_in_world from ..common.modules.logger import logger - +# pylint: disable=too-many-instance-attributes class ClusterEstimation: """ Estimate landing pad locations based on landing pad ground detection. Estimation diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index 0f378625..48a74bd5 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -20,6 +20,7 @@ def cluster_estimation_worker( input_queue: queue_proxy_wrapper.QueueProxyWrapper, output_queue: queue_proxy_wrapper.QueueProxyWrapper, controller: worker_controller.WorkerController, + min_points_per_cluster: int, ) -> None: """ Estimation worker process. @@ -64,6 +65,7 @@ def cluster_estimation_worker( max_num_components, random_state, local_logger, + min_points_per_cluster, ) if not result: local_logger.error("Worker failed to create class object", True) From 5b25de846289be44808587d44397ce29664f6be1 Mon Sep 17 00:00:00 2001 From: Bill Date: Thu, 13 Feb 2025 20:49:25 -0500 Subject: [PATCH 05/14] format --- 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 e681dc14..02d81398 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -13,6 +13,7 @@ from .. import detection_in_world from ..common.modules.logger import logger + # pylint: disable=too-many-instance-attributes class ClusterEstimation: """ From 4e6b1041e3cf43409070ebcb8e0c4dc1ece5f536 Mon Sep 17 00:00:00 2001 From: Bill Date: Thu, 13 Feb 2025 21:02:13 -0500 Subject: [PATCH 06/14] update --- tests/unit/test_cluster_detection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 155cca06..9f2df66d 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -16,6 +16,7 @@ MAX_NUM_COMPONENTS = 10 RNG_SEED = 0 CENTRE_BOX_SIZE = 500 +MIN_POINTS_PER_CLUSTER = 3 # Test functions use test fixture signature names and access class privates # No enable @@ -37,6 +38,7 @@ def cluster_model() -> cluster_estimation.ClusterEstimation: # type: ignore MAX_NUM_COMPONENTS, RNG_SEED, test_logger, + MIN_POINTS_PER_CLUSTER, ) assert result assert model is not None From 95fe301a780d4b398f951343c3e3873f06e3d22d Mon Sep 17 00:00:00 2001 From: Bill Date: Wed, 5 Mar 2025 08:38:49 -0500 Subject: [PATCH 07/14] update --- .../cluster_estimation/cluster_estimation.py | 4 +-- tests/unit/test_cluster_detection.py | 30 +++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index 02d81398..1cab5fd6 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -348,7 +348,7 @@ def __filter_by_points_ownership( # List of each point's cluster index cluster_assignment = self.__vgmm.predict(self.__all_points) # type: ignore - # Get counts for each cluster index + # Check each cluster has enough points associated to it by index unique, counts = np.unique(cluster_assignment, return_counts=True) cluster_counts = dict(zip(unique, counts)) @@ -357,7 +357,7 @@ def __filter_by_points_ownership( # By cluster index # pylint: disable-next=consider-using-enumerate for i in range(len(model_output)): - if cluster_counts.get(i, 0) >= self.__min_points_per_cluster: + if cluster_counts.get(i) >= self.__min_points_per_cluster: filtered_output.append(model_output[i]) return filtered_output diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 9f2df66d..046d494e 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -16,7 +16,6 @@ MAX_NUM_COMPONENTS = 10 RNG_SEED = 0 CENTRE_BOX_SIZE = 500 -MIN_POINTS_PER_CLUSTER = 3 # Test functions use test fixture signature names and access class privates # No enable @@ -38,7 +37,6 @@ def cluster_model() -> cluster_estimation.ClusterEstimation: # type: ignore MAX_NUM_COMPONENTS, RNG_SEED, test_logger, - MIN_POINTS_PER_CLUSTER, ) assert result assert model is not None @@ -491,3 +489,31 @@ def test_position_regular_data( break assert is_match + +class TestMinimumPointsPerCluster: + """ + Tests that clusters with fewer than the minimum required points are filtered out. + """ + __STD_DEV_REG = 1 + + def test_outlier_is_filtered(self, cluster_model: cluster_estimation.ClusterEstimation) -> None: + """ + Verify that a single outlier (cluster with only one point) is filtered out, + while a valid cluster with enough points is retained. + """ + # Setup + valid_detections, valid_cluster_positions = generate_cluster_data([5], self.__STD_DEV_REG) + outlier_detections = generate_points_away_from_cluster( + num_points_to_generate=1, + minimum_distance_from_cluster=20, + cluster_positions=valid_cluster_positions + ) + generated_detections = valid_detections + outlier_detections + + # Run + result, detections_in_world = cluster_model.run(generated_detections, False) + + # Test + assert result + assert detections_in_world is not None + assert len(detections_in_world) == 1 From 241adcf1399d66e8e94f119448d87da79ef22361 Mon Sep 17 00:00:00 2001 From: Bill Date: Wed, 5 Mar 2025 08:44:37 -0500 Subject: [PATCH 08/14] 'format' --- tests/unit/test_cluster_detection.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 046d494e..06f2a4d4 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -490,10 +490,12 @@ def test_position_regular_data( assert is_match + class TestMinimumPointsPerCluster: """ Tests that clusters with fewer than the minimum required points are filtered out. """ + __STD_DEV_REG = 1 def test_outlier_is_filtered(self, cluster_model: cluster_estimation.ClusterEstimation) -> None: @@ -506,13 +508,13 @@ def test_outlier_is_filtered(self, cluster_model: cluster_estimation.ClusterEsti outlier_detections = generate_points_away_from_cluster( num_points_to_generate=1, minimum_distance_from_cluster=20, - cluster_positions=valid_cluster_positions + cluster_positions=valid_cluster_positions, ) generated_detections = valid_detections + outlier_detections - + # Run result, detections_in_world = cluster_model.run(generated_detections, False) - + # Test assert result assert detections_in_world is not None From 48156132444ac1478d7f07dd78b57fd673f42d69 Mon Sep 17 00:00:00 2001 From: Bill Date: Wed, 5 Mar 2025 08:58:36 -0500 Subject: [PATCH 09/14] 'format' --- tests/unit/test_cluster_detection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 06f2a4d4..81b826fc 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -16,6 +16,7 @@ MAX_NUM_COMPONENTS = 10 RNG_SEED = 0 CENTRE_BOX_SIZE = 500 +MIN_POINTS_PER_CLUSTER = 3 # Test functions use test fixture signature names and access class privates # No enable @@ -37,6 +38,7 @@ def cluster_model() -> cluster_estimation.ClusterEstimation: # type: ignore MAX_NUM_COMPONENTS, RNG_SEED, test_logger, + MIN_POINTS_PER_CLUSTER, ) assert result assert model is not None From a1904f854f6c19caa2865957be179094efd25cf0 Mon Sep 17 00:00:00 2001 From: Bill Date: Wed, 5 Mar 2025 09:16:33 -0500 Subject: [PATCH 10/14] 'update' --- tests/unit/test_cluster_detection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 81b826fc..372c5e28 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -506,7 +506,7 @@ def test_outlier_is_filtered(self, cluster_model: cluster_estimation.ClusterEsti while a valid cluster with enough points is retained. """ # Setup - valid_detections, valid_cluster_positions = generate_cluster_data([5], self.__STD_DEV_REG) + valid_detections, valid_cluster_positions = generate_cluster_data([100], self.__STD_DEV_REG) outlier_detections = generate_points_away_from_cluster( num_points_to_generate=1, minimum_distance_from_cluster=20, From e2a5177a5856f1535c998251b46f0cea787bdf68 Mon Sep 17 00:00:00 2001 From: Bill Date: Wed, 5 Mar 2025 09:24:48 -0500 Subject: [PATCH 11/14] debug --- modules/cluster_estimation/cluster_estimation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index 1cab5fd6..c3d6c082 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -357,7 +357,7 @@ def __filter_by_points_ownership( # By cluster index # pylint: disable-next=consider-using-enumerate for i in range(len(model_output)): - if cluster_counts.get(i) >= self.__min_points_per_cluster: + if cluster_counts.get(i, 0) >= self.__min_points_per_cluster: filtered_output.append(model_output[i]) return filtered_output From 848b8af0d3a688f7106e61fd8c12b30ef5543f8d Mon Sep 17 00:00:00 2001 From: Bill Date: Wed, 12 Mar 2025 00:55:53 -0400 Subject: [PATCH 12/14] integration --- tests/integration/test_cluster_estimation_worker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_cluster_estimation_worker.py b/tests/integration/test_cluster_estimation_worker.py index de3392d1..559ec616 100644 --- a/tests/integration/test_cluster_estimation_worker.py +++ b/tests/integration/test_cluster_estimation_worker.py @@ -17,7 +17,7 @@ MIN_NEW_POINTS_TO_RUN = 0 MAX_NUM_COMPONENTS = 3 RANDOM_STATE = 0 - +MIN_POINTS_PER_CLUSTER = 3 def check_output_results(output_queue: queue_proxy_wrapper.QueueProxyWrapper) -> None: """ @@ -49,6 +49,7 @@ def test_cluster_estimation_worker() -> int: MIN_NEW_POINTS_TO_RUN, MAX_NUM_COMPONENTS, RANDOM_STATE, + MIN_POINTS_PER_CLUSTER, input_queue, output_queue, controller, From cfcab8f43d7f49060258b6a8c9ccbdc3a93aa8a2 Mon Sep 17 00:00:00 2001 From: Bill Date: Wed, 12 Mar 2025 01:00:35 -0400 Subject: [PATCH 13/14] format --- tests/integration/test_cluster_estimation_worker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/test_cluster_estimation_worker.py b/tests/integration/test_cluster_estimation_worker.py index 559ec616..595cda3c 100644 --- a/tests/integration/test_cluster_estimation_worker.py +++ b/tests/integration/test_cluster_estimation_worker.py @@ -19,6 +19,7 @@ RANDOM_STATE = 0 MIN_POINTS_PER_CLUSTER = 3 + def check_output_results(output_queue: queue_proxy_wrapper.QueueProxyWrapper) -> None: """ Checking if the output from the worker is of the correct type From f9fabb6ae3edba719bc1bcd36d367b96a4781208 Mon Sep 17 00:00:00 2001 From: Bill Date: Thu, 13 Mar 2025 21:25:52 -0400 Subject: [PATCH 14/14] main_edit --- main_2025.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main_2025.py b/main_2025.py index 81383697..642a524c 100644 --- a/main_2025.py +++ b/main_2025.py @@ -154,6 +154,7 @@ def main() -> int: MIN_NEW_POINTS_TO_RUN = config["cluster_estimation"]["min_new_points_to_run"] MAX_NUM_COMPONENTS = config["cluster_estimation"]["max_num_components"] RANDOM_STATE = config["cluster_estimation"]["random_state"] + MIN_POINTS_PER_CLUSTER = config["cluster_estimation"]["min_points_per_cluster"] COMMUNICATIONS_TIMEOUT = config["communications"]["timeout"] COMMUNICATIONS_WORKER_PERIOD = config["communications"]["worker_period"] @@ -354,6 +355,7 @@ def main() -> int: MIN_NEW_POINTS_TO_RUN, MAX_NUM_COMPONENTS, RANDOM_STATE, + MIN_POINTS_PER_CLUSTER, ), input_queues=[geolocation_to_cluster_estimation_queue], output_queues=[cluster_estimation_to_communications_queue],