267 lines
12 KiB
Python
267 lines
12 KiB
Python
"""Tests that the route classes in :mod:`pyrate.plan.geometry.route` work correctly."""
|
|
|
|
# Typing
|
|
from typing import cast
|
|
|
|
# Generic testing
|
|
from unittest import TestCase
|
|
|
|
# Geometry
|
|
from shapely.geometry import LineString
|
|
|
|
# Scientific
|
|
from numpy import array
|
|
|
|
# Scientific testing
|
|
from numpy.testing import assert_array_less
|
|
|
|
# Hypothesis testing
|
|
from hypothesis import given
|
|
import hypothesis.strategies as st
|
|
|
|
# Package under test
|
|
from pyrate.plan.geometry import CartesianLocation
|
|
from pyrate.plan.geometry import CartesianRoute
|
|
from pyrate.plan.geometry import LocationType
|
|
from pyrate.plan.geometry import PolarLocation
|
|
from pyrate.plan.geometry import PolarRoute
|
|
|
|
# Test helpers
|
|
from pyrate.common.testing.strategies.geometry import cartesian_routes
|
|
from pyrate.common.testing.strategies.geometry import geo_bearings
|
|
from pyrate.common.testing.strategies.geometry import polar_locations
|
|
from pyrate.common.testing.strategies.geometry import polar_routes
|
|
|
|
# Local test helpers
|
|
from . import is_any_near_special_point
|
|
from . import simple_property_only_few_examples
|
|
from . import slow_route_max_examples
|
|
|
|
|
|
class TestPolarRoutes(TestCase):
|
|
"""Asserts general properties of the polar routes."""
|
|
|
|
@given(polar_routes())
|
|
@simple_property_only_few_examples # this only checks the call signatures so no need for many examples
|
|
def test_numpy_conversion_invertible(self, polar_route: PolarRoute) -> None:
|
|
"""Tests that the route conversion can be inverted."""
|
|
recreated = PolarRoute.from_numpy(
|
|
polar_route.to_numpy(),
|
|
name=polar_route.name,
|
|
location_type=polar_route.location_type,
|
|
identifier=polar_route.identifier,
|
|
)
|
|
self.assertEqual(polar_route, recreated)
|
|
|
|
@given(polar_routes(), polar_locations(), st.booleans())
|
|
@slow_route_max_examples
|
|
def test_distance_to_vertices_is_non_negative(
|
|
self, polar_route: PolarRoute, polar_location: PolarLocation, approximate: bool
|
|
) -> None:
|
|
"""Tests that all distances to vertices are non-negative."""
|
|
distance = polar_route.distance_to_vertices(polar_location, approximate)
|
|
self.assertGreaterEqual(distance, 0, "distances must be non-negative")
|
|
|
|
@given(polar_routes())
|
|
@slow_route_max_examples
|
|
def test_length_is_non_negative(self, polar_route: PolarRoute) -> None:
|
|
"""Tests that the length of a route is always non-negative."""
|
|
self.assertGreaterEqual(polar_route.length(), 0, "lengths must be non-negative")
|
|
|
|
@given(polar_routes(min_vertices=3, max_vertices=3))
|
|
@slow_route_max_examples
|
|
def test_length_values(self, polar_route: PolarRoute) -> None:
|
|
"""Tests that the length of a route with three locations is plausible."""
|
|
location_a, location_b, location_c = polar_route.locations
|
|
distance = location_a.distance(location_b) + location_b.distance(location_c)
|
|
self.assertAlmostEqual(polar_route.length(), distance, msg="lengths must be non-negative")
|
|
|
|
@given(
|
|
st.sampled_from(
|
|
[
|
|
PolarRoute(
|
|
locations=[
|
|
PolarLocation(latitude=-76.40057132099628, longitude=-171.92454675519284),
|
|
PolarLocation(latitude=-76, longitude=-171),
|
|
],
|
|
name="K",
|
|
),
|
|
PolarRoute(
|
|
locations=[
|
|
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
|
PolarLocation(latitude=-33, longitude=89),
|
|
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
|
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
|
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
|
],
|
|
location_type=LocationType.TESTING,
|
|
name="_1",
|
|
),
|
|
PolarRoute(
|
|
locations=[
|
|
PolarLocation(latitude=0.0, longitude=0.029771743643124182),
|
|
PolarLocation(latitude=0.0, longitude=0.0),
|
|
PolarLocation(latitude=0.0, longitude=0.029771743643124182),
|
|
PolarLocation(latitude=0.0, longitude=0.029771743643124182),
|
|
],
|
|
name="",
|
|
),
|
|
]
|
|
),
|
|
geo_bearings(),
|
|
st.floats(min_value=1.0, max_value=100_000.0),
|
|
)
|
|
def test_translation_is_invertible(self, original: PolarRoute, direction: float, distance: float) -> None:
|
|
"""Tests that translation is invertible and a valid bearing is returned.
|
|
|
|
Warning:
|
|
Only tests in-depth in the case where latitudes and longitudes are not near the poles.
|
|
Since the tests are quite flaky due to the underling library, we only test specific routes.
|
|
"""
|
|
|
|
# translate
|
|
translated, back_direction = original.translate(direction, distance)
|
|
assert_array_less(0.0 - 1e-12, back_direction)
|
|
assert_array_less(back_direction, 360.0 + 1e-12)
|
|
|
|
# translate back
|
|
translated_translated, back_back_direction = translated.translate(back_direction[0], distance)
|
|
assert_array_less(0.0 - 1e-12, back_back_direction)
|
|
assert_array_less(back_back_direction, 360.0 + 1e-12)
|
|
|
|
# the method seems to have problems at poles
|
|
if not is_any_near_special_point(original) and not is_any_near_special_point(translated):
|
|
# the method is rather rough, so we want to add larger tolerances than usual while checking
|
|
self.assertAlmostEqual(direction, back_back_direction[0], delta=0.1)
|
|
self.assertTrue(original.equals_exact(translated_translated, tolerance=1e-2 * distance))
|
|
|
|
def test_zero_length_route(self) -> None:
|
|
"""Test that :meth:`pyrate.plan.geometry.route.PolarRoute.__init__` raises an exception."""
|
|
|
|
with self.assertRaises(ValueError):
|
|
PolarRoute(locations=[PolarLocation(0.0, 0.0)] * 2)
|
|
|
|
def test_non_finite_from_numpy_raises(self) -> None:
|
|
"""Tests that invalid parameter to :meth:`~PolarRoute.from_numpy` warn about it."""
|
|
|
|
with self.assertRaises(AssertionError):
|
|
PolarRoute.from_numpy(array([(1, 2), (2, 4), (4, float("NaN"))]))
|
|
with self.assertRaises(AssertionError):
|
|
PolarRoute.from_numpy(array([(float("Inf"), 2), (2, 4), (4, 1)]))
|
|
with self.assertRaises(AssertionError):
|
|
PolarRoute.from_numpy(array([(1, 2), (2, float("-Inf")), (4, 4)]))
|
|
|
|
|
|
class TestCartesianRoutes(TestCase):
|
|
"""Asserts general properties of the cartesian routes."""
|
|
|
|
@given(cartesian_routes())
|
|
@simple_property_only_few_examples # this only checks the call signatures so no need for many examples
|
|
def test_numpy_conversion_invertible(self, cartesian_route: CartesianRoute) -> None:
|
|
"""Tests that the polygon conversion can be inverted."""
|
|
|
|
recreated = CartesianRoute.from_numpy(
|
|
cartesian_route.to_numpy(),
|
|
origin=cartesian_route.origin,
|
|
name=cartesian_route.name,
|
|
location_type=cartesian_route.location_type,
|
|
identifier=cartesian_route.identifier,
|
|
)
|
|
|
|
self.assertEqual(cartesian_route, recreated)
|
|
|
|
@given(cartesian_routes(origin=polar_locations()))
|
|
@slow_route_max_examples
|
|
def test_projection_and_back_projection_origin_in_route(self, cartesian_route: CartesianRoute) -> None:
|
|
"""Test the projection with an origin already being present in the geometry."""
|
|
|
|
# Try since generated primitives might cause an exception to be thrown
|
|
# e.g. if projected routes become length 0
|
|
try:
|
|
recreated = cartesian_route.to_polar().to_cartesian(cast(PolarLocation, cartesian_route.origin))
|
|
self.assertTrue(recreated.equals_exact(recreated, tolerance=1e-6))
|
|
except ValueError:
|
|
pass
|
|
|
|
@given(cartesian_routes(origin=st.none()), polar_locations())
|
|
@simple_property_only_few_examples # this only checks very simple additional logic
|
|
def test_projection_and_back_projection_origin_given_extra(
|
|
self, cartesian_route: CartesianRoute, origin: PolarLocation
|
|
) -> None:
|
|
"""Test the projection with an origin being provided."""
|
|
|
|
# Try since generated primitives might cause an exception to be thrown
|
|
# e.g. if projected routes become length 0
|
|
try:
|
|
recreated = cartesian_route.to_polar(origin).to_cartesian(origin)
|
|
self.assertTrue(recreated.equals_exact(recreated, tolerance=1e-6))
|
|
except ValueError:
|
|
pass
|
|
|
|
@given(cartesian_routes(origin=st.none()))
|
|
@simple_property_only_few_examples # this only checks very simple additional logic
|
|
def test_projection_and_back_projection_origin_not_given(self, cartesian_route: CartesianRoute) -> None:
|
|
"""Test the projection with no origin being given."""
|
|
|
|
with self.assertRaises(ValueError):
|
|
cartesian_route.to_polar()
|
|
|
|
@given(cartesian_routes(origin=polar_locations()), polar_locations())
|
|
@simple_property_only_few_examples # this only checks very simple additional logic
|
|
def test_projection_and_back_projection_origin_given_twice(
|
|
self, cartesian_route: CartesianRoute, origin: PolarLocation
|
|
) -> None:
|
|
"""Test the projection with ambiguous origin being provided."""
|
|
|
|
with self.assertRaises(ValueError):
|
|
cartesian_route.to_polar(origin)
|
|
|
|
@given(cartesian_routes())
|
|
@slow_route_max_examples
|
|
def test_locations_property_attributes(self, cartesian_route: CartesianRoute) -> None:
|
|
"""Test that all contained locations share the same attributes."""
|
|
|
|
for location in cartesian_route.locations:
|
|
self.assertEqual(location.location_type, cartesian_route.location_type)
|
|
self.assertEqual(location.name, cartesian_route.name)
|
|
self.assertEqual(location.identifier, cartesian_route.identifier)
|
|
self.assertEqual(location.origin, cartesian_route.origin)
|
|
|
|
@given(cartesian_routes())
|
|
@slow_route_max_examples
|
|
def test_from_shapely_conversion(self, cartesian_route: CartesianRoute) -> None:
|
|
"""Test that :meth:`pyrate.plan.geometry.route.CartesianRoute.from_shapely` works."""
|
|
|
|
# we only want to compare the coordinates, so create a new instance without the identifier, name, etc.
|
|
bare = CartesianRoute.from_numpy(cartesian_route.to_numpy())
|
|
bare_shapely = LineString(cartesian_route.to_numpy())
|
|
recreated = CartesianRoute.from_shapely(bare_shapely)
|
|
self.assertEqual(recreated, bare)
|
|
|
|
@given(cartesian_routes())
|
|
@simple_property_only_few_examples # this only checks very simple additional logic
|
|
def test_locations_property(self, cartesian_route: CartesianRoute) -> None:
|
|
"""Test that :meth:`pyrate.plan.geometry.route.CartesianRoute.locations` works."""
|
|
|
|
locations = cartesian_route.locations
|
|
self.assertEqual(len(cartesian_route.coords), len(locations))
|
|
for i, (x, y) in enumerate(cartesian_route.coords):
|
|
self.assertEqual(x, locations[i].x)
|
|
self.assertEqual(y, locations[i].y)
|
|
|
|
def test_zero_length_route(self) -> None:
|
|
"""Test that :meth:`pyrate.plan.geometry.route.CartesianRoute.__init__` raises an exception."""
|
|
|
|
with self.assertRaises(ValueError):
|
|
CartesianRoute(locations=[CartesianLocation(0.0, 0.0)] * 2)
|
|
|
|
def test_non_finite_from_numpy_raises(self) -> None:
|
|
"""Tests that invalid parameter to :meth:`~CartesianRoute.from_numpy` warn about it."""
|
|
|
|
with self.assertRaises(AssertionError):
|
|
CartesianRoute.from_numpy(array([(1, 2), (2, 4), (4, float("NaN"))]))
|
|
with self.assertRaises(AssertionError):
|
|
CartesianRoute.from_numpy(array([(float("Inf"), 2), (2, 4), (4, 1)]))
|
|
with self.assertRaises(AssertionError):
|
|
CartesianRoute.from_numpy(array([(1, 2), (2, float("-Inf")), (4, 4)]))
|