Skip to content
This repository was archived by the owner on Nov 13, 2025. It is now read-only.
1 change: 1 addition & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 16 additions & 4 deletions modules/cluster_estimation/cluster_estimation.py
Original file line number Diff line number Diff line change
@@ -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.
"""

Expand All @@ -14,6 +14,7 @@
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
Expand Down Expand Up @@ -63,6 +64,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.
Expand All @@ -83,6 +85,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
"""
if min_activation_threshold < max_num_components:
Expand All @@ -97,13 +102,17 @@ def create(
if random_state < 0:
return False, None

if min_points_per_cluster < 1:
return False, None

return True, ClusterEstimation(
cls.__create_key,
min_activation_threshold,
min_new_points_to_run,
max_num_components,
random_state,
local_logger,
min_points_per_cluster,
)

def __init__(
Expand All @@ -114,6 +123,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.
Expand All @@ -140,6 +150,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
Expand Down Expand Up @@ -337,15 +348,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
Expand Down
2 changes: 2 additions & 0 deletions modules/cluster_estimation/cluster_estimation_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_communications_to_ground_station.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Test MAVLink integration test
Test MAVLink integration test
"""

import multiprocessing as mp
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/test_cluster_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,6 +38,7 @@ def cluster_model() -> cluster_estimation.ClusterEstimation: # type: ignore
MAX_NUM_COMPONENTS,
RNG_SEED,
test_logger,
MIN_POINTS_PER_CLUSTER,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you verify the test cases to make sure that all the clusters have at least 3 points? Can you also make a test where there is a cluster that only has like 1 point (ie make a very far outlier)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, did you check the integration tests? They are under tests/integration

)
assert result
assert model is not None
Expand Down