From af78849ca8397090cba4b1d7c7600033ffabec79 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sat, 17 Feb 2024 21:16:43 -0500 Subject: [PATCH 01/18] Lint pass 1 --- documentation/main_multiprocess_example.py | 2 +- .../add_random/add_random.py | 5 +- .../add_random/add_random_worker.py | 20 +- .../concatenator/concatenator.py | 11 +- .../concatenator/concatenator_worker.py | 10 +- .../multiprocess_example/countup/countup.py | 6 +- .../countup/countup_worker.py | 10 +- .../intermediate_struct.py | 7 +- documentation/tests/add_or_multiply.py | 2 + documentation/tests/test_add_or_multiply.py | 23 +- documentation/tests/test_pytest_example.py | 14 +- main_2023.py | 13 +- main_2024.py | 14 +- .../cluster_estimation/cluster_estimation.py | 82 +++-- .../cluster_estimation_worker.py | 28 +- modules/data_merge/data_merge_worker.py | 19 +- modules/decision/decision.py | 112 +++--- modules/decision/landing_pad_tracking.py | 17 +- modules/decision_command.py | 81 ++--- modules/detect_target/detect_target.py | 31 +- modules/detect_target/detect_target_worker.py | 24 +- modules/detection_in_world.py | 29 +- modules/detections_and_time.py | 50 +-- modules/drone_odometry_local.py | 52 ++- modules/flight_interface/flight_interface.py | 18 +- .../flight_interface_worker.py | 16 +- .../local_global_conversion.py | 22 +- modules/geolocation/camera_properties.py | 100 +++--- modules/geolocation/geolocation.py | 59 ++-- modules/geolocation/geolocation_worker.py | 19 +- modules/image_and_time.py | 14 +- modules/merged_odometry_detections.py | 29 +- modules/message_and_time.py | 21 -- modules/object_in_world.py | 37 +- modules/odometry_and_time.py | 20 +- modules/video_input/video_input.py | 5 +- modules/video_input/video_input_worker.py | 12 +- requirements.txt | 2 + tests/model_example/generate_expected.py | 20 +- tests/test_camera_properties.py | 253 +++++++------- tests/test_cluster_detection.py | 323 ++++++++++-------- tests/test_data_merge_worker.py | 13 +- tests/test_decision.py | 125 ++++--- tests/test_detect_target.py | 86 +++-- tests/test_detect_target_worker.py | 8 +- tests/test_flight_interface_worker.py | 2 +- tests/test_geolocation.py | 291 ++++++++-------- tests/test_landing_pad_tracking.py | 176 ++++++---- tests/test_video_input_worker.py | 2 +- utilities/workers/queue_proxy_wrapper.py | 1 + utilities/workers/worker_controller.py | 1 + utilities/workers/worker_manager.py | 1 + 52 files changed, 1276 insertions(+), 1062 deletions(-) delete mode 100644 modules/message_and_time.py diff --git a/documentation/main_multiprocess_example.py b/documentation/main_multiprocess_example.py index a80a3ffa..19ad4882 100644 --- a/documentation/main_multiprocess_example.py +++ b/documentation/main_multiprocess_example.py @@ -1,5 +1,5 @@ """ -main process. +Main process. """ import multiprocessing as mp import time diff --git a/documentation/multiprocess_example/add_random/add_random.py b/documentation/multiprocess_example/add_random/add_random.py index 7563ff25..2bd5c45a 100644 --- a/documentation/multiprocess_example/add_random/add_random.py +++ b/documentation/multiprocess_example/add_random/add_random.py @@ -8,13 +8,14 @@ # This class does very little, but still has state -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class AddRandom: """ Adds a random number to the input. A new random number is generated every `__ADD_SWITCH_COUNT` times. """ + def __init__(self, seed: int, max_random_term: int, add_change_count: int): """ Constructor seeds the RNG and sets the max add and @@ -70,5 +71,3 @@ def run_add_random(self, term: int) -> "tuple[bool, intermediate_struct.Intermed # Function returns result and the output # The class is responsible for packing the intermediate type return True, output - -# pylint: enable=too-few-public-methods diff --git a/documentation/multiprocess_example/add_random/add_random_worker.py b/documentation/multiprocess_example/add_random/add_random_worker.py index 01308950..875c8518 100644 --- a/documentation/multiprocess_example/add_random/add_random_worker.py +++ b/documentation/multiprocess_example/add_random/add_random_worker.py @@ -7,14 +7,16 @@ from . import add_random -# As kwargs is not being used, this function needs many parameters -# pylint: disable=too-many-arguments -def add_random_worker(seed: int, - max_random_term: int, - add_change_count: int, - input_queue: queue_proxy_wrapper.QueueProxyWrapper, - output_queue: queue_proxy_wrapper.QueueProxyWrapper, - controller: worker_controller.WorkerController): +# Worker has both class and control parameters +# pylint: disable-next=too-many-arguments +def add_random_worker( + seed: int, + max_random_term: int, + add_change_count: int, + input_queue: queue_proxy_wrapper.QueueProxyWrapper, + output_queue: queue_proxy_wrapper.QueueProxyWrapper, + controller: worker_controller.WorkerController, +): """ Worker process. @@ -52,5 +54,3 @@ def add_random_worker(seed: int, # If the queue is full, the worker process will block # until the queue is non-empty output_queue.queue.put(value) - -# pylint: enable=too-many-arguments diff --git a/documentation/multiprocess_example/concatenator/concatenator.py b/documentation/multiprocess_example/concatenator/concatenator.py index 333f1ab7..e50cb188 100644 --- a/documentation/multiprocess_example/concatenator/concatenator.py +++ b/documentation/multiprocess_example/concatenator/concatenator.py @@ -7,11 +7,12 @@ # This class does very little, but still has state -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class Concatenator: """ Concatenates a prefix and suffix to the object. """ + def __init__(self, prefix: str, suffix: str): """ Constructor sets the prefix and suffix. @@ -19,10 +20,10 @@ def __init__(self, prefix: str, suffix: str): self.__prefix = prefix self.__suffix = suffix - # The working function - def run_concatenation(self, - middle: intermediate_struct.IntermediateStruct) -> "tuple[bool, str]": + def run_concatenation( + self, middle: intermediate_struct.IntermediateStruct + ) -> "tuple[bool, str]": """ Concatenate the prefix and suffix to the input. """ @@ -42,5 +43,3 @@ def run_concatenation(self, # Function returns result and the output return True, concatenated_string - -# pylint: enable=too-few-public-methods diff --git a/documentation/multiprocess_example/concatenator/concatenator_worker.py b/documentation/multiprocess_example/concatenator/concatenator_worker.py index 5e44e2fa..05ca6e88 100644 --- a/documentation/multiprocess_example/concatenator/concatenator_worker.py +++ b/documentation/multiprocess_example/concatenator/concatenator_worker.py @@ -7,10 +7,12 @@ from . import concatenator -def concatenator_worker(prefix: str, - suffix: str, - input_queue: queue_proxy_wrapper.QueueProxyWrapper, - controller: worker_controller.WorkerController): +def concatenator_worker( + prefix: str, + suffix: str, + input_queue: queue_proxy_wrapper.QueueProxyWrapper, + controller: worker_controller.WorkerController, +): """ Worker process. diff --git a/documentation/multiprocess_example/countup/countup.py b/documentation/multiprocess_example/countup/countup.py index 0523f9c8..30cc200f 100644 --- a/documentation/multiprocess_example/countup/countup.py +++ b/documentation/multiprocess_example/countup/countup.py @@ -5,11 +5,12 @@ # This class does very little, but still has state -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class Countup: """ Increments its internal counter and outputs current counter. """ + def __init__(self, start_thousands: int, max_iterations: int): """ Constructor initializes the start and max points. @@ -18,7 +19,6 @@ def __init__(self, start_thousands: int, max_iterations: int): self.__max_count = self.__start_count + max_iterations self.__current_count = self.__start_count - def run_countup(self) -> "tuple[bool, int]": """ Counts upward. @@ -33,5 +33,3 @@ def run_countup(self) -> "tuple[bool, int]": # Function returns result and the output return True, self.__current_count - -# pylint: enable=too-few-public-methods diff --git a/documentation/multiprocess_example/countup/countup_worker.py b/documentation/multiprocess_example/countup/countup_worker.py index 03eaba96..6769df1c 100644 --- a/documentation/multiprocess_example/countup/countup_worker.py +++ b/documentation/multiprocess_example/countup/countup_worker.py @@ -7,10 +7,12 @@ from . import countup -def countup_worker(start_thousands:int, - max_iterations: int, - output_queue: queue_proxy_wrapper.QueueProxyWrapper, - controller: worker_controller.WorkerController): +def countup_worker( + start_thousands: int, + max_iterations: int, + output_queue: queue_proxy_wrapper.QueueProxyWrapper, + controller: worker_controller.WorkerController, +): """ Worker process. diff --git a/documentation/multiprocess_example/intermediate_struct.py b/documentation/multiprocess_example/intermediate_struct.py index 6870f6ed..ac1b7730 100644 --- a/documentation/multiprocess_example/intermediate_struct.py +++ b/documentation/multiprocess_example/intermediate_struct.py @@ -4,16 +4,15 @@ # This class is just a struct containing some members -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class IntermediateStruct: """ Example of a simple struct. """ + def __init__(self, number: int, sentence: str): """ - Constructor + Constructor. """ self.number = number self.sentence = sentence - -# pylint: enable=too-few-public-methods diff --git a/documentation/tests/add_or_multiply.py b/documentation/tests/add_or_multiply.py index 3eb69781..855665bd 100644 --- a/documentation/tests/add_or_multiply.py +++ b/documentation/tests/add_or_multiply.py @@ -8,6 +8,7 @@ class MathOperation(enum.Enum): """ Enumeration for Add or Multiply. """ + ADD = 0 MULTIPLY = 1 @@ -16,6 +17,7 @@ class AddOrMultiply: """ Add or multiply depending on state. """ + def __init__(self, switch: MathOperation): self.__operator = switch diff --git a/documentation/tests/test_add_or_multiply.py b/documentation/tests/test_add_or_multiply.py index 89d589d8..02382ba6 100644 --- a/documentation/tests/test_add_or_multiply.py +++ b/documentation/tests/test_add_or_multiply.py @@ -12,6 +12,11 @@ import add_or_multiply +# Test functions use test fixture signature names and access class privates +# No enable +# pylint: disable=protected-access,redefined-outer-name + + # Pytest fixtures are reusable setup components # Makes it easier when setup is complicated @pytest.fixture() @@ -22,6 +27,7 @@ def adder(): add = add_or_multiply.AddOrMultiply(add_or_multiply.MathOperation.ADD) yield add + @pytest.fixture() def multiplier(): """ @@ -36,6 +42,7 @@ class TestAddition: Unit tests can be organized into groups under classes. The function or method name contains test somewhere for Pytest to run it. """ + def test_add_positive(self, adder: add_or_multiply.AddOrMultiply): """ Add 2 positive numbers. @@ -125,10 +132,13 @@ def test_add_negative(self, adder: add_or_multiply.AddOrMultiply): # * Large negative with large negative +# Test class +# pylint: disable-next=too-few-public-methods class TestMultiply: """ Many multiplication cases need to be covered as well. """ + def test_multiply_positive(self, multiplier: add_or_multiply.AddOrMultiply): """ Multiply 2 positive numbers. @@ -150,6 +160,7 @@ class TestSwap: """ Test a different method. """ + def test_swap_add_to_multiply(self, adder: add_or_multiply.AddOrMultiply): """ Add and then multiply. @@ -163,10 +174,8 @@ def test_swap_add_to_multiply(self, adder: add_or_multiply.AddOrMultiply): # Test # Better to test private members directly rather than rely on other class methods # since the more dependencies a test has the less unit and independent it is - # Access required for test - # pylint: disable=protected-access - actual = adder._AddOrMultiply__operator - # pylint: enable=protected-access + actual = adder._AddOrMultiply__operator # type: ignore + assert actual == expected def test_swap_multiply_to_add(self, multiplier: add_or_multiply.AddOrMultiply): @@ -180,8 +189,6 @@ def test_swap_multiply_to_add(self, multiplier: add_or_multiply.AddOrMultiply): multiplier.swap_state() # Test - # Access required for test - # pylint: disable=protected-access - actual = multiplier._AddOrMultiply__operator - # pylint: enable=protected-access + actual = multiplier._AddOrMultiply__operator # type: ignore + assert actual == expected diff --git a/documentation/tests/test_pytest_example.py b/documentation/tests/test_pytest_example.py index 8c2f4122..1eb4a905 100644 --- a/documentation/tests/test_pytest_example.py +++ b/documentation/tests/test_pytest_example.py @@ -13,12 +13,24 @@ def test_trivial_pass(): """ Unit test will pass by default. """ + # Keyword used to indicate end of empty function + # pylint: disable-next=unnecessary-pass pass +def test_1_plus_1_equals_2(): + """ + Easy unit test. + """ + expected = 2 + actual = 1 + 1 + + assert actual == expected + + def test_expect_exception(): """ If an exception is expected. """ with pytest.raises(Exception): - x = 1 / 0 + _ = 1 / 0 diff --git a/main_2023.py b/main_2023.py index 9f0f5193..1ca237df 100644 --- a/main_2023.py +++ b/main_2023.py @@ -18,6 +18,8 @@ CONFIG_FILE_PATH = pathlib.Path("config.yaml") +# Main function +# pylint: disable-next=too-many-locals def main() -> int: """ Main function for airside code. @@ -45,6 +47,8 @@ def main() -> int: # Set constants try: + # Local constants + # pylint: disable=invalid-name QUEUE_MAX_SIZE = config["queue_max_size"] VIDEO_INPUT_CAMERA_NAME = config["video_input"]["camera_name"] @@ -52,10 +56,11 @@ def main() -> int: VIDEO_INPUT_SAVE_PREFIX = config["video_input"]["save_prefix"] DETECT_TARGET_WORKER_COUNT = config["detect_target"]["worker_count"] - DETECT_TARGET_DEVICE = "cpu" if args.cpu else config["detect_target"]["device"] + DETECT_TARGET_DEVICE = "cpu" if args.cpu else config["detect_target"]["device"] DETECT_TARGET_MODEL_PATH = config["detect_target"]["model_path"] DETECT_TARGET_OVERRIDE_FULL_PRECISION = args.full DETECT_TARGET_SAVE_PREFIX = config["detect_target"]["save_prefix"] + # pylint: enable=invalid-name except KeyError: print("Config key(s) not found") return -1 @@ -110,8 +115,8 @@ def main() -> int: if image is None: continue - cv2.imshow("Landing Pad Detector", image) - if cv2.waitKey(1) & 0xFF == ord('q'): + cv2.imshow("Landing Pad Detector", image) # type: ignore + if cv2.waitKey(1) & 0xFF == ord("q"): # type: ignore break # Teardown @@ -127,6 +132,8 @@ def main() -> int: if __name__ == "__main__": + # Not a constant + # pylint: disable-next=invalid-name result_run = main() if result_run < 0: print(f"ERROR: Status code: {result_run}") diff --git a/main_2024.py b/main_2024.py index 17d37306..3c4859b5 100644 --- a/main_2024.py +++ b/main_2024.py @@ -23,7 +23,8 @@ CONFIG_FILE_PATH = pathlib.Path("config.yaml") -# Main Function + +# Main function # pylint: disable-next=too-many-locals,too-many-statements def main() -> int: """ @@ -57,6 +58,8 @@ def main() -> int: # Set constants try: + # Local constants + # pylint: disable=invalid-name QUEUE_MAX_SIZE = config["queue_max_size"] LOG_DIRECTORY_PATH = config["log_directory_path"] @@ -67,7 +70,7 @@ def main() -> int: VIDEO_INPUT_SAVE_PREFIX = f"{LOG_DIRECTORY_PATH}/{VIDEO_INPUT_SAVE_NAME_PREFIX}" DETECT_TARGET_WORKER_COUNT = config["detect_target"]["worker_count"] - DETECT_TARGET_DEVICE = "cpu" if args.cpu else config["detect_target"]["device"] + DETECT_TARGET_DEVICE = "cpu" if args.cpu else config["detect_target"]["device"] DETECT_TARGET_MODEL_PATH = config["detect_target"]["model_path"] DETECT_TARGET_OVERRIDE_FULL_PRECISION = args.full DETECT_TARGET_SAVE_NAME_PREFIX = config["detect_target"]["save_prefix"] @@ -79,6 +82,7 @@ def main() -> int: FLIGHT_INTERFACE_WORKER_PERIOD = config["flight_interface"]["worker_period"] DATA_MERGE_TIMEOUT = config["data_merge"]["timeout"] + # pylint: enable=invalid-name except KeyError: print("Config key(s) not found") return -1 @@ -190,7 +194,7 @@ def main() -> int: print("merged confidence: " + str(detection.confidence)) print("") - if cv2.waitKey(1) == ord('q'): + if cv2.waitKey(1) == ord("q"): # type: ignore print("Exiting main loop") break @@ -207,12 +211,14 @@ def main() -> int: flight_interface_manager.join_workers() data_merge_manager.join_workers() - cv2.destroyAllWindows() + cv2.destroyAllWindows() # type: ignore return 0 if __name__ == "__main__": + # Not a constant + # pylint: disable-next=invalid-name result_run = main() if result_run < 0: print(f"ERROR: Status code: {result_run}") diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index f5ecdfb6..b142fc39 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -51,13 +51,14 @@ class ClusterEstimation: __filter_by_covariances() Removes any cluster with covariances much higher than the lowest covariance value. """ + __create_key = object() # VGMM Hyperparameters __COVAR_TYPE = "spherical" __MODEL_INIT_PARAM = "k-means++" __WEIGHT_CONCENTRATION_PRIOR = 100 - __MEAN_PRECISION_PRIOR = 1E-6 + __MEAN_PRECISION_PRIOR = 1e-6 __MAX_MODEL_ITERATIONS = 1000 # Real-world scenario Hyperparameters @@ -68,10 +69,9 @@ class ClusterEstimation: __MAX_COVARIANCE_THRESHOLD = 10 @classmethod - def create(cls, - min_activation_threshold: int, - min_new_points_to_run: int, - random_state: int) -> "tuple[bool, ClusterEstimation | None]": + def create( + cls, min_activation_threshold: int, min_new_points_to_run: int, random_state: int + ) -> "tuple[bool, ClusterEstimation | None]": """ Data requirement conditions for estimation model to run. """ @@ -83,17 +83,20 @@ def create(cls, if min_activation_threshold < 1: return False, None - return True, ClusterEstimation(cls.__create_key, + return True, ClusterEstimation( + cls.__create_key, min_activation_threshold, min_new_points_to_run, random_state, ) - def __init__(self, - class_private_create_key: object, - min_activation_threshold: int, - min_new_points_to_run: int, - random_state: int): + def __init__( + self, + class_private_create_key: object, + min_activation_threshold: int, + min_new_points_to_run: int, + random_state: int, + ): """ Private constructor, use create() method. """ @@ -111,16 +114,17 @@ def __init__(self, ) # Points storage - self.__all_points = [] - self.__current_bucket = [] + 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.__has_ran_once = False - def run(self, detections: "list[detection_in_world.DetectionInWorld]", run_override: bool) \ - -> "tuple[bool, list[object_in_world.ObjectInWorld] | None]": + def run( + 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 if number of detections is sufficient, or if manually forced to run. @@ -130,7 +134,7 @@ def run(self, detections: "list[detection_in_world.DetectionInWorld]", run_overr 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. @@ -152,7 +156,7 @@ def run(self, detections: "list[detection_in_world.DetectionInWorld]", run_overr return False, None # Fit points and get cluster data - self.__vgmm = self.__vgmm.fit(self.__all_points) + self.__vgmm = self.__vgmm.fit(self.__all_points) # type: ignore # Check convergence if not self.__vgmm.converged_: @@ -161,9 +165,9 @@ def run(self, detections: "list[detection_in_world.DetectionInWorld]", run_overr # Get predictions from cluster model model_output: "list[tuple[np.ndarray, float, float]]" = list( zip( - self.__vgmm.means_, - self.__vgmm.weights_, - self.__vgmm.covariances_, + self.__vgmm.means_, # type: ignore + self.__vgmm.weights_, # type: ignore + self.__vgmm.covariances_, # type: ignore ) ) @@ -215,17 +219,20 @@ def __decide_to_run(self, run_override: bool) -> bool: 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 len(self.__all_points) + len(self.__current_bucket) < self.__min_activation_threshold: + if count_all + count_current < self.__min_activation_threshold: return False # Don't run if not enough new points - if len(self.__current_bucket) < self.__min_new_points_to_run and 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 - if len(self.__all_points) + len(self.__current_bucket) == 0: + if count_all + count_current == 0: return False # Requirements met, empty bucket and run @@ -236,8 +243,9 @@ def __decide_to_run(self, run_override: bool) -> bool: return True @staticmethod - def __sort_by_weights(model_output: "list[tuple[np.ndarray, float, float]]") \ - -> "list[tuple[np.ndarray, float, float]]": + def __sort_by_weights( + model_output: "list[tuple[np.ndarray, float, float]]", + ) -> "list[tuple[np.ndarray, float, float]]": """ Sort input model output list by weights in descending order. @@ -254,10 +262,10 @@ def __sort_by_weights(model_output: "list[tuple[np.ndarray, float, float]]") \ """ return sorted(model_output, key=lambda x: x[1], reverse=True) - @staticmethod - def __convert_detections_to_point(detections: "list[detection_in_world.DetectionInWorld]") \ - -> "list[tuple[float, float]]": + def __convert_detections_to_point( + detections: "list[detection_in_world.DetectionInWorld]", + ) -> "list[tuple[float, float]]": """ Convert DetectionInWorld input object to a list of points- (x,y) positions, to store. @@ -286,9 +294,9 @@ def __convert_detections_to_point(detections: "list[detection_in_world.Detection return points - def __filter_by_points_ownership(self, - model_output: "list[tuple[np.ndarray, float, float]]") \ - -> "list[tuple[np.ndarray, float, float]]": + def __filter_by_points_ownership( + self, model_output: "list[tuple[np.ndarray, float, float]]" + ) -> "list[tuple[np.ndarray, float, float]]": """ Removes any clusters that don't have any points belonging to it. @@ -303,21 +311,23 @@ def __filter_by_points_ownership(self, filtered_output: list[tuple[np.ndarray, float, float]] List containing predicted cluster centres after filtering. """ - - results = self.__vgmm.predict(self.__all_points) + results = self.__vgmm.predict(self.__all_points) # type: ignore filtered_output = [] # Filtering by each cluster's point ownership - unique_clusters, num_points_per_cluster = np.unique(results, return_counts=True) + unique_clusters, _ = np.unique(results, return_counts=True) # Remove empty clusters + # Integer index required + # pylint: disable-next=consider-using-enumerate for i in range(len(model_output)): if i in unique_clusters: filtered_output.append(model_output[i]) return filtered_output - def __filter_by_covariances(self, model_output: "list[tuple[np.ndarray, float, float]]") \ - -> "list[tuple[np.ndarray, float, float]]": + def __filter_by_covariances( + self, model_output: "list[tuple[np.ndarray, float, float]]" + ) -> "list[tuple[np.ndarray, float, float]]": """ Removes any cluster with covariances much higher than the lowest covariance value. diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index 2aa947a9..e3fad44c 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -7,12 +7,16 @@ from . import cluster_estimation -def cluster_estimation_worker(min_activation_threshold: int, - min_new_points_to_run: int, - random_state: int, - input_queue: queue_proxy_wrapper.QueueProxyWrapper, - output_queue: queue_proxy_wrapper.QueueProxyWrapper, - worker_controller: worker_controller.WorkerController): +# Worker has both class and control parameters +# pylint: disable-next=too-many-arguments +def cluster_estimation_worker( + min_activation_threshold: int, + min_new_points_to_run: int, + random_state: int, + input_queue: queue_proxy_wrapper.QueueProxyWrapper, + output_queue: queue_proxy_wrapper.QueueProxyWrapper, + controller: worker_controller.WorkerController, +): """ Estimation worker process. @@ -36,14 +40,20 @@ def cluster_estimation_worker(min_activation_threshold: int, worker_controller: worker_controller.WorkerController How the main process communicates to this worker process. """ - estimator_created, estimator = cluster_estimation.ClusterEstimation.create( + result, estimator = cluster_estimation.ClusterEstimation.create( min_activation_threshold, min_new_points_to_run, random_state, ) + if not result: + print("ERROR: Worker failed to create class object") + return - while not worker_controller.is_exit_requested(): - worker_controller.check_pause() + # Get Pylance to stop complaining + assert estimator is not None + + while not controller.is_exit_requested(): + controller.check_pause() input_data = input_queue.queue.get() if input_data is None: diff --git a/modules/data_merge/data_merge_worker.py b/modules/data_merge/data_merge_worker.py index 82807541..085cd398 100644 --- a/modules/data_merge/data_merge_worker.py +++ b/modules/data_merge/data_merge_worker.py @@ -10,11 +10,13 @@ from .. import odometry_and_time -def data_merge_worker(timeout: float, - detections_input_queue: queue_proxy_wrapper.QueueProxyWrapper, - odometry_input_queue: queue_proxy_wrapper.QueueProxyWrapper, - output_queue: queue_proxy_wrapper.QueueProxyWrapper, - controller: worker_controller.WorkerController): +def data_merge_worker( + timeout: float, + detections_input_queue: queue_proxy_wrapper.QueueProxyWrapper, + odometry_input_queue: queue_proxy_wrapper.QueueProxyWrapper, + output_queue: queue_proxy_wrapper.QueueProxyWrapper, + controller: worker_controller.WorkerController, +): """ Worker process. Expects telemetry to be more frequent than detections. Queue is monotonic (i.e. timestamps never decrease). @@ -28,7 +30,6 @@ def data_merge_worker(timeout: float, # TODO: Logging? # Mitigate potential deadlock caused by early program exit - # TODO: Timeout try: previous_odometry: odometry_and_time.OdometryAndTime = odometry_input_queue.queue.get( timeout=timeout, @@ -37,6 +38,7 @@ def data_merge_worker(timeout: float, timeout=timeout, ) except queue.Empty: + print("ERROR: Queue timed out on startup") return while not controller.is_exit_requested(): @@ -61,8 +63,9 @@ def data_merge_worker(timeout: float, break # Merge with closest timestamp - if ((detections.timestamp - previous_odometry.timestamp) - < (current_odometry.timestamp - detections.timestamp)): + if (detections.timestamp - previous_odometry.timestamp) < ( + current_odometry.timestamp - detections.timestamp + ): # Required for separation result, merged = merged_odometry_detections.MergedOdometryDetections.create( previous_odometry.odometry_data, diff --git a/modules/decision/decision.py b/modules/decision/decision.py index cc7c0d71..26e02bd6 100644 --- a/modules/decision/decision.py +++ b/modules/decision/decision.py @@ -7,35 +7,46 @@ from .. import odometry_and_time +# Basically a struct +# pylint: disable-next=too-few-public-methods class ScoredLandingPad: + """ + Landing pad with score for decision. + """ + def __init__(self, landing_pad: object_in_world.ObjectInWorld, score: float): self.landing_pad = landing_pad self.score = score - + +# This class still has state +# pylint: disable-next=too-few-public-methods class Decision: """ - Chooses next action to take based on known landing pad information + Chooses next action to take based on known landing pad information. """ def __init__(self, tolerance: float): - self.__best_landing_pad = None + self.__best_landing_pad: "object_in_world.ObjectInWorld | None" = None self.__weighted_pads = [] self.__distance_tolerance = tolerance @staticmethod - def __distance_to_pad(pad: object_in_world.ObjectInWorld, - current_position: odometry_and_time.OdometryAndTime) -> float: + def __distance_to_pad( + pad: object_in_world.ObjectInWorld, current_position: odometry_and_time.OdometryAndTime + ) -> float: """ Calculate squared Euclidean distance to landing pad based on current position. """ - dx = pad.position_x - current_position.odometry_data.position.north - dy = pad.position_y - current_position.odometry_data.position.east + dx = pad.location_x - current_position.odometry_data.position.north + dy = pad.location_y - current_position.odometry_data.position.east return dx**2 + dy**2 - def __weight_pads(self, - pads: "list[object_in_world.ObjectInWorld]", - current_position: odometry_and_time.OdometryAndTime) -> "list[ScoredLandingPad] | None": + def __weight_pads( + self, + pads: "list[object_in_world.ObjectInWorld]", + current_position: odometry_and_time.OdometryAndTime, + ) -> "list[ScoredLandingPad] | None": """ Weights the pads based on normalized variance and distance. """ @@ -48,63 +59,70 @@ def __weight_pads(self, max_distance = max(distances) max_variance = max(variances) - - # if all variance is zero, assumes no significant difference amongst pads - # if max_distance is zero, assumes landing pad is directly below + + # if all variance is zero, assumes no significant difference amongst pads + # if max_distance is zero, assumes landing pad is directly below if max_variance == 0 or max_distance == 0: return [ScoredLandingPad(pad, 0) for pad in pads] - + return [ ScoredLandingPad(pad, distance / max_distance + variance / max_variance) for pad, distance, variance in zip(pads, distances, variances) ] @staticmethod - def __find_best_pad(weighted_pads: "list[ScoredLandingPad]") -> object_in_world.ObjectInWorld: + def __find_best_pad( + weighted_pads: "list[ScoredLandingPad]", + ) -> "object_in_world.ObjectInWorld | None": """ Determine the best pad to land on based on the weighted scores. """ if len(weighted_pads) == 0: return None + # Find the pad with the smallest weight as the best pad best_landing_pad = min(weighted_pads, key=lambda pad: pad.score).landing_pad return best_landing_pad - def run(self, - curr_state: odometry_and_time.OdometryAndTime, - pads: "list[object_in_world.ObjectInWorld]") -> "tuple[bool, decision_command.DecisionCommand | None]": + def run( + self, + curr_state: odometry_and_time.OdometryAndTime, + pads: "list[object_in_world.ObjectInWorld]", + ) -> "tuple[bool, decision_command.DecisionCommand | None]": """ Determine the best landing pad and issue a command to land there. """ self.__weighted_pads = self.__weight_pads(pads, curr_state) - - if not self.__weighted_pads: + + if self.__weighted_pads is None: return False, None - + self.__best_landing_pad = self.__find_best_pad(self.__weighted_pads) - if self.__best_landing_pad: - distance_to_best_bad = self.__distance_to_pad(self.__best_landing_pad, curr_state) - - # Issue a landing command if within tolerance - if distance_to_best_bad <= self.__distance_tolerance: - return ( - True, - decision_command.DecisionCommand.create_land_at_absolute_position_command( - self.__best_landing_pad.position_x, - self.__best_landing_pad.position_y, - curr_state.odometry_data.position.down, - ), - ) - # Move to best location if not within tolerance - else: - return ( - True, - decision_command.DecisionCommand.create_move_to_absolute_position_command( - self.__best_landing_pad.position_x, - self.__best_landing_pad.position_y, - -curr_state.odometry_data.position.down, # Assuming down is negative for landing - ), - ) - # Default to do nothing if no pads are found - return False, None - + if self.__best_landing_pad is None: + return False, None + + # Get Pylance to stop complaining + assert self.__best_landing_pad is not None + + distance_to_best_bad = self.__distance_to_pad(self.__best_landing_pad, curr_state) + + # Issue a landing command if over the landing pad + if distance_to_best_bad <= self.__distance_tolerance: + return ( + True, + decision_command.DecisionCommand.create_land_at_absolute_position_command( + self.__best_landing_pad.location_x, + self.__best_landing_pad.location_y, + curr_state.odometry_data.position.down, + ), + ) + + # Move to landing pad + return ( + True, + decision_command.DecisionCommand.create_move_to_absolute_position_command( + self.__best_landing_pad.location_x, + self.__best_landing_pad.location_y, + curr_state.odometry_data.position.down, + ), + ) diff --git a/modules/decision/landing_pad_tracking.py b/modules/decision/landing_pad_tracking.py index 279deded..cbf40ff8 100644 --- a/modules/decision/landing_pad_tracking.py +++ b/modules/decision/landing_pad_tracking.py @@ -10,6 +10,7 @@ class LandingPadTracking: Tracks the real world location of detected landing pads, labelling them as either confirmed positive, unconfirmed positive, or false positive. """ + def __init__(self, distance_squared_threshold: float): self.__unconfirmed_positives = [] self.__false_positives = [] @@ -19,15 +20,18 @@ def __init__(self, distance_squared_threshold: float): self.__distance_squared_threshold = distance_squared_threshold @staticmethod - def __is_similar(detection_1: object_in_world.ObjectInWorld, - detection_2: object_in_world.ObjectInWorld, - distance_squared_threshold: float) -> bool: + def __is_similar( + detection_1: object_in_world.ObjectInWorld, + detection_2: object_in_world.ObjectInWorld, + distance_squared_threshold: float, + ) -> bool: """ Returns whether detection_1 and detection_2 are close enough to be considered the same landing pad. """ - distance_squared = (detection_2.position_x - detection_1.position_x) ** 2 \ - + (detection_2.position_y - detection_1.position_y) ** 2 + distance_squared = (detection_2.location_x - detection_1.location_x) ** 2 + ( + detection_2.location_y - detection_1.location_y + ) ** 2 return distance_squared < distance_squared_threshold def mark_false_positive(self, detection: object_in_world.ObjectInWorld): @@ -37,7 +41,8 @@ def mark_false_positive(self, detection: object_in_world.ObjectInWorld): """ self.__false_positives.append(detection) self.__unconfirmed_positives = [ - landing_pad for landing_pad in self.__unconfirmed_positives + landing_pad + for landing_pad in self.__unconfirmed_positives if not self.__is_similar(landing_pad, detection, self.__distance_squared_threshold) ] diff --git a/modules/decision_command.py b/modules/decision_command.py index ae7aaa68..c7933279 100644 --- a/modules/decision_command.py +++ b/modules/decision_command.py @@ -20,52 +20,50 @@ class DecisionCommand: * Command.create_land_at_absolute_position_command """ + + __create_key = object() + class CommandType(enum.Enum): """ Different types of commands. """ + MOVE_TO_RELATIVE_POSITION = 0 # Move relative to current position MOVE_TO_ABSOLUTE_POSITION = 1 # Move to absolute position within local space - LAND_AT_CURRENT_POSITION = 2 # Stop the drone at current position + LAND_AT_CURRENT_POSITION = 2 # Stop the drone at current position LAND_AT_RELATIVE_POSITION = 3 # Stop the drone at relative position within local space LAND_AT_ABSOLUTE_POSITION = 4 # Stop the drone at absolute position within local space - __create_key = object() - @classmethod - def create_move_to_relative_position_command(cls, - relative_x: float, - relative_y: float, - relative_z: float) -> "DecisionCommand": + def create_move_to_relative_position_command( + cls, relative_x: float, relative_y: float, relative_z: float + ) -> "DecisionCommand": """ Command for drone movement relative to current position, using - the NED coordinate system. (+x, +y, +z) corresponds to the north east and down - directions. + the NED coordinate system. (+x, +y, +z) corresponds to the north, east, and down directions. """ return DecisionCommand( cls.__create_key, DecisionCommand.CommandType.MOVE_TO_RELATIVE_POSITION, relative_x, relative_y, - relative_z + relative_z, ) @classmethod - def create_move_to_absolute_position_command(cls, - absolute_x: float, - absolute_y: float, - absolute_z: float) -> "DecisionCommand": + def create_move_to_absolute_position_command( + cls, absolute_x: float, absolute_y: float, absolute_z: float + ) -> "DecisionCommand": """ Command for drone movement to absolute position within local space, using - the NED coordinate system. (+x, +y, +z) corresponds to the north east and down - directions. + the NED coordinate system. (+x, +y, +z) corresponds to the north, east, and down directions. """ return DecisionCommand( cls.__create_key, DecisionCommand.CommandType.MOVE_TO_ABSOLUTE_POSITION, absolute_x, absolute_y, - absolute_z + absolute_z, ) @classmethod @@ -74,55 +72,51 @@ def create_land_at_current_position_command(cls) -> "DecisionCommand": Command for landing at current position. """ return DecisionCommand( - cls.__create_key, - DecisionCommand.CommandType.LAND_AT_CURRENT_POSITION, - 0.0, - 0.0, - 0.0 + cls.__create_key, DecisionCommand.CommandType.LAND_AT_CURRENT_POSITION, 0.0, 0.0, 0.0 ) @classmethod - def create_land_at_relative_position_command(cls, - relative_x: float, - relative_y: float, - relative_z: float) -> "DecisionCommand": + def create_land_at_relative_position_command( + cls, relative_x: float, relative_y: float, relative_z: float + ) -> "DecisionCommand": """ Command to land the drone at a relative position within local space, using - the NED coordinate system. (+x, +y, +z) corresponds to the north east and down - directions. + the NED coordinate system. (+x, +y, +z) corresponds to the north, east, and down directions. """ return DecisionCommand( cls.__create_key, DecisionCommand.CommandType.LAND_AT_RELATIVE_POSITION, relative_x, relative_y, - relative_z + relative_z, ) @classmethod - def create_land_at_absolute_position_command(cls, - absolute_x: float, - absolute_y: float, - absolute_z: float) -> "DecisionCommand": + def create_land_at_absolute_position_command( + cls, absolute_x: float, absolute_y: float, absolute_z: float + ) -> "DecisionCommand": """ Command to land the drone at an absolute position within local space, using - the NED coordinate system. (+x, +y, +z) corresponds to the north east and down - directions. + the NED coordinate system. (+x, +y, +z) corresponds to the north, east, and down directions. """ return DecisionCommand( cls.__create_key, DecisionCommand.CommandType.LAND_AT_ABSOLUTE_POSITION, absolute_x, absolute_y, - absolute_z + absolute_z, ) - def __init__(self, - class_private_create_key, - command_type: CommandType, - command_x: float, - command_y: float, - command_z: float): + # Create key required + # pylint: disable-next=too-many-arguments + def __init__( + self, + class_private_create_key, + command_type: CommandType, + command_x: float, + command_y: float, + command_z: float, + ): """ Private constructor, use create() method. """ @@ -142,8 +136,7 @@ def get_command_type(self) -> CommandType: def get_command_position(self) -> "tuple[float, float, float]": """ Returns the command position in x, y, z tuple, using - the NED coordinate system. (+x, +y, +z) corresponds to the north east and down - directions. + the NED coordinate system. (+x, +y, +z) corresponds to the north, east, and down directions. """ return self.__command_x, self.__command_y, self.__command_z diff --git a/modules/detect_target/detect_target.py b/modules/detect_target/detect_target.py index e49134e4..6b2676b3 100644 --- a/modules/detect_target/detect_target.py +++ b/modules/detect_target/detect_target.py @@ -11,19 +11,22 @@ # This is just an interface -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class DetectTarget: """ Contains the YOLOv8 model for prediction. """ + # Required for logging # pylint: disable-next=too-many-arguments - def __init__(self, - device: "str | int", - model_path: str, - override_full: bool, - show_annotations: bool = False, - save_name: str = ""): + def __init__( + self, + device: "str | int", + model_path: str, + override_full: bool, + show_annotations: bool = False, + save_name: str = "", + ): """ device: name of target device to run inference on (i.e. "cpu" or cuda device 0, 1, 2, 3). model_path: path to the YOLOv8 model. @@ -34,7 +37,7 @@ def __init__(self, self.__device = device self.__model = ultralytics.YOLO(model_path) self.__counter = 0 - self.__enable_half_precision = False if self.__device == "cpu" else True + self.__enable_half_precision = not self.__device == "cpu" self.__show_annotations = show_annotations if override_full: self.__enable_half_precision = False @@ -44,9 +47,9 @@ def __init__(self, # Required for logging # pylint: disable-next=too-many-locals - def run(self, - data: image_and_time.ImageAndTime) \ - -> "tuple[bool, detections_and_time.DetectionsAndTime | None]": + def run( + self, data: image_and_time.ImageAndTime + ) -> "tuple[bool, detections_and_time.DetectionsAndTime | None]": """ Runs object detection on the provided image and returns the detections. @@ -100,13 +103,11 @@ def run(self, file.write(repr(detections)) # Annotated image - cv2.imwrite(filename + ".png", image_annotated) + cv2.imwrite(filename + ".png", image_annotated) # type: ignore self.__counter += 1 if self.__show_annotations: - cv2.imshow("Annotated", image_annotated) + cv2.imshow("Annotated", image_annotated) # type: ignore return True, detections - -# pylint: enable=too-few-public-methods diff --git a/modules/detect_target/detect_target_worker.py b/modules/detect_target/detect_target_worker.py index a53d115d..9add47a1 100644 --- a/modules/detect_target/detect_target_worker.py +++ b/modules/detect_target/detect_target_worker.py @@ -7,16 +7,18 @@ from . import detect_target -# Extra parameters required for worker communication -# pylint: disable=too-many-arguments -def detect_target_worker(device: "str | int", - model_path: str, - override_full: bool, - show_annotations: bool, - save_name: str, - input_queue: queue_proxy_wrapper.QueueProxyWrapper, - output_queue: queue_proxy_wrapper.QueueProxyWrapper, - controller: worker_controller.WorkerController): +# Worker has both class and control parameters +# pylint: disable-next=too-many-arguments +def detect_target_worker( + device: "str | int", + model_path: str, + override_full: bool, + show_annotations: bool, + save_name: str, + input_queue: queue_proxy_wrapper.QueueProxyWrapper, + output_queue: queue_proxy_wrapper.QueueProxyWrapper, + controller: worker_controller.WorkerController, +): """ Worker process. @@ -44,5 +46,3 @@ def detect_target_worker(device: "str | int", continue output_queue.queue.put(value) - -# pylint: enable=too-many-arguments diff --git a/modules/detection_in_world.py b/modules/detection_in_world.py index 55380a59..2aca6aca 100644 --- a/modules/detection_in_world.py +++ b/modules/detection_in_world.py @@ -6,19 +6,18 @@ # Basically a struct -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class DetectionInWorld: """ Typically on the ground. """ + __create_key = object() @classmethod - def create(cls, - vertices: np.ndarray, - centre: np.ndarray, - label: int, - confidence: float) -> "tuple[bool, DetectionInWorld | None]": + def create( + cls, vertices: np.ndarray, centre: np.ndarray, label: int, confidence: float + ) -> "tuple[bool, DetectionInWorld | None]": """ vertices is a quadrilateral of 4 points. centre is a point. @@ -38,12 +37,16 @@ def create(cls, return True, DetectionInWorld(cls.__create_key, vertices, centre, label, confidence) - def __init__(self, - class_private_create_key, - vertices: np.ndarray, - centre: np.ndarray, - label: int, - confidence: float): + # Create key required + # pylint: disable-next=too-many-arguments + def __init__( + self, + class_private_create_key, + vertices: np.ndarray, + centre: np.ndarray, + label: int, + confidence: float, + ): """ Private constructor, use create() method. """ @@ -53,5 +56,3 @@ def __init__(self, self.centre = centre self.label = label self.confidence = confidence - -# pylint: enable=too-few-public-methods diff --git a/modules/detections_and_time.py b/modules/detections_and_time.py index b840b078..48491436 100644 --- a/modules/detections_and_time.py +++ b/modules/detections_and_time.py @@ -6,18 +6,18 @@ # Basically a struct -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class Detection: """ A detected object in image space. """ + __create_key = object() @classmethod - def create(cls, - bounds: np.ndarray, - label: int, - confidence: float) -> "tuple[bool, Detection | None]": + def create( + cls, bounds: np.ndarray, label: int, confidence: float + ) -> "tuple[bool, Detection | None]": """ bounds are of form x1, y1, x2, y2 . """ @@ -54,14 +54,20 @@ def __init__(self, class_private_create_key, bounds: np.ndarray, label: int, con self.confidence = confidence def __repr__(self) -> str: - representation = \ - "cls: " + str(self.label) \ - + ", conf: " + str(self.confidence) \ - + ", bounds: " \ - + str(self.x1) + " " \ - + str(self.y1) + " " \ - + str(self.x2) + " " \ - + str(self.y2) + representation = ( + "cls: " + + str(self.label) + + ", conf: " + + str(self.confidence) + + ", bounds: " + + str(self.x1) + + " " + + str(self.y1) + + " " + + str(self.x2) + + " " + + str(self.y2) + ) return representation @@ -83,15 +89,14 @@ def get_corners(self) -> "list[tuple[float, float]]": bottom_right = self.x2, self.y2 return [top_left, top_right, bottom_left, bottom_right] -# pylint: enable=too-few-public-methods - # Basically a struct -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class DetectionsAndTime: """ Contains detected object and timestamp. """ + __create_key = object() @classmethod @@ -115,10 +120,13 @@ def __init__(self, class_private_create_key, timestamp: float): self.timestamp = timestamp def __repr__(self) -> str: - representation = \ - str(self.__class__) \ - + ", time: " + str(int(self.timestamp)) \ - + ", size: " + str(len(self)) + representation = ( + str(self.__class__) + + ", time: " + + str(int(self.timestamp)) + + ", size: " + + str(len(self)) + ) representation += "\n" + repr(self.detections) return representation @@ -134,5 +142,3 @@ def append(self, detection: Detection): Appends a detected object. """ self.detections.append(detection) - -# pylint: enable=too-few-public-methods diff --git a/modules/drone_odometry_local.py b/modules/drone_odometry_local.py index 167bd697..81be5519 100644 --- a/modules/drone_odometry_local.py +++ b/modules/drone_odometry_local.py @@ -6,28 +6,24 @@ # Basically a struct -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class DronePositionLocal: """ Drone position in NED system. """ + __create_key = object() @classmethod - def create(cls, - north: float, - east: float, - down: float) -> "tuple[bool, DronePositionLocal | None]": + def create( + cls, north: float, east: float, down: float + ) -> "tuple[bool, DronePositionLocal | None]": """ north, east, down in metres. """ return True, DronePositionLocal(cls.__create_key, north, east, down) - def __init__(self, - class_private_create_key, - north: float, - east: float, - down: float): + def __init__(self, class_private_create_key, north: float, east: float, down: float): """ Private constructor, use create() method. """ @@ -37,22 +33,20 @@ def __init__(self, self.east = east self.down = down -# pylint: enable=too-few-public-methods - # Basically a struct -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class DroneOrientationLocal: """ Wrapper for DroneOrientation as it is the same in both local and global space. """ + __create_key = object() @classmethod - def create_new(cls, - yaw: float, - pitch: float, - roll: float) -> "tuple[bool, DroneOrientationLocal | None]": + def create_new( + cls, yaw: float, pitch: float, roll: float + ) -> "tuple[bool, DroneOrientationLocal | None]": """ yaw, pitch, roll in radians. """ @@ -66,8 +60,7 @@ def create_new(cls, return True, DroneOrientationLocal(cls.__create_key, orientation) @classmethod - def create_wrap(cls, - orientation: drone_odometry.DroneOrientation): + def create_wrap(cls, orientation: drone_odometry.DroneOrientation): """ Wrap existing orientation. """ @@ -81,21 +74,20 @@ def __init__(self, class_private_create_key, orientation: drone_odometry.DroneOr self.orientation = orientation -# pylint: enable=too-few-public-methods - # Basically a struct -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class DroneOdometryLocal: """ Wrapper for DronePositionLocal and DroneOrientationLocal. """ + __create_key = object() @classmethod - def create(cls, - position: DronePositionLocal, - orientation: DroneOrientationLocal) -> "tuple[bool, DroneOdometryLocal | None]": + def create( + cls, position: DronePositionLocal, orientation: DroneOrientationLocal + ) -> "tuple[bool, DroneOdometryLocal | None]": """ Position and orientation in one class. """ @@ -107,10 +99,12 @@ def create(cls, return True, DroneOdometryLocal(cls.__create_key, position, orientation) - def __init__(self, - class_private_create_key, - position: DronePositionLocal, - orientation: DroneOrientationLocal): + def __init__( + self, + class_private_create_key, + position: DronePositionLocal, + orientation: DroneOrientationLocal, + ): """ Private constructor, use create() method. """ diff --git a/modules/flight_interface/flight_interface.py b/modules/flight_interface/flight_interface.py index 47a594f8..f3eaf0a5 100644 --- a/modules/flight_interface/flight_interface.py +++ b/modules/flight_interface/flight_interface.py @@ -9,17 +9,18 @@ # This is just an interface -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class FlightInterface: """ Create flight controller and combines odometry data and timestamp. """ + __create_key = object() @classmethod def create(cls, address: str, timeout_home: float) -> "tuple[bool, FlightInterface | None]": """ - address: TCP or port. + address: TCP address or port. timeout_home: Timeout for home location in seconds. """ result, controller = flight_controller.FlightController.create(address) @@ -38,10 +39,12 @@ def create(cls, address: str, timeout_home: float) -> "tuple[bool, FlightInterfa return True, FlightInterface(cls.__create_key, controller, home_location) - def __init__(self, - class_private_create_key, - controller: flight_controller.FlightController, - home_location: drone_odometry.DronePosition): + def __init__( + self, + class_private_create_key, + controller: flight_controller.FlightController, + home_location: drone_odometry.DronePosition, + ): """ Private constructor, use create() method. """ @@ -50,7 +53,6 @@ def __init__(self, self.controller = controller self.__home_location = home_location - def run(self) -> "tuple[bool, odometry_and_time.OdometryAndTime | None]": """ Returns a possible OdometryAndTime with current timestamp. @@ -73,5 +75,3 @@ def run(self) -> "tuple[bool, odometry_and_time.OdometryAndTime | None]": assert odometry_local is not None return odometry_and_time.OdometryAndTime.create(odometry_local) - -# pylint: enable=too-few-public-methods diff --git a/modules/flight_interface/flight_interface_worker.py b/modules/flight_interface/flight_interface_worker.py index 02fe004a..9de90ead 100644 --- a/modules/flight_interface/flight_interface_worker.py +++ b/modules/flight_interface/flight_interface_worker.py @@ -8,13 +8,15 @@ from . import flight_interface -def flight_interface_worker(address: str, - timeout: float, - period: float, - output_queue: queue_proxy_wrapper.QueueProxyWrapper, - controller: worker_controller.WorkerController): +def flight_interface_worker( + address: str, + timeout: float, + period: float, + output_queue: queue_proxy_wrapper.QueueProxyWrapper, + controller: worker_controller.WorkerController, +): """ - Worker process. + Worker process. address, timeout is initial setting. period is minimum period between loops. @@ -26,7 +28,7 @@ def flight_interface_worker(address: str, result, interface = flight_interface.FlightInterface.create(address, timeout) if not result: - print("ERROR: flight_interface_worker could not create interface") + print("ERROR: Worker failed to create class object") return # Get Pylance to stop complaining diff --git a/modules/flight_interface/local_global_conversion.py b/modules/flight_interface/local_global_conversion.py index ed795da0..1797fa27 100644 --- a/modules/flight_interface/local_global_conversion.py +++ b/modules/flight_interface/local_global_conversion.py @@ -8,10 +8,10 @@ from ..common.mavlink.modules import drone_odometry -def drone_position_global_from_local(home_location: drone_odometry.DronePosition, - drone_position_local: - drone_odometry_local.DronePositionLocal) \ - -> "tuple[bool, drone_odometry.DronePosition | None]": +def drone_position_global_from_local( + home_location: drone_odometry.DronePosition, + drone_position_local: drone_odometry_local.DronePositionLocal, +) -> "tuple[bool, drone_odometry.DronePosition | None]": """ Local coordinates to global coordinates. Return: Drone position in WGS 84. @@ -38,9 +38,10 @@ def drone_position_global_from_local(home_location: drone_odometry.DronePosition return True, drone_position -def __drone_position_local_from_global(home_location: drone_odometry.DronePosition, - drone_position: drone_odometry.DronePosition) \ - -> "tuple[bool, drone_odometry_local.DronePositionLocal | None]": + +def __drone_position_local_from_global( + home_location: drone_odometry.DronePosition, drone_position: drone_odometry.DronePosition +) -> "tuple[bool, drone_odometry_local.DronePositionLocal | None]": """ Global coordinates to local coordinates. Return: Drone position relative to home location (NED system). @@ -67,9 +68,10 @@ def __drone_position_local_from_global(home_location: drone_odometry.DronePositi return True, drone_position_local -def drone_odometry_local_from_global(odometry: drone_odometry.DroneOdometry, - home_location: drone_odometry.DronePosition) \ - -> "tuple[bool, drone_odometry_local.DroneOdometryLocal | None]": + +def drone_odometry_local_from_global( + odometry: drone_odometry.DroneOdometry, home_location: drone_odometry.DronePosition +) -> "tuple[bool, drone_odometry_local.DroneOdometryLocal | None]": """ Converts global odometry to local. """ diff --git a/modules/geolocation/camera_properties.py b/modules/geolocation/camera_properties.py index 2f774e60..4aa5011e 100644 --- a/modules/geolocation/camera_properties.py +++ b/modules/geolocation/camera_properties.py @@ -19,9 +19,9 @@ def is_matrix_r3x3(matrix: np.ndarray) -> bool: return matrix.shape == (3, 3) -def create_rotation_matrix_from_orientation(yaw: float, - pitch: float, - roll: float) -> "tuple[bool, np.ndarray | None]": +def create_rotation_matrix_from_orientation( + yaw: float, pitch: float, roll: float +) -> "tuple[bool, np.ndarray | None]": """ Creates a rotation matrix from yaw pitch roll in NED system (x forward, y right, z down). Specifically, intrinsic (Tait-Bryan) rotations in the zyx/3-2-1 order. @@ -41,16 +41,16 @@ def create_rotation_matrix_from_orientation(yaw: float, yaw_matrix = np.array( [ [np.cos(yaw), -np.sin(yaw), 0.0], - [np.sin(yaw), np.cos(yaw), 0.0], - [ 0.0, 0.0, 1.0], + [np.sin(yaw), np.cos(yaw), 0.0], + [0.0, 0.0, 1.0], ], dtype=np.float32, ) pitch_matrix = np.array( [ - [ np.cos(pitch), 0.0, np.sin(pitch)], - [ 0.0, 1.0, 0.0], + [np.cos(pitch), 0.0, np.sin(pitch)], + [0.0, 1.0, 0.0], [-np.sin(pitch), 0.0, np.cos(pitch)], ], dtype=np.float32, @@ -58,9 +58,9 @@ def create_rotation_matrix_from_orientation(yaw: float, roll_matrix = np.array( [ - [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], [0.0, np.cos(roll), -np.sin(roll)], - [0.0, np.sin(roll), np.cos(roll)], + [0.0, np.sin(roll), np.cos(roll)], ], dtype=np.float32, ) @@ -77,14 +77,15 @@ class CameraIntrinsics: Image follows xy system with top-left as origin, pixel coordinates refer to their top-left corner. """ + __create_key = object() @classmethod - def create(cls, - resolution_x: int, - resolution_y: int, - fov_x: float, - fov_y: float) -> "tuple[bool, CameraIntrinsics | None]": + # Required for checks + # pylint: disable-next=too-many-return-statements + def create( + cls, resolution_x: int, resolution_y: int, fov_x: float, fov_y: float + ) -> "tuple[bool, CameraIntrinsics | None]": """ Resolution is in pixels. Field of view in radians horizontally and vertically across the image (edge to edge). @@ -117,28 +118,32 @@ def create(cls, v_scalar, ) - def __init__(self, - class_private_create_key, - resolution_x: int, - resolution_y: int, - u_scalar: float, - v_scalar: float): + # Create key required + # pylint: disable-next=too-many-arguments + def __init__( + self, + class_private_create_key, + resolution_x: int, + resolution_y: int, + u_scalar: float, + v_scalar: float, + ): """ - Private constructor, use create() method + Private constructor, use create() method. """ assert class_private_create_key is CameraIntrinsics.__create_key, "Use create() method" self.resolution_x = resolution_x self.resolution_y = resolution_y - self.__vec_c = np.array([1.0, 0.0, 0.0], dtype=np.float32) - self.__vec_u = np.array([0.0, u_scalar, 0.0], dtype=np.float32) - self.__vec_v = np.array([0.0, 0.0, v_scalar], dtype=np.float32) + self.__vec_c = np.array([1.0, 0.0, 0.0], dtype=np.float32) + self.__vec_u = np.array([0.0, u_scalar, 0.0], dtype=np.float32) + self.__vec_v = np.array([0.0, 0.0, v_scalar], dtype=np.float32) @staticmethod - def __pixel_vector_from_image_space(pixel: int, - resolution: int, - vec_base: np.ndarray) -> "tuple[bool, np.ndarray | None]": + def __pixel_vector_from_image_space( + pixel: int, resolution: int, vec_base: np.ndarray + ) -> "tuple[bool, np.ndarray | None]": """ Get u or v vector from pixel coordinate. """ @@ -162,9 +167,9 @@ def __pixel_vector_from_image_space(pixel: int, return True, vec_pixel - def camera_space_from_image_space(self, - pixel_x: int, - pixel_y: int) -> "tuple[bool, np.ndarray | None]": + def camera_space_from_image_space( + self, pixel_x: int, pixel_y: int + ) -> "tuple[bool, np.ndarray | None]": """ Pixel in image space to vector in camera space. """ @@ -199,17 +204,21 @@ def camera_space_from_image_space(self, return True, vec_camera +# Basically a struct +# pylint: disable-next=too-few-public-methods class CameraDroneExtrinsics: """ Camera in relation to drone. """ + __create_key = object() @classmethod - def create(cls, - camera_position_xyz: "tuple[float, float, float]", - camera_orientation_ypr: "tuple[float, float, float]") \ - -> "tuple[bool, CameraDroneExtrinsics | None]": + def create( + cls, + camera_position_xyz: "tuple[float, float, float]", + camera_orientation_ypr: "tuple[float, float, float]", + ) -> "tuple[bool, CameraDroneExtrinsics | None]": """ camera_position_xyz: Camera position is x, y, z. camera_orientation_ypr: Camera orientation is yaw, pitch, roll. @@ -223,12 +232,11 @@ def create(cls, vec_camera_on_drone_position = np.array([camera_x, camera_y, camera_z], dtype=np.float32) - result, camera_to_drone_rotation_matrix = \ - create_rotation_matrix_from_orientation( - camera_yaw, - camera_pitch, - camera_roll, - ) + result, camera_to_drone_rotation_matrix = create_rotation_matrix_from_orientation( + camera_yaw, + camera_pitch, + camera_roll, + ) if not result: return False, None @@ -244,12 +252,14 @@ def create(cls, camera_to_drone_rotation_matrix, ) - def __init__(self, - class_private_create_key, - vec_camera_on_drone_position: np.ndarray, - camera_to_drone_rotation_matrix: np.ndarray): + def __init__( + self, + class_private_create_key, + vec_camera_on_drone_position: np.ndarray, + camera_to_drone_rotation_matrix: np.ndarray, + ): """ - Private constructor, use create() method + Private constructor, use create() method. """ assert class_private_create_key is CameraDroneExtrinsics.__create_key, "Use create() method" diff --git a/modules/geolocation/geolocation.py b/modules/geolocation/geolocation.py index d00d0490..0aa162b6 100644 --- a/modules/geolocation/geolocation.py +++ b/modules/geolocation/geolocation.py @@ -15,15 +15,17 @@ class Geolocation: """ Converts image space into world space. """ + __create_key = object() - __MIN_DOWN_COS_ANGLE = 0.1 # ~84° + __MIN_DOWN_COS_ANGLE = 0.1 # radians, ~84° @classmethod - def create(cls, - camera_intrinsics: camera_properties.CameraIntrinsics, - camera_drone_extrinsics: camera_properties.CameraDroneExtrinsics) \ - -> "tuple[bool, Geolocation | None]": + def create( + cls, + camera_intrinsics: camera_properties.CameraIntrinsics, + camera_drone_extrinsics: camera_properties.CameraDroneExtrinsics, + ) -> "tuple[bool, Geolocation | None]": """ camera_intrinsics: Camera information without any outside space. camera_drone_extrinsics: Camera information related to the drone without any world space. @@ -59,13 +61,15 @@ def create(cls, rotated_source_vectors, ) - def __init__(self, - class_private_create_key, - camera_drone_extrinsics: camera_properties.CameraDroneExtrinsics, - perspective_transform_sources: "list[list[float]]", - rotated_source_vectors: "list[np.ndarray]"): + def __init__( + self, + class_private_create_key, + camera_drone_extrinsics: camera_properties.CameraDroneExtrinsics, + perspective_transform_sources: "list[list[float]]", + rotated_source_vectors: "list[np.ndarray]", + ): """ - Private constructor, use create() method + Private constructor, use create() method. """ assert class_private_create_key is Geolocation.__create_key, "Use create() method" @@ -74,9 +78,9 @@ def __init__(self, self.__rotated_source_vectors = rotated_source_vectors @staticmethod - def __ground_intersection_from_vector(vec_camera_in_world_position: np.ndarray, - vec_down: np.ndarray) \ - -> "tuple[bool, np.ndarray | None]": + def __ground_intersection_from_vector( + vec_camera_in_world_position: np.ndarray, vec_down: np.ndarray + ) -> "tuple[bool, np.ndarray | None]": """ Get 2D coordinates of where the downwards pointing vector intersects the ground. """ @@ -107,10 +111,9 @@ def __ground_intersection_from_vector(vec_camera_in_world_position: np.ndarray, return True, vec_ground[:2] - def __get_perspective_transform_matrix(self, - drone_rotation_matrix: np.ndarray, - drone_position_ned: np.ndarray) \ - -> "tuple[bool, np.ndarray | None]": + def __get_perspective_transform_matrix( + self, drone_rotation_matrix: np.ndarray, drone_position_ned: np.ndarray + ) -> "tuple[bool, np.ndarray | None]": """ Calculates the destination points, then uses OpenCV to get the matrix. """ @@ -127,9 +130,10 @@ def __get_perspective_transform_matrix(self, vec_downs.append(vec_down) # Get the camera position in world space - vec_camera_position = \ - drone_position_ned \ + vec_camera_position = ( + drone_position_ned + drone_rotation_matrix @ self.__camera_drone_extrinsics.vec_camera_on_drone_position + ) # Find the points on the ground ground_points = [] @@ -155,19 +159,18 @@ def __get_perspective_transform_matrix(self, ) # pylint: enable=no-member # All exceptions must be caught and logged as early as possible - # pylint: disable=bare-except + # pylint: disable-next=bare-except except: # TODO: Logging return False, None - # pylint: enable=bare-except return True, matrix @staticmethod # pylint: disable-next=too-many-locals - def __convert_detection_to_world_from_image(detection: detections_and_time.Detection, - perspective_transform_matrix: np.ndarray) \ - -> "tuple[bool, detection_in_world.DetectionInWorld | None]": + def __convert_detection_to_world_from_image( + detection: detections_and_time.Detection, perspective_transform_matrix: np.ndarray + ) -> "tuple[bool, detection_in_world.DetectionInWorld | None]": """ Applies the transform matrix to the detection. perspective_transform_matrix: Element in last row and column must be 1 . @@ -235,9 +238,9 @@ def __convert_detection_to_world_from_image(detection: detections_and_time.Detec return True, detection_world - def run(self, - detections: merged_odometry_detections.MergedOdometryDetections) \ - -> "tuple[bool, list[detection_in_world.DetectionInWorld] | None]": + def run( + self, detections: merged_odometry_detections.MergedOdometryDetections + ) -> "tuple[bool, list[detection_in_world.DetectionInWorld] | None]": """ Returns detections in world space. """ diff --git a/modules/geolocation/geolocation_worker.py b/modules/geolocation/geolocation_worker.py index 4163657b..97811a94 100644 --- a/modules/geolocation/geolocation_worker.py +++ b/modules/geolocation/geolocation_worker.py @@ -8,13 +8,15 @@ from . import geolocation -# Extra parameters required for worker communication -# pylint: disable=too-many-arguments -def geolocation_worker(camera_intrinsics: camera_properties.CameraIntrinsics, - camera_drone_extrinsics: camera_properties.CameraDroneExtrinsics, - input_queue: queue_proxy_wrapper.QueueProxyWrapper, - output_queue: queue_proxy_wrapper.QueueProxyWrapper, - controller: worker_controller.WorkerController): +# Worker has both class and control parameters +# pylint: disable-next=too-many-arguments +def geolocation_worker( + camera_intrinsics: camera_properties.CameraIntrinsics, + camera_drone_extrinsics: camera_properties.CameraDroneExtrinsics, + input_queue: queue_proxy_wrapper.QueueProxyWrapper, + output_queue: queue_proxy_wrapper.QueueProxyWrapper, + controller: worker_controller.WorkerController, +): """ Worker process. @@ -29,6 +31,7 @@ def geolocation_worker(camera_intrinsics: camera_properties.CameraIntrinsics, camera_drone_extrinsics, ) if not result: + print("Worker failed to create class object") return # Get Pylance to stop complaining @@ -46,5 +49,3 @@ def geolocation_worker(camera_intrinsics: camera_properties.CameraIntrinsics, continue output_queue.queue.put(value) - -# pylint: enable=too-many-arguments diff --git a/modules/image_and_time.py b/modules/image_and_time.py index 79d7c4b8..76cc4f10 100644 --- a/modules/image_and_time.py +++ b/modules/image_and_time.py @@ -7,11 +7,12 @@ # Basically a struct -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class ImageAndTime: """ Contains image and timestamp. """ + __create_key = object() @classmethod @@ -25,16 +26,15 @@ def create(cls, image: np.ndarray) -> "tuple[bool, ImageAndTime | None]": if image.shape[2] != 3: return False, None - return True, ImageAndTime(cls.__create_key, image) + current_time = time.time() + + return True, ImageAndTime(cls.__create_key, image, current_time) - def __init__(self, class_private_create_key, image: np.ndarray): + def __init__(self, class_private_create_key, image: np.ndarray, timestamp: float): """ Private constructor, use create() method. - Constructor sets timestamp to current time. """ assert class_private_create_key is ImageAndTime.__create_key, "Use create() method" self.image = image - self.timestamp = time.time() - -# pylint: enable=too-few-public-methods + self.timestamp = timestamp diff --git a/modules/merged_odometry_detections.py b/modules/merged_odometry_detections.py index 323d83c3..8cf9af7c 100644 --- a/modules/merged_odometry_detections.py +++ b/modules/merged_odometry_detections.py @@ -7,18 +7,20 @@ # Basically a struct -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class MergedOdometryDetections: """ Contains odometry/telemetry and detections merged by closest timestamp. """ + __create_key = object() @classmethod - def create(cls, - odometry_local: drone_odometry_local.DroneOdometryLocal, - detections: "list[detections_and_time.Detection]") \ - -> "tuple[bool, MergedOdometryDetections | None]": + def create( + cls, + odometry_local: drone_odometry_local.DroneOdometryLocal, + detections: "list[detections_and_time.Detection]", + ) -> "tuple[bool, MergedOdometryDetections | None]": """ odometry_local: Drone position and orientation in local space. detections: List of Detections from detections_and_time. @@ -28,17 +30,18 @@ def create(cls, return True, MergedOdometryDetections(cls.__create_key, odometry_local, detections) - def __init__(self, - class_private_create_key, - odometry_local: drone_odometry_local.DroneOdometryLocal, - detections: "list[detections_and_time.Detection]"): + def __init__( + self, + class_private_create_key, + odometry_local: drone_odometry_local.DroneOdometryLocal, + detections: "list[detections_and_time.Detection]", + ): """ Private constructor, use create() method. """ - assert class_private_create_key is MergedOdometryDetections.__create_key, \ - "Use create() method" + assert ( + class_private_create_key is MergedOdometryDetections.__create_key + ), "Use create() method" self.odometry_local = odometry_local self.detections = detections - -# pylint: enable=too-few-public-methods diff --git a/modules/message_and_time.py b/modules/message_and_time.py deleted file mode 100644 index ab3e28ed..00000000 --- a/modules/message_and_time.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -ZP message and timestamp. -""" -import time - - -# Basically a struct -# pylint: disable=too-few-public-methods -class MessageAndTime: - """ - Contains ZP message and timestamp. - """ - def __init__(self, message): - """ - Constructor sets timestamp to current time. - message: 1 of TelemMessages. No type annotation due to several possible types. - """ - self.message = message - self.timestamp = time.time() - -# pylint: enable=too-few-public-methods diff --git a/modules/object_in_world.py b/modules/object_in_world.py index cd0230f6..dc1561db 100644 --- a/modules/object_in_world.py +++ b/modules/object_in_world.py @@ -1,41 +1,42 @@ """ -Position of the object in world space +Location of the object in world space. """ # Basically a struct -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class ObjectInWorld: """ - Contains the estimated position of the object. + Contains the estimated location of the object in local coordinates. """ + __create_key = object() @classmethod - def create(cls, - position_x: float, - position_y: float, - spherical_variance: float) -> "tuple[bool, ObjectInWorld | None]": + def create( + cls, location_x: float, location_y: float, spherical_variance: float + ) -> "tuple[bool, ObjectInWorld | None]": """ - Position in local coordinates. + location_x, location_y: Location of the object. + spherical_variance: Uncertainty of the location. """ if spherical_variance < 0.0: return False, None - return True, ObjectInWorld(cls.__create_key, position_x, position_y, spherical_variance) + return True, ObjectInWorld(cls.__create_key, location_x, location_y, spherical_variance) - def __init__(self, - class_private_create_key, - position_x: float, - position_y: float, - spherical_variance: float): + def __init__( + self, + class_private_create_key, + location_x: float, + location_y: float, + spherical_variance: float, + ): """ Private constructor, use create() method. """ assert class_private_create_key is ObjectInWorld.__create_key, "Use create() method" - self.position_x = position_x - self.position_y = position_y + self.location_x = location_x + self.location_y = location_y self.spherical_variance = spherical_variance - -# pylint: enable=too-few-public-methods diff --git a/modules/odometry_and_time.py b/modules/odometry_and_time.py index 56568603..bf22e853 100644 --- a/modules/odometry_and_time.py +++ b/modules/odometry_and_time.py @@ -7,16 +7,18 @@ # Basically a struct -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class OdometryAndTime: """ Contains odometry/telemetry and timestamp. """ + __create_key = object() @classmethod - def create(cls, odometry_data: drone_odometry_local.DroneOdometryLocal) \ - -> "tuple[bool, OdometryAndTime | None]": + def create( + cls, odometry_data: drone_odometry_local.DroneOdometryLocal + ) -> "tuple[bool, OdometryAndTime | None]": """ Timestamps the odometry with the current time. odometry_data: Drone odometry data. @@ -28,10 +30,12 @@ def create(cls, odometry_data: drone_odometry_local.DroneOdometryLocal) \ return True, OdometryAndTime(cls.__create_key, odometry_data, timestamp) - def __init__(self, - class_private_create_key, - odometry_data: drone_odometry_local.DroneOdometryLocal, - timestamp: float): + def __init__( + self, + class_private_create_key, + odometry_data: drone_odometry_local.DroneOdometryLocal, + timestamp: float, + ): """ Private constructor, use create() method. """ @@ -39,5 +43,3 @@ def __init__(self, self.odometry_data = odometry_data self.timestamp = timestamp - -# pylint: enable=too-few-public-methods diff --git a/modules/video_input/video_input.py b/modules/video_input/video_input.py index 25e3020b..1e3cc15a 100644 --- a/modules/video_input/video_input.py +++ b/modules/video_input/video_input.py @@ -7,11 +7,12 @@ # This is just an interface -# pylint: disable=too-few-public-methods +# pylint: disable-next=too-few-public-methods class VideoInput: """ Combines image and timestamp together. """ + def __init__(self, camera_name: "int | str", save_name: str = ""): self.device = camera_device.CameraDevice(camera_name, 1, save_name) @@ -24,5 +25,3 @@ def run(self) -> "tuple[bool, image_and_time.ImageAndTime | None]": return False, None return image_and_time.ImageAndTime.create(image) - -# pylint: enable=too-few-public-methods diff --git a/modules/video_input/video_input_worker.py b/modules/video_input/video_input_worker.py index 1282114b..f76e2f92 100644 --- a/modules/video_input/video_input_worker.py +++ b/modules/video_input/video_input_worker.py @@ -8,11 +8,13 @@ from . import video_input -def video_input_worker(camera_name: "int | str", - period: float, - save_name: str, - output_queue: queue_proxy_wrapper.QueueProxyWrapper, - controller: worker_controller.WorkerController): +def video_input_worker( + camera_name: "int | str", + period: float, + save_name: str, + output_queue: queue_proxy_wrapper.QueueProxyWrapper, + controller: worker_controller.WorkerController, +): """ Worker process. diff --git a/requirements.txt b/requirements.txt index 58fca348..43012613 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ # Packages listed in alphabetical order +black numpy opencv-python +pylint pymap3d pytest pyyaml diff --git a/tests/model_example/generate_expected.py b/tests/model_example/generate_expected.py index cd869864..26182be2 100644 --- a/tests/model_example/generate_expected.py +++ b/tests/model_example/generate_expected.py @@ -24,20 +24,20 @@ if __name__ == "__main__": model = ultralytics.YOLO(MODEL_PATH) - image_bus = cv2.imread(BUS_IMAGE_PATH) - image_zidane = cv2.imread(ZIDANE_IMAGE_PATH) + image_bus = cv2.imread(BUS_IMAGE_PATH) # type: ignore + image_zidane = cv2.imread(ZIDANE_IMAGE_PATH) # type: ignore # Ultralytics saves as .jpg , bad for testing reproducibility results_bus = model.predict( - source=image_bus, - half=True, - stream=False, + source=image_bus, + half=True, + stream=False, ) results_zidane = model.predict( - source=image_zidane, - half=True, - stream=False, + source=image_zidane, + half=True, + stream=False, ) # Generate image @@ -45,8 +45,8 @@ image_zidane_annotated = results_zidane[0].plot(conf=True) # Save image - cv2.imwrite(BUS_IMAGE_ANNOTATED_PATH, image_bus_annotated) - cv2.imwrite(ZIDANE_IMAGE_ANNOTATED_PATH, image_zidane_annotated) + cv2.imwrite(BUS_IMAGE_ANNOTATED_PATH, image_bus_annotated) # type: ignore + cv2.imwrite(ZIDANE_IMAGE_ANNOTATED_PATH, image_zidane_annotated) # type: ignore # Generate expected bounding_box_bus = results_bus[0].boxes.xyxy.detach().cpu().numpy() diff --git a/tests/test_camera_properties.py b/tests/test_camera_properties.py index c31f400c..a25dabd0 100644 --- a/tests/test_camera_properties.py +++ b/tests/test_camera_properties.py @@ -1,3 +1,6 @@ +# Large test file +# No enable +# pylint: disable=too-many-lines """ Test camera intrinsics and extrinsics. """ @@ -8,8 +11,16 @@ from modules.geolocation import camera_properties +# Test functions use test fixture signature names and access class privates +# No enable +# pylint: disable=protected-access,redefined-outer-name + + @pytest.fixture def camera_intrinsic(): + """ + Intrinsic camera properties. + """ resolution_x = 2000 resolution_y = 2000 fov_x = np.pi / 2 @@ -31,6 +42,7 @@ class TestVectorR3Check: """ Test 3D vector check. """ + def test_r3(self): """ R^3 . @@ -88,6 +100,7 @@ class TestMatrixR3x3Check: """ Test 3x3 matrix check. """ + def test_r3x3(self): """ R^{3x3} . @@ -145,6 +158,7 @@ class TestRotationMatrix: """ Test rotation matrix. """ + def test_no_rotation(self): """ Identity. @@ -180,10 +194,10 @@ def test_yaw_quarter(self): expected = np.array( [ [0.0, -1.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 0.0, 1.0], + [1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], ], - dtype=np.float32 + dtype=np.float32, ) # Run @@ -209,11 +223,11 @@ def test_pitch_quarter(self): expected = np.array( [ - [ 0.0, 0.0, 1.0], - [ 0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + [0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], ], - dtype=np.float32 + dtype=np.float32, ) # Run @@ -239,11 +253,11 @@ def test_roll_quarter(self): expected = np.array( [ - [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], - [0.0, 1.0, 0.0], + [0.0, 1.0, 0.0], ], - dtype=np.float32 + dtype=np.float32, ) # Run @@ -269,11 +283,11 @@ def test_combined_rotations_positive(self): expected = np.array( [ - [ 1 / 2, (np.sqrt(2) - 2) / 4, (np.sqrt(2) + 2) / 4], - [ 1 / 2, (np.sqrt(2) + 2) / 4, (np.sqrt(2) - 2) / 4], - [-np.sqrt(2) / 2, 1 / 2, 1 / 2], + [1 / 2, (np.sqrt(2) - 2) / 4, (np.sqrt(2) + 2) / 4], + [1 / 2, (np.sqrt(2) + 2) / 4, (np.sqrt(2) - 2) / 4], + [-np.sqrt(2) / 2, 1 / 2, 1 / 2], ], - dtype=np.float32 + dtype=np.float32, ) # Run @@ -299,11 +313,11 @@ def test_combined_rotations_negative(self): expected = np.array( [ - [ 1 / 2, (np.sqrt(2) + 2) / 4, (-np.sqrt(2) + 2) / 4], - [ -1 / 2, (-np.sqrt(2) + 2) / 4, (np.sqrt(2) + 2) / 4], - [np.sqrt(2) / 2, -1 / 2, 1 / 2], + [1 / 2, (np.sqrt(2) + 2) / 4, (-np.sqrt(2) + 2) / 4], + [-1 / 2, (-np.sqrt(2) + 2) / 4, (np.sqrt(2) + 2) / 4], + [np.sqrt(2) / 2, -1 / 2, 1 / 2], ], - dtype=np.float32 + dtype=np.float32, ) # Run @@ -320,7 +334,7 @@ def test_combined_rotations_negative(self): def test_bad_yaw_too_negative(self): """ - False, None + Expect failure. """ # Setup yaw = -4.0 @@ -340,7 +354,7 @@ def test_bad_yaw_too_negative(self): def test_bad_yaw_too_positive(self): """ - False, None + Expect failure. """ # Setup yaw = 4.0 @@ -360,7 +374,7 @@ def test_bad_yaw_too_positive(self): def test_bad_pitch_too_negative(self): """ - False, None + Expect failure. """ # Setup yaw = 0.0 @@ -380,7 +394,7 @@ def test_bad_pitch_too_negative(self): def test_bad_pitch_too_positive(self): """ - False, None + Expect failure. """ # Setup yaw = 0.0 @@ -400,7 +414,7 @@ def test_bad_pitch_too_positive(self): def test_bad_roll_too_negative(self): """ - False, None + Expect failure. """ # Setup yaw = 0.0 @@ -420,7 +434,7 @@ def test_bad_roll_too_negative(self): def test_bad_roll_too_positive(self): """ - False, None + Expect failure. """ # Setup yaw = 0.0 @@ -443,6 +457,7 @@ class TestCameraIntrinsicsCreate: """ Test constructor. """ + def test_normal(self): """ Successful construction. @@ -467,7 +482,7 @@ def test_normal(self): def test_bad_resolution_x(self): """ - False, None . + Expect failure. """ # Setup resolution_x = -1 @@ -489,7 +504,7 @@ def test_bad_resolution_x(self): def test_bad_resolution_y(self): """ - False, None . + Expect failure. """ # Setup resolution_x = 2000 @@ -511,7 +526,7 @@ def test_bad_resolution_y(self): def test_bad_fov_x_negative(self): """ - False, None . + Expect failure. """ # Setup resolution_x = 2000 @@ -533,7 +548,7 @@ def test_bad_fov_x_negative(self): def test_bad_fov_x_zero(self): """ - False, None . + Expect failure. """ # Setup resolution_x = 2000 @@ -555,7 +570,7 @@ def test_bad_fov_x_zero(self): def test_bad_fov_x_too_positive(self): """ - False, None . + Expect failure. """ # Setup resolution_x = 2000 @@ -577,7 +592,7 @@ def test_bad_fov_x_too_positive(self): def test_bad_fov_y_negative(self): """ - False, None . + Expect failure. """ # Setup resolution_x = 2000 @@ -599,7 +614,7 @@ def test_bad_fov_y_negative(self): def test_bad_fov_y_zero(self): """ - False, None . + Expect failure. """ # Setup resolution_x = 2000 @@ -621,7 +636,7 @@ def test_bad_fov_y_zero(self): def test_bad_fov_y_too_positive(self): """ - False, None . + Expect failure. """ # Setup resolution_x = 2000 @@ -646,6 +661,7 @@ class TestImagePixelToVector: """ Test convert from image pixel to image vector. """ + def test_centre(self): """ Centre of image. @@ -658,15 +674,14 @@ def test_centre(self): expected = np.zeros(3) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore - pixel, - resolution, - vec_base, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore + pixel, + resolution, + vec_base, + ) # Test assert result @@ -685,15 +700,14 @@ def test_left(self): expected = np.array([-1.0, -1.0, -1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore - pixel, - resolution, - vec_base, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore + pixel, + resolution, + vec_base, + ) # Test assert result @@ -712,15 +726,14 @@ def test_right(self): expected = np.array([1.0, 1.0, 1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore - pixel, - resolution, - vec_base, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore + pixel, + resolution, + vec_base, + ) # Test assert result @@ -739,15 +752,14 @@ def test_half_right(self): expected = np.array([0.5, 0.5, 0.5], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore - pixel, - resolution, - vec_base, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore + pixel, + resolution, + vec_base, + ) # Test assert result @@ -756,7 +768,7 @@ def test_half_right(self): def test_bad_pixel_too_positive(self): """ - False, None . + Expect failure. """ # Setup pixel = 2001 @@ -764,15 +776,14 @@ def test_bad_pixel_too_positive(self): vec_base = np.array([1.0, 1.0, 1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore - pixel, - resolution, - vec_base, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore + pixel, + resolution, + vec_base, + ) # Test assert not result @@ -780,7 +791,7 @@ def test_bad_pixel_too_positive(self): def test_bad_pixel_negative(self): """ - False, None . + Expect failure. """ # Setup pixel = -1 @@ -788,15 +799,14 @@ def test_bad_pixel_negative(self): vec_base = np.array([1.0, 1.0, 1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore - pixel, - resolution, - vec_base, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore + pixel, + resolution, + vec_base, + ) # Test assert not result @@ -804,7 +814,7 @@ def test_bad_pixel_negative(self): def test_bad_resolution_zero(self): """ - False, None . + Expect failure. """ # Setup pixel = 1000 @@ -812,15 +822,14 @@ def test_bad_resolution_zero(self): vec_base = np.array([1.0, 1.0, 1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore - pixel, - resolution, - vec_base, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore + pixel, + resolution, + vec_base, + ) # Test assert not result @@ -828,7 +837,7 @@ def test_bad_resolution_zero(self): def test_bad_resolution_negative(self): """ - False, None . + Expect failure. """ # Setup pixel = 1000 @@ -836,15 +845,14 @@ def test_bad_resolution_negative(self): vec_base = np.array([1.0, 1.0, 1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore - pixel, - resolution, - vec_base, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore + pixel, + resolution, + vec_base, + ) # Test assert not result @@ -852,7 +860,7 @@ def test_bad_resolution_negative(self): def test_bad_vec_base_not_r3(self): """ - False, None . + Expect failure. """ # Setup pixel = 1000 @@ -860,15 +868,14 @@ def test_bad_vec_base_not_r3(self): vec_r2 = np.array([1.0, 1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore - pixel, - resolution, - vec_r2, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = camera_properties.CameraIntrinsics._CameraIntrinsics__pixel_vector_from_image_space( # type: ignore + pixel, + resolution, + vec_r2, + ) # Test assert not result @@ -879,6 +886,7 @@ class TestImageToCameraSpace: """ Test convert from image point to camera vector. """ + def test_centre(self, camera_intrinsic: camera_properties.CameraIntrinsics): """ Centre of image. @@ -935,7 +943,7 @@ def test_bottom_right(self, camera_intrinsic: camera_properties.CameraIntrinsics def test_bad_pixel_x_negative(self, camera_intrinsic: camera_properties.CameraIntrinsics): """ - False, None . + Expect failure. """ # Setup pixel_x = -1 @@ -950,7 +958,7 @@ def test_bad_pixel_x_negative(self, camera_intrinsic: camera_properties.CameraIn def test_bad_pixel_x_too_positive(self, camera_intrinsic: camera_properties.CameraIntrinsics): """ - False, None . + Expect failure. """ # Setup pixel_x = 2001 @@ -965,7 +973,7 @@ def test_bad_pixel_x_too_positive(self, camera_intrinsic: camera_properties.Came def test_bad_pixel_y_negative(self, camera_intrinsic: camera_properties.CameraIntrinsics): """ - False, None . + Expect failure. """ # Setup pixel_x = 1000 @@ -980,7 +988,7 @@ def test_bad_pixel_y_negative(self, camera_intrinsic: camera_properties.CameraIn def test_bad_pixel_y_too_positive(self, camera_intrinsic: camera_properties.CameraIntrinsics): """ - False, None . + Expect failure. """ # Setup pixel_x = 1000 @@ -998,6 +1006,7 @@ class TestCameraExtrinsicsCreate: """ Test constructor. """ + def test_normal(self): """ Successful construction. @@ -1018,7 +1027,7 @@ def test_normal(self): def test_bad_orientation(self): """ - False, None . + Expect failure. """ # Setup camera_position_xyz = (0.0, 0.0, 0.0) diff --git a/tests/test_cluster_detection.py b/tests/test_cluster_detection.py index b1ad6c71..342208cf 100644 --- a/tests/test_cluster_detection.py +++ b/tests/test_cluster_detection.py @@ -1,5 +1,5 @@ """ -Testing Cluster Estimation worker. +Testing ClusterEstimation. """ import numpy as np @@ -11,23 +11,34 @@ MIN_TOTAL_POINTS_THRESHOLD = 100 MIN_NEW_POINTS_TO_RUN = 10 -RANDOM_STATE = 0 -CENTER_BOX_SIZE = 500 +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 @pytest.fixture() def cluster_model(): - _, model = cluster_estimation.ClusterEstimation.create( + """ + Cluster estimation object. + """ + result, model = cluster_estimation.ClusterEstimation.create( MIN_TOTAL_POINTS_THRESHOLD, MIN_NEW_POINTS_TO_RUN, - RANDOM_STATE, + RNG_SEED, ) + assert result assert model is not None + yield model -def generate_cluster_data(n_samples_per_cluster: "list[int]", cluster_standard_deviation: int) \ - -> "tuple[list[detection_in_world.DetectionInWorld], list[np.ndarray]]": +def generate_cluster_data( + 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 and standard deviation. @@ -36,7 +47,7 @@ def generate_cluster_data(n_samples_per_cluster: "list[int]", cluster_standard_d ---------- 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, + 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 @@ -58,12 +69,12 @@ def generate_cluster_data(n_samples_per_cluster: "list[int]", cluster_standard_d # 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( + 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, CENTER_BOX_SIZE), - random_state=RANDOM_STATE, + center_box=(0, CENTRE_BOX_SIZE), + random_state=RNG_SEED, return_centers=True, ) @@ -88,10 +99,11 @@ def generate_cluster_data(n_samples_per_cluster: "list[int]", cluster_standard_d return detections, cluster_positions.tolist() -def generate_points_away_from_cluster(num_points_to_generate: int, - minimum_distance_from_cluster: float, - cluster_positions: "list[np.ndarray]") \ - -> "list[detection_in_world.DetectionInWorld]": +def generate_points_away_from_cluster( + num_points_to_generate: int, + minimum_distance_from_cluster: float, + cluster_positions: "list[np.ndarray]", +) -> "list[detection_in_world.DetectionInWorld]": """ Returns a list of points with each point being the specified distance away from input cluster centre positions. @@ -115,11 +127,11 @@ def generate_points_away_from_cluster(num_points_to_generate: int, detections = [] # Initialize random generator - rng = np.random.default_rng(seed=RANDOM_STATE) + rng = np.random.default_rng(seed=RNG_SEED) while len(detections) < num_points_to_generate: # Generate random point - point = rng.uniform(0, CENTER_BOX_SIZE, size=2) + point = rng.uniform(0, CENTRE_BOX_SIZE, size=2) valid = True # Check if outside minimum distance to cluster centres @@ -128,25 +140,27 @@ def generate_points_away_from_cluster(num_points_to_generate: int, valid = False break - if valid: - # 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 + if not valid: + continue - result, detection_to_add = detection_in_world.DetectionInWorld.create( - placeholder_vertices, - point, - placeholder_label, - placeholder_confidence, - ) + # 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 - # Check that DetectionInWorld object created correctly - assert result - assert detection_to_add is not None + result, detection_to_add = detection_in_world.DetectionInWorld.create( + placeholder_vertices, + point, + placeholder_label, + placeholder_confidence, + ) - # Add to list for return - detections.append(detection_to_add) + # Check that DetectionInWorld object created correctly + assert result + assert detection_to_add is not None + + # Add to list for return + detections.append(detection_to_add) return detections @@ -156,21 +170,22 @@ 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 + + __STD_DEV_REG = 1 # Regular standard deviation is 1m def test_under_min_total_threshold(self, cluster_model: cluster_estimation.ClusterEstimation): """ Total data under threshold should not run. """ - # Setup - TOTAL_NUM_DETECTIONS = MIN_TOTAL_POINTS_THRESHOLD - 1 # Less than min threshold (100) - generated_detections, _ = generate_cluster_data([TOTAL_NUM_DETECTIONS], self.STD_DEV_REG) + # Setup + original_count = MIN_TOTAL_POINTS_THRESHOLD - 1 # Less than min threshold (100) + generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG) # Run - model_ran, detections_in_world = cluster_model.run(generated_detections, False) + result, detections_in_world = cluster_model.run(generated_detections, False) # Test - assert not model_ran + assert not result assert detections_in_world is None def test_at_min_total_threshold(self, cluster_model: cluster_estimation.ClusterEstimation): @@ -179,20 +194,20 @@ def test_at_min_total_threshold(self, cluster_model: cluster_estimation.ClusterE current bucket size. """ # Setup - NUM_DATA_POINTS = MIN_TOTAL_POINTS_THRESHOLD - 1 # Should not run the first time - NEW_DATA_POINTS = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points + 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([NUM_DATA_POINTS], self.STD_DEV_REG) - generated_detections_2, _ = generate_cluster_data([NEW_DATA_POINTS], self.STD_DEV_REG) + generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG) + generated_detections_2, _ = generate_cluster_data([new_count], self.__STD_DEV_REG) # Run - model_ran, detections_in_world = cluster_model.run(generated_detections, False) - model_ran_2, detections_in_world_2 = cluster_model.run(generated_detections_2, False) + result, detections_in_world = cluster_model.run(generated_detections, False) + result_2, detections_in_world_2 = cluster_model.run(generated_detections_2, False) # Test - assert not model_ran + assert not result assert detections_in_world is None - assert model_ran_2 + assert result_2 assert detections_in_world_2 is not None def test_under_min_bucket_size(self, cluster_model: cluster_estimation.ClusterEstimation): @@ -200,253 +215,261 @@ def test_under_min_bucket_size(self, cluster_model: cluster_estimation.ClusterEs New data under threshold should not run. """ # Setup - NUM_DATA_POINTS = MIN_TOTAL_POINTS_THRESHOLD + 10 # Should run the first time - NEW_DATA_POINTS = MIN_NEW_POINTS_TO_RUN - 1 # Under 10 new points, shouldn't run + 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([NUM_DATA_POINTS], self.STD_DEV_REG) - generated_detections_2, _ = generate_cluster_data([NEW_DATA_POINTS], self.STD_DEV_REG) + generated_detections, _ = generate_cluster_data([original_count], self.__STD_DEV_REG) + generated_detections_2, _ = generate_cluster_data([new_count], self.__STD_DEV_REG) # Run - model_ran, detections_in_world = cluster_model.run(generated_detections, False) - model_ran_2, detections_in_world_2 = cluster_model.run(generated_detections_2, False) + result, detections_in_world = cluster_model.run(generated_detections, False) + result_2, detections_in_world_2 = cluster_model.run(generated_detections_2, False) # Test - assert model_ran + assert result assert detections_in_world is not None - assert not model_ran_2 + assert not result_2 assert detections_in_world_2 is None def test_good_data(self, cluster_model: cluster_estimation.ClusterEstimation): """ All conditions met should run. """ - NUM_DATA_POINTS = MIN_TOTAL_POINTS_THRESHOLD + 1 # More than min total threshold should run - generated_detections, _ = generate_cluster_data([NUM_DATA_POINTS], self.STD_DEV_REG) + 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) # Run - model_ran, detections_in_world = cluster_model.run(generated_detections, False) + result, detections_in_world = cluster_model.run(generated_detections, False) # Test - assert model_ran + assert result assert detections_in_world is not None class TestCorrectNumberClusterOutputs: """ Tests if cluster estimation output matches number of input clusters - at different data scenarios. + at different data scenarios. """ - STD_DEV_REGULAR = 1 - STD_DEV_LARGE = 5 - def test_detect_normal_data_single_cluster(self, - cluster_model: cluster_estimation.ClusterEstimation): + __STD_DEV_REGULAR = 1 + __STD_DEV_LARGE = 5 + + def test_detect_normal_data_single_cluster( + self, cluster_model: cluster_estimation.ClusterEstimation + ): """ Data with small distribution and equal number of points per cluster centre. """ # Setup - POINTS_PER_CLUSTER = [100] - generated_detections, _ = generate_cluster_data(POINTS_PER_CLUSTER, self.STD_DEV_REGULAR) + points_per_cluster = [100] + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) # Run - model_ran, detections_in_world = cluster_model.run(generated_detections, False) + result, detections_in_world = cluster_model.run(generated_detections, False) # Test - assert model_ran + assert result assert detections_in_world is not None - def test_detect_normal_data_five_clusters(self, - cluster_model: cluster_estimation.ClusterEstimation): + def test_detect_normal_data_five_clusters( + self, cluster_model: cluster_estimation.ClusterEstimation + ): """ Data with small distribution and equal number of points per cluster centre. """ # 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) + 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) # Run - model_ran, detections_in_world = cluster_model.run(generated_detections, False) + result, detections_in_world = cluster_model.run(generated_detections, False) # Test - assert model_ran + assert result assert detections_in_world is not None - assert len(detections_in_world) == EXPECTED_CLUSTER_COUNT + assert len(detections_in_world) == expected_cluster_count - def test_detect_large_std_dev_single_cluster(self, - cluster_model: cluster_estimation.ClusterEstimation): + def test_detect_large_std_dev_single_cluster( + self, cluster_model: cluster_estimation.ClusterEstimation + ): """ Data with large distribution and equal number of points per cluster centre. """ # Setup - POINTS_PER_CLUSTER = [100] - EXPECTED_CLUSTER_COUNT = len(POINTS_PER_CLUSTER) - generated_detections, _ = generate_cluster_data(POINTS_PER_CLUSTER, self.STD_DEV_LARGE) + points_per_cluster = [100] + expected_cluster_count = len(points_per_cluster) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_LARGE) # Run - model_ran, detections_in_world = cluster_model.run(generated_detections, False) + result, detections_in_world = cluster_model.run(generated_detections, False) # Test - assert model_ran + assert result assert detections_in_world is not None - assert len(detections_in_world) == EXPECTED_CLUSTER_COUNT + assert len(detections_in_world) == expected_cluster_count - def test_detect_large_std_dev_five_clusters(self, - cluster_model: cluster_estimation.ClusterEstimation): + def test_detect_large_std_dev_five_clusters( + self, cluster_model: cluster_estimation.ClusterEstimation + ): """ Data with large distribution and equal number of points per cluster centre. """ # 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) + 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) # Run - model_ran, detections_in_world = cluster_model.run(generated_detections, False) + result, detections_in_world = cluster_model.run(generated_detections, False) # Test - assert model_ran + assert result assert detections_in_world is not None - assert len(detections_in_world) == EXPECTED_CLUSTER_COUNT + assert len(detections_in_world) == expected_cluster_count - def test_detect_skewed_data_single_cluster(self, - cluster_model: cluster_estimation.ClusterEstimation): + def test_detect_skewed_data_single_cluster( + self, cluster_model: cluster_estimation.ClusterEstimation + ): """ Data with small distribution but varying number of points per cluster centre and random outlier points to simulate false detections. """ # 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) + points_per_cluster = [10, 100] + expected_cluster_count = len(points_per_cluster) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) # Run - model_ran, detections_in_world = cluster_model.run(generated_detections, False) + result, detections_in_world = cluster_model.run(generated_detections, False) # Test - assert model_ran + assert result assert detections_in_world is not None - assert len(detections_in_world) == EXPECTED_CLUSTER_COUNT + assert len(detections_in_world) == expected_cluster_count - def test_detect_skewed_data_five_clusters(self, - cluster_model: cluster_estimation.ClusterEstimation): + def test_detect_skewed_data_five_clusters( + self, cluster_model: cluster_estimation.ClusterEstimation + ): """ Data with small distribution but varying number of points per cluster centre and random outlier points to simulate false detections. """ # Setup - POINTS_PER_CLUSTER = [20, 100, 100, 100, 100] - EXPECTED_CLUSTER_COUNT = len(POINTS_PER_CLUSTER) + points_per_cluster = [20, 100, 100, 100, 100] + expected_cluster_count = len(points_per_cluster) generated_detections, cluster_positions = generate_cluster_data( - POINTS_PER_CLUSTER, - self.STD_DEV_REGULAR, + points_per_cluster, + self.__STD_DEV_REGULAR, ) # Add 5 random points to dataset, each being at least 20m away from cluster centres - NUM_OUTLIER_POINTS = 5 - OUTLIER_DISTANCE_FROM_CLUSTER_CENTRE = 20 + outlier_count = 5 + outlier_distance_from_cluster_centre = 20 outlier_detections = generate_points_away_from_cluster( - NUM_OUTLIER_POINTS, - OUTLIER_DISTANCE_FROM_CLUSTER_CENTRE, + outlier_count, + outlier_distance_from_cluster_centre, cluster_positions, ) generated_detections += outlier_detections # Run - model_ran, detections_in_world = cluster_model.run(generated_detections, False) + result, detections_in_world = cluster_model.run(generated_detections, False) # Test - assert model_ran + assert result assert detections_in_world is not None - assert len(detections_in_world) == EXPECTED_CLUSTER_COUNT + assert len(detections_in_world) == expected_cluster_count - def test_detect_consecutive_inputs_single_cluster(self, - cluster_model: cluster_estimation.ClusterEstimation): + def test_detect_consecutive_inputs_single_cluster( + self, cluster_model: cluster_estimation.ClusterEstimation + ): """ Previous tests executed model with all points available at once. This test feeds the model one point from the dataset one at a time (and calls .run() each time), checking for correct number of output clusters once all points have been inputted. """ # Setup - POINTS_PER_CLUSTER = [100] - EXPECTED_CLUSTER_COUNT = len(POINTS_PER_CLUSTER) - generated_detections, _ = generate_cluster_data(POINTS_PER_CLUSTER, self.STD_DEV_REGULAR) + points_per_cluster = [100] + expected_cluster_count = len(points_per_cluster) + generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) # Run - model_ran = False detections_in_world = [] - for i, point in enumerate(generated_detections): - model_ran, detections_in_world = cluster_model.run([point], False) + for point in generated_detections: + result, detections_in_world = cluster_model.run([point], False) + assert result + assert detections_in_world is not None # Test - assert model_ran - assert detections_in_world is not None - assert len(detections_in_world) == EXPECTED_CLUSTER_COUNT + assert len(detections_in_world) == expected_cluster_count - def test_detect_consecutive_inputs_five_clusters(self, - cluster_model: cluster_estimation.ClusterEstimation): + def test_detect_consecutive_inputs_five_clusters( + self, cluster_model: cluster_estimation.ClusterEstimation + ): """ Previous tests executed model with all points available at once. This test feeds the model one point from the dataset one at a time (and calls .run() each time), checking for correct number of output clusters once all points have been inputted. """ # 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) - + 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) + # Run - model_ran = False detections_in_world = [] - for i, point in enumerate(generated_detections): - model_ran, detections_in_world = cluster_model.run([point], False) + for point in generated_detections: + result, detections_in_world = cluster_model.run([point], False) + assert result + assert detections_in_world is not None # Test - assert model_ran - assert detections_in_world is not None - assert len(detections_in_world) == EXPECTED_CLUSTER_COUNT + assert len(detections_in_world) == expected_cluster_count +# Test class +# pylint: disable-next=too-few-public-methods class TestCorrectClusterPositionOutput: """ Tests if cluster estimation output falls within acceptable distance to input cluster positions. """ - STD_DEV_REG = 1 # Regular standard deviation is 1m - MAX_POSITION_TOLERANCE = 1 + + __STD_DEV_REG = 1 # Regular standard deviation is 1m + __MAX_POSITION_TOLERANCE = 1 def test_position_regular_data(self, cluster_model: cluster_estimation.ClusterEstimation): """ Five clusters with small standard deviation and large number of points per cluster. """ # Setup - POINTS_PER_CLUSTER = [100, 100, 100, 100, 100] + points_per_cluster = [100, 100, 100, 100, 100] generated_detections, cluster_positions = generate_cluster_data( - POINTS_PER_CLUSTER, - self.STD_DEV_REG, + points_per_cluster, + self.__STD_DEV_REG, ) # Run - model_ran, detections_in_world = cluster_model.run(generated_detections, False) + result, detections_in_world = cluster_model.run(generated_detections, False) # Test - assert model_ran + assert result assert detections_in_world is not None # Check if within acceptable distance for detection in detections_in_world: - has_match = False + is_match = False for position in cluster_positions: - # Get distance between predicted cluster and actual cluster distance = np.linalg.norm( - [detection.position_x - position[0], - detection.position_y - position[1]] + [detection.location_x - position[0], detection.location_y - position[1]] ) # Check tolerance - if distance < self.MAX_POSITION_TOLERANCE: - has_match = True + if distance < self.__MAX_POSITION_TOLERANCE: + is_match = True break - assert has_match + assert is_match diff --git a/tests/test_data_merge_worker.py b/tests/test_data_merge_worker.py index 06f011ae..0891c18b 100644 --- a/tests/test_data_merge_worker.py +++ b/tests/test_data_merge_worker.py @@ -18,8 +18,9 @@ DATA_MERGE_WORKER_TIMEOUT = 10.0 # seconds -def simulate_detect_target_worker(timestamp: float, - detections_queue: queue_proxy_wrapper.QueueProxyWrapper): +def simulate_detect_target_worker( + timestamp: float, detections_queue: queue_proxy_wrapper.QueueProxyWrapper +): """ Place the detection into the queue. """ @@ -40,8 +41,9 @@ def simulate_detect_target_worker(timestamp: float, detections_queue.queue.put(detections) -def simulate_flight_input_worker(timestamp: float, - odometry_queue: queue_proxy_wrapper.QueueProxyWrapper): +def simulate_flight_input_worker( + timestamp: float, odometry_queue: queue_proxy_wrapper.QueueProxyWrapper +): """ Place the odometry into the queue. """ @@ -120,8 +122,9 @@ def simulate_flight_input_worker(timestamp: float, # Test for expected_time in expected_times: - merged: merged_odometry_detections.MergedOdometryDetections = \ + merged: merged_odometry_detections.MergedOdometryDetections = ( merged_out_queue.queue.get_nowait() + ) assert int(merged.odometry_local.position.north) == expected_time assert merged_out_queue.queue.empty() diff --git a/tests/test_decision.py b/tests/test_decision.py index 9e0a9833..3c5ff20d 100644 --- a/tests/test_decision.py +++ b/tests/test_decision.py @@ -1,10 +1,9 @@ """ -Tests the decision class +Tests the decision class. """ import pytest - from modules.decision import decision from modules import decision_command from modules import object_in_world @@ -12,7 +11,17 @@ from modules import drone_odometry_local -LANDING_PAD_LOCATION_TOLERANCE = 2 # Test parameters +LANDING_PAD_LOCATION_TOLERANCE = 2 + +BEST_PAD_LOCATION_X = 10.0 +BEST_PAD_LOCATION_Y = 20.0 + +DRONE_OFFSET_FROM_PAD = 1.0 + + +# Test functions use test fixture signature names and access class privates +# No enable +# pylint: disable=protected-access,redefined-outer-name @pytest.fixture() @@ -29,13 +38,13 @@ def best_pad_within_tolerance(): """ Create an ObjectInWorld instance within distance to pad tolerance. """ - position_x = 10.0 - position_y = 20.0 + location_x = BEST_PAD_LOCATION_X + location_y = BEST_PAD_LOCATION_Y spherical_variance = 1.0 - result, pad = object_in_world.ObjectInWorld.create( - position_x, position_y, spherical_variance - ) + result, pad = object_in_world.ObjectInWorld.create(location_x, location_y, spherical_variance) assert result + assert pad is not None + yield pad @@ -44,13 +53,13 @@ def best_pad_outside_tolerance(): """ Creates an ObjectInWorld instance outside of distance to pad tolerance. """ - position_x = 100.0 - position_y = 200.0 + location_x = 100.0 + location_y = 200.0 spherical_variance = 5.0 # variance outside tolerance - result, pad = object_in_world.ObjectInWorld.create( - position_x, position_y, spherical_variance - ) + result, pad = object_in_world.ObjectInWorld.create(location_x, location_y, spherical_variance) assert result + assert pad is not None + yield pad @@ -59,77 +68,91 @@ def pads(): """ Create a list of ObjectInWorld instances for the landing pads. """ - pad1 = object_in_world.ObjectInWorld.create(30.0, 40.0, 2.0)[1] - pad2 = object_in_world.ObjectInWorld.create(50.0, 60.0, 3.0)[1] - pad3 = object_in_world.ObjectInWorld.create(70.0, 80.0, 4.0)[1] + result, pad1 = object_in_world.ObjectInWorld.create(30.0, 40.0, 2.0) + assert result + assert pad1 is not None + + result, pad2 = object_in_world.ObjectInWorld.create(50.0, 60.0, 3.0) + assert result + assert pad2 is not None + + result, pad3 = object_in_world.ObjectInWorld.create(70.0, 80.0, 4.0) + assert result + assert pad3 is not None + yield [pad1, pad2, pad3] @pytest.fixture() -def state(): +def drone_odometry_and_time(): """ - Create an OdometryAndTime instance with the drone positioned within tolerance of the landing pad. + Create an OdometryAndTime instance with the drone positioned within tolerance of landing pad. """ # Creating the position within tolerance of the specified landing pad. - position = drone_odometry_local.DronePositionLocal.create(9.0, 19.0, -5.0)[1] + result, position = drone_odometry_local.DronePositionLocal.create( + BEST_PAD_LOCATION_X - DRONE_OFFSET_FROM_PAD, + BEST_PAD_LOCATION_Y - DRONE_OFFSET_FROM_PAD, + -5.0, + ) + assert result + assert position is not None - orientation = drone_odometry_local.DroneOrientationLocal.create_new(0.0, 0.0, 0.0)[1] + result, orientation = drone_odometry_local.DroneOrientationLocal.create_new(0.0, 0.0, 0.0) + assert result + assert orientation is not None - odometry_data = drone_odometry_local.DroneOdometryLocal.create(position, orientation)[1] + result, odometry_data = drone_odometry_local.DroneOdometryLocal.create(position, orientation) + assert result + assert odometry_data is not None # Creating the OdometryAndTime instance with current time stamp - result, state = odometry_and_time.OdometryAndTime.create(odometry_data) + result, odometry_with_time = odometry_and_time.OdometryAndTime.create(odometry_data) assert result - yield state + assert odometry_with_time is not None + + yield odometry_with_time class TestDecision: """ - Tests for the Decision.run() method and weight and distance methods - """ - - def test_decision_within_tolerance(self, - decision_maker, - best_pad_within_tolerance, - pads, - state): + Tests for the Decision.run() method and weight and distance methods. + """ + + def test_decision_within_tolerance( + self, decision_maker, best_pad_within_tolerance, pads, drone_odometry_and_time + ): """ Test decision making when the best pad is within tolerance. """ expected = decision_command.DecisionCommand.CommandType.LAND_AT_ABSOLUTE_POSITION total_pads = [best_pad_within_tolerance] + pads - - result, actual = decision_maker.run(state, total_pads) + + result, command = decision_maker.run(drone_odometry_and_time, total_pads) assert result - assert actual.get_command_type() == expected + assert command is not None + assert command.get_command_type() == expected - def test_decision_outside_tolerance(self, - decision_maker, - best_pad_outside_tolerance, - pads, - state): + def test_decision_outside_tolerance( + self, decision_maker, best_pad_outside_tolerance, pads, drone_odometry_and_time + ): """ Test decision making when the best pad is outside tolerance. """ expected = decision_command.DecisionCommand.CommandType.MOVE_TO_ABSOLUTE_POSITION total_pads = [best_pad_outside_tolerance] + pads - - result, actual = decision_maker.run(state, total_pads) + + result, command = decision_maker.run(drone_odometry_and_time, total_pads) assert result - assert actual.get_command_type() == expected + assert command is not None + assert command.get_command_type() == expected - def test_decision_no_pads(self, - decision_maker, - state): + def test_decision_no_pads(self, decision_maker, drone_odometry_and_time): """ Test decision making when no pads are available. """ - expected = None - - result, actual = decision_maker.run(state, []) + result, command = decision_maker.run(drone_odometry_and_time, []) - assert result == False - assert actual == expected - + assert not result + assert command is None diff --git a/tests/test_detect_target.py b/tests/test_detect_target.py index 21a2073f..429472bc 100644 --- a/tests/test_detect_target.py +++ b/tests/test_detect_target.py @@ -13,21 +13,28 @@ from modules import image_and_time from modules import detections_and_time -TEST_PATH = pathlib.Path("tests", "model_example") -DEVICE = 0 if torch.cuda.is_available() else "cpu" -MODEL_PATH = pathlib.Path(TEST_PATH, "yolov8s_ultralytics_pretrained_default.pt") -OVERRIDE_FULL = not torch.cuda.is_available() # CPU does not support half precision -IMAGE_BUS_PATH = pathlib.Path(TEST_PATH, "bus.jpg") -BOUNDING_BOX_BUS_PATH = pathlib.Path(TEST_PATH, "bounding_box_bus.txt") -IMAGE_ZIDANE_PATH = pathlib.Path(TEST_PATH, "zidane.jpg") + +TEST_PATH = pathlib.Path("tests", "model_example") +DEVICE = 0 if torch.cuda.is_available() else "cpu" +MODEL_PATH = pathlib.Path(TEST_PATH, "yolov8s_ultralytics_pretrained_default.pt") +OVERRIDE_FULL = not torch.cuda.is_available() # CPU does not support half precision +IMAGE_BUS_PATH = pathlib.Path(TEST_PATH, "bus.jpg") +BOUNDING_BOX_BUS_PATH = pathlib.Path(TEST_PATH, "bounding_box_bus.txt") +IMAGE_ZIDANE_PATH = pathlib.Path(TEST_PATH, "zidane.jpg") BOUNDING_BOX_ZIDANE_PATH = pathlib.Path(TEST_PATH, "bounding_box_zidane.txt") -BOUNDING_BOX_DECIMAL_TOLERANCE = 0 -CONFIDENCE_DECIMAL_TOLERANCE = 2 +BOUNDING_BOX_PRECISION_TOLERANCE = 0 +CONFIDENCE_PRECISION_TOLERANCE = 2 + +# Test functions use test fixture signature names and access class privates +# No enable +# pylint: disable=protected-access,redefined-outer-name -def compare_detections(actual: detections_and_time.DetectionsAndTime, - expected: detections_and_time.DetectionsAndTime) -> None: + +def compare_detections( + actual: detections_and_time.DetectionsAndTime, expected: detections_and_time.DetectionsAndTime +) -> None: """ Compare expected and actual detections. """ @@ -43,38 +50,38 @@ def compare_detections(actual: detections_and_time.DetectionsAndTime, np.testing.assert_almost_equal( expected_detection.confidence, actual_detection.confidence, - decimal=CONFIDENCE_DECIMAL_TOLERANCE, + decimal=CONFIDENCE_PRECISION_TOLERANCE, ) np.testing.assert_almost_equal( actual_detection.x1, expected_detection.x1, - decimal=BOUNDING_BOX_DECIMAL_TOLERANCE, + decimal=BOUNDING_BOX_PRECISION_TOLERANCE, ) np.testing.assert_almost_equal( actual_detection.y1, expected_detection.y1, - decimal=BOUNDING_BOX_DECIMAL_TOLERANCE, + decimal=BOUNDING_BOX_PRECISION_TOLERANCE, ) np.testing.assert_almost_equal( actual_detection.x2, expected_detection.x2, - decimal=BOUNDING_BOX_DECIMAL_TOLERANCE, + decimal=BOUNDING_BOX_PRECISION_TOLERANCE, ) np.testing.assert_almost_equal( actual_detection.y2, expected_detection.y2, - decimal=BOUNDING_BOX_DECIMAL_TOLERANCE, + decimal=BOUNDING_BOX_PRECISION_TOLERANCE, ) def create_detections(detections_from_file: np.ndarray) -> detections_and_time.DetectionsAndTime: """ Create DetectionsAndTime from expected. - Format: [confidence, label, x1, y1, x2, y2] + Format: [confidence, label, x1, y1, x2, y2] . """ assert detections_from_file.shape[1] == 6 @@ -100,7 +107,7 @@ def detector(): """ Construct DetectTarget. """ - detection = detect_target.DetectTarget(DEVICE, MODEL_PATH, OVERRIDE_FULL) + detection = detect_target.DetectTarget(DEVICE, str(MODEL_PATH), OVERRIDE_FULL) yield detection @@ -109,7 +116,7 @@ def image_bus(): """ Load bus image. """ - image = cv2.imread(str(IMAGE_BUS_PATH)) + image = cv2.imread(str(IMAGE_BUS_PATH)) # type: ignore result, bus_image = image_and_time.ImageAndTime.create(image) assert result assert bus_image is not None @@ -121,7 +128,7 @@ def image_zidane(): """ Load Zidane image. """ - image = cv2.imread(str(IMAGE_ZIDANE_PATH)) + image = cv2.imread(str(IMAGE_ZIDANE_PATH)) # type: ignore result, zidane_image = image_and_time.ImageAndTime.create(image) assert result assert zidane_image is not None @@ -150,10 +157,13 @@ class TestDetector: """ Tests `DetectTarget.run()` . """ - def test_single_bus_image(self, - detector: detect_target.DetectTarget, - image_bus: image_and_time.ImageAndTime, - expected_bus: detections_and_time.DetectionsAndTime): + + def test_single_bus_image( + self, + detector: detect_target.DetectTarget, + image_bus: image_and_time.ImageAndTime, + expected_bus: detections_and_time.DetectionsAndTime, + ): """ Bus image. """ @@ -166,10 +176,12 @@ def test_single_bus_image(self, compare_detections(actual, expected_bus) - def test_single_zidane_image(self, - detector: detect_target.DetectTarget, - image_zidane: image_and_time.ImageAndTime, - expected_zidane: detections_and_time.DetectionsAndTime): + def test_single_zidane_image( + self, + detector: detect_target.DetectTarget, + image_zidane: image_and_time.ImageAndTime, + expected_zidane: detections_and_time.DetectionsAndTime, + ): """ Zidane image. """ @@ -182,28 +194,30 @@ def test_single_zidane_image(self, compare_detections(actual, expected_zidane) - def test_multiple_zidane_image(self, - detector: detect_target.DetectTarget, - image_zidane: image_and_time.ImageAndTime, - expected_zidane: detections_and_time.DetectionsAndTime): + def test_multiple_zidane_image( + self, + detector: detect_target.DetectTarget, + image_zidane: image_and_time.ImageAndTime, + expected_zidane: detections_and_time.DetectionsAndTime, + ): """ Multiple Zidane images. """ - IMAGE_COUNT = 4 + image_count = 4 input_images = [] - for _ in range(0, IMAGE_COUNT): + for _ in range(0, image_count): input_image = copy.deepcopy(image_zidane) input_images.append(input_image) # Run outputs = [] - for i in range(0, IMAGE_COUNT): + for i in range(0, image_count): output = detector.run(input_images[i]) outputs.append(output) # Test - for i in range(0, IMAGE_COUNT): + for i in range(0, image_count): output: "tuple[bool, detections_and_time.DetectionsAndTime | None]" = outputs[i] result, actual = output diff --git a/tests/test_detect_target_worker.py b/tests/test_detect_target_worker.py index cc9cf784..57c9b1e4 100644 --- a/tests/test_detect_target_worker.py +++ b/tests/test_detect_target_worker.py @@ -29,11 +29,13 @@ SAVE_NAME = "" # No need to save images -def simulate_previous_worker(image_path: str, in_queue: queue_proxy_wrapper.QueueProxyWrapper): +def simulate_previous_worker( + image_path: pathlib.Path, in_queue: queue_proxy_wrapper.QueueProxyWrapper +): """ Place the image into the queue. """ - image = cv2.imread(image_path) + image = cv2.imread(image_path) # type: ignore result, value = image_and_time.ImageAndTime.create(image) assert result assert value is not None @@ -42,6 +44,8 @@ def simulate_previous_worker(image_path: str, in_queue: queue_proxy_wrapper.Queu if __name__ == "__main__": # Setup + # Not a constant + # pylint: disable-next=invalid-name device = 0 if torch.cuda.is_available() else "cpu" controller = worker_controller.WorkerController() diff --git a/tests/test_flight_interface_worker.py b/tests/test_flight_interface_worker.py index 4738d64c..f0e2f985 100644 --- a/tests/test_flight_interface_worker.py +++ b/tests/test_flight_interface_worker.py @@ -47,7 +47,7 @@ while True: try: input_data: odometry_and_time.OdometryAndTime = out_queue.queue.get_nowait() - assert str(type(input_data)) == "" + assert str(type(input_data)) == "" assert input_data.odometry_data is not None except queue.Empty: diff --git a/tests/test_geolocation.py b/tests/test_geolocation.py index ba914aed..a89d73d4 100644 --- a/tests/test_geolocation.py +++ b/tests/test_geolocation.py @@ -1,3 +1,6 @@ +# Large test file +# No enable +# pylint: disable=too-many-lines """ Test geolocation. """ @@ -16,6 +19,11 @@ FLOAT_PRECISION_TOLERANCE = 4 +# Test functions use test fixture signature names and access class privates +# No enable +# pylint: disable=protected-access,redefined-outer-name + + @pytest.fixture def basic_locator(): """ @@ -183,13 +191,14 @@ def affine_matrix(): [ [0.0, 1.0, -1.0], [2.0, 0.0, -1.0], - [0.0, 0.0, 1.0], + [0.0, 0.0, 1.0], ], dtype=np.float32, ) yield matrix + @pytest.fixture def non_affine_matrix(): """ @@ -199,7 +208,7 @@ def non_affine_matrix(): [ [0.0, 1.0, -1.0], [2.0, 0.0, -1.0], - [1.0, 1.0, 1.0], + [1.0, 1.0, 1.0], ], dtype=np.float32, ) @@ -211,6 +220,7 @@ class TestGeolocationCreate: """ Test constructor. """ + def test_normal(self): """ Successful construction. @@ -243,6 +253,7 @@ class TestGroundIntersection: """ Test where vector intersects with ground. """ + def test_above_origin_directly_down(self): """ Above origin, directly down. @@ -254,14 +265,13 @@ def test_above_origin_directly_down(self): expected = np.array([0.0, 0.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore - vec_camera_in_world_position, - vec_down, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore + vec_camera_in_world_position, + vec_down, + ) # Test assert result @@ -279,14 +289,13 @@ def test_non_origin_directly_down(self): expected = np.array([100.0, -100.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore - vec_camera_in_world_position, - vec_down, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore + vec_camera_in_world_position, + vec_down, + ) # Test assert result @@ -304,14 +313,13 @@ def test_above_origin_angled_down(self): expected = np.array([100.0, 100.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore - vec_camera_in_world_position, - vec_down, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore + vec_camera_in_world_position, + vec_down, + ) # Test assert result @@ -329,14 +337,13 @@ def test_non_origin_angled_down(self): expected = np.array([0.0, 0.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore - vec_camera_in_world_position, - vec_down, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore + vec_camera_in_world_position, + vec_down, + ) # Test assert result @@ -352,14 +359,13 @@ def test_bad_almost_horizontal(self): vec_horizontal = np.array([10.0, 0.0, 1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore - vec_camera_in_world_position, - vec_horizontal, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore + vec_camera_in_world_position, + vec_horizontal, + ) # Test assert not result @@ -374,14 +380,13 @@ def test_bad_upwards(self): vec_up = np.array([0.0, 0.0, -1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore - vec_camera_in_world_position, - vec_up, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore + vec_camera_in_world_position, + vec_up, + ) # Test assert not result @@ -396,14 +401,13 @@ def test_bad_underground(self): vec_down = np.array([0.0, 0.0, 1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore - vec_underground, - vec_down, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = geolocation.Geolocation._Geolocation__ground_intersection_from_vector( # type: ignore + vec_underground, + vec_down, + ) # Test assert not result @@ -414,6 +418,7 @@ class TestPerspectiveTransformMatrix: """ Test perspective transform creation. """ + def test_basic_above_origin_pointed_down(self, basic_locator: geolocation.Geolocation): """ Above origin, directly down. @@ -432,14 +437,13 @@ def test_basic_above_origin_pointed_down(self, basic_locator: geolocation.Geoloc vec_ground_expected = np.array([-100.0, 100.0, 1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - basic_locator._Geolocation__get_perspective_transform_matrix( # type: ignore - drone_rotation_matrix, - drone_position_ned, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = basic_locator._Geolocation__get_perspective_transform_matrix( # type: ignore + drone_rotation_matrix, + drone_position_ned, + ) # Test assert result @@ -453,8 +457,9 @@ def test_basic_above_origin_pointed_down(self, basic_locator: geolocation.Geoloc decimal=FLOAT_PRECISION_TOLERANCE, ) - def test_intermediate_above_origin_pointing_north(self, - intermediate_locator: geolocation.Geolocation): + def test_intermediate_above_origin_pointing_north( + self, intermediate_locator: geolocation.Geolocation + ): """ Positioned so that the camera is above the origin directly down (but the drone is not). """ @@ -472,14 +477,13 @@ def test_intermediate_above_origin_pointing_north(self, vec_ground_expected = np.array([-100.0, 100.0, 1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - intermediate_locator._Geolocation__get_perspective_transform_matrix( # type: ignore - drone_rotation_matrix, - drone_position_ned, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = intermediate_locator._Geolocation__get_perspective_transform_matrix( # type: ignore + drone_rotation_matrix, + drone_position_ned, + ) # Test assert result @@ -493,8 +497,9 @@ def test_intermediate_above_origin_pointing_north(self, decimal=FLOAT_PRECISION_TOLERANCE, ) - def test_intermediate_above_origin_pointing_west(self, - intermediate_locator: geolocation.Geolocation): + def test_intermediate_above_origin_pointing_west( + self, intermediate_locator: geolocation.Geolocation + ): """ Positioned so that the camera is above the origin directly down (but the drone is not). """ @@ -512,14 +517,13 @@ def test_intermediate_above_origin_pointing_west(self, vec_ground_expected = np.array([100.0, 100.0, 1.0], dtype=np.float32) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - intermediate_locator._Geolocation__get_perspective_transform_matrix( # type: ignore - drone_rotation_matrix, - drone_position_ned, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = intermediate_locator._Geolocation__get_perspective_transform_matrix( # type: ignore + drone_rotation_matrix, + drone_position_ned, + ) # Test assert result @@ -538,7 +542,6 @@ def test_advanced(self, advanced_locator: geolocation.Geolocation): Camera is north of origin with an angle from vertical. Also rotated. """ # Setup - # TODO result, drone_rotation_matrix = camera_properties.create_rotation_matrix_from_orientation( 0.0, np.pi / 12, @@ -567,14 +570,13 @@ def test_advanced(self, advanced_locator: geolocation.Geolocation): ) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - advanced_locator._Geolocation__get_perspective_transform_matrix( # type: ignore - drone_rotation_matrix, - drone_position_ned, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = advanced_locator._Geolocation__get_perspective_transform_matrix( # type: ignore + drone_rotation_matrix, + drone_position_ned, + ) # Test assert result @@ -619,14 +621,13 @@ def test_bad_direction(self, basic_locator: geolocation.Geolocation): ) # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - basic_locator._Geolocation__get_perspective_transform_matrix( # type: ignore - drone_rotation_matrix, - drone_position_ned, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = basic_locator._Geolocation__get_perspective_transform_matrix( # type: ignore + drone_rotation_matrix, + drone_position_ned, + ) # Test assert not result @@ -637,6 +638,7 @@ class TestGeolocationConvertDetection: """ Test extract and convert. """ + def test_normal1(self, detection1: detections_and_time.Detection, affine_matrix: np.ndarray): """ Normal detection and matrix. @@ -645,9 +647,9 @@ def test_normal1(self, detection1: detections_and_time.Detection, affine_matrix: result, expected = detection_in_world.DetectionInWorld.create( np.array( [ - [ -1.0, -1.0], - [ -1.0, 3999.0], - [1999.0, -1.0], + [-1.0, -1.0], + [-1.0, 3999.0], + [1999.0, -1.0], [1999.0, 3999.0], ], dtype=np.float32, @@ -663,14 +665,13 @@ def test_normal1(self, detection1: detections_and_time.Detection, affine_matrix: assert expected is not None # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - geolocation.Geolocation._Geolocation__convert_detection_to_world_from_image( # type: ignore - detection1, - affine_matrix, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = geolocation.Geolocation._Geolocation__convert_detection_to_world_from_image( # type: ignore + detection1, + affine_matrix, + ) # Test assert result @@ -689,9 +690,9 @@ def test_normal2(self, detection2: detections_and_time.Detection, affine_matrix: result, expected = detection_in_world.DetectionInWorld.create( np.array( [ - [ -1.0, -1.0], - [ -1.0, 1999.0], - [999.0, -1.0], + [-1.0, -1.0], + [-1.0, 1999.0], + [999.0, -1.0], [999.0, 1999.0], ], dtype=np.float32, @@ -707,14 +708,13 @@ def test_normal2(self, detection2: detections_and_time.Detection, affine_matrix: assert expected is not None # Run - # Access required for test - # pylint: disable=protected-access - result, actual = \ - geolocation.Geolocation._Geolocation__convert_detection_to_world_from_image( # type: ignore - detection2, - affine_matrix, - ) - # pylint: enable=protected-access + ( + result, + actual, + ) = geolocation.Geolocation._Geolocation__convert_detection_to_world_from_image( # type: ignore + detection2, + affine_matrix, + ) # Test assert result @@ -730,10 +730,13 @@ class TestGeolocationRun: """ Run. """ - def test_basic(self, - basic_locator: geolocation.Geolocation, - detection1: detections_and_time.Detection, - detection2: detections_and_time.Detection): + + def test_basic( + self, + basic_locator: geolocation.Geolocation, + detection1: detections_and_time.Detection, + detection2: detections_and_time.Detection, + ): """ 2 detections. """ @@ -774,10 +777,10 @@ def test_basic(self, result, expected_detection1 = detection_in_world.DetectionInWorld.create( np.array( [ - [ 100.0, -100.0], - [ 100.0, 100.0], + [100.0, -100.0], + [100.0, 100.0], [-100.0, -100.0], - [-100.0, 100.0], + [-100.0, 100.0], ], dtype=np.float32, ), @@ -794,10 +797,10 @@ def test_basic(self, result, expected_detection2 = detection_in_world.DetectionInWorld.create( np.array( [ - [ 100.0, -100.0], - [ 100.0, 0.0], - [ 0.0, -100.0], - [ 0.0, 0.0], + [100.0, -100.0], + [100.0, 0.0], + [0.0, -100.0], + [0.0, 0.0], ], dtype=np.float32, ), @@ -830,10 +833,12 @@ def test_basic(self, assert actual.label == expected_list[i].label np.testing.assert_almost_equal(actual.confidence, expected_list[i].confidence) - def test_advanced(self, - advanced_locator: geolocation.Geolocation, - detection_bottom_right_point: detections_and_time.Detection, - detection_centre_left_point: detections_and_time.Detection): + def test_advanced( + self, + advanced_locator: geolocation.Geolocation, + detection_bottom_right_point: detections_and_time.Detection, + detection_centre_left_point: detections_and_time.Detection, + ): """ 2 point detections. """ @@ -938,9 +943,9 @@ def test_advanced(self, assert actual.label == expected_list[i].label np.testing.assert_almost_equal(actual.confidence, expected_list[i].confidence) - def test_bad_direction(self, - basic_locator: geolocation.Geolocation, - detection1: detections_and_time.Detection): + def test_bad_direction( + self, basic_locator: geolocation.Geolocation, detection1: detections_and_time.Detection + ): """ Bad direction. """ diff --git a/tests/test_landing_pad_tracking.py b/tests/test_landing_pad_tracking.py index de4005c1..5a3a2d6e 100644 --- a/tests/test_landing_pad_tracking.py +++ b/tests/test_landing_pad_tracking.py @@ -11,6 +11,11 @@ DISTANCE_SQUARED_THRESHOLD = 2 # Actual distance threshold is sqrt(2) +# Test functions use test fixture signature names and access class privates +# No enable +# pylint: disable=protected-access,redefined-outer-name + + @pytest.fixture() def tracker(): """ @@ -25,13 +30,26 @@ def detections_1(): """ Sample instances of ObjectInWorld for testing. """ - _, obj_1 = object_in_world.ObjectInWorld.create(0,0,8) - _, obj_2 = object_in_world.ObjectInWorld.create(2,2,4) - _, obj_3 = object_in_world.ObjectInWorld.create(-2,-2,2) - _, obj_4 = object_in_world.ObjectInWorld.create(3,3,10) - _, obj_5 = object_in_world.ObjectInWorld.create(-3,-3,6) - assert obj_1 is not None and obj_2 is not None and obj_3 is not None and obj_4 is not None \ - and obj_5 is not None + 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) + assert result + assert obj_2 is not None + + 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) + assert result + assert obj_4 is not None + + result, obj_5 = object_in_world.ObjectInWorld.create(-3, -3, 6) + assert result + assert obj_5 is not None + detections = [obj_1, obj_2, obj_3, obj_4, obj_5] yield detections @@ -41,13 +59,26 @@ def detections_2(): """ Sample instances of ObjectInWorld for testing. """ - _, obj_1 = object_in_world.ObjectInWorld.create(0.5,0.5,1) - _, obj_2 = object_in_world.ObjectInWorld.create(1.5,1.5,3) - _, obj_3 = object_in_world.ObjectInWorld.create(4,4,7) - _, obj_4 = object_in_world.ObjectInWorld.create(-4,-4,5) - _, obj_5 = object_in_world.ObjectInWorld.create(5,5,9) - assert obj_1 is not None and obj_2 is not None and obj_3 is not None and obj_4 is not None \ - and obj_5 is not None + 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) + assert result + assert obj_2 is not None + + 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) + assert result + assert obj_4 is not None + + result, obj_5 = object_in_world.ObjectInWorld.create(5, 5, 9) + assert result + assert obj_5 is not None + detections = [obj_1, obj_2, obj_3, obj_4, obj_5] yield detections @@ -57,13 +88,26 @@ def detections_3(): """ Sample instances of ObjectInWorld for testing. """ - _, obj_1 = object_in_world.ObjectInWorld.create(0,0,8) - _, obj_2 = object_in_world.ObjectInWorld.create(0.5,0.5,4) - _, obj_3 = object_in_world.ObjectInWorld.create(-2,-2,2) - _, obj_4 = object_in_world.ObjectInWorld.create(3,3,10) - _, obj_5 = object_in_world.ObjectInWorld.create(-3,-3,6) - assert obj_1 is not None and obj_2 is not None and obj_3 is not None and obj_4 is not None \ - and obj_5 is not None + 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) + assert result + assert obj_2 is not None + + 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) + assert result + assert obj_4 is not None + + result, obj_5 = object_in_world.ObjectInWorld.create(-3, -3, 6) + assert result + assert obj_5 is not None + detections = [obj_1, obj_2, obj_3, obj_4, obj_5] yield detections @@ -73,8 +117,7 @@ class TestSimilar: Test if similar function correctly determines if 2 landing pads are close enough to be considered similar. """ - # Required for testing - # pylint: disable=protected-access + def test_is_similar_positive_equal_to_threshold(self): """ Test case where the second landing pad has positive coordinates and the distance between @@ -177,17 +220,17 @@ def test_is_similar_negative_more_than_threshold(self): assert actual == expected - # pylint: enable=protected-access class TestMarkFalsePositive: """ Test if landing pad tracking correctly marks a detection as a false positive. """ - # Required for testing - # pylint: disable=protected-access - def test_mark_false_positive_no_similar(self, - tracker: landing_pad_tracking.LandingPadTracking, - detections_1: "list[object_in_world.ObjectInWorld]"): + + def test_mark_false_positive_no_similar( + self, + tracker: landing_pad_tracking.LandingPadTracking, + detections_1: "list[object_in_world.ObjectInWorld]", + ): """ Test if marking false positive adds detection to list of false positives. """ @@ -209,9 +252,11 @@ def test_mark_false_positive_no_similar(self, assert tracker._LandingPadTracking__false_positives == expected # type: ignore assert tracker._LandingPadTracking__unconfirmed_positives == expected_unconfirmed_positives # type: ignore - def test_mark_false_positive_with_similar(self, - tracker: landing_pad_tracking.LandingPadTracking, - detections_2: "list[object_in_world.ObjectInWorld]"): + def test_mark_false_positive_with_similar( + self, + tracker: landing_pad_tracking.LandingPadTracking, + detections_2: "list[object_in_world.ObjectInWorld]", + ): """ Test if marking false positive adds detection to list of false positives and removes. similar landing pads @@ -228,9 +273,11 @@ def test_mark_false_positive_with_similar(self, assert tracker._LandingPadTracking__false_positives == expected # type: ignore assert tracker._LandingPadTracking__unconfirmed_positives == expected_unconfirmed_positives # type: ignore - def test_mark_multiple_false_positive(self, - tracker: landing_pad_tracking.LandingPadTracking, - detections_1: "list[object_in_world.ObjectInWorld]"): + def test_mark_multiple_false_positive( + self, + tracker: landing_pad_tracking.LandingPadTracking, + detections_1: "list[object_in_world.ObjectInWorld]", + ): """ Test if marking false positive adds detection to list of false positives. """ @@ -250,15 +297,12 @@ def test_mark_multiple_false_positive(self, assert tracker._LandingPadTracking__false_positives == expected # type: ignore assert tracker._LandingPadTracking__unconfirmed_positives == expected_unconfirmed_positives # type: ignore - # pylint: enable=protected-access - class TestMarkConfirmedPositive: """ Test if landing pad tracking correctly marks a detection as a confirmed positive. """ - # Required for testing - # pylint: disable=protected-access + def test_mark_confirmed_positive(self, tracker: landing_pad_tracking.LandingPadTracking): """ Test if marking confirmed positive adds detection to list of confirmed positives. @@ -272,8 +316,9 @@ def test_mark_confirmed_positive(self, tracker: landing_pad_tracking.LandingPadT assert tracker._LandingPadTracking__confirmed_positives == expected # type: ignore - def test_mark_multiple_confirmed_positives(self, - tracker: landing_pad_tracking.LandingPadTracking): + def test_mark_multiple_confirmed_positives( + self, tracker: landing_pad_tracking.LandingPadTracking + ): """ Test if marking confirmed positive adds detection to list of confirmed positives. """ @@ -290,15 +335,12 @@ def test_mark_multiple_confirmed_positives(self, assert tracker._LandingPadTracking__confirmed_positives == expected # type: ignore - # pylint: enable=protected-access - class TestLandingPadTracking: """ Test landing pad tracking run function. """ - # Required for testing - # pylint: disable=protected-access + def test_run_with_empty_detections_list(self, tracker: landing_pad_tracking.LandingPadTracking): """ Test run method with empty detections list. @@ -307,9 +349,11 @@ def test_run_with_empty_detections_list(self, tracker: landing_pad_tracking.Land assert not result assert actual is None - def test_run_one_input(self, - tracker: landing_pad_tracking.LandingPadTracking, - detections_1: "list[object_in_world.ObjectInWorld]"): + def test_run_one_input( + self, + tracker: landing_pad_tracking.LandingPadTracking, + detections_1: "list[object_in_world.ObjectInWorld]", + ): """ Test run with only 1 input. """ @@ -328,9 +372,11 @@ def test_run_one_input(self, assert actual == expected_output assert tracker._LandingPadTracking__unconfirmed_positives == expected_unconfirmed_positives # type: ignore - def test_run_one_input_similar_detections(self, - tracker: landing_pad_tracking.LandingPadTracking, - detections_3: "list[object_in_world.ObjectInWorld]"): + def test_run_one_input_similar_detections( + self, + tracker: landing_pad_tracking.LandingPadTracking, + detections_3: "list[object_in_world.ObjectInWorld]", + ): """ Test run with only 1 input where 2 landing pads are similar. """ @@ -348,10 +394,12 @@ def test_run_one_input_similar_detections(self, assert actual == expected_output assert tracker._LandingPadTracking__unconfirmed_positives == expected_unconfirmed_positives # type: ignore - def test_run_multiple_inputs(self, - tracker: landing_pad_tracking.LandingPadTracking, - detections_1: "list[object_in_world.ObjectInWorld]", - detections_2: "list[object_in_world.ObjectInWorld]"): + def test_run_multiple_inputs( + self, + tracker: landing_pad_tracking.LandingPadTracking, + detections_1: "list[object_in_world.ObjectInWorld]", + detections_2: "list[object_in_world.ObjectInWorld]", + ): """ Test run with 2 inputs where some landing pads are similar. """ @@ -374,9 +422,11 @@ def test_run_multiple_inputs(self, assert actual == expected_output assert tracker._LandingPadTracking__unconfirmed_positives == expected_unconfirmed_positives # type: ignore - def test_run_with_confirmed_positive(self, - tracker: landing_pad_tracking.LandingPadTracking, - detections_1: "list[object_in_world.ObjectInWorld]"): + def test_run_with_confirmed_positive( + self, + tracker: landing_pad_tracking.LandingPadTracking, + detections_1: "list[object_in_world.ObjectInWorld]", + ): """ Test run when there is a confirmed positive. """ @@ -391,9 +441,11 @@ def test_run_with_confirmed_positive(self, assert result assert actual == expected - def test_run_with_false_positive(self, - tracker: landing_pad_tracking.LandingPadTracking, - detections_2: "list[object_in_world.ObjectInWorld]"): + def test_run_with_false_positive( + self, + tracker: landing_pad_tracking.LandingPadTracking, + detections_2: "list[object_in_world.ObjectInWorld]", + ): """ Test to see if run function doesn't add landing pads that are similar to false positives. """ @@ -406,5 +458,3 @@ def test_run_with_false_positive(self, tracker.run(detections_2) assert tracker._LandingPadTracking__unconfirmed_positives == expected_unconfirmed_positives # type: ignore - - # pylint: enable=protected-access diff --git a/tests/test_video_input_worker.py b/tests/test_video_input_worker.py index 4a0c1a21..7cdc5430 100644 --- a/tests/test_video_input_worker.py +++ b/tests/test_video_input_worker.py @@ -39,7 +39,7 @@ while True: try: input_data: image_and_time.ImageAndTime = out_queue.queue.get_nowait() - assert str(type(input_data)) == "" + assert str(type(input_data)) == "" assert input_data.image is not None except queue.Empty: diff --git a/utilities/workers/queue_proxy_wrapper.py b/utilities/workers/queue_proxy_wrapper.py index 39ef7455..37bf6574 100644 --- a/utilities/workers/queue_proxy_wrapper.py +++ b/utilities/workers/queue_proxy_wrapper.py @@ -10,6 +10,7 @@ class QueueProxyWrapper: """ Wrapper for an underlying queue proxy which also stores maxsize. """ + __QUEUE_TIMEOUT = 0.1 # seconds __QUEUE_DELAY = 0.1 # seconds diff --git a/utilities/workers/worker_controller.py b/utilities/workers/worker_controller.py index dfa8ec18..8c8bb8b0 100644 --- a/utilities/workers/worker_controller.py +++ b/utilities/workers/worker_controller.py @@ -10,6 +10,7 @@ class WorkerController: For interprocess communication from main to worker. Contains exit and pause requests. """ + __QUEUE_DELAY = 0.1 # seconds def __init__(self): diff --git a/utilities/workers/worker_manager.py b/utilities/workers/worker_manager.py index 26e74415..d584e1a4 100644 --- a/utilities/workers/worker_manager.py +++ b/utilities/workers/worker_manager.py @@ -9,6 +9,7 @@ class WorkerManager: For interprocess communication from main to worker. Contains exit and pause requests. """ + def __init__(self, workers: "list[mp.Process] | None" = None): """ Constructor creates internal queue and semaphore. From 0e9fdce9ad4342ad92b5e1c2f57461c6e0fd4834 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sun, 18 Feb 2024 00:27:36 -0500 Subject: [PATCH 02/18] Lint pass 2 and move Pylint to settings --- documentation/main_multiprocess_example.py | 21 +++++- .../add_random/add_random.py | 21 ++---- .../add_random/add_random_worker.py | 2 - .../concatenator/concatenator.py | 2 - .../multiprocess_example/countup/countup.py | 2 - .../intermediate_struct.py | 2 - documentation/tests/test_add_or_multiply.py | 2 - main_2023.py | 17 ++--- main_2024.py | 12 ++-- .../cluster_estimation_worker.py | 2 - modules/data_merge/data_merge_worker.py | 3 + modules/decision/decision.py | 4 -- modules/decision_command.py | 2 - modules/detect_target/detect_target.py | 6 -- modules/detect_target/detect_target_worker.py | 2 - modules/detection_in_world.py | 4 -- modules/detections_and_time.py | 41 ++++++------ modules/drone_odometry_local.py | 10 +-- modules/flight_interface/flight_interface.py | 2 - modules/geolocation/camera_properties.py | 6 -- modules/geolocation/geolocation.py | 4 -- modules/geolocation/geolocation_worker.py | 2 - modules/image_and_time.py | 2 - modules/merged_odometry_detections.py | 2 - modules/object_in_world.py | 2 - modules/odometry_and_time.py | 2 - modules/video_input/video_input.py | 2 - pyproject.toml | 67 +++++++++++++++++++ pytest.ini | 2 - tests/model_example/generate_expected.py | 15 ++++- tests/test_camera_properties.py | 3 - tests/test_cluster_detection.py | 24 +++---- tests/test_data_merge_worker.py | 13 +++- tests/test_detect_target.py | 18 ++--- tests/test_detect_target_worker.py | 15 ++++- tests/test_flight_interface_hardware.py | 13 +++- tests/test_flight_interface_worker.py | 16 ++++- tests/test_geolocation.py | 3 - tests/test_geolocation_worker.py | 16 ++++- tests/test_video_input_hardware.py | 13 +++- tests/test_video_input_worker.py | 13 +++- 41 files changed, 250 insertions(+), 160 deletions(-) create mode 100644 pyproject.toml delete mode 100644 pytest.ini diff --git a/documentation/main_multiprocess_example.py b/documentation/main_multiprocess_example.py index 19ad4882..c62dc002 100644 --- a/documentation/main_multiprocess_example.py +++ b/documentation/main_multiprocess_example.py @@ -1,5 +1,8 @@ """ -Main process. +Main process. To run: +``` +python -m documentation.main_multiprocess_example +``` """ import multiprocessing as mp import time @@ -17,8 +20,11 @@ ADD_RANDOM_TO_CONCATENATOR_QUEUE_MAX_SIZE = 5 -# Command: python -m documentation.main_multiprocess_example -if __name__ == "__main__": +# main() is required for early return +def main() -> int: + """ + Main function. + """ # Main is managing all worker processes and is responsible # for creating supporting interprocess communication controller = worker_controller.WorkerController() @@ -143,4 +149,13 @@ # Alternatively, create a new WorkerController instance controller.clear_exit() + return 0 + + +# Main guard is only used to call main() +if __name__ == "__main__": + result_main = main() + if result_main < 0: + print(f"ERROR: Status code: {result_main}") + print("Done!") diff --git a/documentation/multiprocess_example/add_random/add_random.py b/documentation/multiprocess_example/add_random/add_random.py index 2bd5c45a..030bdfdd 100644 --- a/documentation/multiprocess_example/add_random/add_random.py +++ b/documentation/multiprocess_example/add_random/add_random.py @@ -7,8 +7,6 @@ from .. import intermediate_struct -# This class does very little, but still has state -# pylint: disable-next=too-few-public-methods class AddRandom: """ Adds a random number to the input. @@ -23,18 +21,12 @@ def __init__(self, seed: int, max_random_term: int, add_change_count: int): """ random.seed(seed) # Maximum value that can be added - # Constant within class does not follow Pylint naming - # pylint: disable=invalid-name - self.__MAX_RANDOM_TERM = max_random_term - # pylint: enable=invalid-name + self.__max_random_term = max_random_term # Number of adds before getting a new random number - # Constant within class does not follow Pylint naming - # pylint: disable=invalid-name - self.__ADD_CHANGE_COUNT = add_change_count - # pylint: enable=invalid-name + self.__add_change_count = add_change_count - self.__current_random_term = self.__generate_random_number(0, self.__MAX_RANDOM_TERM) + self.__current_random_term = self.__generate_random_number(0, self.__max_random_term) self.__add_count = 0 @staticmethod @@ -52,8 +44,8 @@ def run_add_random(self, term: int) -> "tuple[bool, intermediate_struct.Intermed # Change the random term if the add count has been reached self.__add_count += 1 - if self.__add_count >= self.__ADD_CHANGE_COUNT: - self.__current_random_term = self.__generate_random_number(0, self.__MAX_RANDOM_TERM) + if self.__add_count >= self.__add_change_count: + self.__current_random_term = self.__generate_random_number(0, self.__max_random_term) self.__add_count = 0 # Pretending this class is hard at work @@ -63,10 +55,7 @@ def run_add_random(self, term: int) -> "tuple[bool, intermediate_struct.Intermed if add_sum % 2 == 0: add_string = "even" - # For some reason Pylint hates having more than 1 parameter in a constructor - # pylint: disable=too-many-function-args output = intermediate_struct.IntermediateStruct(add_sum, add_string) - # pylint: enable=too-many-function-args # Function returns result and the output # The class is responsible for packing the intermediate type diff --git a/documentation/multiprocess_example/add_random/add_random_worker.py b/documentation/multiprocess_example/add_random/add_random_worker.py index 875c8518..512a162e 100644 --- a/documentation/multiprocess_example/add_random/add_random_worker.py +++ b/documentation/multiprocess_example/add_random/add_random_worker.py @@ -7,8 +7,6 @@ from . import add_random -# Worker has both class and control parameters -# pylint: disable-next=too-many-arguments def add_random_worker( seed: int, max_random_term: int, diff --git a/documentation/multiprocess_example/concatenator/concatenator.py b/documentation/multiprocess_example/concatenator/concatenator.py index e50cb188..1adae6d2 100644 --- a/documentation/multiprocess_example/concatenator/concatenator.py +++ b/documentation/multiprocess_example/concatenator/concatenator.py @@ -6,8 +6,6 @@ from .. import intermediate_struct -# This class does very little, but still has state -# pylint: disable-next=too-few-public-methods class Concatenator: """ Concatenates a prefix and suffix to the object. diff --git a/documentation/multiprocess_example/countup/countup.py b/documentation/multiprocess_example/countup/countup.py index 30cc200f..e458b739 100644 --- a/documentation/multiprocess_example/countup/countup.py +++ b/documentation/multiprocess_example/countup/countup.py @@ -4,8 +4,6 @@ import time -# This class does very little, but still has state -# pylint: disable-next=too-few-public-methods class Countup: """ Increments its internal counter and outputs current counter. diff --git a/documentation/multiprocess_example/intermediate_struct.py b/documentation/multiprocess_example/intermediate_struct.py index ac1b7730..07c712ea 100644 --- a/documentation/multiprocess_example/intermediate_struct.py +++ b/documentation/multiprocess_example/intermediate_struct.py @@ -3,8 +3,6 @@ """ -# This class is just a struct containing some members -# pylint: disable-next=too-few-public-methods class IntermediateStruct: """ Example of a simple struct. diff --git a/documentation/tests/test_add_or_multiply.py b/documentation/tests/test_add_or_multiply.py index 02382ba6..a337e793 100644 --- a/documentation/tests/test_add_or_multiply.py +++ b/documentation/tests/test_add_or_multiply.py @@ -132,8 +132,6 @@ def test_add_negative(self, adder: add_or_multiply.AddOrMultiply): # * Large negative with large negative -# Test class -# pylint: disable-next=too-few-public-methods class TestMultiply: """ Many multiplication cases need to be covered as well. diff --git a/main_2023.py b/main_2023.py index 1ca237df..31c9f80c 100644 --- a/main_2023.py +++ b/main_2023.py @@ -18,11 +18,11 @@ CONFIG_FILE_PATH = pathlib.Path("config.yaml") -# Main function -# pylint: disable-next=too-many-locals +# Code copied into main_2024.py +# pylint: disable=duplicate-code def main() -> int: """ - Main function for airside code. + Main function. """ # Open config file try: @@ -131,11 +131,12 @@ def main() -> int: return 0 +# pylint: enable=duplicate-code + + if __name__ == "__main__": - # Not a constant - # pylint: disable-next=invalid-name - result_run = main() - if result_run < 0: - print(f"ERROR: Status code: {result_run}") + result_main = main() + if result_main < 0: + print(f"ERROR: Status code: {result_main}") print("Done!") diff --git a/main_2024.py b/main_2024.py index 3c4859b5..ed90bcea 100644 --- a/main_2024.py +++ b/main_2024.py @@ -24,11 +24,9 @@ CONFIG_FILE_PATH = pathlib.Path("config.yaml") -# Main function -# pylint: disable-next=too-many-locals,too-many-statements def main() -> int: """ - Main function for airside code. + Main function. """ # Open config file try: @@ -217,10 +215,8 @@ def main() -> int: if __name__ == "__main__": - # Not a constant - # pylint: disable-next=invalid-name - result_run = main() - if result_run < 0: - print(f"ERROR: Status code: {result_run}") + result_main = main() + if result_main < 0: + print(f"ERROR: Status code: {result_main}") print("Done!") diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index e3fad44c..1d0b5afa 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -7,8 +7,6 @@ from . import cluster_estimation -# Worker has both class and control parameters -# pylint: disable-next=too-many-arguments def cluster_estimation_worker( min_activation_threshold: int, min_new_points_to_run: int, diff --git a/modules/data_merge/data_merge_worker.py b/modules/data_merge/data_merge_worker.py index 085cd398..2987f54a 100644 --- a/modules/data_merge/data_merge_worker.py +++ b/modules/data_merge/data_merge_worker.py @@ -80,4 +80,7 @@ def data_merge_worker( if not result: continue + # Get Pylance to stop complaining + assert merged is not None + output_queue.queue.put(merged) diff --git a/modules/decision/decision.py b/modules/decision/decision.py index 26e02bd6..ce906155 100644 --- a/modules/decision/decision.py +++ b/modules/decision/decision.py @@ -7,8 +7,6 @@ from .. import odometry_and_time -# Basically a struct -# pylint: disable-next=too-few-public-methods class ScoredLandingPad: """ Landing pad with score for decision. @@ -19,8 +17,6 @@ def __init__(self, landing_pad: object_in_world.ObjectInWorld, score: float): self.score = score -# This class still has state -# pylint: disable-next=too-few-public-methods class Decision: """ Chooses next action to take based on known landing pad information. diff --git a/modules/decision_command.py b/modules/decision_command.py index c7933279..faa4f453 100644 --- a/modules/decision_command.py +++ b/modules/decision_command.py @@ -107,8 +107,6 @@ def create_land_at_absolute_position_command( absolute_z, ) - # Create key required - # pylint: disable-next=too-many-arguments def __init__( self, class_private_create_key, diff --git a/modules/detect_target/detect_target.py b/modules/detect_target/detect_target.py index 6b2676b3..9d532353 100644 --- a/modules/detect_target/detect_target.py +++ b/modules/detect_target/detect_target.py @@ -10,15 +10,11 @@ from .. import detections_and_time -# This is just an interface -# pylint: disable-next=too-few-public-methods class DetectTarget: """ Contains the YOLOv8 model for prediction. """ - # Required for logging - # pylint: disable-next=too-many-arguments def __init__( self, device: "str | int", @@ -45,8 +41,6 @@ def __init__( if save_name != "": self.__filename_prefix = save_name + "_" + str(int(time.time())) + "_" - # Required for logging - # pylint: disable-next=too-many-locals def run( self, data: image_and_time.ImageAndTime ) -> "tuple[bool, detections_and_time.DetectionsAndTime | None]": diff --git a/modules/detect_target/detect_target_worker.py b/modules/detect_target/detect_target_worker.py index 9add47a1..63f2fce9 100644 --- a/modules/detect_target/detect_target_worker.py +++ b/modules/detect_target/detect_target_worker.py @@ -7,8 +7,6 @@ from . import detect_target -# Worker has both class and control parameters -# pylint: disable-next=too-many-arguments def detect_target_worker( device: "str | int", model_path: str, diff --git a/modules/detection_in_world.py b/modules/detection_in_world.py index 2aca6aca..f4ae1d89 100644 --- a/modules/detection_in_world.py +++ b/modules/detection_in_world.py @@ -5,8 +5,6 @@ import numpy as np -# Basically a struct -# pylint: disable-next=too-few-public-methods class DetectionInWorld: """ Typically on the ground. @@ -37,8 +35,6 @@ def create( return True, DetectionInWorld(cls.__create_key, vertices, centre, label, confidence) - # Create key required - # pylint: disable-next=too-many-arguments def __init__( self, class_private_create_key, diff --git a/modules/detections_and_time.py b/modules/detections_and_time.py index 48491436..5ac28892 100644 --- a/modules/detections_and_time.py +++ b/modules/detections_and_time.py @@ -5,8 +5,6 @@ import numpy as np -# Basically a struct -# pylint: disable-next=too-few-public-methods class Detection: """ A detected object in image space. @@ -19,7 +17,7 @@ def create( cls, bounds: np.ndarray, label: int, confidence: float ) -> "tuple[bool, Detection | None]": """ - bounds are of form x1, y1, x2, y2 . + bounds are of form x_1, y_1, x_2, y_2 . """ # Check every element in bounds is >= 0.0 if bounds.shape != (4,) or not np.greater_equal(bounds, 0.0).all(): @@ -43,13 +41,11 @@ def __init__(self, class_private_create_key, bounds: np.ndarray, label: int, con """ assert class_private_create_key is Detection.__create_key, "Use create() method" - # Mixing letters and numbers confuses Pylint - # pylint: disable=invalid-name - self.x1 = bounds[0] - self.y1 = bounds[1] - self.x2 = bounds[2] - self.y2 = bounds[3] - # pylint: enable=invalid-name + self.x_1 = bounds[0] + self.y_1 = bounds[1] + self.x_2 = bounds[2] + self.y_2 = bounds[3] + self.label = label self.confidence = confidence @@ -60,13 +56,13 @@ def __repr__(self) -> str: + ", conf: " + str(self.confidence) + ", bounds: " - + str(self.x1) + + str(self.x_1) + " " - + str(self.y1) + + str(self.y_1) + " " - + str(self.x2) + + str(self.x_2) + " " - + str(self.y2) + + str(self.y_2) ) return representation @@ -75,23 +71,21 @@ def get_centre(self) -> "tuple[float, float]": """ Gets the xy centre of the bounding box. """ - centre_x = (self.x1 + self.x2) / 2 - centre_y = (self.y1 + self.y2) / 2 + centre_x = (self.x_1 + self.x_2) / 2 + centre_y = (self.y_1 + self.y_2) / 2 return centre_x, centre_y def get_corners(self) -> "list[tuple[float, float]]": """ Gets the xy corners of the bounding box. """ - top_left = self.x1, self.y1 - top_right = self.x2, self.y1 - bottom_left = self.x1, self.y2 - bottom_right = self.x2, self.y2 + top_left = self.x_1, self.y_1 + top_right = self.x_2, self.y_1 + bottom_left = self.x_1, self.y_2 + bottom_right = self.x_2, self.y_2 return [top_left, top_right, bottom_left, bottom_right] -# Basically a struct -# pylint: disable-next=too-few-public-methods class DetectionsAndTime: """ Contains detected object and timestamp. @@ -120,6 +114,9 @@ def __init__(self, class_private_create_key, timestamp: float): self.timestamp = timestamp def __repr__(self) -> str: + """ + String representation. + """ representation = ( str(self.__class__) + ", time: " diff --git a/modules/drone_odometry_local.py b/modules/drone_odometry_local.py index 81be5519..f5c88276 100644 --- a/modules/drone_odometry_local.py +++ b/modules/drone_odometry_local.py @@ -5,8 +5,6 @@ from .common.mavlink.modules import drone_odometry -# Basically a struct -# pylint: disable-next=too-few-public-methods class DronePositionLocal: """ Drone position in NED system. @@ -19,7 +17,7 @@ def create( cls, north: float, east: float, down: float ) -> "tuple[bool, DronePositionLocal | None]": """ - north, east, down in metres. + North, east, down in metres. """ return True, DronePositionLocal(cls.__create_key, north, east, down) @@ -34,8 +32,6 @@ def __init__(self, class_private_create_key, north: float, east: float, down: fl self.down = down -# Basically a struct -# pylint: disable-next=too-few-public-methods class DroneOrientationLocal: """ Wrapper for DroneOrientation as it is the same in both local and global space. @@ -48,7 +44,7 @@ def create_new( cls, yaw: float, pitch: float, roll: float ) -> "tuple[bool, DroneOrientationLocal | None]": """ - yaw, pitch, roll in radians. + Yaw, pitch, roll in radians. """ result, orientation = drone_odometry.DroneOrientation.create(yaw, pitch, roll) if not result: @@ -75,8 +71,6 @@ def __init__(self, class_private_create_key, orientation: drone_odometry.DroneOr self.orientation = orientation -# Basically a struct -# pylint: disable-next=too-few-public-methods class DroneOdometryLocal: """ Wrapper for DronePositionLocal and DroneOrientationLocal. diff --git a/modules/flight_interface/flight_interface.py b/modules/flight_interface/flight_interface.py index f3eaf0a5..c8ac7361 100644 --- a/modules/flight_interface/flight_interface.py +++ b/modules/flight_interface/flight_interface.py @@ -8,8 +8,6 @@ from ..common.mavlink.modules import flight_controller -# This is just an interface -# pylint: disable-next=too-few-public-methods class FlightInterface: """ Create flight controller and combines odometry data and timestamp. diff --git a/modules/geolocation/camera_properties.py b/modules/geolocation/camera_properties.py index 4aa5011e..a8a9f7e0 100644 --- a/modules/geolocation/camera_properties.py +++ b/modules/geolocation/camera_properties.py @@ -81,8 +81,6 @@ class CameraIntrinsics: __create_key = object() @classmethod - # Required for checks - # pylint: disable-next=too-many-return-statements def create( cls, resolution_x: int, resolution_y: int, fov_x: float, fov_y: float ) -> "tuple[bool, CameraIntrinsics | None]": @@ -118,8 +116,6 @@ def create( v_scalar, ) - # Create key required - # pylint: disable-next=too-many-arguments def __init__( self, class_private_create_key, @@ -204,8 +200,6 @@ def camera_space_from_image_space( return True, vec_camera -# Basically a struct -# pylint: disable-next=too-few-public-methods class CameraDroneExtrinsics: """ Camera in relation to drone. diff --git a/modules/geolocation/geolocation.py b/modules/geolocation/geolocation.py index 0aa162b6..12b10b0b 100644 --- a/modules/geolocation/geolocation.py +++ b/modules/geolocation/geolocation.py @@ -151,13 +151,10 @@ def __get_perspective_transform_matrix( src = np.array(self.__perspective_transform_sources, dtype=np.float32) dst = np.array(ground_points, dtype=np.float32) try: - # Pylint does not like the OpenCV module - # pylint: disable=no-member matrix = cv2.getPerspectiveTransform( # type: ignore src, dst, ) - # pylint: enable=no-member # All exceptions must be caught and logged as early as possible # pylint: disable-next=bare-except except: @@ -167,7 +164,6 @@ def __get_perspective_transform_matrix( return True, matrix @staticmethod - # pylint: disable-next=too-many-locals def __convert_detection_to_world_from_image( detection: detections_and_time.Detection, perspective_transform_matrix: np.ndarray ) -> "tuple[bool, detection_in_world.DetectionInWorld | None]": diff --git a/modules/geolocation/geolocation_worker.py b/modules/geolocation/geolocation_worker.py index 97811a94..e1f2968d 100644 --- a/modules/geolocation/geolocation_worker.py +++ b/modules/geolocation/geolocation_worker.py @@ -8,8 +8,6 @@ from . import geolocation -# Worker has both class and control parameters -# pylint: disable-next=too-many-arguments def geolocation_worker( camera_intrinsics: camera_properties.CameraIntrinsics, camera_drone_extrinsics: camera_properties.CameraDroneExtrinsics, diff --git a/modules/image_and_time.py b/modules/image_and_time.py index 76cc4f10..71d9adae 100644 --- a/modules/image_and_time.py +++ b/modules/image_and_time.py @@ -6,8 +6,6 @@ import numpy as np -# Basically a struct -# pylint: disable-next=too-few-public-methods class ImageAndTime: """ Contains image and timestamp. diff --git a/modules/merged_odometry_detections.py b/modules/merged_odometry_detections.py index 8cf9af7c..b630dc51 100644 --- a/modules/merged_odometry_detections.py +++ b/modules/merged_odometry_detections.py @@ -6,8 +6,6 @@ from . import drone_odometry_local -# Basically a struct -# pylint: disable-next=too-few-public-methods class MergedOdometryDetections: """ Contains odometry/telemetry and detections merged by closest timestamp. diff --git a/modules/object_in_world.py b/modules/object_in_world.py index dc1561db..4dd3e7de 100644 --- a/modules/object_in_world.py +++ b/modules/object_in_world.py @@ -3,8 +3,6 @@ """ -# Basically a struct -# pylint: disable-next=too-few-public-methods class ObjectInWorld: """ Contains the estimated location of the object in local coordinates. diff --git a/modules/odometry_and_time.py b/modules/odometry_and_time.py index bf22e853..4d7f7294 100644 --- a/modules/odometry_and_time.py +++ b/modules/odometry_and_time.py @@ -6,8 +6,6 @@ from . import drone_odometry_local -# Basically a struct -# pylint: disable-next=too-few-public-methods class OdometryAndTime: """ Contains odometry/telemetry and timestamp. diff --git a/modules/video_input/video_input.py b/modules/video_input/video_input.py index 1e3cc15a..f0432ff2 100644 --- a/modules/video_input/video_input.py +++ b/modules/video_input/video_input.py @@ -6,8 +6,6 @@ from .. import image_and_time -# This is just an interface -# pylint: disable-next=too-few-public-methods class VideoInput: """ Combines image and timestamp together. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..1e1edd96 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,67 @@ +[tool.pylint.main] +# Minimum Python version to use for version dependent checks. Will default to the +# version used to run pylint. +py-version = "3.8" + +# [tool.pylint.basic] +# Good variable names which should always be accepted, separated by a comma. +good-names = [ + "i", + "j", + "k", + "ex", + "Run", + "_", + "result_main", # Return of main() +] + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs = "computer-vision-python" + +[tool.pylint."messages control"] +# Disable the message, report, category or checker with the given id(s). You can +# either give multiple identifiers separated by comma (,) or put this option +# multiple times (only on the command line, not in the configuration file where +# it should appear only once). You can also use "--disable=all" to disable +# everything first and then re-enable specific checks. For example, if you want +# to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable = [ + "raw-checker-failed", + "bad-inline-option", + "locally-disabled", + "file-ignored", + "suppressed-message", + "useless-suppression", + "deprecated-pragma", + "use-symbolic-message-instead", + "use-implicit-booleaness-not-comparison-to-string", + "use-implicit-booleaness-not-comparison-to-zero", + "fixme", # Ignore TODOs + "import-error", # Pylint cannot find modules + "line-too-long", # Covered by black formatter + "no-member", # Pylint cannot handle 3rd party imports + "too-few-public-methods", # Some classes are simple + "too-many-arguments", # Function signatures + "too-many-lines", # Line count in file + "too-many-locals", # Don't care + "too-many-statements", # Don't care + "too-many-return-statements", # Don't care +] + +[tool.pylint.similarities] +# Minimum lines number of a similarity. +min-similarity-lines = 10 # Main guard + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "--ignore=modules/common/" + +[tool.black] +line-length = 100 +target-version = ["py38"] +# Excludes files or directories in addition to the defaults +extend-exclude = "modules/common/*" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 05d29aa8..00000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -addopts = --ignore=2022/ --ignore=modules/common/ --ignore=test_main.py diff --git a/tests/model_example/generate_expected.py b/tests/model_example/generate_expected.py index 26182be2..e8b24b5d 100644 --- a/tests/model_example/generate_expected.py +++ b/tests/model_example/generate_expected.py @@ -22,7 +22,10 @@ ZIDANE_BOUNDING_BOX_PATH = pathlib.Path(TEST_PATH, "bounding_box_zidane.txt") -if __name__ == "__main__": +def main() -> int: + """ + Main function. + """ model = ultralytics.YOLO(MODEL_PATH) image_bus = cv2.imread(BUS_IMAGE_PATH) # type: ignore image_zidane = cv2.imread(ZIDANE_IMAGE_PATH) # type: ignore @@ -62,8 +65,16 @@ predictions_zidane = np.insert(bounding_box_zidane, 0, [conf_zidane, labels_zidane], axis=1) # Save expected to text file - # Format: [confidence, label, x1, y1, x2, y2] + # Format: [confidence, label, x_1, y_1, x_2, y_2] np.savetxt(BUS_BOUNDING_BOX_PATH, predictions_bus) np.savetxt(ZIDANE_BOUNDING_BOX_PATH, predictions_zidane) + return 0 + + +if __name__ == "__main__": + result_main = main() + if result_main < 0: + print(f"ERROR: Status code: {result_main}") + print("Done!") diff --git a/tests/test_camera_properties.py b/tests/test_camera_properties.py index a25dabd0..6ca73953 100644 --- a/tests/test_camera_properties.py +++ b/tests/test_camera_properties.py @@ -1,6 +1,3 @@ -# Large test file -# No enable -# pylint: disable=too-many-lines """ Test camera intrinsics and extrinsics. """ diff --git a/tests/test_cluster_detection.py b/tests/test_cluster_detection.py index 342208cf..6195149d 100644 --- a/tests/test_cluster_detection.py +++ b/tests/test_cluster_detection.py @@ -396,14 +396,15 @@ def test_detect_consecutive_inputs_single_cluster( generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) # Run - detections_in_world = [] + result_latest = False + detections_in_world_latest = [] for point in generated_detections: - result, detections_in_world = cluster_model.run([point], False) - assert result - assert detections_in_world is not None + result_latest, detections_in_world_latest = cluster_model.run([point], False) # Test - assert len(detections_in_world) == expected_cluster_count + assert result_latest + assert detections_in_world_latest is not None + assert len(detections_in_world_latest) == expected_cluster_count def test_detect_consecutive_inputs_five_clusters( self, cluster_model: cluster_estimation.ClusterEstimation @@ -419,18 +420,17 @@ def test_detect_consecutive_inputs_five_clusters( generated_detections, _ = generate_cluster_data(points_per_cluster, self.__STD_DEV_REGULAR) # Run - detections_in_world = [] + result_latest = False + detections_in_world_latest = [] for point in generated_detections: - result, detections_in_world = cluster_model.run([point], False) - assert result - assert detections_in_world is not None + result_latest, detections_in_world_latest = cluster_model.run([point], False) # Test - assert len(detections_in_world) == expected_cluster_count + assert result_latest + assert detections_in_world_latest is not None + assert len(detections_in_world_latest) == expected_cluster_count -# Test class -# pylint: disable-next=too-few-public-methods class TestCorrectClusterPositionOutput: """ Tests if cluster estimation output falls within acceptable distance to diff --git a/tests/test_data_merge_worker.py b/tests/test_data_merge_worker.py index 0891c18b..3f492c22 100644 --- a/tests/test_data_merge_worker.py +++ b/tests/test_data_merge_worker.py @@ -69,7 +69,10 @@ def simulate_flight_input_worker( odometry_queue.queue.put(odometry_time) -if __name__ == "__main__": +def main() -> int: + """ + Main function. + """ # Setup controller = worker_controller.WorkerController() @@ -134,4 +137,12 @@ def simulate_flight_input_worker( odometry_in_queue.fill_and_drain_queue() worker.join() + return 0 + + +if __name__ == "__main__": + result_main = main() + if result_main < 0: + print(f"ERROR: Status code: {result_main}") + print("Done!") diff --git a/tests/test_detect_target.py b/tests/test_detect_target.py index 429472bc..8c7835da 100644 --- a/tests/test_detect_target.py +++ b/tests/test_detect_target.py @@ -54,26 +54,26 @@ def compare_detections( ) np.testing.assert_almost_equal( - actual_detection.x1, - expected_detection.x1, + actual_detection.x_1, + expected_detection.x_1, decimal=BOUNDING_BOX_PRECISION_TOLERANCE, ) np.testing.assert_almost_equal( - actual_detection.y1, - expected_detection.y1, + actual_detection.y_1, + expected_detection.y_1, decimal=BOUNDING_BOX_PRECISION_TOLERANCE, ) np.testing.assert_almost_equal( - actual_detection.x2, - expected_detection.x2, + actual_detection.x_2, + expected_detection.x_2, decimal=BOUNDING_BOX_PRECISION_TOLERANCE, ) np.testing.assert_almost_equal( - actual_detection.y2, - expected_detection.y2, + actual_detection.y_2, + expected_detection.y_2, decimal=BOUNDING_BOX_PRECISION_TOLERANCE, ) @@ -81,7 +81,7 @@ def compare_detections( def create_detections(detections_from_file: np.ndarray) -> detections_and_time.DetectionsAndTime: """ Create DetectionsAndTime from expected. - Format: [confidence, label, x1, y1, x2, y2] . + Format: [confidence, label, x_1, y_1, x_2, y_2] . """ assert detections_from_file.shape[1] == 6 diff --git a/tests/test_detect_target_worker.py b/tests/test_detect_target_worker.py index 57c9b1e4..cb06cba4 100644 --- a/tests/test_detect_target_worker.py +++ b/tests/test_detect_target_worker.py @@ -35,14 +35,17 @@ def simulate_previous_worker( """ Place the image into the queue. """ - image = cv2.imread(image_path) # type: ignore + image = cv2.imread(str(image_path)) # type: ignore result, value = image_and_time.ImageAndTime.create(image) assert result assert value is not None in_queue.queue.put(value) -if __name__ == "__main__": +def main() -> int: + """ + Main function. + """ # Setup # Not a constant # pylint: disable-next=invalid-name @@ -99,4 +102,12 @@ def simulate_previous_worker( image_in_queue.fill_and_drain_queue() worker.join() + return 0 + + +if __name__ == "__main__": + result_main = main() + if result_main < 0: + print(f"ERROR: Status code: {result_main}") + print("Done!") diff --git a/tests/test_flight_interface_hardware.py b/tests/test_flight_interface_hardware.py index cac9ba98..35616a8f 100644 --- a/tests/test_flight_interface_hardware.py +++ b/tests/test_flight_interface_hardware.py @@ -9,7 +9,10 @@ FLIGHT_INTERFACE_TIMEOUT = 10.0 # seconds -if __name__ == "__main__": +def main() -> int: + """ + Main function. + """ # Setup result, interface = flight_interface.FlightInterface.create( MAVLINK_CONNECTION_ADDRESS, @@ -25,4 +28,12 @@ assert result assert odometry_time is not None + return 0 + + +if __name__ == "__main__": + result_main = main() + if result_main < 0: + print(f"ERROR: Status code: {result_main}") + print("Done!") diff --git a/tests/test_flight_interface_worker.py b/tests/test_flight_interface_worker.py index f0e2f985..dd835e43 100644 --- a/tests/test_flight_interface_worker.py +++ b/tests/test_flight_interface_worker.py @@ -1,5 +1,5 @@ """ -Test worker process. +To test, start Mission Planner and forward MAVLink over TCP. """ import multiprocessing as mp import queue @@ -16,8 +16,10 @@ FLIGHT_INTERFACE_WORKER_PERIOD = 0.1 # seconds -# To test, start Mission Planner and forward MAVLink over TCP -if __name__ == "__main__": +def main() -> int: + """ + Main function. + """ # Setup controller = worker_controller.WorkerController() @@ -56,4 +58,12 @@ # Teardown worker.join() + return 0 + + +if __name__ == "__main__": + result_main = main() + if result_main < 0: + print(f"ERROR: Status code: {result_main}") + print("Done!") diff --git a/tests/test_geolocation.py b/tests/test_geolocation.py index a89d73d4..93087411 100644 --- a/tests/test_geolocation.py +++ b/tests/test_geolocation.py @@ -1,6 +1,3 @@ -# Large test file -# No enable -# pylint: disable=too-many-lines """ Test geolocation. """ diff --git a/tests/test_geolocation_worker.py b/tests/test_geolocation_worker.py index 3557e701..e63e2d65 100644 --- a/tests/test_geolocation_worker.py +++ b/tests/test_geolocation_worker.py @@ -62,8 +62,13 @@ def simulate_previous_worker(in_queue: queue_proxy_wrapper.QueueProxyWrapper): in_queue.queue.put(merged) -if __name__ == "__main__": +def main() -> int: + """ + Main function. + """ # Setup + # Similar to Geolocation tests + # pylint: disable=duplicate-code result, camera_intrinsics = camera_properties.CameraIntrinsics.create( 2000, 2000, @@ -79,6 +84,7 @@ def simulate_previous_worker(in_queue: queue_proxy_wrapper.QueueProxyWrapper): ) assert result assert camera_extrinsics is not None + # pylint: enable=duplicate-code controller = worker_controller.WorkerController() @@ -122,4 +128,12 @@ def simulate_previous_worker(in_queue: queue_proxy_wrapper.QueueProxyWrapper): detection_in_queue.fill_and_drain_queue() worker.join() + return 0 + + +if __name__ == "__main__": + result_main = main() + if result_main < 0: + print(f"ERROR: Status code: {result_main}") + print("Done!") diff --git a/tests/test_video_input_hardware.py b/tests/test_video_input_hardware.py index 16f19644..db53caf0 100644 --- a/tests/test_video_input_hardware.py +++ b/tests/test_video_input_hardware.py @@ -8,7 +8,10 @@ CAMERA = 0 -if __name__ == "__main__": +def main() -> int: + """ + Main function. + """ # Setup camera = video_input.VideoInput( CAMERA, @@ -21,4 +24,12 @@ assert result assert image is not None + return 0 + + +if __name__ == "__main__": + result_main = main() + if result_main < 0: + print(f"ERROR: Status code: {result_main}") + print("Done!") diff --git a/tests/test_video_input_worker.py b/tests/test_video_input_worker.py index 7cdc5430..8bf3f2c8 100644 --- a/tests/test_video_input_worker.py +++ b/tests/test_video_input_worker.py @@ -15,7 +15,10 @@ CAMERA = 0 -if __name__ == "__main__": +def main() -> int: + """ + Main function. + """ # Setup controller = worker_controller.WorkerController() @@ -48,4 +51,12 @@ # Teardown worker.join() + return 0 + + +if __name__ == "__main__": + result_main = main() + if result_main < 0: + print(f"ERROR: Status code: {result_main}") + print("Done!") From 0d9d20c202b32de8a7a6d392ae046a5dc9b15d33 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sun, 18 Feb 2024 00:39:08 -0500 Subject: [PATCH 03/18] CI --- .github/workflows/run-tests.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index aca9d927..a60f7853 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -36,6 +36,14 @@ jobs: pip install -r requirements.txt pip install -r requirements-pytorch.txt - # Run tests with PyTest - - name: Run tests + # Run Black formatter inn check mode + - name: Black formatter check + run: black --check . + + # Run Pylint linter + - name: Pylint linter + run: pylint . + + # Run unit tests with PyTest + - name: Run unit tests run: pytest -vv From 347fb37c481de994263d6b595e1833cf565534f1 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sun, 18 Feb 2024 01:24:55 -0500 Subject: [PATCH 04/18] Testing --- .github/workflows/run-tests.yml | 1 + modules/drone_odometry_local.py | 33 +++++++++++++++++++++++++++ modules/merged_odometry_detections.py | 11 +++++++++ modules/odometry_and_time.py | 9 ++++++++ tests/test_data_merge_worker.py | 6 +---- 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index a60f7853..3a4e36e3 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -35,6 +35,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-pytorch.txt + pip list # Run Black formatter inn check mode - name: Black formatter check diff --git a/modules/drone_odometry_local.py b/modules/drone_odometry_local.py index f5c88276..1446106d 100644 --- a/modules/drone_odometry_local.py +++ b/modules/drone_odometry_local.py @@ -31,6 +31,19 @@ def __init__(self, class_private_create_key, north: float, east: float, down: fl self.east = east self.down = down + def __repr__(self) -> str: + """ + String representation. + """ + return ( + "DronePositionLocal (NED): " + + str(self.north) + + ", " + + str(self.east) + + ", " + + str(self.down) + ) + class DroneOrientationLocal: """ @@ -70,6 +83,20 @@ def __init__(self, class_private_create_key, orientation: drone_odometry.DroneOr self.orientation = orientation + def __repr__(self) -> str: + """ + String representation. + """ + # TODO: Update common + return ( + "DroneOrientationLocal (YPR): " + + str(self.orientation.yaw) + + ", " + + str(self.orientation.pitch) + + ", " + + str(self.orientation.roll) + ) + class DroneOdometryLocal: """ @@ -106,3 +133,9 @@ def __init__( self.position = position self.orientation = orientation + + def __repr__(self) -> str: + """ + String representation. + """ + return "DroneOdometryLocal: " + str(self.position) + ", " + str(self.orientation) diff --git a/modules/merged_odometry_detections.py b/modules/merged_odometry_detections.py index b630dc51..706642f5 100644 --- a/modules/merged_odometry_detections.py +++ b/modules/merged_odometry_detections.py @@ -43,3 +43,14 @@ def __init__( self.odometry_local = odometry_local self.detections = detections + + def __repr__(self) -> str: + """ + String representation. + """ + representation = ( + "Merged: " + str(self.odometry_local) + ", detections: " + str(len(self.detections)) + ) + + representation += "\n" + str(self.detections) + return representation diff --git a/modules/odometry_and_time.py b/modules/odometry_and_time.py index 4d7f7294..2d5f8961 100644 --- a/modules/odometry_and_time.py +++ b/modules/odometry_and_time.py @@ -41,3 +41,12 @@ def __init__( self.odometry_data = odometry_data self.timestamp = timestamp + + def __repr__(self) -> str: + """ + String representation. + """ + representation = str(self.__class__) + ", time: " + str(int(self.timestamp)) + + representation += "\n" + repr(self.odometry_data) + return representation diff --git a/tests/test_data_merge_worker.py b/tests/test_data_merge_worker.py index 3f492c22..8a395d78 100644 --- a/tests/test_data_merge_worker.py +++ b/tests/test_data_merge_worker.py @@ -28,11 +28,7 @@ def simulate_detect_target_worker( assert result assert detections is not None - result, detection = detections_and_time.Detection.create( - np.array([0.0, 0.0, 1.0, 1.0]), - 0, - 1.0, - ) + result, detection = detections_and_time.Detection.create(np.array([0.0, 0.0, 1.0, 1.0]), 0, 1.0) assert result assert detection is not None From b13ab8b05166a18aac6d7e27b103079ab9c1e2e5 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sun, 18 Feb 2024 18:30:28 -0500 Subject: [PATCH 05/18] Black version made explicit --- documentation/main_multiprocess_example.py | 1 + documentation/multiprocess_example/add_random/add_random.py | 1 + .../multiprocess_example/concatenator/concatenator.py | 1 + documentation/multiprocess_example/countup/countup.py | 1 + documentation/tests/add_or_multiply.py | 1 + documentation/tests/test_add_or_multiply.py | 1 + main_2023.py | 1 + main_2024.py | 1 + modules/data_merge/data_merge_worker.py | 1 + modules/decision_command.py | 1 + modules/detect_target/detect_target.py | 1 + modules/flight_interface/flight_interface_worker.py | 1 + modules/image_and_time.py | 1 + modules/odometry_and_time.py | 1 + modules/video_input/video_input_worker.py | 1 + requirements.txt | 5 +++-- tests/model_example/generate_expected.py | 1 + tests/test_data_merge_worker.py | 1 + tests/test_detect_target.py | 1 + tests/test_detect_target_worker.py | 1 + tests/test_flight_interface_worker.py | 1 + tests/test_geolocation_worker.py | 1 + tests/test_video_input_worker.py | 1 + utilities/workers/queue_proxy_wrapper.py | 1 + utilities/workers/worker_controller.py | 1 + utilities/workers/worker_manager.py | 1 + 26 files changed, 28 insertions(+), 2 deletions(-) diff --git a/documentation/main_multiprocess_example.py b/documentation/main_multiprocess_example.py index c62dc002..0bbd46c4 100644 --- a/documentation/main_multiprocess_example.py +++ b/documentation/main_multiprocess_example.py @@ -4,6 +4,7 @@ python -m documentation.main_multiprocess_example ``` """ + import multiprocessing as mp import time diff --git a/documentation/multiprocess_example/add_random/add_random.py b/documentation/multiprocess_example/add_random/add_random.py index 030bdfdd..45a120ea 100644 --- a/documentation/multiprocess_example/add_random/add_random.py +++ b/documentation/multiprocess_example/add_random/add_random.py @@ -1,6 +1,7 @@ """ Contains the AddRandom class. """ + import time import random diff --git a/documentation/multiprocess_example/concatenator/concatenator.py b/documentation/multiprocess_example/concatenator/concatenator.py index 1adae6d2..1ed4fc3b 100644 --- a/documentation/multiprocess_example/concatenator/concatenator.py +++ b/documentation/multiprocess_example/concatenator/concatenator.py @@ -1,6 +1,7 @@ """ Contains the Concatenator class. """ + import time from .. import intermediate_struct diff --git a/documentation/multiprocess_example/countup/countup.py b/documentation/multiprocess_example/countup/countup.py index e458b739..1c8c3458 100644 --- a/documentation/multiprocess_example/countup/countup.py +++ b/documentation/multiprocess_example/countup/countup.py @@ -1,6 +1,7 @@ """ Contains the Countup class. """ + import time diff --git a/documentation/tests/add_or_multiply.py b/documentation/tests/add_or_multiply.py index 855665bd..797ec907 100644 --- a/documentation/tests/add_or_multiply.py +++ b/documentation/tests/add_or_multiply.py @@ -1,6 +1,7 @@ """ Simple class for Pytest example. """ + import enum diff --git a/documentation/tests/test_add_or_multiply.py b/documentation/tests/test_add_or_multiply.py index a337e793..9f1880e8 100644 --- a/documentation/tests/test_add_or_multiply.py +++ b/documentation/tests/test_add_or_multiply.py @@ -5,6 +5,7 @@ pytest ``` """ + import math import pytest diff --git a/main_2023.py b/main_2023.py index 31c9f80c..6b30caa1 100644 --- a/main_2023.py +++ b/main_2023.py @@ -1,6 +1,7 @@ """ For 2022-2023 UAS competition. """ + import argparse import multiprocessing as mp import pathlib diff --git a/main_2024.py b/main_2024.py index ed90bcea..a17c6e12 100644 --- a/main_2024.py +++ b/main_2024.py @@ -1,6 +1,7 @@ """ For 2023-2024 UAS competition. """ + import argparse import multiprocessing as mp import pathlib diff --git a/modules/data_merge/data_merge_worker.py b/modules/data_merge/data_merge_worker.py index 2987f54a..e5954e65 100644 --- a/modules/data_merge/data_merge_worker.py +++ b/modules/data_merge/data_merge_worker.py @@ -1,6 +1,7 @@ """ Merges detections and telemetry by time. """ + import queue from utilities.workers import queue_proxy_wrapper diff --git a/modules/decision_command.py b/modules/decision_command.py index faa4f453..8613e8b8 100644 --- a/modules/decision_command.py +++ b/modules/decision_command.py @@ -1,6 +1,7 @@ """ Commands for the decision module. """ + import enum diff --git a/modules/detect_target/detect_target.py b/modules/detect_target/detect_target.py index 9d532353..1069521c 100644 --- a/modules/detect_target/detect_target.py +++ b/modules/detect_target/detect_target.py @@ -1,6 +1,7 @@ """ Detects objects using the provided model. """ + import time import cv2 diff --git a/modules/flight_interface/flight_interface_worker.py b/modules/flight_interface/flight_interface_worker.py index 9de90ead..915a69ec 100644 --- a/modules/flight_interface/flight_interface_worker.py +++ b/modules/flight_interface/flight_interface_worker.py @@ -1,6 +1,7 @@ """ Gets odometry information from drone. """ + import time from utilities.workers import queue_proxy_wrapper diff --git a/modules/image_and_time.py b/modules/image_and_time.py index 71d9adae..0b308f16 100644 --- a/modules/image_and_time.py +++ b/modules/image_and_time.py @@ -1,6 +1,7 @@ """ Image and timestamp. """ + import time import numpy as np diff --git a/modules/odometry_and_time.py b/modules/odometry_and_time.py index 2d5f8961..0c4df80b 100644 --- a/modules/odometry_and_time.py +++ b/modules/odometry_and_time.py @@ -1,6 +1,7 @@ """ Drone odometry in local space and timestamp. """ + import time from . import drone_odometry_local diff --git a/modules/video_input/video_input_worker.py b/modules/video_input/video_input_worker.py index f76e2f92..be57b6be 100644 --- a/modules/video_input/video_input_worker.py +++ b/modules/video_input/video_input_worker.py @@ -1,6 +1,7 @@ """ Gets images from the camera. """ + import time from utilities.workers import queue_proxy_wrapper diff --git a/requirements.txt b/requirements.txt index 43012613..2d559018 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,9 @@ # Packages listed in alphabetical order -black +# Linters and formatters are explicitly versioned +black==24.2.0 numpy opencv-python -pylint +pylint==3.0.3 pymap3d pytest pyyaml diff --git a/tests/model_example/generate_expected.py b/tests/model_example/generate_expected.py index e8b24b5d..422b9093 100644 --- a/tests/model_example/generate_expected.py +++ b/tests/model_example/generate_expected.py @@ -1,6 +1,7 @@ """ Generates expected output using pretrained default model and images. """ + import pathlib import cv2 diff --git a/tests/test_data_merge_worker.py b/tests/test_data_merge_worker.py index 8a395d78..4913bfa6 100644 --- a/tests/test_data_merge_worker.py +++ b/tests/test_data_merge_worker.py @@ -1,6 +1,7 @@ """ Test worker process. """ + import multiprocessing as mp import time diff --git a/tests/test_detect_target.py b/tests/test_detect_target.py index 8c7835da..72764601 100644 --- a/tests/test_detect_target.py +++ b/tests/test_detect_target.py @@ -1,6 +1,7 @@ """ Test DetectTarget module. """ + import copy import pathlib diff --git a/tests/test_detect_target_worker.py b/tests/test_detect_target_worker.py index cb06cba4..9223cc9f 100644 --- a/tests/test_detect_target_worker.py +++ b/tests/test_detect_target_worker.py @@ -1,6 +1,7 @@ """ Test worker process. """ + import multiprocessing as mp import pathlib import time diff --git a/tests/test_flight_interface_worker.py b/tests/test_flight_interface_worker.py index dd835e43..00634ad1 100644 --- a/tests/test_flight_interface_worker.py +++ b/tests/test_flight_interface_worker.py @@ -1,6 +1,7 @@ """ To test, start Mission Planner and forward MAVLink over TCP. """ + import multiprocessing as mp import queue import time diff --git a/tests/test_geolocation_worker.py b/tests/test_geolocation_worker.py index e63e2d65..7ca0bcc5 100644 --- a/tests/test_geolocation_worker.py +++ b/tests/test_geolocation_worker.py @@ -1,6 +1,7 @@ """ Test worker process. """ + import multiprocessing as mp import time diff --git a/tests/test_video_input_worker.py b/tests/test_video_input_worker.py index 8bf3f2c8..5de29148 100644 --- a/tests/test_video_input_worker.py +++ b/tests/test_video_input_worker.py @@ -1,6 +1,7 @@ """ Test worker process. """ + import multiprocessing as mp import queue import time diff --git a/utilities/workers/queue_proxy_wrapper.py b/utilities/workers/queue_proxy_wrapper.py index 37bf6574..9894af2e 100644 --- a/utilities/workers/queue_proxy_wrapper.py +++ b/utilities/workers/queue_proxy_wrapper.py @@ -1,6 +1,7 @@ """ Queue. """ + import multiprocessing.managers import queue import time diff --git a/utilities/workers/worker_controller.py b/utilities/workers/worker_controller.py index 8c8bb8b0..6effff48 100644 --- a/utilities/workers/worker_controller.py +++ b/utilities/workers/worker_controller.py @@ -1,6 +1,7 @@ """ For controlling workers. """ + import multiprocessing as mp import time diff --git a/utilities/workers/worker_manager.py b/utilities/workers/worker_manager.py index d584e1a4..e4eb1290 100644 --- a/utilities/workers/worker_manager.py +++ b/utilities/workers/worker_manager.py @@ -1,6 +1,7 @@ """ For managing workers. """ + import multiprocessing as mp From 8523b7a9b9685c463f7080c07f68abd5984164e2 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sun, 18 Feb 2024 18:31:55 -0500 Subject: [PATCH 06/18] Github workflow --- .github/workflows/run-tests.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 3a4e36e3..56bf1afe 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -35,15 +35,12 @@ jobs: python -m pip install --upgrade pip pip install -r requirements.txt pip install -r requirements-pytorch.txt - pip list - # Run Black formatter inn check mode - - name: Black formatter check - run: black --check . - - # Run Pylint linter - - name: Pylint linter - run: pylint . + # Run linters and formatters + - name: Linters and formatters + run: | + black --check . + pylint . # Run unit tests with PyTest - name: Run unit tests From 858bbc3ba0da62a47d7d138ce5301f607819c1f1 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Mon, 19 Feb 2024 02:45:51 -0500 Subject: [PATCH 07/18] Flake8 annotations --- .github/workflows/run-tests.yml | 1 + requirements.txt | 1 + setup.cfg | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 setup.cfg diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 56bf1afe..d579c995 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -40,6 +40,7 @@ jobs: - name: Linters and formatters run: | black --check . + flake8 . pylint . # Run unit tests with PyTest diff --git a/requirements.txt b/requirements.txt index 2d559018..d3df19ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ # Packages listed in alphabetical order # Linters and formatters are explicitly versioned black==24.2.0 +flake8-annotations==3.0.1 numpy opencv-python pylint==3.0.3 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..8ccfb634 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,23 @@ +[flake8] +# For annotations only +select=ANN +# Disable annotations for `self` and `cls` +# https://github.com/sco1/flake8-annotations +ignore=ANN101,ANN102 +# File exclusion +extend-exclude= + # From .gitignore + # IDEs + .idea/, + .vscode/, + # Python + __pycache__/, + .pytest_cache/, + venv/, + # Logging + logs/, + # Outside of .gitignore + # Submodules + modules/common/, + # Tests + *tests/ From 549bac27e31d5f10c1f6b8fad8ffb1ffeb55e549 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Mon, 19 Feb 2024 02:53:20 -0500 Subject: [PATCH 08/18] Type annotations for Flake8 --- .../add_random/add_random.py | 2 +- .../add_random/add_random_worker.py | 2 +- .../concatenator/concatenator.py | 2 +- .../concatenator/concatenator_worker.py | 2 +- .../multiprocess_example/countup/countup.py | 2 +- .../countup/countup_worker.py | 2 +- .../multiprocess_example/intermediate_struct.py | 2 +- documentation/tests/add_or_multiply.py | 2 +- modules/cluster_estimation/cluster_estimation.py | 2 +- .../cluster_estimation_worker.py | 2 +- modules/data_merge/data_merge_worker.py | 2 +- modules/decision/decision.py | 4 ++-- modules/decision/landing_pad_tracking.py | 16 +++++++++------- modules/decision_command.py | 4 ++-- modules/detect_target/detect_target.py | 2 +- modules/detect_target/detect_target_worker.py | 2 +- modules/detection_in_world.py | 4 ++-- modules/detections_and_time.py | 11 ++++++++--- modules/drone_odometry_local.py | 16 +++++++++++----- modules/flight_interface/flight_interface.py | 4 ++-- .../flight_interface/flight_interface_worker.py | 2 +- modules/geolocation/camera_properties.py | 8 ++++---- modules/geolocation/geolocation.py | 4 ++-- modules/geolocation/geolocation_worker.py | 2 +- modules/image_and_time.py | 4 +++- modules/merged_odometry_detections.py | 4 ++-- modules/object_in_world.py | 4 ++-- modules/odometry_and_time.py | 4 ++-- modules/video_input/video_input.py | 2 +- modules/video_input/video_input_worker.py | 2 +- utilities/workers/queue_proxy_wrapper.py | 10 +++++----- utilities/workers/worker_controller.py | 12 ++++++------ utilities/workers/worker_manager.py | 10 +++++----- 33 files changed, 84 insertions(+), 69 deletions(-) diff --git a/documentation/multiprocess_example/add_random/add_random.py b/documentation/multiprocess_example/add_random/add_random.py index 45a120ea..b90e00cf 100644 --- a/documentation/multiprocess_example/add_random/add_random.py +++ b/documentation/multiprocess_example/add_random/add_random.py @@ -15,7 +15,7 @@ class AddRandom: A new random number is generated every `__ADD_SWITCH_COUNT` times. """ - def __init__(self, seed: int, max_random_term: int, add_change_count: int): + def __init__(self, seed: int, max_random_term: int, add_change_count: int) -> None: """ Constructor seeds the RNG and sets the max add and number of adds before a new random number is chosen. diff --git a/documentation/multiprocess_example/add_random/add_random_worker.py b/documentation/multiprocess_example/add_random/add_random_worker.py index 512a162e..2700714f 100644 --- a/documentation/multiprocess_example/add_random/add_random_worker.py +++ b/documentation/multiprocess_example/add_random/add_random_worker.py @@ -14,7 +14,7 @@ def add_random_worker( input_queue: queue_proxy_wrapper.QueueProxyWrapper, output_queue: queue_proxy_wrapper.QueueProxyWrapper, controller: worker_controller.WorkerController, -): +) -> None: """ Worker process. diff --git a/documentation/multiprocess_example/concatenator/concatenator.py b/documentation/multiprocess_example/concatenator/concatenator.py index 1ed4fc3b..fd188073 100644 --- a/documentation/multiprocess_example/concatenator/concatenator.py +++ b/documentation/multiprocess_example/concatenator/concatenator.py @@ -12,7 +12,7 @@ class Concatenator: Concatenates a prefix and suffix to the object. """ - def __init__(self, prefix: str, suffix: str): + def __init__(self, prefix: str, suffix: str) -> None: """ Constructor sets the prefix and suffix. """ diff --git a/documentation/multiprocess_example/concatenator/concatenator_worker.py b/documentation/multiprocess_example/concatenator/concatenator_worker.py index 05ca6e88..8bf2d27d 100644 --- a/documentation/multiprocess_example/concatenator/concatenator_worker.py +++ b/documentation/multiprocess_example/concatenator/concatenator_worker.py @@ -12,7 +12,7 @@ def concatenator_worker( suffix: str, input_queue: queue_proxy_wrapper.QueueProxyWrapper, controller: worker_controller.WorkerController, -): +) -> None: """ Worker process. diff --git a/documentation/multiprocess_example/countup/countup.py b/documentation/multiprocess_example/countup/countup.py index 1c8c3458..029cef97 100644 --- a/documentation/multiprocess_example/countup/countup.py +++ b/documentation/multiprocess_example/countup/countup.py @@ -10,7 +10,7 @@ class Countup: Increments its internal counter and outputs current counter. """ - def __init__(self, start_thousands: int, max_iterations: int): + def __init__(self, start_thousands: int, max_iterations: int) -> None: """ Constructor initializes the start and max points. """ diff --git a/documentation/multiprocess_example/countup/countup_worker.py b/documentation/multiprocess_example/countup/countup_worker.py index 6769df1c..c9e7c0c0 100644 --- a/documentation/multiprocess_example/countup/countup_worker.py +++ b/documentation/multiprocess_example/countup/countup_worker.py @@ -12,7 +12,7 @@ def countup_worker( max_iterations: int, output_queue: queue_proxy_wrapper.QueueProxyWrapper, controller: worker_controller.WorkerController, -): +) -> None: """ Worker process. diff --git a/documentation/multiprocess_example/intermediate_struct.py b/documentation/multiprocess_example/intermediate_struct.py index 07c712ea..ab49ffad 100644 --- a/documentation/multiprocess_example/intermediate_struct.py +++ b/documentation/multiprocess_example/intermediate_struct.py @@ -8,7 +8,7 @@ class IntermediateStruct: Example of a simple struct. """ - def __init__(self, number: int, sentence: str): + def __init__(self, number: int, sentence: str) -> None: """ Constructor. """ diff --git a/documentation/tests/add_or_multiply.py b/documentation/tests/add_or_multiply.py index 797ec907..9748ee91 100644 --- a/documentation/tests/add_or_multiply.py +++ b/documentation/tests/add_or_multiply.py @@ -34,7 +34,7 @@ def add_or_multiply(self, num1: float, num2: float) -> float: raise NotImplementedError - def swap_state(self): + def swap_state(self) -> None: """ Swaps internal state. """ diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index b142fc39..d7f6780a 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -96,7 +96,7 @@ def __init__( min_activation_threshold: int, min_new_points_to_run: int, random_state: int, - ): + ) -> None: """ Private constructor, use create() method. """ diff --git a/modules/cluster_estimation/cluster_estimation_worker.py b/modules/cluster_estimation/cluster_estimation_worker.py index 1d0b5afa..b4243b34 100644 --- a/modules/cluster_estimation/cluster_estimation_worker.py +++ b/modules/cluster_estimation/cluster_estimation_worker.py @@ -14,7 +14,7 @@ def cluster_estimation_worker( input_queue: queue_proxy_wrapper.QueueProxyWrapper, output_queue: queue_proxy_wrapper.QueueProxyWrapper, controller: worker_controller.WorkerController, -): +) -> None: """ Estimation worker process. diff --git a/modules/data_merge/data_merge_worker.py b/modules/data_merge/data_merge_worker.py index e5954e65..dc19e5b7 100644 --- a/modules/data_merge/data_merge_worker.py +++ b/modules/data_merge/data_merge_worker.py @@ -17,7 +17,7 @@ def data_merge_worker( odometry_input_queue: queue_proxy_wrapper.QueueProxyWrapper, output_queue: queue_proxy_wrapper.QueueProxyWrapper, controller: worker_controller.WorkerController, -): +) -> None: """ Worker process. Expects telemetry to be more frequent than detections. Queue is monotonic (i.e. timestamps never decrease). diff --git a/modules/decision/decision.py b/modules/decision/decision.py index ce906155..34260ad5 100644 --- a/modules/decision/decision.py +++ b/modules/decision/decision.py @@ -12,7 +12,7 @@ class ScoredLandingPad: Landing pad with score for decision. """ - def __init__(self, landing_pad: object_in_world.ObjectInWorld, score: float): + def __init__(self, landing_pad: object_in_world.ObjectInWorld, score: float) -> None: self.landing_pad = landing_pad self.score = score @@ -22,7 +22,7 @@ class Decision: Chooses next action to take based on known landing pad information. """ - def __init__(self, tolerance: float): + def __init__(self, tolerance: float) -> None: self.__best_landing_pad: "object_in_world.ObjectInWorld | None" = None self.__weighted_pads = [] self.__distance_tolerance = tolerance diff --git a/modules/decision/landing_pad_tracking.py b/modules/decision/landing_pad_tracking.py index cbf40ff8..c5b0e796 100644 --- a/modules/decision/landing_pad_tracking.py +++ b/modules/decision/landing_pad_tracking.py @@ -11,10 +11,10 @@ class LandingPadTracking: positive, unconfirmed positive, or false positive. """ - def __init__(self, distance_squared_threshold: float): - self.__unconfirmed_positives = [] - self.__false_positives = [] - self.__confirmed_positives = [] + def __init__(self, distance_squared_threshold: float) -> None: + self.__unconfirmed_positives: "list[object_in_world.ObjectInWorld]" = [] + self.__false_positives: "list[object_in_world.ObjectInWorld]" = [] + self.__confirmed_positives: "list[object_in_world.ObjectInWorld]" = [] # Landing pads within the square root of this distance are considered the same landing pad self.__distance_squared_threshold = distance_squared_threshold @@ -34,7 +34,7 @@ def __is_similar( ) ** 2 return distance_squared < distance_squared_threshold - def mark_false_positive(self, detection: object_in_world.ObjectInWorld): + def mark_false_positive(self, detection: object_in_world.ObjectInWorld) -> None: """ Marks a detection as false positive and removes similar landing pads from the list of unconfirmed positives. @@ -46,13 +46,15 @@ def mark_false_positive(self, detection: object_in_world.ObjectInWorld): if not self.__is_similar(landing_pad, detection, self.__distance_squared_threshold) ] - def mark_confirmed_positive(self, detection: object_in_world.ObjectInWorld): + def mark_confirmed_positive(self, detection: object_in_world.ObjectInWorld) -> None: """ Marks a detection as a confimred positive for future use. """ self.__confirmed_positives.append(detection) - def run(self, detections: "list[object_in_world.ObjectInWorld]"): + def run( + self, detections: "list[object_in_world.ObjectInWorld]" + ) -> "tuple[bool, object_in_world.ObjectInWorld | None]": """ Updates the list of unconfirmed positives and returns the a first confirmed positive if one exists, else the unconfirmed positive with the lowest variance. diff --git a/modules/decision_command.py b/modules/decision_command.py index 8613e8b8..2f8b989e 100644 --- a/modules/decision_command.py +++ b/modules/decision_command.py @@ -110,12 +110,12 @@ def create_land_at_absolute_position_command( def __init__( self, - class_private_create_key, + class_private_create_key: object, command_type: CommandType, command_x: float, command_y: float, command_z: float, - ): + ) -> None: """ Private constructor, use create() method. """ diff --git a/modules/detect_target/detect_target.py b/modules/detect_target/detect_target.py index 1069521c..5404a270 100644 --- a/modules/detect_target/detect_target.py +++ b/modules/detect_target/detect_target.py @@ -23,7 +23,7 @@ def __init__( override_full: bool, show_annotations: bool = False, save_name: str = "", - ): + ) -> None: """ device: name of target device to run inference on (i.e. "cpu" or cuda device 0, 1, 2, 3). model_path: path to the YOLOv8 model. diff --git a/modules/detect_target/detect_target_worker.py b/modules/detect_target/detect_target_worker.py index 63f2fce9..ece68427 100644 --- a/modules/detect_target/detect_target_worker.py +++ b/modules/detect_target/detect_target_worker.py @@ -16,7 +16,7 @@ def detect_target_worker( input_queue: queue_proxy_wrapper.QueueProxyWrapper, output_queue: queue_proxy_wrapper.QueueProxyWrapper, controller: worker_controller.WorkerController, -): +) -> None: """ Worker process. diff --git a/modules/detection_in_world.py b/modules/detection_in_world.py index f4ae1d89..cef6207d 100644 --- a/modules/detection_in_world.py +++ b/modules/detection_in_world.py @@ -37,12 +37,12 @@ def create( def __init__( self, - class_private_create_key, + class_private_create_key: object, vertices: np.ndarray, centre: np.ndarray, label: int, confidence: float, - ): + ) -> None: """ Private constructor, use create() method. """ diff --git a/modules/detections_and_time.py b/modules/detections_and_time.py index 5ac28892..cbcfc6ca 100644 --- a/modules/detections_and_time.py +++ b/modules/detections_and_time.py @@ -35,7 +35,9 @@ def create( return True, Detection(cls.__create_key, bounds, label, confidence) - def __init__(self, class_private_create_key, bounds: np.ndarray, label: int, confidence: float): + def __init__( + self, class_private_create_key: object, bounds: np.ndarray, label: int, confidence: float + ) -> None: """ Private constructor, use create() method. """ @@ -50,6 +52,9 @@ def __init__(self, class_private_create_key, bounds: np.ndarray, label: int, con self.confidence = confidence def __repr__(self) -> str: + """ + String representation. + """ representation = ( "cls: " + str(self.label) @@ -104,7 +109,7 @@ def create(cls, timestamp: float) -> "tuple[bool, DetectionsAndTime | None]": return True, DetectionsAndTime(cls.__create_key, timestamp) - def __init__(self, class_private_create_key, timestamp: float): + def __init__(self, class_private_create_key: object, timestamp: float) -> None: """ Private constructor, use create() method. """ @@ -134,7 +139,7 @@ def __len__(self) -> int: """ return len(self.detections) - def append(self, detection: Detection): + def append(self, detection: Detection) -> None: """ Appends a detected object. """ diff --git a/modules/drone_odometry_local.py b/modules/drone_odometry_local.py index 1446106d..6a09ab89 100644 --- a/modules/drone_odometry_local.py +++ b/modules/drone_odometry_local.py @@ -21,7 +21,9 @@ def create( """ return True, DronePositionLocal(cls.__create_key, north, east, down) - def __init__(self, class_private_create_key, north: float, east: float, down: float): + def __init__( + self, class_private_create_key: object, north: float, east: float, down: float + ) -> None: """ Private constructor, use create() method. """ @@ -69,13 +71,17 @@ def create_new( return True, DroneOrientationLocal(cls.__create_key, orientation) @classmethod - def create_wrap(cls, orientation: drone_odometry.DroneOrientation): + def create_wrap( + cls, orientation: drone_odometry.DroneOrientation + ) -> "tuple[bool, DroneOrientationLocal | None]": """ Wrap existing orientation. """ return True, DroneOrientationLocal(cls.__create_key, orientation) - def __init__(self, class_private_create_key, orientation: drone_odometry.DroneOrientation): + def __init__( + self, class_private_create_key: object, orientation: drone_odometry.DroneOrientation + ) -> None: """ Private constructor, use create() method. """ @@ -122,10 +128,10 @@ def create( def __init__( self, - class_private_create_key, + class_private_create_key: object, position: DronePositionLocal, orientation: DroneOrientationLocal, - ): + ) -> None: """ Private constructor, use create() method. """ diff --git a/modules/flight_interface/flight_interface.py b/modules/flight_interface/flight_interface.py index c8ac7361..422c2ee4 100644 --- a/modules/flight_interface/flight_interface.py +++ b/modules/flight_interface/flight_interface.py @@ -39,10 +39,10 @@ def create(cls, address: str, timeout_home: float) -> "tuple[bool, FlightInterfa def __init__( self, - class_private_create_key, + class_private_create_key: object, controller: flight_controller.FlightController, home_location: drone_odometry.DronePosition, - ): + ) -> None: """ Private constructor, use create() method. """ diff --git a/modules/flight_interface/flight_interface_worker.py b/modules/flight_interface/flight_interface_worker.py index 915a69ec..e4a82403 100644 --- a/modules/flight_interface/flight_interface_worker.py +++ b/modules/flight_interface/flight_interface_worker.py @@ -15,7 +15,7 @@ def flight_interface_worker( period: float, output_queue: queue_proxy_wrapper.QueueProxyWrapper, controller: worker_controller.WorkerController, -): +) -> None: """ Worker process. diff --git a/modules/geolocation/camera_properties.py b/modules/geolocation/camera_properties.py index a8a9f7e0..f7ef5722 100644 --- a/modules/geolocation/camera_properties.py +++ b/modules/geolocation/camera_properties.py @@ -118,12 +118,12 @@ def create( def __init__( self, - class_private_create_key, + class_private_create_key: object, resolution_x: int, resolution_y: int, u_scalar: float, v_scalar: float, - ): + ) -> None: """ Private constructor, use create() method. """ @@ -248,10 +248,10 @@ def create( def __init__( self, - class_private_create_key, + class_private_create_key: object, vec_camera_on_drone_position: np.ndarray, camera_to_drone_rotation_matrix: np.ndarray, - ): + ) -> None: """ Private constructor, use create() method. """ diff --git a/modules/geolocation/geolocation.py b/modules/geolocation/geolocation.py index 12b10b0b..5b344054 100644 --- a/modules/geolocation/geolocation.py +++ b/modules/geolocation/geolocation.py @@ -63,11 +63,11 @@ def create( def __init__( self, - class_private_create_key, + class_private_create_key: object, camera_drone_extrinsics: camera_properties.CameraDroneExtrinsics, perspective_transform_sources: "list[list[float]]", rotated_source_vectors: "list[np.ndarray]", - ): + ) -> None: """ Private constructor, use create() method. """ diff --git a/modules/geolocation/geolocation_worker.py b/modules/geolocation/geolocation_worker.py index e1f2968d..b4195542 100644 --- a/modules/geolocation/geolocation_worker.py +++ b/modules/geolocation/geolocation_worker.py @@ -14,7 +14,7 @@ def geolocation_worker( input_queue: queue_proxy_wrapper.QueueProxyWrapper, output_queue: queue_proxy_wrapper.QueueProxyWrapper, controller: worker_controller.WorkerController, -): +) -> None: """ Worker process. diff --git a/modules/image_and_time.py b/modules/image_and_time.py index 0b308f16..9eec1131 100644 --- a/modules/image_and_time.py +++ b/modules/image_and_time.py @@ -29,7 +29,9 @@ def create(cls, image: np.ndarray) -> "tuple[bool, ImageAndTime | None]": return True, ImageAndTime(cls.__create_key, image, current_time) - def __init__(self, class_private_create_key, image: np.ndarray, timestamp: float): + def __init__( + self, class_private_create_key: object, image: np.ndarray, timestamp: float + ) -> None: """ Private constructor, use create() method. """ diff --git a/modules/merged_odometry_detections.py b/modules/merged_odometry_detections.py index 706642f5..31dd82cf 100644 --- a/modules/merged_odometry_detections.py +++ b/modules/merged_odometry_detections.py @@ -30,10 +30,10 @@ def create( def __init__( self, - class_private_create_key, + class_private_create_key: object, odometry_local: drone_odometry_local.DroneOdometryLocal, detections: "list[detections_and_time.Detection]", - ): + ) -> None: """ Private constructor, use create() method. """ diff --git a/modules/object_in_world.py b/modules/object_in_world.py index 4dd3e7de..1ce9f65e 100644 --- a/modules/object_in_world.py +++ b/modules/object_in_world.py @@ -25,11 +25,11 @@ def create( def __init__( self, - class_private_create_key, + class_private_create_key: object, location_x: float, location_y: float, spherical_variance: float, - ): + ) -> None: """ Private constructor, use create() method. """ diff --git a/modules/odometry_and_time.py b/modules/odometry_and_time.py index 0c4df80b..087ea507 100644 --- a/modules/odometry_and_time.py +++ b/modules/odometry_and_time.py @@ -31,10 +31,10 @@ def create( def __init__( self, - class_private_create_key, + class_private_create_key: object, odometry_data: drone_odometry_local.DroneOdometryLocal, timestamp: float, - ): + ) -> None: """ Private constructor, use create() method. """ diff --git a/modules/video_input/video_input.py b/modules/video_input/video_input.py index f0432ff2..28ebe114 100644 --- a/modules/video_input/video_input.py +++ b/modules/video_input/video_input.py @@ -11,7 +11,7 @@ class VideoInput: Combines image and timestamp together. """ - def __init__(self, camera_name: "int | str", save_name: str = ""): + def __init__(self, camera_name: "int | str", save_name: str = "") -> None: self.device = camera_device.CameraDevice(camera_name, 1, save_name) def run(self) -> "tuple[bool, image_and_time.ImageAndTime | None]": diff --git a/modules/video_input/video_input_worker.py b/modules/video_input/video_input_worker.py index be57b6be..aa7ea171 100644 --- a/modules/video_input/video_input_worker.py +++ b/modules/video_input/video_input_worker.py @@ -15,7 +15,7 @@ def video_input_worker( save_name: str, output_queue: queue_proxy_wrapper.QueueProxyWrapper, controller: worker_controller.WorkerController, -): +) -> None: """ Worker process. diff --git a/utilities/workers/queue_proxy_wrapper.py b/utilities/workers/queue_proxy_wrapper.py index 9894af2e..633efece 100644 --- a/utilities/workers/queue_proxy_wrapper.py +++ b/utilities/workers/queue_proxy_wrapper.py @@ -15,11 +15,11 @@ class QueueProxyWrapper: __QUEUE_TIMEOUT = 0.1 # seconds __QUEUE_DELAY = 0.1 # seconds - def __init__(self, mp_manager: multiprocessing.managers.SyncManager, maxsize: int = 0): + def __init__(self, mp_manager: multiprocessing.managers.SyncManager, maxsize: int = 0) -> None: self.queue = mp_manager.Queue(maxsize) self.maxsize = maxsize - def fill_queue_with_sentinel(self, timeout: float = 0.0): + def fill_queue_with_sentinel(self, timeout: float = 0.0) -> None: """ Fills the queue with sentinel (None ). @@ -35,11 +35,11 @@ def fill_queue_with_sentinel(self, timeout: float = 0.0): except queue.Full: return - def drain_queue(self, timeout: float = 0.0): + def drain_queue(self, timeout: float = 0.0) -> None: """ Drains the queue. - timeout: Time waiting before giving up, must be greater than 0 . + timeout: Time waiting in seconds before giving up, must be greater than 0 . """ if timeout <= 0.0: timeout = self.__QUEUE_TIMEOUT @@ -49,7 +49,7 @@ def drain_queue(self, timeout: float = 0.0): except queue.Empty: return - def fill_and_drain_queue(self): + def fill_and_drain_queue(self) -> None: """ Fill with sentinel and then drain. """ diff --git a/utilities/workers/worker_controller.py b/utilities/workers/worker_controller.py index 6effff48..19b059ee 100644 --- a/utilities/workers/worker_controller.py +++ b/utilities/workers/worker_controller.py @@ -14,7 +14,7 @@ class WorkerController: __QUEUE_DELAY = 0.1 # seconds - def __init__(self): + def __init__(self) -> None: """ Constructor creates internal queue and semaphore. """ @@ -22,7 +22,7 @@ def __init__(self): self.__is_paused = False self.__exit_queue = mp.Queue(1) - def request_pause(self): + def request_pause(self) -> None: """ Requests worker processes to pause. """ @@ -30,7 +30,7 @@ def request_pause(self): self.__pause.acquire() self.__is_paused = True - def request_resume(self): + def request_resume(self) -> None: """ Requests worker processes to resume. """ @@ -38,14 +38,14 @@ def request_resume(self): self.__pause.release() self.__is_paused = False - def check_pause(self): + def check_pause(self) -> None: """ Blocks worker if main has requested it to pause, otherwise continues. """ self.__pause.acquire() self.__pause.release() - def request_exit(self): + def request_exit(self) -> None: """ Requests worker processes to exit. Does nothing if already requested. @@ -54,7 +54,7 @@ def request_exit(self): if self.__exit_queue.empty(): self.__exit_queue.put(None) - def clear_exit(self): + def clear_exit(self) -> None: """ Clears the exit request condition. Does nothing if already cleared. diff --git a/utilities/workers/worker_manager.py b/utilities/workers/worker_manager.py index e4eb1290..85ca8bbd 100644 --- a/utilities/workers/worker_manager.py +++ b/utilities/workers/worker_manager.py @@ -11,13 +11,13 @@ class WorkerManager: Contains exit and pause requests. """ - def __init__(self, workers: "list[mp.Process] | None" = None): + def __init__(self, workers: "list[mp.Process] | None" = None) -> None: """ Constructor creates internal queue and semaphore. """ self.__workers = [] if workers is None else workers - def create_workers(self, count: int, target, args): + def create_workers(self, count: int, target: "(...) -> object", args: tuple) -> None: # type: ignore """ Create identical workers. @@ -29,20 +29,20 @@ def create_workers(self, count: int, target, args): worker = mp.Process(target=target, args=args) self.__workers.append(worker) - def concatenate_workers(self, workers: "list[mp.Process]"): + def concatenate_workers(self, workers: "list[mp.Process]") -> None: """ Add workers. """ self.__workers += workers - def start_workers(self): + def start_workers(self) -> None: """ Start workers. """ for worker in self.__workers: worker.start() - def join_workers(self): + def join_workers(self) -> None: """ Join workers. """ From 9ebea38c8abac6bf7eb5c839b2b73aeee19fffe2 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Mon, 19 Feb 2024 03:08:32 -0500 Subject: [PATCH 09/18] Flake covers integration tests --- documentation/tests/{ => unit}/add_or_multiply.py | 0 documentation/tests/{ => unit}/test_add_or_multiply.py | 0 documentation/tests/{ => unit}/test_pytest_example.py | 0 setup.cfg | 2 +- tests/integration/__init__.py | 0 tests/{ => integration}/test_data_merge_worker.py | 4 ++-- tests/{ => integration}/test_detect_target_worker.py | 2 +- tests/{ => integration}/test_flight_interface_hardware.py | 0 tests/{ => integration}/test_flight_interface_worker.py | 0 tests/{ => integration}/test_geolocation_worker.py | 2 +- tests/{ => integration}/test_video_input_hardware.py | 1 + tests/{ => integration}/test_video_input_worker.py | 0 tests/unit/__init__.py | 0 tests/{ => unit}/test_camera_properties.py | 0 tests/{ => unit}/test_cluster_detection.py | 0 tests/{ => unit}/test_decision.py | 0 tests/{ => unit}/test_detect_target.py | 0 tests/{ => unit}/test_geolocation.py | 0 tests/{ => unit}/test_landing_pad_tracking.py | 0 19 files changed, 6 insertions(+), 5 deletions(-) rename documentation/tests/{ => unit}/add_or_multiply.py (100%) rename documentation/tests/{ => unit}/test_add_or_multiply.py (100%) rename documentation/tests/{ => unit}/test_pytest_example.py (100%) create mode 100644 tests/integration/__init__.py rename tests/{ => integration}/test_data_merge_worker.py (99%) rename tests/{ => integration}/test_detect_target_worker.py (99%) rename tests/{ => integration}/test_flight_interface_hardware.py (100%) rename tests/{ => integration}/test_flight_interface_worker.py (100%) rename tests/{ => integration}/test_geolocation_worker.py (99%) rename tests/{ => integration}/test_video_input_hardware.py (92%) rename tests/{ => integration}/test_video_input_worker.py (100%) create mode 100644 tests/unit/__init__.py rename tests/{ => unit}/test_camera_properties.py (100%) rename tests/{ => unit}/test_cluster_detection.py (100%) rename tests/{ => unit}/test_decision.py (100%) rename tests/{ => unit}/test_detect_target.py (100%) rename tests/{ => unit}/test_geolocation.py (100%) rename tests/{ => unit}/test_landing_pad_tracking.py (100%) diff --git a/documentation/tests/add_or_multiply.py b/documentation/tests/unit/add_or_multiply.py similarity index 100% rename from documentation/tests/add_or_multiply.py rename to documentation/tests/unit/add_or_multiply.py diff --git a/documentation/tests/test_add_or_multiply.py b/documentation/tests/unit/test_add_or_multiply.py similarity index 100% rename from documentation/tests/test_add_or_multiply.py rename to documentation/tests/unit/test_add_or_multiply.py diff --git a/documentation/tests/test_pytest_example.py b/documentation/tests/unit/test_pytest_example.py similarity index 100% rename from documentation/tests/test_pytest_example.py rename to documentation/tests/unit/test_pytest_example.py diff --git a/setup.cfg b/setup.cfg index 8ccfb634..079e3f6d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,4 +20,4 @@ extend-exclude= # Submodules modules/common/, # Tests - *tests/ + */unit/ diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_data_merge_worker.py b/tests/integration/test_data_merge_worker.py similarity index 99% rename from tests/test_data_merge_worker.py rename to tests/integration/test_data_merge_worker.py index 4913bfa6..48898826 100644 --- a/tests/test_data_merge_worker.py +++ b/tests/integration/test_data_merge_worker.py @@ -21,7 +21,7 @@ def simulate_detect_target_worker( timestamp: float, detections_queue: queue_proxy_wrapper.QueueProxyWrapper -): +) -> None: """ Place the detection into the queue. """ @@ -40,7 +40,7 @@ def simulate_detect_target_worker( def simulate_flight_input_worker( timestamp: float, odometry_queue: queue_proxy_wrapper.QueueProxyWrapper -): +) -> None: """ Place the odometry into the queue. """ diff --git a/tests/test_detect_target_worker.py b/tests/integration/test_detect_target_worker.py similarity index 99% rename from tests/test_detect_target_worker.py rename to tests/integration/test_detect_target_worker.py index 9223cc9f..50bd8961 100644 --- a/tests/test_detect_target_worker.py +++ b/tests/integration/test_detect_target_worker.py @@ -32,7 +32,7 @@ def simulate_previous_worker( image_path: pathlib.Path, in_queue: queue_proxy_wrapper.QueueProxyWrapper -): +) -> None: """ Place the image into the queue. """ diff --git a/tests/test_flight_interface_hardware.py b/tests/integration/test_flight_interface_hardware.py similarity index 100% rename from tests/test_flight_interface_hardware.py rename to tests/integration/test_flight_interface_hardware.py diff --git a/tests/test_flight_interface_worker.py b/tests/integration/test_flight_interface_worker.py similarity index 100% rename from tests/test_flight_interface_worker.py rename to tests/integration/test_flight_interface_worker.py diff --git a/tests/test_geolocation_worker.py b/tests/integration/test_geolocation_worker.py similarity index 99% rename from tests/test_geolocation_worker.py rename to tests/integration/test_geolocation_worker.py index 7ca0bcc5..2a73cee4 100644 --- a/tests/test_geolocation_worker.py +++ b/tests/integration/test_geolocation_worker.py @@ -19,7 +19,7 @@ WORK_COUNT = 3 -def simulate_previous_worker(in_queue: queue_proxy_wrapper.QueueProxyWrapper): +def simulate_previous_worker(in_queue: queue_proxy_wrapper.QueueProxyWrapper) -> None: """ Place the image into the queue. """ diff --git a/tests/test_video_input_hardware.py b/tests/integration/test_video_input_hardware.py similarity index 92% rename from tests/test_video_input_hardware.py rename to tests/integration/test_video_input_hardware.py index db53caf0..2557e53d 100644 --- a/tests/test_video_input_hardware.py +++ b/tests/integration/test_video_input_hardware.py @@ -13,6 +13,7 @@ def main() -> int: Main function. """ # Setup + # TODO: Common change logging option camera = video_input.VideoInput( CAMERA, ) diff --git a/tests/test_video_input_worker.py b/tests/integration/test_video_input_worker.py similarity index 100% rename from tests/test_video_input_worker.py rename to tests/integration/test_video_input_worker.py diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_camera_properties.py b/tests/unit/test_camera_properties.py similarity index 100% rename from tests/test_camera_properties.py rename to tests/unit/test_camera_properties.py diff --git a/tests/test_cluster_detection.py b/tests/unit/test_cluster_detection.py similarity index 100% rename from tests/test_cluster_detection.py rename to tests/unit/test_cluster_detection.py diff --git a/tests/test_decision.py b/tests/unit/test_decision.py similarity index 100% rename from tests/test_decision.py rename to tests/unit/test_decision.py diff --git a/tests/test_detect_target.py b/tests/unit/test_detect_target.py similarity index 100% rename from tests/test_detect_target.py rename to tests/unit/test_detect_target.py diff --git a/tests/test_geolocation.py b/tests/unit/test_geolocation.py similarity index 100% rename from tests/test_geolocation.py rename to tests/unit/test_geolocation.py diff --git a/tests/test_landing_pad_tracking.py b/tests/unit/test_landing_pad_tracking.py similarity index 100% rename from tests/test_landing_pad_tracking.py rename to tests/unit/test_landing_pad_tracking.py From 0ec23bef38232c1e0aa5980115f5b8d394045d3d Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Tue, 20 Feb 2024 06:16:49 -0500 Subject: [PATCH 10/18] Increase Flake8 and reconcile Pylint and Pytest --- .gitignore | 7 +- __init__.py | 0 config.yaml | 12 +- documentation/tests/unit/add_or_multiply.py | 8 +- .../tests/unit/test_add_or_multiply.py | 60 +++++----- .../tests/unit/test_pytest_example.py | 6 +- modules/detections_and_time.py | 2 +- pyproject.toml | 27 +++++ requirements.txt | 9 +- setup.cfg | 3 - tests/unit/test_camera_properties.py | 106 ++++++++++-------- tests/unit/test_cluster_detection.py | 39 ++++--- tests/unit/test_decision.py | 54 +++++---- tests/unit/test_detect_target.py | 26 ++--- tests/unit/test_geolocation.py | 104 ++++++++--------- tests/unit/test_landing_pad_tracking.py | 54 ++++----- 16 files changed, 289 insertions(+), 228 deletions(-) delete mode 100644 __init__.py diff --git a/.gitignore b/.gitignore index ba733274..e8e58574 100644 --- a/.gitignore +++ b/.gitignore @@ -4,12 +4,11 @@ # Python __pycache__/ -.pytest_cache/ venv/ +# Logging +logs/ + # Pytorch and Ultralytics *.pt *.pth - -# Logging -*log* diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/config.yaml b/config.yaml index 11f54fd1..5f0fa871 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,4 @@ -# Global constants for main with cuda +# Global constants for main with CUDA queue_max_size: 10 @@ -6,19 +6,19 @@ log_directory_path: "logs" video_input: camera_name: 0 - worker_period: 1.0 # seconds + worker_period: 1.0 # seconds save_prefix: "log_image" detect_target: worker_count: 1 device: 0 - model_path: "tests/model_example/yolov8s_ultralytics_pretrained_default.pt" # TODO: update + model_path: "tests/model_example/yolov8s_ultralytics_pretrained_default.pt" # TODO: update save_prefix: "log_comp" flight_interface: address: "tcp:127.0.0.1:14550" - timeout: 10.0 # seconds - worker_period: 0.1 # seconds + timeout: 10.0 # seconds + worker_period: 0.1 # seconds data_merge: - timeout: 10.0 # seconds + timeout: 10.0 # seconds diff --git a/documentation/tests/unit/add_or_multiply.py b/documentation/tests/unit/add_or_multiply.py index 9748ee91..028f1204 100644 --- a/documentation/tests/unit/add_or_multiply.py +++ b/documentation/tests/unit/add_or_multiply.py @@ -19,18 +19,18 @@ class AddOrMultiply: Add or multiply depending on state. """ - def __init__(self, switch: MathOperation): + def __init__(self, switch: MathOperation) -> None: self.__operator = switch - def add_or_multiply(self, num1: float, num2: float) -> float: + def add_or_multiply(self, num_1: float, num_2: float) -> float: """ Adds or multiplies numbers based on internal state. """ if self.__operator == MathOperation.ADD: - return num1 + num2 + return num_1 + num_2 if self.__operator == MathOperation.MULTIPLY: - return num1 * num2 + return num_1 * num_2 raise NotImplementedError diff --git a/documentation/tests/unit/test_add_or_multiply.py b/documentation/tests/unit/test_add_or_multiply.py index 9f1880e8..794c0d9a 100644 --- a/documentation/tests/unit/test_add_or_multiply.py +++ b/documentation/tests/unit/test_add_or_multiply.py @@ -21,21 +21,21 @@ # Pytest fixtures are reusable setup components # Makes it easier when setup is complicated @pytest.fixture() -def adder(): +def adder() -> add_or_multiply.AddOrMultiply: # type: ignore """ Creates AddOrMultiply in addition state. """ add = add_or_multiply.AddOrMultiply(add_or_multiply.MathOperation.ADD) - yield add + yield add # type: ignore @pytest.fixture() -def multiplier(): +def multiplier() -> add_or_multiply.AddOrMultiply: # type: ignore """ Creates AddOrMultiply in multiplication state. """ multiply = add_or_multiply.AddOrMultiply(add_or_multiply.MathOperation.MULTIPLY) - yield multiply + yield multiply # type: ignore class TestAddition: @@ -44,21 +44,21 @@ class TestAddition: The function or method name contains test somewhere for Pytest to run it. """ - def test_add_positive(self, adder: add_or_multiply.AddOrMultiply): + def test_add_positive(self, adder: add_or_multiply.AddOrMultiply) -> None: """ Add 2 positive numbers. The parameter names must match the fixture function name to be used. """ # Setup # Hardcode values on the right side if possible - input1 = 1.2 - input2 = 3.4 + input_1 = 1.2 + input_2 = 3.4 # expected is the variable name to compare against expected = 4.6 # Run # actual is the variable name to compare - actual = adder.add_or_multiply(input1, input2) + actual = adder.add_or_multiply(input_1, input_2) # Test # Pytest unit tests pass if there are no unexpected exceptions @@ -66,62 +66,62 @@ def test_add_positive(self, adder: add_or_multiply.AddOrMultiply): # Use math.isclose() for floating point comparison assert math.isclose(actual, expected) - def test_add_large_positive(self, adder: add_or_multiply.AddOrMultiply): + def test_add_large_positive(self, adder: add_or_multiply.AddOrMultiply) -> None: """ Add 2 positive numbers. """ # Setup - input1 = 400_000_000.0 - input2 = 0.1 + input_1 = 400_000_000.0 + input_2 = 0.1 expected = 400_000_000.1 # Run - actual = adder.add_or_multiply(input1, input2) + actual = adder.add_or_multiply(input_1, input_2) # Test assert math.isclose(actual, expected) - def test_add_positive_negative(self, adder: add_or_multiply.AddOrMultiply): + def test_add_positive_negative(self, adder: add_or_multiply.AddOrMultiply) -> None: """ Add positive and negative number. """ # Setup - input1 = 0.1 - input2 = -1.0 + input_1 = 0.1 + input_2 = -1.0 expected = -0.9 # Run - actual = adder.add_or_multiply(input1, input2) + actual = adder.add_or_multiply(input_1, input_2) # Test assert math.isclose(actual, expected) - def test_add_positive_and_same_negative(self, adder: add_or_multiply.AddOrMultiply): + def test_add_positive_and_same_negative(self, adder: add_or_multiply.AddOrMultiply) -> None: """ Add positive and negative number. """ # Setup - input1 = 1.5 - input2 = -1.5 + input_1 = 1.5 + input_2 = -1.5 expected = 0.0 # Run - actual = adder.add_or_multiply(input1, input2) + actual = adder.add_or_multiply(input_1, input_2) # Test assert math.isclose(actual, expected) - def test_add_negative(self, adder: add_or_multiply.AddOrMultiply): + def test_add_negative(self, adder: add_or_multiply.AddOrMultiply) -> None: """ Add positive and negative number. """ # Setup - input1 = -0.5 - input2 = -1.0 + input_1 = -0.5 + input_2 = -1.0 expected = -1.5 # Run - actual = adder.add_or_multiply(input1, input2) + actual = adder.add_or_multiply(input_1, input_2) # Test assert math.isclose(actual, expected) @@ -138,18 +138,18 @@ class TestMultiply: Many multiplication cases need to be covered as well. """ - def test_multiply_positive(self, multiplier: add_or_multiply.AddOrMultiply): + def test_multiply_positive(self, multiplier: add_or_multiply.AddOrMultiply) -> None: """ Multiply 2 positive numbers. Different fixture so different parameter name. """ # Setup - input1 = 1.2 - input2 = 3.4 + input_1 = 1.2 + input_2 = 3.4 expected = 4.08 # Run - actual = multiplier.add_or_multiply(input1, input2) + actual = multiplier.add_or_multiply(input_1, input_2) # Test assert math.isclose(actual, expected) @@ -160,7 +160,7 @@ class TestSwap: Test a different method. """ - def test_swap_add_to_multiply(self, adder: add_or_multiply.AddOrMultiply): + def test_swap_add_to_multiply(self, adder: add_or_multiply.AddOrMultiply) -> None: """ Add and then multiply. """ @@ -177,7 +177,7 @@ def test_swap_add_to_multiply(self, adder: add_or_multiply.AddOrMultiply): assert actual == expected - def test_swap_multiply_to_add(self, multiplier: add_or_multiply.AddOrMultiply): + def test_swap_multiply_to_add(self, multiplier: add_or_multiply.AddOrMultiply) -> None: """ Multiply and then add. """ diff --git a/documentation/tests/unit/test_pytest_example.py b/documentation/tests/unit/test_pytest_example.py index 1eb4a905..1cbee19b 100644 --- a/documentation/tests/unit/test_pytest_example.py +++ b/documentation/tests/unit/test_pytest_example.py @@ -9,7 +9,7 @@ import pytest -def test_trivial_pass(): +def test_trivial_pass() -> None: """ Unit test will pass by default. """ @@ -18,7 +18,7 @@ def test_trivial_pass(): pass -def test_1_plus_1_equals_2(): +def test_1_plus_1_equals_2() -> None: """ Easy unit test. """ @@ -28,7 +28,7 @@ def test_1_plus_1_equals_2(): assert actual == expected -def test_expect_exception(): +def test_expect_exception() -> None: """ If an exception is expected. """ diff --git a/modules/detections_and_time.py b/modules/detections_and_time.py index cbcfc6ca..b70cb278 100644 --- a/modules/detections_and_time.py +++ b/modules/detections_and_time.py @@ -23,7 +23,7 @@ def create( if bounds.shape != (4,) or not np.greater_equal(bounds, 0.0).all(): return False, None - # n1 <= n2 + # n_1 <= n_2 if bounds[0] > bounds[2] or bounds[1] > bounds[3]: return False, None diff --git a/pyproject.toml b/pyproject.toml index 1e1edd96..33d8d835 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,34 @@ [tool.pylint.main] +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, it +# can't be used as an escape character. +ignore-paths = [ + # From .gitignore + # IDEs + ".idea/", + ".vscode/", + + # Python + "__pycache__/", + "venv/", + + # Logging + "logs/", +] + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs = 0 + # Minimum Python version to use for version dependent checks. Will default to the # version used to run pylint. py-version = "3.8" +# Discover python modules and packages in the file system subtree. +recursive = true + # [tool.pylint.basic] # Good variable names which should always be accepted, separated by a comma. good-names = [ @@ -46,6 +72,7 @@ disable = [ "no-member", # Pylint cannot handle 3rd party imports "too-few-public-methods", # Some classes are simple "too-many-arguments", # Function signatures + "too-many-branches", # Don't care "too-many-lines", # Line count in file "too-many-locals", # Don't care "too-many-statements", # Don't care diff --git a/requirements.txt b/requirements.txt index d3df19ae..3f730a3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,13 @@ # Packages listed in alphabetical order -# Linters and formatters are explicitly versioned -black==24.2.0 -flake8-annotations==3.0.1 numpy opencv-python -pylint==3.0.3 pymap3d pytest pyyaml scikit-learn # pytorch special case, see requirements-pytorch.txt + +# Linters and formatters are explicitly versioned +black==24.2.0 +flake8-annotations==3.0.1 +pylint==3.0.3 diff --git a/setup.cfg b/setup.cfg index 079e3f6d..89fc0506 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,12 +12,9 @@ extend-exclude= .vscode/, # Python __pycache__/, - .pytest_cache/, venv/, # Logging logs/, # Outside of .gitignore # Submodules modules/common/, - # Tests - */unit/ diff --git a/tests/unit/test_camera_properties.py b/tests/unit/test_camera_properties.py index 6ca73953..1f91a17a 100644 --- a/tests/unit/test_camera_properties.py +++ b/tests/unit/test_camera_properties.py @@ -14,7 +14,7 @@ @pytest.fixture -def camera_intrinsic(): +def camera_intrinsic() -> camera_properties.CameraIntrinsics: # type: ignore """ Intrinsic camera properties. """ @@ -32,7 +32,7 @@ def camera_intrinsic(): assert result assert camera is not None - yield camera + yield camera # type: ignore class TestVectorR3Check: @@ -40,7 +40,7 @@ class TestVectorR3Check: Test 3D vector check. """ - def test_r3(self): + def test_r3(self) -> None: """ R^3 . """ @@ -53,7 +53,7 @@ def test_r3(self): # Test assert result_actual - def test_r2(self): + def test_r2(self) -> None: """ Not R^3 . """ @@ -66,7 +66,7 @@ def test_r2(self): # Test assert not result_actual - def test_matrix(self): + def test_matrix(self) -> None: """ Matrix. """ @@ -79,7 +79,7 @@ def test_matrix(self): # Test assert not result_actual - def test_weird_r3(self): + def test_weird_r3(self) -> None: """ Weird R^3 should not pass. """ @@ -98,7 +98,7 @@ class TestMatrixR3x3Check: Test 3x3 matrix check. """ - def test_r3x3(self): + def test_r3x3(self) -> None: """ R^{3x3} . """ @@ -111,7 +111,7 @@ def test_r3x3(self): # Test assert result_actual - def test_r2x2(self): + def test_r2x2(self) -> None: """ Vector. """ @@ -124,7 +124,7 @@ def test_r2x2(self): # Test assert not result_actual - def test_r3(self): + def test_r3(self) -> None: """ Vector. """ @@ -137,7 +137,7 @@ def test_r3(self): # Test assert not result_actual - def test_weird_r3x3(self): + def test_weird_r3x3(self) -> None: """ Vector. """ @@ -156,7 +156,7 @@ class TestRotationMatrix: Test rotation matrix. """ - def test_no_rotation(self): + def test_no_rotation(self) -> None: """ Identity. """ @@ -179,7 +179,7 @@ def test_no_rotation(self): assert actual is not None np.testing.assert_allclose(actual, expected) - def test_yaw_quarter(self): + def test_yaw_quarter(self) -> None: """ Quarter turn towards east from north. """ @@ -209,7 +209,7 @@ def test_yaw_quarter(self): assert actual is not None np.testing.assert_almost_equal(actual, expected) - def test_pitch_quarter(self): + def test_pitch_quarter(self) -> None: """ Quarter turn towards up from forward. """ @@ -239,7 +239,7 @@ def test_pitch_quarter(self): assert actual is not None np.testing.assert_almost_equal(actual, expected) - def test_roll_quarter(self): + def test_roll_quarter(self) -> None: """ Quarter turn leaning right. """ @@ -269,7 +269,7 @@ def test_roll_quarter(self): assert actual is not None np.testing.assert_almost_equal(actual, expected) - def test_combined_rotations_positive(self): + def test_combined_rotations_positive(self) -> None: """ Each in 45° positive direction. """ @@ -299,7 +299,7 @@ def test_combined_rotations_positive(self): assert actual is not None np.testing.assert_almost_equal(actual, expected) - def test_combined_rotations_negative(self): + def test_combined_rotations_negative(self) -> None: """ Each in 45° negative direction. """ @@ -329,7 +329,7 @@ def test_combined_rotations_negative(self): assert actual is not None np.testing.assert_almost_equal(actual, expected) - def test_bad_yaw_too_negative(self): + def test_bad_yaw_too_negative(self) -> None: """ Expect failure. """ @@ -349,7 +349,7 @@ def test_bad_yaw_too_negative(self): assert not result assert actual is None - def test_bad_yaw_too_positive(self): + def test_bad_yaw_too_positive(self) -> None: """ Expect failure. """ @@ -369,7 +369,7 @@ def test_bad_yaw_too_positive(self): assert not result assert actual is None - def test_bad_pitch_too_negative(self): + def test_bad_pitch_too_negative(self) -> None: """ Expect failure. """ @@ -389,7 +389,7 @@ def test_bad_pitch_too_negative(self): assert not result assert actual is None - def test_bad_pitch_too_positive(self): + def test_bad_pitch_too_positive(self) -> None: """ Expect failure. """ @@ -409,7 +409,7 @@ def test_bad_pitch_too_positive(self): assert not result assert actual is None - def test_bad_roll_too_negative(self): + def test_bad_roll_too_negative(self) -> None: """ Expect failure. """ @@ -429,7 +429,7 @@ def test_bad_roll_too_negative(self): assert not result assert actual is None - def test_bad_roll_too_positive(self): + def test_bad_roll_too_positive(self) -> None: """ Expect failure. """ @@ -455,7 +455,7 @@ class TestCameraIntrinsicsCreate: Test constructor. """ - def test_normal(self): + def test_normal(self) -> None: """ Successful construction. """ @@ -477,7 +477,7 @@ def test_normal(self): assert result assert actual is not None - def test_bad_resolution_x(self): + def test_bad_resolution_x(self) -> None: """ Expect failure. """ @@ -499,7 +499,7 @@ def test_bad_resolution_x(self): assert not result assert actual is None - def test_bad_resolution_y(self): + def test_bad_resolution_y(self) -> None: """ Expect failure. """ @@ -521,7 +521,7 @@ def test_bad_resolution_y(self): assert not result assert actual is None - def test_bad_fov_x_negative(self): + def test_bad_fov_x_negative(self) -> None: """ Expect failure. """ @@ -543,7 +543,7 @@ def test_bad_fov_x_negative(self): assert not result assert actual is None - def test_bad_fov_x_zero(self): + def test_bad_fov_x_zero(self) -> None: """ Expect failure. """ @@ -565,7 +565,7 @@ def test_bad_fov_x_zero(self): assert not result assert actual is None - def test_bad_fov_x_too_positive(self): + def test_bad_fov_x_too_positive(self) -> None: """ Expect failure. """ @@ -587,7 +587,7 @@ def test_bad_fov_x_too_positive(self): assert not result assert actual is None - def test_bad_fov_y_negative(self): + def test_bad_fov_y_negative(self) -> None: """ Expect failure. """ @@ -609,7 +609,7 @@ def test_bad_fov_y_negative(self): assert not result assert actual is None - def test_bad_fov_y_zero(self): + def test_bad_fov_y_zero(self) -> None: """ Expect failure. """ @@ -631,7 +631,7 @@ def test_bad_fov_y_zero(self): assert not result assert actual is None - def test_bad_fov_y_too_positive(self): + def test_bad_fov_y_too_positive(self) -> None: """ Expect failure. """ @@ -659,7 +659,7 @@ class TestImagePixelToVector: Test convert from image pixel to image vector. """ - def test_centre(self): + def test_centre(self) -> None: """ Centre of image. """ @@ -685,7 +685,7 @@ def test_centre(self): assert actual is not None np.testing.assert_almost_equal(actual, expected) - def test_left(self): + def test_left(self) -> None: """ Left of image. """ @@ -711,7 +711,7 @@ def test_left(self): assert actual is not None np.testing.assert_almost_equal(actual, expected) - def test_right(self): + def test_right(self) -> None: """ Right of image. """ @@ -737,7 +737,7 @@ def test_right(self): assert actual is not None np.testing.assert_almost_equal(actual, expected) - def test_half_right(self): + def test_half_right(self) -> None: """ Halfway between centre and right of image. """ @@ -763,7 +763,7 @@ def test_half_right(self): assert actual is not None np.testing.assert_almost_equal(actual, expected) - def test_bad_pixel_too_positive(self): + def test_bad_pixel_too_positive(self) -> None: """ Expect failure. """ @@ -786,7 +786,7 @@ def test_bad_pixel_too_positive(self): assert not result assert actual is None - def test_bad_pixel_negative(self): + def test_bad_pixel_negative(self) -> None: """ Expect failure. """ @@ -809,7 +809,7 @@ def test_bad_pixel_negative(self): assert not result assert actual is None - def test_bad_resolution_zero(self): + def test_bad_resolution_zero(self) -> None: """ Expect failure. """ @@ -832,7 +832,7 @@ def test_bad_resolution_zero(self): assert not result assert actual is None - def test_bad_resolution_negative(self): + def test_bad_resolution_negative(self) -> None: """ Expect failure. """ @@ -855,7 +855,7 @@ def test_bad_resolution_negative(self): assert not result assert actual is None - def test_bad_vec_base_not_r3(self): + def test_bad_vec_base_not_r3(self) -> None: """ Expect failure. """ @@ -884,7 +884,7 @@ class TestImageToCameraSpace: Test convert from image point to camera vector. """ - def test_centre(self, camera_intrinsic: camera_properties.CameraIntrinsics): + def test_centre(self, camera_intrinsic: camera_properties.CameraIntrinsics) -> None: """ Centre of image. """ @@ -902,7 +902,7 @@ def test_centre(self, camera_intrinsic: camera_properties.CameraIntrinsics): assert actual is not None np.testing.assert_almost_equal(actual, expected) - def test_top_left(self, camera_intrinsic: camera_properties.CameraIntrinsics): + def test_top_left(self, camera_intrinsic: camera_properties.CameraIntrinsics) -> None: """ Top left corner of image. """ @@ -920,7 +920,7 @@ def test_top_left(self, camera_intrinsic: camera_properties.CameraIntrinsics): assert actual is not None np.testing.assert_almost_equal(actual, expected) - def test_bottom_right(self, camera_intrinsic: camera_properties.CameraIntrinsics): + def test_bottom_right(self, camera_intrinsic: camera_properties.CameraIntrinsics) -> None: """ Bottom right corner of image. """ @@ -938,7 +938,9 @@ def test_bottom_right(self, camera_intrinsic: camera_properties.CameraIntrinsics assert actual is not None np.testing.assert_almost_equal(actual, expected) - def test_bad_pixel_x_negative(self, camera_intrinsic: camera_properties.CameraIntrinsics): + def test_bad_pixel_x_negative( + self, camera_intrinsic: camera_properties.CameraIntrinsics + ) -> None: """ Expect failure. """ @@ -953,7 +955,9 @@ def test_bad_pixel_x_negative(self, camera_intrinsic: camera_properties.CameraIn assert not result assert actual is None - def test_bad_pixel_x_too_positive(self, camera_intrinsic: camera_properties.CameraIntrinsics): + def test_bad_pixel_x_too_positive( + self, camera_intrinsic: camera_properties.CameraIntrinsics + ) -> None: """ Expect failure. """ @@ -968,7 +972,9 @@ def test_bad_pixel_x_too_positive(self, camera_intrinsic: camera_properties.Came assert not result assert actual is None - def test_bad_pixel_y_negative(self, camera_intrinsic: camera_properties.CameraIntrinsics): + def test_bad_pixel_y_negative( + self, camera_intrinsic: camera_properties.CameraIntrinsics + ) -> None: """ Expect failure. """ @@ -983,7 +989,9 @@ def test_bad_pixel_y_negative(self, camera_intrinsic: camera_properties.CameraIn assert not result assert actual is None - def test_bad_pixel_y_too_positive(self, camera_intrinsic: camera_properties.CameraIntrinsics): + def test_bad_pixel_y_too_positive( + self, camera_intrinsic: camera_properties.CameraIntrinsics + ) -> None: """ Expect failure. """ @@ -1004,7 +1012,7 @@ class TestCameraExtrinsicsCreate: Test constructor. """ - def test_normal(self): + def test_normal(self) -> None: """ Successful construction. """ @@ -1022,7 +1030,7 @@ def test_normal(self): assert result assert actual is not None - def test_bad_orientation(self): + def test_bad_orientation(self) -> None: """ Expect failure. """ diff --git a/tests/unit/test_cluster_detection.py b/tests/unit/test_cluster_detection.py index 6195149d..3063ab7a 100644 --- a/tests/unit/test_cluster_detection.py +++ b/tests/unit/test_cluster_detection.py @@ -9,6 +9,7 @@ from modules.cluster_estimation import cluster_estimation from modules import detection_in_world + MIN_TOTAL_POINTS_THRESHOLD = 100 MIN_NEW_POINTS_TO_RUN = 10 RNG_SEED = 0 @@ -21,7 +22,7 @@ @pytest.fixture() -def cluster_model(): +def cluster_model() -> cluster_estimation.ClusterEstimation: # type: ignore """ Cluster estimation object. """ @@ -33,7 +34,7 @@ def cluster_model(): assert result assert model is not None - yield model + yield model # type: ignore def generate_cluster_data( @@ -173,7 +174,9 @@ class TestModelExecutionCondition: __STD_DEV_REG = 1 # Regular standard deviation is 1m - def test_under_min_total_threshold(self, cluster_model: cluster_estimation.ClusterEstimation): + def test_under_min_total_threshold( + self, cluster_model: cluster_estimation.ClusterEstimation + ) -> None: """ Total data under threshold should not run. """ @@ -188,7 +191,9 @@ def test_under_min_total_threshold(self, cluster_model: cluster_estimation.Clust assert not result assert detections_in_world is None - def test_at_min_total_threshold(self, cluster_model: cluster_estimation.ClusterEstimation): + def test_at_min_total_threshold( + self, cluster_model: cluster_estimation.ClusterEstimation + ) -> None: """ Should run once total threshold reached regardless of current bucket size. @@ -210,7 +215,9 @@ def test_at_min_total_threshold(self, cluster_model: cluster_estimation.ClusterE assert result_2 assert detections_in_world_2 is not None - def test_under_min_bucket_size(self, cluster_model: cluster_estimation.ClusterEstimation): + def test_under_min_bucket_size( + self, cluster_model: cluster_estimation.ClusterEstimation + ) -> None: """ New data under threshold should not run. """ @@ -231,7 +238,7 @@ def test_under_min_bucket_size(self, cluster_model: cluster_estimation.ClusterEs assert not result_2 assert detections_in_world_2 is None - def test_good_data(self, cluster_model: cluster_estimation.ClusterEstimation): + def test_good_data(self, cluster_model: cluster_estimation.ClusterEstimation) -> None: """ All conditions met should run. """ @@ -257,7 +264,7 @@ class TestCorrectNumberClusterOutputs: def test_detect_normal_data_single_cluster( self, cluster_model: cluster_estimation.ClusterEstimation - ): + ) -> None: """ Data with small distribution and equal number of points per cluster centre. """ @@ -274,7 +281,7 @@ def test_detect_normal_data_single_cluster( def test_detect_normal_data_five_clusters( self, cluster_model: cluster_estimation.ClusterEstimation - ): + ) -> None: """ Data with small distribution and equal number of points per cluster centre. """ @@ -293,7 +300,7 @@ def test_detect_normal_data_five_clusters( def test_detect_large_std_dev_single_cluster( self, cluster_model: cluster_estimation.ClusterEstimation - ): + ) -> None: """ Data with large distribution and equal number of points per cluster centre. """ @@ -312,7 +319,7 @@ def test_detect_large_std_dev_single_cluster( def test_detect_large_std_dev_five_clusters( self, cluster_model: cluster_estimation.ClusterEstimation - ): + ) -> None: """ Data with large distribution and equal number of points per cluster centre. """ @@ -331,7 +338,7 @@ def test_detect_large_std_dev_five_clusters( def test_detect_skewed_data_single_cluster( self, cluster_model: cluster_estimation.ClusterEstimation - ): + ) -> None: """ Data with small distribution but varying number of points per cluster centre and random outlier points to simulate false detections. @@ -351,7 +358,7 @@ def test_detect_skewed_data_single_cluster( def test_detect_skewed_data_five_clusters( self, cluster_model: cluster_estimation.ClusterEstimation - ): + ) -> None: """ Data with small distribution but varying number of points per cluster centre and random outlier points to simulate false detections. @@ -384,7 +391,7 @@ def test_detect_skewed_data_five_clusters( def test_detect_consecutive_inputs_single_cluster( self, cluster_model: cluster_estimation.ClusterEstimation - ): + ) -> None: """ Previous tests executed model with all points available at once. This test feeds the model one point from the dataset one at a time (and calls .run() each time), checking for correct @@ -408,7 +415,7 @@ def test_detect_consecutive_inputs_single_cluster( def test_detect_consecutive_inputs_five_clusters( self, cluster_model: cluster_estimation.ClusterEstimation - ): + ) -> None: """ Previous tests executed model with all points available at once. This test feeds the model one point from the dataset one at a time (and calls .run() each time), checking for correct @@ -440,7 +447,9 @@ class TestCorrectClusterPositionOutput: __STD_DEV_REG = 1 # Regular standard deviation is 1m __MAX_POSITION_TOLERANCE = 1 - def test_position_regular_data(self, cluster_model: cluster_estimation.ClusterEstimation): + def test_position_regular_data( + self, cluster_model: cluster_estimation.ClusterEstimation + ) -> None: """ Five clusters with small standard deviation and large number of points per cluster. """ diff --git a/tests/unit/test_decision.py b/tests/unit/test_decision.py index 3c5ff20d..9b24211b 100644 --- a/tests/unit/test_decision.py +++ b/tests/unit/test_decision.py @@ -25,16 +25,16 @@ @pytest.fixture() -def decision_maker(): +def decision_maker() -> decision.Decision: # type: ignore """ Construct a Decision instance with predefined tolerance. """ decision_instance = decision.Decision(LANDING_PAD_LOCATION_TOLERANCE) - yield decision_instance + yield decision_instance # type: ignore @pytest.fixture() -def best_pad_within_tolerance(): +def best_pad_within_tolerance() -> object_in_world.ObjectInWorld: # type: ignore """ Create an ObjectInWorld instance within distance to pad tolerance. """ @@ -45,11 +45,11 @@ def best_pad_within_tolerance(): assert result assert pad is not None - yield pad + yield pad # type: ignore @pytest.fixture() -def best_pad_outside_tolerance(): +def best_pad_outside_tolerance() -> object_in_world.ObjectInWorld: # type: ignore """ Creates an ObjectInWorld instance outside of distance to pad tolerance. """ @@ -60,31 +60,31 @@ def best_pad_outside_tolerance(): assert result assert pad is not None - yield pad + yield pad # type: ignore @pytest.fixture() -def pads(): +def pads() -> "list[object_in_world.ObjectInWorld]": # type: ignore """ Create a list of ObjectInWorld instances for the landing pads. """ - result, pad1 = 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) assert result - assert pad1 is not None + assert pad_1 is not None - result, pad2 = 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) assert result - assert pad2 is not None + assert pad_2 is not None - result, pad3 = 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) assert result - assert pad3 is not None + assert pad_3 is not None - yield [pad1, pad2, pad3] + yield [pad_1, pad_2, pad_3] # type: ignore @pytest.fixture() -def drone_odometry_and_time(): +def drone_odometry_and_time() -> odometry_and_time.OdometryAndTime: # type: ignore """ Create an OdometryAndTime instance with the drone positioned within tolerance of landing pad. """ @@ -110,7 +110,7 @@ def drone_odometry_and_time(): assert result assert odometry_with_time is not None - yield odometry_with_time + yield odometry_with_time # type: ignore class TestDecision: @@ -119,8 +119,12 @@ class TestDecision: """ def test_decision_within_tolerance( - self, decision_maker, best_pad_within_tolerance, pads, drone_odometry_and_time - ): + self, + decision_maker: decision.Decision, + best_pad_within_tolerance: object_in_world.ObjectInWorld, + pads: "list[object_in_world.ObjectInWorld]", + drone_odometry_and_time: odometry_and_time.OdometryAndTime, + ) -> None: """ Test decision making when the best pad is within tolerance. """ @@ -134,8 +138,12 @@ def test_decision_within_tolerance( assert command.get_command_type() == expected def test_decision_outside_tolerance( - self, decision_maker, best_pad_outside_tolerance, pads, drone_odometry_and_time - ): + self, + decision_maker: decision.Decision, + best_pad_outside_tolerance: object_in_world.ObjectInWorld, + pads: "list[object_in_world.ObjectInWorld]", + drone_odometry_and_time: odometry_and_time.OdometryAndTime, + ) -> None: """ Test decision making when the best pad is outside tolerance. """ @@ -148,7 +156,11 @@ def test_decision_outside_tolerance( assert command is not None assert command.get_command_type() == expected - def test_decision_no_pads(self, decision_maker, drone_odometry_and_time): + def test_decision_no_pads( + self, + decision_maker: decision.Decision, + drone_odometry_and_time: odometry_and_time.OdometryAndTime, + ) -> None: """ Test decision making when no pads are available. """ diff --git a/tests/unit/test_detect_target.py b/tests/unit/test_detect_target.py index 72764601..a716c29b 100644 --- a/tests/unit/test_detect_target.py +++ b/tests/unit/test_detect_target.py @@ -104,16 +104,16 @@ def create_detections(detections_from_file: np.ndarray) -> detections_and_time.D @pytest.fixture() -def detector(): +def detector() -> detect_target.DetectTarget: # type: ignore """ Construct DetectTarget. """ detection = detect_target.DetectTarget(DEVICE, str(MODEL_PATH), OVERRIDE_FULL) - yield detection + yield detection # type: ignore @pytest.fixture() -def image_bus(): +def image_bus() -> image_and_time.ImageAndTime: # type: ignore """ Load bus image. """ @@ -121,11 +121,11 @@ def image_bus(): result, bus_image = image_and_time.ImageAndTime.create(image) assert result assert bus_image is not None - yield bus_image + yield bus_image # type: ignore @pytest.fixture() -def image_zidane(): +def image_zidane() -> image_and_time.ImageAndTime: # type: ignore """ Load Zidane image. """ @@ -133,25 +133,25 @@ def image_zidane(): result, zidane_image = image_and_time.ImageAndTime.create(image) assert result assert zidane_image is not None - yield zidane_image + yield zidane_image # type: ignore @pytest.fixture() -def expected_bus(): +def expected_bus() -> detections_and_time.DetectionsAndTime: # type: ignore """ Load expected bus detections. """ expected = np.loadtxt(BOUNDING_BOX_BUS_PATH) - yield create_detections(expected) + yield create_detections(expected) # type: ignore @pytest.fixture() -def expected_zidane(): +def expected_zidane() -> detections_and_time.DetectionsAndTime: # type: ignore """ Load expected Zidane detections. """ expected = np.loadtxt(BOUNDING_BOX_ZIDANE_PATH) - yield create_detections(expected) + yield create_detections(expected) # type: ignore class TestDetector: @@ -164,7 +164,7 @@ def test_single_bus_image( detector: detect_target.DetectTarget, image_bus: image_and_time.ImageAndTime, expected_bus: detections_and_time.DetectionsAndTime, - ): + ) -> None: """ Bus image. """ @@ -182,7 +182,7 @@ def test_single_zidane_image( detector: detect_target.DetectTarget, image_zidane: image_and_time.ImageAndTime, expected_zidane: detections_and_time.DetectionsAndTime, - ): + ) -> None: """ Zidane image. """ @@ -200,7 +200,7 @@ def test_multiple_zidane_image( detector: detect_target.DetectTarget, image_zidane: image_and_time.ImageAndTime, expected_zidane: detections_and_time.DetectionsAndTime, - ): + ) -> None: """ Multiple Zidane images. """ diff --git a/tests/unit/test_geolocation.py b/tests/unit/test_geolocation.py index 93087411..88820e2d 100644 --- a/tests/unit/test_geolocation.py +++ b/tests/unit/test_geolocation.py @@ -22,7 +22,7 @@ @pytest.fixture -def basic_locator(): +def basic_locator() -> geolocation.Geolocation: # type: ignore """ Forwards pointing camera. """ @@ -49,11 +49,11 @@ def basic_locator(): assert result assert locator is not None - yield locator + yield locator # type: ignore @pytest.fixture -def intermediate_locator(): +def intermediate_locator() -> geolocation.Geolocation: # type: ignore """ Downwards pointing camera offset towards front of drone. """ @@ -80,11 +80,11 @@ def intermediate_locator(): assert result assert locator is not None - yield locator + yield locator # type: ignore @pytest.fixture -def advanced_locator(): +def advanced_locator() -> geolocation.Geolocation: # type: ignore """ Camera angled at 75° upward. Drone is expected to rotate it downwards. @@ -112,11 +112,11 @@ def advanced_locator(): assert result assert locator is not None - yield locator + yield locator # type: ignore @pytest.fixture -def detection_bottom_right_point(): +def detection_bottom_right_point() -> detections_and_time.Detection: # type: ignore """ Bounding box is a single point. """ @@ -128,11 +128,11 @@ def detection_bottom_right_point(): assert result assert detection is not None - yield detection + yield detection # type: ignore @pytest.fixture -def detection_centre_left_point(): +def detection_centre_left_point() -> detections_and_time.Detection: # type: ignore """ Bounding box is a single point. """ @@ -144,11 +144,11 @@ def detection_centre_left_point(): assert result assert detection is not None - yield detection + yield detection # type: ignore @pytest.fixture -def detection1(): +def detection_1() -> detections_and_time.Detection: # type: ignore """ Entire image. """ @@ -160,11 +160,11 @@ def detection1(): assert result assert detection is not None - yield detection + yield detection # type: ignore @pytest.fixture -def detection2(): +def detection_2() -> detections_and_time.Detection: # type: ignore """ Quadrant. """ @@ -176,11 +176,11 @@ def detection2(): assert result assert detection is not None - yield detection + yield detection # type: ignore @pytest.fixture -def affine_matrix(): +def affine_matrix() -> np.ndarray: # type: ignore """ 3x3 homogeneous. """ @@ -193,11 +193,11 @@ def affine_matrix(): dtype=np.float32, ) - yield matrix + yield matrix # type: ignore @pytest.fixture -def non_affine_matrix(): +def non_affine_matrix() -> np.ndarray: # type: ignore """ 3x3 homogeneous. """ @@ -210,7 +210,7 @@ def non_affine_matrix(): dtype=np.float32, ) - yield matrix + yield matrix # type: ignore class TestGeolocationCreate: @@ -218,7 +218,7 @@ class TestGeolocationCreate: Test constructor. """ - def test_normal(self): + def test_normal(self) -> None: """ Successful construction. """ @@ -251,7 +251,7 @@ class TestGroundIntersection: Test where vector intersects with ground. """ - def test_above_origin_directly_down(self): + def test_above_origin_directly_down(self) -> None: """ Above origin, directly down. """ @@ -275,7 +275,7 @@ def test_above_origin_directly_down(self): assert actual is not None np.testing.assert_almost_equal(actual, expected, decimal=FLOAT_PRECISION_TOLERANCE) - def test_non_origin_directly_down(self): + def test_non_origin_directly_down(self) -> None: """ Directly down. """ @@ -299,7 +299,7 @@ def test_non_origin_directly_down(self): assert actual is not None np.testing.assert_almost_equal(actual, expected, decimal=FLOAT_PRECISION_TOLERANCE) - def test_above_origin_angled_down(self): + def test_above_origin_angled_down(self) -> None: """ Above origin, angled down towards positive. """ @@ -323,7 +323,7 @@ def test_above_origin_angled_down(self): assert actual is not None np.testing.assert_almost_equal(actual, expected, decimal=FLOAT_PRECISION_TOLERANCE) - def test_non_origin_angled_down(self): + def test_non_origin_angled_down(self) -> None: """ Angled down towards origin. """ @@ -347,7 +347,7 @@ def test_non_origin_angled_down(self): assert actual is not None np.testing.assert_almost_equal(actual, expected, decimal=FLOAT_PRECISION_TOLERANCE) - def test_bad_almost_horizontal(self): + def test_bad_almost_horizontal(self) -> None: """ False, None . """ @@ -368,7 +368,7 @@ def test_bad_almost_horizontal(self): assert not result assert actual is None - def test_bad_upwards(self): + def test_bad_upwards(self) -> None: """ False, None . """ @@ -389,7 +389,7 @@ def test_bad_upwards(self): assert not result assert actual is None - def test_bad_underground(self): + def test_bad_underground(self) -> None: """ False, None . """ @@ -416,7 +416,7 @@ class TestPerspectiveTransformMatrix: Test perspective transform creation. """ - def test_basic_above_origin_pointed_down(self, basic_locator: geolocation.Geolocation): + def test_basic_above_origin_pointed_down(self, basic_locator: geolocation.Geolocation) -> None: """ Above origin, directly down. """ @@ -456,7 +456,7 @@ def test_basic_above_origin_pointed_down(self, basic_locator: geolocation.Geoloc def test_intermediate_above_origin_pointing_north( self, intermediate_locator: geolocation.Geolocation - ): + ) -> None: """ Positioned so that the camera is above the origin directly down (but the drone is not). """ @@ -496,7 +496,7 @@ def test_intermediate_above_origin_pointing_north( def test_intermediate_above_origin_pointing_west( self, intermediate_locator: geolocation.Geolocation - ): + ) -> None: """ Positioned so that the camera is above the origin directly down (but the drone is not). """ @@ -534,7 +534,7 @@ def test_intermediate_above_origin_pointing_west( decimal=FLOAT_PRECISION_TOLERANCE, ) - def test_advanced(self, advanced_locator: geolocation.Geolocation): + def test_advanced(self, advanced_locator: geolocation.Geolocation) -> None: """ Camera is north of origin with an angle from vertical. Also rotated. """ @@ -595,7 +595,7 @@ def test_advanced(self, advanced_locator: geolocation.Geolocation): decimal=FLOAT_PRECISION_TOLERANCE, ) - def test_bad_direction(self, basic_locator: geolocation.Geolocation): + def test_bad_direction(self, basic_locator: geolocation.Geolocation) -> None: """ Camera pointing forward. """ @@ -636,7 +636,9 @@ class TestGeolocationConvertDetection: Test extract and convert. """ - def test_normal1(self, detection1: detections_and_time.Detection, affine_matrix: np.ndarray): + def test_normal1( + self, detection_1: detections_and_time.Detection, affine_matrix: np.ndarray + ) -> None: """ Normal detection and matrix. """ @@ -666,7 +668,7 @@ def test_normal1(self, detection1: detections_and_time.Detection, affine_matrix: result, actual, ) = geolocation.Geolocation._Geolocation__convert_detection_to_world_from_image( # type: ignore - detection1, + detection_1, affine_matrix, ) @@ -679,7 +681,9 @@ def test_normal1(self, detection1: detections_and_time.Detection, affine_matrix: assert actual.label == expected.label np.testing.assert_almost_equal(actual.confidence, expected.confidence) - def test_normal2(self, detection2: detections_and_time.Detection, affine_matrix: np.ndarray): + def test_normal2( + self, detection_2: detections_and_time.Detection, affine_matrix: np.ndarray + ) -> None: """ Normal detection and matrix. """ @@ -709,7 +713,7 @@ def test_normal2(self, detection2: detections_and_time.Detection, affine_matrix: result, actual, ) = geolocation.Geolocation._Geolocation__convert_detection_to_world_from_image( # type: ignore - detection2, + detection_2, affine_matrix, ) @@ -731,9 +735,9 @@ class TestGeolocationRun: def test_basic( self, basic_locator: geolocation.Geolocation, - detection1: detections_and_time.Detection, - detection2: detections_and_time.Detection, - ): + detection_1: detections_and_time.Detection, + detection_2: detections_and_time.Detection, + ) -> None: """ 2 detections. """ @@ -764,14 +768,14 @@ def test_basic( result, merged_detections = merged_odometry_detections.MergedOdometryDetections.create( drone_odometry, [ - detection1, - detection2, + detection_1, + detection_2, ], ) assert result assert merged_detections is not None - result, expected_detection1 = detection_in_world.DetectionInWorld.create( + result, expected_detection_1 = detection_in_world.DetectionInWorld.create( np.array( [ [100.0, -100.0], @@ -789,9 +793,9 @@ def test_basic( 1.0, ) assert result - assert expected_detection1 is not None + assert expected_detection_1 is not None - result, expected_detection2 = detection_in_world.DetectionInWorld.create( + result, expected_detection_2 = detection_in_world.DetectionInWorld.create( np.array( [ [100.0, -100.0], @@ -809,11 +813,11 @@ def test_basic( 0.5, ) assert result - assert expected_detection2 is not None + assert expected_detection_2 is not None expected_list = [ - expected_detection1, - expected_detection2, + expected_detection_1, + expected_detection_2, ] # Run @@ -835,7 +839,7 @@ def test_advanced( advanced_locator: geolocation.Geolocation, detection_bottom_right_point: detections_and_time.Detection, detection_centre_left_point: detections_and_time.Detection, - ): + ) -> None: """ 2 point detections. """ @@ -941,8 +945,8 @@ def test_advanced( np.testing.assert_almost_equal(actual.confidence, expected_list[i].confidence) def test_bad_direction( - self, basic_locator: geolocation.Geolocation, detection1: detections_and_time.Detection - ): + self, basic_locator: geolocation.Geolocation, detection_1: detections_and_time.Detection + ) -> None: """ Bad direction. """ @@ -972,7 +976,7 @@ def test_bad_direction( result, merged_detections = merged_odometry_detections.MergedOdometryDetections.create( drone_odometry, - [detection1], + [detection_1], ) assert result assert merged_detections is not None diff --git a/tests/unit/test_landing_pad_tracking.py b/tests/unit/test_landing_pad_tracking.py index 5a3a2d6e..2d8db21e 100644 --- a/tests/unit/test_landing_pad_tracking.py +++ b/tests/unit/test_landing_pad_tracking.py @@ -17,16 +17,16 @@ @pytest.fixture() -def tracker(): +def tracker() -> landing_pad_tracking.LandingPadTracking: # type: ignore """ Instantiates model tracking. """ tracking = landing_pad_tracking.LandingPadTracking(DISTANCE_SQUARED_THRESHOLD) - yield tracking + yield tracking # type: ignore @pytest.fixture -def detections_1(): +def detections_1() -> "list[object_in_world.ObjectInWorld]": # type: ignore """ Sample instances of ObjectInWorld for testing. """ @@ -51,11 +51,11 @@ def detections_1(): assert obj_5 is not None detections = [obj_1, obj_2, obj_3, obj_4, obj_5] - yield detections + yield detections # type: ignore @pytest.fixture -def detections_2(): +def detections_2() -> "list[object_in_world.ObjectInWorld]": # type: ignore """ Sample instances of ObjectInWorld for testing. """ @@ -80,11 +80,11 @@ def detections_2(): assert obj_5 is not None detections = [obj_1, obj_2, obj_3, obj_4, obj_5] - yield detections + yield detections # type: ignore @pytest.fixture -def detections_3(): +def detections_3() -> "list[object_in_world.ObjectInWorld]": # type: ignore """ Sample instances of ObjectInWorld for testing. """ @@ -109,7 +109,7 @@ def detections_3(): assert obj_5 is not None detections = [obj_1, obj_2, obj_3, obj_4, obj_5] - yield detections + yield detections # type: ignore class TestSimilar: @@ -118,7 +118,7 @@ class TestSimilar: considered similar. """ - def test_is_similar_positive_equal_to_threshold(self): + 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. @@ -135,7 +135,7 @@ def test_is_similar_positive_equal_to_threshold(self): assert actual == expected - def test_is_similar_negative_equal_to_threshold(self): + 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. @@ -152,7 +152,7 @@ def test_is_similar_negative_equal_to_threshold(self): assert actual == expected - def test_is_similar_positive_less_than_threshold(self): + 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. @@ -169,7 +169,7 @@ def test_is_similar_positive_less_than_threshold(self): assert actual == expected - def test_is_similar_negative_less_than_threshold(self): + 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. @@ -186,7 +186,7 @@ def test_is_similar_negative_less_than_threshold(self): assert actual == expected - def test_is_similar_positive_more_than_threshold(self): + 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. @@ -203,7 +203,7 @@ def test_is_similar_positive_more_than_threshold(self): assert actual == expected - def test_is_similar_negative_more_than_threshold(self): + 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. @@ -230,7 +230,7 @@ def test_mark_false_positive_no_similar( self, tracker: landing_pad_tracking.LandingPadTracking, detections_1: "list[object_in_world.ObjectInWorld]", - ): + ) -> None: """ Test if marking false positive adds detection to list of false positives. """ @@ -256,7 +256,7 @@ def test_mark_false_positive_with_similar( self, tracker: landing_pad_tracking.LandingPadTracking, detections_2: "list[object_in_world.ObjectInWorld]", - ): + ) -> None: """ Test if marking false positive adds detection to list of false positives and removes. similar landing pads @@ -277,7 +277,7 @@ def test_mark_multiple_false_positive( self, tracker: landing_pad_tracking.LandingPadTracking, detections_1: "list[object_in_world.ObjectInWorld]", - ): + ) -> None: """ Test if marking false positive adds detection to list of false positives. """ @@ -303,7 +303,9 @@ class TestMarkConfirmedPositive: Test if landing pad tracking correctly marks a detection as a confirmed positive. """ - def test_mark_confirmed_positive(self, tracker: landing_pad_tracking.LandingPadTracking): + def test_mark_confirmed_positive( + self, tracker: landing_pad_tracking.LandingPadTracking + ) -> None: """ Test if marking confirmed positive adds detection to list of confirmed positives. """ @@ -318,7 +320,7 @@ def test_mark_confirmed_positive(self, tracker: landing_pad_tracking.LandingPadT def test_mark_multiple_confirmed_positives( self, tracker: landing_pad_tracking.LandingPadTracking - ): + ) -> None: """ Test if marking confirmed positive adds detection to list of confirmed positives. """ @@ -341,7 +343,9 @@ class TestLandingPadTracking: Test landing pad tracking run function. """ - def test_run_with_empty_detections_list(self, tracker: landing_pad_tracking.LandingPadTracking): + def test_run_with_empty_detections_list( + self, tracker: landing_pad_tracking.LandingPadTracking + ) -> None: """ Test run method with empty detections list. """ @@ -353,7 +357,7 @@ def test_run_one_input( self, tracker: landing_pad_tracking.LandingPadTracking, detections_1: "list[object_in_world.ObjectInWorld]", - ): + ) -> None: """ Test run with only 1 input. """ @@ -376,7 +380,7 @@ def test_run_one_input_similar_detections( self, tracker: landing_pad_tracking.LandingPadTracking, detections_3: "list[object_in_world.ObjectInWorld]", - ): + ) -> None: """ Test run with only 1 input where 2 landing pads are similar. """ @@ -399,7 +403,7 @@ def test_run_multiple_inputs( tracker: landing_pad_tracking.LandingPadTracking, detections_1: "list[object_in_world.ObjectInWorld]", detections_2: "list[object_in_world.ObjectInWorld]", - ): + ) -> None: """ Test run with 2 inputs where some landing pads are similar. """ @@ -426,7 +430,7 @@ def test_run_with_confirmed_positive( self, tracker: landing_pad_tracking.LandingPadTracking, detections_1: "list[object_in_world.ObjectInWorld]", - ): + ) -> None: """ Test run when there is a confirmed positive. """ @@ -445,7 +449,7 @@ def test_run_with_false_positive( self, tracker: landing_pad_tracking.LandingPadTracking, detections_2: "list[object_in_world.ObjectInWorld]", - ): + ) -> None: """ Test to see if run function doesn't add landing pads that are similar to false positives. """ From 83b8425aff2ffd6d79b16e3d9d395f8b17ae9959 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sat, 24 Feb 2024 18:05:25 -0500 Subject: [PATCH 11/18] Pylint submodule ignore --- pyproject.toml | 47 ++++++++++++++++++++++++++++++++++------------- setup.cfg | 3 +++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 33d8d835..2218bf35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,10 @@ ignore-paths = [ # Logging "logs/", + + # Outside of .gitignore + # Submodules + "modules/common/", ] # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the @@ -38,7 +42,9 @@ good-names = [ "ex", "Run", "_", - "result_main", # Return of main() + + # Return of main() + "result_main", ] # Good variable names regexes, separated by a comma. If names match any regex, @@ -66,29 +72,44 @@ disable = [ "use-symbolic-message-instead", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero", - "fixme", # Ignore TODOs - "import-error", # Pylint cannot find modules - "line-too-long", # Covered by black formatter - "no-member", # Pylint cannot handle 3rd party imports - "too-few-public-methods", # Some classes are simple - "too-many-arguments", # Function signatures - "too-many-branches", # Don't care - "too-many-lines", # Line count in file - "too-many-locals", # Don't care - "too-many-statements", # Don't care - "too-many-return-statements", # Don't care + # WARG + # Ignore TODOs + "fixme", + # Pylint cannot find modules + "import-error", + # Covered by Black formatter + "line-too-long", + # Pylint cannot handle 3rd party imports + "no-member", + # Some classes are simple + "too-few-public-methods", + # Function signatures + "too-many-arguments", + # Don't care + "too-many-branches", + # Line count in file + "too-many-lines", + # Don't care + "too-many-locals", + # Don't care + "too-many-statements", + # Don't care + "too-many-return-statements", ] [tool.pylint.similarities] # Minimum lines number of a similarity. -min-similarity-lines = 10 # Main guard +# Main guard +min-similarity-lines = 10 [tool.pytest.ini_options] minversion = "6.0" +# Submodules addopts = "--ignore=modules/common/" [tool.black] line-length = 100 target-version = ["py38"] # Excludes files or directories in addition to the defaults +# Submodules extend-exclude = "modules/common/*" diff --git a/setup.cfg b/setup.cfg index 89fc0506..6a4f4bf2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,11 +10,14 @@ extend-exclude= # IDEs .idea/, .vscode/, + # Python __pycache__/, venv/, + # Logging logs/, + # Outside of .gitignore # Submodules modules/common/, From 359ff627b1bb4d147530b67fffa1678eb224c4e3 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sat, 24 Feb 2024 18:22:08 -0500 Subject: [PATCH 12/18] Github workflow comment --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index d579c995..37e5de31 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -29,7 +29,7 @@ jobs: git submodule update --init --recursive --remote pip install -r ./modules/common/requirements.txt - # Install computer-vision-python dependencies + # Install project dependencies - name: Install project dependencies run: | python -m pip install --upgrade pip From 4a7c0f6437760711131c48c50c227486f85e0df1 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sat, 23 Mar 2024 19:38:18 -0400 Subject: [PATCH 13/18] Matrix formatting --- modules/geolocation/camera_properties.py | 18 ++++++---- tests/unit/test_camera_properties.py | 34 +++++++++++------- tests/unit/test_geolocation.py | 46 ++++++++++++++++-------- 3 files changed, 65 insertions(+), 33 deletions(-) diff --git a/modules/geolocation/camera_properties.py b/modules/geolocation/camera_properties.py index f7ef5722..76f93999 100644 --- a/modules/geolocation/camera_properties.py +++ b/modules/geolocation/camera_properties.py @@ -38,32 +38,38 @@ def create_rotation_matrix_from_orientation( if roll < -np.pi or roll > np.pi: return False, None + # fmt: off yaw_matrix = np.array( [ [np.cos(yaw), -np.sin(yaw), 0.0], - [np.sin(yaw), np.cos(yaw), 0.0], - [0.0, 0.0, 1.0], + [np.sin(yaw), np.cos(yaw), 0.0], + [ 0.0, 0.0, 1.0], ], dtype=np.float32, ) + # fmt: on + # fmt: off pitch_matrix = np.array( [ - [np.cos(pitch), 0.0, np.sin(pitch)], - [0.0, 1.0, 0.0], + [ np.cos(pitch), 0.0, np.sin(pitch)], + [ 0.0, 1.0, 0.0], [-np.sin(pitch), 0.0, np.cos(pitch)], ], dtype=np.float32, ) + # fmt: on + # fmt: off roll_matrix = np.array( [ - [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], [0.0, np.cos(roll), -np.sin(roll)], - [0.0, np.sin(roll), np.cos(roll)], + [0.0, np.sin(roll), np.cos(roll)], ], dtype=np.float32, ) + # fmt: on rotation_matrix = yaw_matrix @ pitch_matrix @ roll_matrix diff --git a/tests/unit/test_camera_properties.py b/tests/unit/test_camera_properties.py index 1f91a17a..d58ce4bc 100644 --- a/tests/unit/test_camera_properties.py +++ b/tests/unit/test_camera_properties.py @@ -188,14 +188,16 @@ def test_yaw_quarter(self) -> None: pitch = 0.0 roll = 0.0 + # fmt: off expected = np.array( [ [0.0, -1.0, 0.0], - [1.0, 0.0, 0.0], - [0.0, 0.0, 1.0], + [1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], ], dtype=np.float32, ) + # fmt: on # Run result, actual = camera_properties.create_rotation_matrix_from_orientation( @@ -218,14 +220,16 @@ def test_pitch_quarter(self) -> None: pitch = np.pi / 2 roll = 0.0 + # fmt: off expected = np.array( [ - [0.0, 0.0, 1.0], - [0.0, 1.0, 0.0], + [ 0.0, 0.0, 1.0], + [ 0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], ], dtype=np.float32, ) + # fmt: on # Run result, actual = camera_properties.create_rotation_matrix_from_orientation( @@ -248,14 +252,16 @@ def test_roll_quarter(self) -> None: pitch = 0.0 roll = np.pi / 2 + # fmt: off expected = np.array( [ - [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], - [0.0, 1.0, 0.0], + [0.0, 1.0, 0.0], ], dtype=np.float32, ) + # fmt: on # Run result, actual = camera_properties.create_rotation_matrix_from_orientation( @@ -278,14 +284,16 @@ def test_combined_rotations_positive(self) -> None: pitch = np.pi / 4 roll = np.pi / 4 + # fmt: off expected = np.array( [ - [1 / 2, (np.sqrt(2) - 2) / 4, (np.sqrt(2) + 2) / 4], - [1 / 2, (np.sqrt(2) + 2) / 4, (np.sqrt(2) - 2) / 4], - [-np.sqrt(2) / 2, 1 / 2, 1 / 2], + [ 1 / 2, (np.sqrt(2) - 2) / 4, (np.sqrt(2) + 2) / 4], + [ 1 / 2, (np.sqrt(2) + 2) / 4, (np.sqrt(2) - 2) / 4], + [-np.sqrt(2) / 2, 1 / 2, 1 / 2], ], dtype=np.float32, ) + # fmt: on # Run result, actual = camera_properties.create_rotation_matrix_from_orientation( @@ -308,14 +316,16 @@ def test_combined_rotations_negative(self) -> None: pitch = -np.pi / 4 roll = -np.pi / 4 + # fmt: off expected = np.array( [ - [1 / 2, (np.sqrt(2) + 2) / 4, (-np.sqrt(2) + 2) / 4], - [-1 / 2, (-np.sqrt(2) + 2) / 4, (np.sqrt(2) + 2) / 4], - [np.sqrt(2) / 2, -1 / 2, 1 / 2], + [ 1 / 2, (np.sqrt(2) + 2) / 4, (-np.sqrt(2) + 2) / 4], + [ -1 / 2, (-np.sqrt(2) + 2) / 4, (np.sqrt(2) + 2) / 4], + [np.sqrt(2) / 2, -1 / 2, 1 / 2], ], dtype=np.float32, ) + # fmt: on # Run result, actual = camera_properties.create_rotation_matrix_from_orientation( diff --git a/tests/unit/test_geolocation.py b/tests/unit/test_geolocation.py index 88820e2d..f8c2dda8 100644 --- a/tests/unit/test_geolocation.py +++ b/tests/unit/test_geolocation.py @@ -184,14 +184,16 @@ def affine_matrix() -> np.ndarray: # type: ignore """ 3x3 homogeneous. """ + # fmt: off matrix = np.array( [ [0.0, 1.0, -1.0], [2.0, 0.0, -1.0], - [0.0, 0.0, 1.0], + [0.0, 0.0, 1.0], ], dtype=np.float32, ) + # fmt: on yield matrix # type: ignore @@ -201,14 +203,16 @@ def non_affine_matrix() -> np.ndarray: # type: ignore """ 3x3 homogeneous. """ + # fmt: off matrix = np.array( [ [0.0, 1.0, -1.0], [2.0, 0.0, -1.0], - [1.0, 1.0, 1.0], + [1.0, 1.0, 1.0], ], dtype=np.float32, ) + # fmt: on yield matrix # type: ignore @@ -644,15 +648,17 @@ def test_normal1( """ # Setup result, expected = detection_in_world.DetectionInWorld.create( + # fmt: off np.array( [ - [-1.0, -1.0], - [-1.0, 3999.0], - [1999.0, -1.0], + [ -1.0, -1.0], + [ -1.0, 3999.0], + [1999.0, -1.0], [1999.0, 3999.0], ], dtype=np.float32, ), + # fmt: on np.array( [999.0, 1999.0], dtype=np.float32, @@ -689,15 +695,17 @@ def test_normal2( """ # Setup result, expected = detection_in_world.DetectionInWorld.create( + # fmt: off np.array( [ - [-1.0, -1.0], - [-1.0, 1999.0], - [999.0, -1.0], + [ -1.0, -1.0], + [ -1.0, 1999.0], + [999.0, -1.0], [999.0, 1999.0], ], dtype=np.float32, ), + # fmt: on np.array( [499.0, 999.0], dtype=np.float32, @@ -776,15 +784,17 @@ def test_basic( assert merged_detections is not None result, expected_detection_1 = detection_in_world.DetectionInWorld.create( + # fmt: off np.array( [ - [100.0, -100.0], - [100.0, 100.0], + [ 100.0, -100.0], + [ 100.0, 100.0], [-100.0, -100.0], - [-100.0, 100.0], + [-100.0, 100.0], ], dtype=np.float32, ), + # fmt: on np.array( [0.0, 0.0], dtype=np.float32, @@ -796,15 +806,17 @@ def test_basic( assert expected_detection_1 is not None result, expected_detection_2 = detection_in_world.DetectionInWorld.create( + # fmt: off np.array( [ - [100.0, -100.0], - [100.0, 0.0], - [0.0, -100.0], - [0.0, 0.0], + [ 100.0, -100.0], + [ 100.0, 0.0], + [ 0.0, -100.0], + [ 0.0, 0.0], ], dtype=np.float32, ), + # fmt: on np.array( [50.0, -50.0], dtype=np.float32, @@ -878,6 +890,7 @@ def test_advanced( assert merged_detections is not None result, expected_bottom_right = detection_in_world.DetectionInWorld.create( + # fmt: off np.array( [ [10.0 + 100.0 * np.sqrt(3), 100.0], @@ -887,6 +900,7 @@ def test_advanced( ], dtype=np.float32, ), + # fmt: on np.array( [10.0 + 100.0 * np.sqrt(3), 100.0], dtype=np.float32, @@ -898,6 +912,7 @@ def test_advanced( assert expected_bottom_right is not None result, expected_centre_left = detection_in_world.DetectionInWorld.create( + # fmt: off np.array( [ [10.0, 0.0], @@ -907,6 +922,7 @@ def test_advanced( ], dtype=np.float32, ), + # fmt: on np.array( [10.0, 0.0], dtype=np.float32, From 72e6efc2ced19deaa6282f51cfae87ef8ad5336a Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sat, 23 Mar 2024 20:20:54 -0400 Subject: [PATCH 14/18] String --- modules/decision_command.py | 6 ++--- modules/detections_and_time.py | 38 +++++++-------------------- modules/drone_odometry_local.py | 32 +++++++--------------- modules/merged_odometry_detections.py | 10 +++---- modules/odometry_and_time.py | 8 +++--- 5 files changed, 29 insertions(+), 65 deletions(-) diff --git a/modules/decision_command.py b/modules/decision_command.py index 2f8b989e..904ec834 100644 --- a/modules/decision_command.py +++ b/modules/decision_command.py @@ -139,13 +139,13 @@ def get_command_position(self) -> "tuple[float, float, float]": """ return self.__command_x, self.__command_y, self.__command_z - def __repr__(self) -> str: + def __str__(self) -> str: """ To string. """ - representation = "Command: " + str(self.__command_type) + representation = f"Command: {self.__command_type}" if self.__command_type != DecisionCommand.CommandType.LAND_AT_CURRENT_POSITION: - representation += " " + str(self.get_command_position()) + representation += f" {self.get_command_position()}" return representation diff --git a/modules/detections_and_time.py b/modules/detections_and_time.py index b70cb278..0ce032fa 100644 --- a/modules/detections_and_time.py +++ b/modules/detections_and_time.py @@ -51,25 +51,11 @@ def __init__( self.label = label self.confidence = confidence - def __repr__(self) -> str: - """ - String representation. - """ - representation = ( - "cls: " - + str(self.label) - + ", conf: " - + str(self.confidence) - + ", bounds: " - + str(self.x_1) - + " " - + str(self.y_1) - + " " - + str(self.x_2) - + " " - + str(self.y_2) - ) - + def __str__(self) -> str: + """ + To string. + """ + representation = f"cls: {self.label}, conf: {self.confidence}, bounds: {self.x_1} {self.y_1} {self.x_2} {self.y_2}" return representation def get_centre(self) -> "tuple[float, float]": @@ -118,19 +104,13 @@ def __init__(self, class_private_create_key: object, timestamp: float) -> None: self.detections = [] self.timestamp = timestamp - def __repr__(self) -> str: + def __str__(self) -> str: """ - String representation. + To string. """ - representation = ( - str(self.__class__) - + ", time: " - + str(int(self.timestamp)) - + ", size: " - + str(len(self)) - ) + representation = f"""{self.__class__}, time: {int(self.timestamp)}, size: {len(self)} +{self.detections}""" - representation += "\n" + repr(self.detections) return representation def __len__(self) -> int: diff --git a/modules/drone_odometry_local.py b/modules/drone_odometry_local.py index 6a09ab89..7071b697 100644 --- a/modules/drone_odometry_local.py +++ b/modules/drone_odometry_local.py @@ -33,18 +33,11 @@ def __init__( self.east = east self.down = down - def __repr__(self) -> str: + def __str__(self) -> str: """ - String representation. + To string. """ - return ( - "DronePositionLocal (NED): " - + str(self.north) - + ", " - + str(self.east) - + ", " - + str(self.down) - ) + return f"DronePositionLocal (NED): {self.north}, {self.east}, {self.down}" class DroneOrientationLocal: @@ -89,19 +82,12 @@ def __init__( self.orientation = orientation - def __repr__(self) -> str: + def __str__(self) -> str: """ - String representation. + To string. """ # TODO: Update common - return ( - "DroneOrientationLocal (YPR): " - + str(self.orientation.yaw) - + ", " - + str(self.orientation.pitch) - + ", " - + str(self.orientation.roll) - ) + return f"DroneOrientationLocal (YPR rad): {self.orientation.yaw}, {self.orientation.pitch}, {self.orientation.roll}" class DroneOdometryLocal: @@ -140,8 +126,8 @@ def __init__( self.position = position self.orientation = orientation - def __repr__(self) -> str: + def __str__(self) -> str: """ - String representation. + To string. """ - return "DroneOdometryLocal: " + str(self.position) + ", " + str(self.orientation) + return f"DroneOdometryLocal: {self.position}, {self.orientation}" diff --git a/modules/merged_odometry_detections.py b/modules/merged_odometry_detections.py index 31dd82cf..421362fb 100644 --- a/modules/merged_odometry_detections.py +++ b/modules/merged_odometry_detections.py @@ -44,13 +44,11 @@ def __init__( self.odometry_local = odometry_local self.detections = detections - def __repr__(self) -> str: + def __str__(self) -> str: """ - String representation. + To string. """ - representation = ( - "Merged: " + str(self.odometry_local) + ", detections: " + str(len(self.detections)) - ) + representation = f"""Merged: {self.odometry_local}, detections: {len(self.detections)} +{self.detections}""" - representation += "\n" + str(self.detections) return representation diff --git a/modules/odometry_and_time.py b/modules/odometry_and_time.py index 087ea507..e863f986 100644 --- a/modules/odometry_and_time.py +++ b/modules/odometry_and_time.py @@ -43,11 +43,11 @@ def __init__( self.odometry_data = odometry_data self.timestamp = timestamp - def __repr__(self) -> str: + def __str__(self) -> str: """ - String representation. + To string. """ - representation = str(self.__class__) + ", time: " + str(int(self.timestamp)) + representation = f"""{self.__class__}, time: {int(self.timestamp)} +{self.odometry_data}""" - representation += "\n" + repr(self.odometry_data) return representation From 1ee05b2e342d6f4927605a9cf794ada8dae2ac40 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sat, 23 Mar 2024 20:21:07 -0400 Subject: [PATCH 15/18] Cleanup --- documentation/tests/unit/test_pytest_example.py | 5 +++++ modules/cluster_estimation/cluster_estimation.py | 14 ++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/documentation/tests/unit/test_pytest_example.py b/documentation/tests/unit/test_pytest_example.py index 1cbee19b..72de1333 100644 --- a/documentation/tests/unit/test_pytest_example.py +++ b/documentation/tests/unit/test_pytest_example.py @@ -9,6 +9,11 @@ import pytest +# Test functions use test fixture signature names and access class privates +# No enable +# pylint: disable=protected-access,redefined-outer-name + + def test_trivial_pass() -> None: """ Unit test will pass by default. diff --git a/modules/cluster_estimation/cluster_estimation.py b/modules/cluster_estimation/cluster_estimation.py index d7f6780a..31675f47 100644 --- a/modules/cluster_estimation/cluster_estimation.py +++ b/modules/cluster_estimation/cluster_estimation.py @@ -311,16 +311,18 @@ def __filter_by_points_ownership( filtered_output: list[tuple[np.ndarray, float, float]] List containing predicted cluster centres after filtering. """ - results = self.__vgmm.predict(self.__all_points) # type: ignore - filtered_output = [] + # 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) - # Filtering by each cluster's point ownership - unique_clusters, _ = np.unique(results, return_counts=True) # Remove empty clusters - # Integer index required + 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 unique_clusters: + if i in clusters_with_points: filtered_output.append(model_output[i]) return filtered_output From ef26020cc1fc24ae6da0e6fa6f1d5609c1b02496 Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sat, 23 Mar 2024 20:24:22 -0400 Subject: [PATCH 16/18] Remove Pylint top level regex --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2218bf35..4927a25b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,10 +47,6 @@ good-names = [ "result_main", ] -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs = "computer-vision-python" - [tool.pylint."messages control"] # Disable the message, report, category or checker with the given id(s). You can # either give multiple identifiers separated by comma (,) or put this option From d373419fc943f5049a059ac89deaa05b126874de Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sat, 23 Mar 2024 20:58:03 -0400 Subject: [PATCH 17/18] Undo multiline --- modules/detections_and_time.py | 7 ++++--- modules/merged_odometry_detections.py | 7 ++++--- modules/odometry_and_time.py | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/modules/detections_and_time.py b/modules/detections_and_time.py index 0ce032fa..6fa84251 100644 --- a/modules/detections_and_time.py +++ b/modules/detections_and_time.py @@ -108,9 +108,10 @@ def __str__(self) -> str: """ To string. """ - representation = f"""{self.__class__}, time: {int(self.timestamp)}, size: {len(self)} -{self.detections}""" - + representation = ( + f"{self.__class__}, time: {int(self.timestamp)}, size: {len(self)}\n" + + f"{self.detections}" + ) return representation def __len__(self) -> int: diff --git a/modules/merged_odometry_detections.py b/modules/merged_odometry_detections.py index 421362fb..d26795e3 100644 --- a/modules/merged_odometry_detections.py +++ b/modules/merged_odometry_detections.py @@ -48,7 +48,8 @@ def __str__(self) -> str: """ To string. """ - representation = f"""Merged: {self.odometry_local}, detections: {len(self.detections)} -{self.detections}""" - + representation = ( + f"Merged: {self.odometry_local}, detections: {len(self.detections)}\n" + + f"{self.detections}" + ) return representation diff --git a/modules/odometry_and_time.py b/modules/odometry_and_time.py index e863f986..9f119472 100644 --- a/modules/odometry_and_time.py +++ b/modules/odometry_and_time.py @@ -47,7 +47,7 @@ def __str__(self) -> str: """ To string. """ - representation = f"""{self.__class__}, time: {int(self.timestamp)} -{self.odometry_data}""" - + representation = ( + f"{self.__class__}, time: {int(self.timestamp)}\n" + f"{self.odometry_data}" + ) return representation From 414c2ce09cf46f9b1a0ee0833a681535fea74f9c Mon Sep 17 00:00:00 2001 From: Xierumeng Date: Sat, 23 Mar 2024 23:58:14 -0400 Subject: [PATCH 18/18] String methods --- modules/detections_and_time.py | 6 ++---- modules/merged_odometry_detections.py | 3 +-- modules/odometry_and_time.py | 5 +---- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/modules/detections_and_time.py b/modules/detections_and_time.py index 6fa84251..da04d00f 100644 --- a/modules/detections_and_time.py +++ b/modules/detections_and_time.py @@ -55,8 +55,7 @@ def __str__(self) -> str: """ To string. """ - representation = f"cls: {self.label}, conf: {self.confidence}, bounds: {self.x_1} {self.y_1} {self.x_2} {self.y_2}" - return representation + return f"cls: {self.label}, conf: {self.confidence}, bounds: {self.x_1} {self.y_1} {self.x_2} {self.y_2}" def get_centre(self) -> "tuple[float, float]": """ @@ -108,11 +107,10 @@ def __str__(self) -> str: """ To string. """ - representation = ( + return ( f"{self.__class__}, time: {int(self.timestamp)}, size: {len(self)}\n" + f"{self.detections}" ) - return representation def __len__(self) -> int: """ diff --git a/modules/merged_odometry_detections.py b/modules/merged_odometry_detections.py index d26795e3..8df23229 100644 --- a/modules/merged_odometry_detections.py +++ b/modules/merged_odometry_detections.py @@ -48,8 +48,7 @@ def __str__(self) -> str: """ To string. """ - representation = ( + return ( f"Merged: {self.odometry_local}, detections: {len(self.detections)}\n" + f"{self.detections}" ) - return representation diff --git a/modules/odometry_and_time.py b/modules/odometry_and_time.py index 9f119472..a6b421a2 100644 --- a/modules/odometry_and_time.py +++ b/modules/odometry_and_time.py @@ -47,7 +47,4 @@ def __str__(self) -> str: """ To string. """ - representation = ( - f"{self.__class__}, time: {int(self.timestamp)}\n" + f"{self.odometry_data}" - ) - return representation + return f"{self.__class__}, time: {int(self.timestamp)}\n" + f"{self.odometry_data}"