"""Tests that the location classes in :mod:`pyrate.plan.geometry.location` work correctly.""" # Python standard math from math import isclose # Typing from typing import cast # Generic testing from unittest import TestCase # Geometry from shapely.geometry import Point # Hypothesis testing from hypothesis import given from hypothesis import HealthCheck from hypothesis import settings import hypothesis.strategies as st # Package under test from pyrate.plan.geometry import CartesianLocation from pyrate.plan.geometry import PolarLocation # Test helpers from pyrate.common.testing.strategies.geometry import cartesian_locations from pyrate.common.testing.strategies.geometry import geo_bearings from pyrate.common.testing.strategies.geometry import polar_locations # Local test helpers from . import is_near_special_point from . import simple_property_only_few_examples class TestLocationConversion(TestCase): """Test for correct runtime behaviour in :mod:`pyrate.plan` location and shape primitives.""" @given(cartesian_locations(origin=polar_locations())) @settings(max_examples=20, suppress_health_check=(HealthCheck.data_too_large,)) # this is a slow test def test_projection_and_back_projection_origin_in_route( self, cartesian_location: CartesianLocation ) -> None: """Test the projection with an origin already being present in the geometry.""" recreated = cartesian_location.to_polar().to_cartesian(cast(PolarLocation, cartesian_location.origin)) self.assertTrue(recreated.equals_exact(recreated, tolerance=1e-6)) @given(cartesian_locations(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_location: CartesianLocation, origin: PolarLocation ) -> None: """Test the projection with an origin being provided.""" recreated = cartesian_location.to_polar(origin).to_cartesian(origin) self.assertTrue(recreated.equals_exact(recreated, tolerance=1e-6)) @given(cartesian_locations(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_location: CartesianLocation ) -> None: """Test the projection with no origin being given.""" with self.assertRaises(ValueError): cartesian_location.to_polar() @given(cartesian_locations(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_location: CartesianLocation, origin: PolarLocation ) -> None: """Test the projection with ambiguous origin being provided.""" with self.assertRaises(ValueError): cartesian_location.to_polar(origin) def test_distance_measuring_specific(self) -> None: """Tests a specific input/output pair.""" location_a = PolarLocation(latitude=55.6544, longitude=139.74477) location_b = PolarLocation(latitude=21.4225, longitude=39.8261) distance = location_a.distance(location_b, approximate=False) self.assertAlmostEqual(distance, 8_665_850.116876071) @given( polar_locations(), geo_bearings(), st.floats(min_value=1.0, max_value=100_000.0, allow_nan=False, allow_infinity=False), ) def test_translation_is_invertible( self, original: PolarLocation, 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. """ # translate translated, back_direction = original.translate(direction, distance) self.assertGreaterEqual(back_direction, 0.0) self.assertLess(back_direction, 360.0) # translate back translated_translated, back_back_direction = translated.translate(back_direction, distance) self.assertGreaterEqual(back_back_direction, 0.0) self.assertLess(back_back_direction, 360.0) # the method seems to have problems at poles if not is_near_special_point(original) and not is_near_special_point(translated): # the method is rather rough, so we want to add larger tolerances than usual while checking self.assertTrue(isclose(direction, back_back_direction, abs_tol=1e-6)) self.assertTrue(original.equals_exact(translated_translated, 1e-6)) @given(cartesian_locations()) def test_from_shapely_conversion(self, cartesian_location: CartesianLocation) -> None: """Test that :meth:`pyrate.plan.geometry.location.CartesianLocation.from_shapely` works.""" # we only want to compare the coordinates, so create a new instance without the identifier, name, etc. bare = CartesianLocation(cartesian_location.x, cartesian_location.y) bare_shapely = Point(cartesian_location.x, cartesian_location.y) recreated = CartesianLocation.from_shapely(bare_shapely) self.assertEqual(recreated, bare) class TestPolarLocationDistanceIsAMetric(TestCase): """Makes sure that :meth:`~pyrate.plan.geometry.location.PolarLocation.distance` is a metric. This should always succeed since we use a very stable external library for this. See `Wikipedia `__ for the axioms. """ @given(polar_locations(), polar_locations(), st.booleans()) def test_distance_measuring_commutes_and_sanity_checks( self, location_a: PolarLocation, location_b: PolarLocation, approximate: bool ) -> None: """Assures flipping the sides when calculating distances does not make a significant difference.""" distance_1 = location_a.distance(location_b, approximate) distance_2 = location_b.distance(location_a, approximate) # make sure it commutes self.assertAlmostEqual(distance_1, distance_2) # make sure the distance is always positive self.assertGreaterEqual(distance_1, 0.0) self.assertGreaterEqual(distance_2, 0.0) @given(polar_locations(), polar_locations(), polar_locations(), st.booleans()) def test_distance_measuring_triangle_inequality( self, location_a: PolarLocation, location_b: PolarLocation, location_c: PolarLocation, approximate: bool, ) -> None: """Assures flipping the sides when calculating distances does not make a significant difference.""" distance_a_b = location_a.distance(location_b, approximate) distance_b_c = location_b.distance(location_c, approximate) distance_a_c = location_a.distance(location_c, approximate) # allow for floating point errors abs_tolerance = 1e-6 # 1 micro meter self.assertGreaterEqual(distance_a_b + distance_b_c + abs_tolerance, distance_a_c) @given(polar_locations(), st.booleans()) def test_distance_measuring_to_itself_is_zero(self, location: PolarLocation, approximate: bool) -> None: """Assures flipping the sides when calculating distances does not make a significant difference.""" distance = location.distance(location, approximate) # make sure the distance is always positive and very close to zero self.assertGreaterEqual(distance, 0.0) self.assertAlmostEqual(distance, 0.0)