-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add hotspot search path generation (#77)
* Add hotspot search path generation * Increase safety of path generation * Add hard coded test numbers * Remove num_points variable
- Loading branch information
Showing
2 changed files
with
200 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
""" | ||
Generates search path for hotspots. | ||
""" | ||
|
||
import math | ||
|
||
from . import plot_circular_path | ||
from .common.modules import position_global_relative_altitude | ||
|
||
|
||
MULTIPLIER = 1.2 | ||
MINIMUM_POINTS = 3 | ||
|
||
|
||
def generate_search_path( | ||
center: position_global_relative_altitude.PositionGlobalRelativeAltitude, | ||
search_radius: float, | ||
search_area_dimensions: "tuple[float, float]", | ||
) -> "tuple[bool, list[position_global_relative_altitude.PositionGlobalRelativeAltitude] | None]": | ||
""" | ||
Generates list of spline waypoints representing concentric rings for drone search path. | ||
center: waypoint for center of circle. | ||
search_radius: drone search radius. | ||
search_area_dimensions: search area width, height | ||
Returns: Success, list of waypoints. | ||
""" | ||
camera_horizontal_size, camera_vertical_size = search_area_dimensions | ||
|
||
if camera_horizontal_size <= 0 or camera_vertical_size <= 0: | ||
print(f"ERROR: Camera dimensions must be greater than 0: {search_area_dimensions}") | ||
return False, None | ||
|
||
if search_radius < 0: | ||
print(f"ERROR: Search radius must be greater than or equal to 0: {search_radius}") | ||
return False, None | ||
|
||
current_radius = camera_horizontal_size / 2 | ||
|
||
all_waypoints = [] | ||
|
||
while current_radius <= search_radius: | ||
circumference = 2 * math.pi * current_radius | ||
|
||
num_points_vertical = circumference / camera_vertical_size | ||
num_points_horizontal = circumference / camera_horizontal_size | ||
num_points = math.ceil(MULTIPLIER * max(num_points_vertical, num_points_horizontal)) | ||
num_points = max(MINIMUM_POINTS, num_points) | ||
|
||
result, waypoints = plot_circular_path.generate_circular_path( | ||
center, current_radius, num_points | ||
) | ||
if not result or waypoints is None: | ||
return False, None | ||
all_waypoints.extend(waypoints) | ||
|
||
current_radius += camera_horizontal_size | ||
|
||
return True, all_waypoints |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
""" | ||
Hotspot search path generation unit tests. | ||
""" | ||
|
||
import pytest | ||
|
||
from modules import generate_hotspot_search_path | ||
from modules.common.modules import position_global_relative_altitude | ||
|
||
|
||
def verify_close_enough( | ||
actual: position_global_relative_altitude.PositionGlobalRelativeAltitude, | ||
expected: position_global_relative_altitude.PositionGlobalRelativeAltitude, | ||
tolerance: float, | ||
) -> bool: | ||
""" | ||
Check equality of positions. | ||
""" | ||
if not actual.latitude == pytest.approx(expected.latitude, rel=tolerance): | ||
return False | ||
|
||
if not actual.longitude == pytest.approx(expected.longitude, rel=tolerance): | ||
return False | ||
|
||
if not actual.relative_altitude == pytest.approx(expected.relative_altitude, rel=tolerance): | ||
return False | ||
|
||
return True | ||
|
||
|
||
class TestGenerateSearchPath: | ||
""" | ||
Test suite for generate_search_path. | ||
""" | ||
|
||
def test_generate_search_path(self) -> None: | ||
""" | ||
Test successful generation of search path. | ||
""" | ||
result, center = position_global_relative_altitude.PositionGlobalRelativeAltitude.create( | ||
0.0, 0.0, 100.0 | ||
) | ||
assert result | ||
assert center is not None | ||
|
||
expected_points = [ | ||
(0.0, 2.245788210298689e-05), | ||
(2.2609236926258363e-05, 1.3751486716526466e-21), | ||
(2.7688329632905757e-21, -2.245788210298689e-05), | ||
(-2.2609236926258363e-05, -4.125446014957939e-21), | ||
(0.0, 6.737364630893306e-05), | ||
(3.3913855389367626e-05, 5.834728924913043e-05), | ||
(5.87405206149349e-05, 3.368682315447818e-05), | ||
(6.782771077874665e-05, 4.1254460149579395e-21), | ||
(5.87405206149349e-05, -3.3686823154478156e-05), | ||
(3.3913855389367653e-05, -5.8347289249130414e-05), | ||
(8.30649888986659e-21, -6.737364630893306e-05), | ||
(-3.3913855389367606e-05, -5.834728924913044e-05), | ||
(-5.874052061493489e-05, -3.36868231544782e-05), | ||
(-6.782771077874665e-05, -1.2376338044873817e-20), | ||
(-5.874052061493492e-05, 3.368682315447813e-05), | ||
(-3.391385538936766e-05, 5.8347289249130414e-05), | ||
] | ||
|
||
search_radius = 10.0 | ||
search_area_dimensions = (5.0, 5.0) | ||
result, waypoints = generate_hotspot_search_path.generate_search_path( | ||
center, search_radius, search_area_dimensions | ||
) | ||
|
||
assert result | ||
assert waypoints is not None | ||
|
||
# Reduced tolerance as the planet is a not a sphere | ||
tolerance = 1e-2 | ||
|
||
assert len(waypoints) == len(expected_points) | ||
for i, expected_point in enumerate(expected_points): | ||
actual = waypoints[i] | ||
assert actual is not None | ||
|
||
expected_latitude, expected_longitude = expected_point | ||
result, expected = ( | ||
position_global_relative_altitude.PositionGlobalRelativeAltitude.create( | ||
expected_latitude, expected_longitude, center.relative_altitude | ||
) | ||
) | ||
|
||
assert verify_close_enough(actual, expected, tolerance) | ||
|
||
def test_generate_search_path_zero_radius(self) -> None: | ||
""" | ||
Test generate_search_path with a zero search radius. | ||
""" | ||
result, center = position_global_relative_altitude.PositionGlobalRelativeAltitude.create( | ||
0.0, 0.0, 100.0 | ||
) | ||
search_radius = 0.0 | ||
search_area_dimensions = (3.0, 3.0) | ||
|
||
result, waypoints = generate_hotspot_search_path.generate_search_path( | ||
center, search_radius, search_area_dimensions | ||
) | ||
|
||
assert result | ||
assert waypoints == [] | ||
|
||
def test_generate_search_path_negative_radius(self) -> None: | ||
""" | ||
Test generate_search_path with a negative search radius. | ||
""" | ||
result, center = position_global_relative_altitude.PositionGlobalRelativeAltitude.create( | ||
0.0, 0.0, 100.0 | ||
) | ||
search_radius = -10.0 | ||
search_area_dimensions = (3.0, 3.0) | ||
|
||
result, waypoints = generate_hotspot_search_path.generate_search_path( | ||
center, search_radius, search_area_dimensions | ||
) | ||
|
||
assert result is False | ||
assert waypoints is None | ||
|
||
def test_generate_search_path_invalid_dimensions(self) -> None: | ||
""" | ||
Test generate_search_path with invalid search area dimensions. | ||
""" | ||
result, center = position_global_relative_altitude.PositionGlobalRelativeAltitude.create( | ||
0.0, 0.0, 100.0 | ||
) | ||
search_radius = 10.0 | ||
search_area_dimensions = (-3, 3.0) | ||
|
||
result, waypoints = generate_hotspot_search_path.generate_search_path( | ||
center, search_radius, search_area_dimensions | ||
) | ||
|
||
assert result is False | ||
assert waypoints is None |