"""Tests that the polygon classes in :mod:`pyrate.plan.geometry.polygon` work correctly.""" # Python standard math from math import sqrt # Typing from typing import cast # Generic testing from unittest import TestCase # Geometry from shapely.geometry import Polygon # 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 CartesianPolygon from pyrate.plan.geometry import LocationType from pyrate.plan.geometry import PolarLocation from pyrate.plan.geometry import PolarPolygon # Test helpers from pyrate.common.testing.strategies.geometry import cartesian_polygons 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_polygons # Local test helpers from . import is_any_near_special_point from . import simple_property_only_few_examples from . import slow_route_max_examples class TestPolarPolygons(TestCase): """Asserts general properties of the polar polygons.""" @given(polar_polygons()) @slow_route_max_examples def test_area_is_non_negative(self, polar_polygon: PolarPolygon) -> None: """Tests that all areas are non-negative.""" self.assertGreaterEqual(polar_polygon.area, 0, "areas must be non-negative") @given(polar_polygons()) @slow_route_max_examples def test_is_valid(self, polygon: PolarPolygon) -> None: """Test that the generated polygons are valid.""" self.assertTrue(polygon.is_valid) def test_is_not_valid(self) -> None: """Test that a known invalid polygon is detected as such.""" location = PolarLocation(12, 23.999) polygon = PolarPolygon([location, location, location]) self.assertFalse(polygon.is_valid) @given(polar_polygons(), polar_locations(), st.booleans()) @slow_route_max_examples def test_distance_to_vertices_is_non_negative( self, polar_polygon: PolarPolygon, polar_location: PolarLocation, approximate: bool ) -> None: """Tests that all distances to vertices are non-negative.""" distance = polar_polygon.distance_to_vertices(polar_location, approximate) self.assertGreaterEqual(distance, 0, "distances must be non-negative") @given(polar_polygons(max_vertices=50)) @slow_route_max_examples def test_simplification(self, original: PolarPolygon) -> None: """Checks the the area change is valid and the rough position is preserved.""" simplified = original.simplify(tolerance=sqrt(original.area) / 10) self.assertLessEqual(len(simplified.locations), len(original.locations)) self.assertTrue(original.almost_congruent(simplified, rel_tolerance=0.3)) @given(polar_polygons()) @slow_route_max_examples def test_simplification_artificial(self, original: PolarPolygon) -> None: """This duplicates the first point and looks whether it is removed.""" locations = original.locations original.locations = [locations[0]] + locations simplified = original.simplify(tolerance=sqrt(original.area) / 1000) # strictly less, as opposed to test_simplification() self.assertLess(len(simplified.locations), len(original.locations)) self.assertTrue(original.almost_congruent(simplified, rel_tolerance=0.05)) @given(polar_polygons()) @simple_property_only_few_examples # this only checks the call signatures so no need for many examples def test_numpy_conversion_invertible(self, polar_polygon: PolarPolygon) -> None: """Tests that the polygon conversion can be inverted.""" recreated = PolarPolygon.from_numpy( polar_polygon.to_numpy(), name=polar_polygon.name, location_type=polar_polygon.location_type, identifier=polar_polygon.identifier, ) self.assertEqual(polar_polygon, recreated) @given( st.sampled_from( [ PolarPolygon( locations=[ PolarLocation(latitude=-76.40057132099628, longitude=-171.92454675519284), PolarLocation(latitude=-76.40057132099628, longitude=-171.92454675519284), PolarLocation(latitude=-76.40057132099628, longitude=-171.92454675519284), ], name="K", ), PolarPolygon( locations=[ PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632), PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632), PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632), PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632), 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", ), PolarPolygon( locations=[ PolarLocation(latitude=0.0, longitude=0.029771743643124182), PolarLocation(latitude=0.0, longitude=0.029771743643124182), 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, allow_nan=False, allow_infinity=False), ) def test_translation_is_invertible( self, original: PolarPolygon, 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 polygons. """ # 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=0.1)) def test_non_finite_from_numpy_raises(self) -> None: """Tests that invalid parameter to :meth:`~PolarPolygon.from_numpy` warn about it.""" with self.assertRaises(AssertionError): PolarPolygon.from_numpy(array([(1, 2), (2, 4), (4, float("NaN"))])) with self.assertRaises(AssertionError): PolarPolygon.from_numpy(array([(float("Inf"), 2), (2, 4), (4, 1)])) with self.assertRaises(AssertionError): PolarPolygon.from_numpy(array([(1, 2), (2, float("-Inf")), (4, 4)])) class TestCartesianPolygons(TestCase): """Asserts general properties of the cartesian polygons.""" @given(cartesian_polygons()) @simple_property_only_few_examples # this only checks the call signatures so no need for many examples def test_numpy_conversion_invertible(self, cartesian_polygon: CartesianPolygon) -> None: """Tests that the polygon conversion can be inverted.""" recreated = CartesianPolygon.from_numpy( cartesian_polygon.to_numpy(), origin=cartesian_polygon.origin, name=cartesian_polygon.name, location_type=cartesian_polygon.location_type, identifier=cartesian_polygon.identifier, ) self.assertEqual(cartesian_polygon, recreated) @given(cartesian_polygons(origin=polar_locations())) @slow_route_max_examples def test_projection_and_back_projection_origin_in_route( self, cartesian_polygon: CartesianPolygon ) -> None: """Test the projection with an origin already being present in the geometry.""" recreated = cartesian_polygon.to_polar().to_cartesian(cast(PolarLocation, cartesian_polygon.origin)) self.assertTrue(recreated.equals_exact(recreated, tolerance=1e-6)) @given(cartesian_polygons(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_polygon: CartesianPolygon, origin: PolarLocation ) -> None: """Test the projection with an origin being provided.""" recreated = cartesian_polygon.to_polar(origin).to_cartesian(origin) self.assertTrue(recreated.equals_exact(recreated, tolerance=1e-6)) @given(cartesian_polygons(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_polygon: CartesianPolygon ) -> None: """Test the projection with no origin being given.""" with self.assertRaises(ValueError): cartesian_polygon.to_polar() @given(cartesian_polygons(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_polygon: CartesianPolygon, origin: PolarLocation ) -> None: """Test the projection with ambiguous origin being provided.""" with self.assertRaises(ValueError): cartesian_polygon.to_polar(origin) @given(cartesian_polygons()) @slow_route_max_examples def test_locations_property_attributes(self, cartesian_polygon: CartesianPolygon) -> None: """Test that all contained locations share the same attributes.""" for location in cartesian_polygon.locations: self.assertEqual(location.location_type, cartesian_polygon.location_type) self.assertEqual(location.name, cartesian_polygon.name) self.assertEqual(location.identifier, cartesian_polygon.identifier) self.assertEqual(location.origin, cartesian_polygon.origin) @given(cartesian_polygons()) @slow_route_max_examples def test_from_shapely_conversion(self, cartesian_polygon: CartesianPolygon) -> None: """Test that :meth:`pyrate.plan.geometry.polygon.CartesianPolygon.from_shapely` works.""" # we only want to compare the coordinates, so create a new instance without the identifier, name, etc. bare = CartesianPolygon.from_numpy(cartesian_polygon.to_numpy()) bare_shapely = Polygon(cartesian_polygon.to_numpy()) recreated = CartesianPolygon.from_shapely(bare_shapely) self.assertEqual(recreated, bare) def test_non_finite_from_numpy_raises(self) -> None: """Tests that invalid parameter to :meth:`~CartesianPolygon.from_numpy` warn about it.""" with self.assertRaises(AssertionError): CartesianPolygon.from_numpy(array([(1, 2), (2, 4), (4, float("NaN"))])) with self.assertRaises(AssertionError): CartesianPolygon.from_numpy(array([(float("Inf"), 2), (2, 4), (4, 1)])) with self.assertRaises(AssertionError): CartesianPolygon.from_numpy(array([(1, 2), (2, float("-Inf")), (4, 4)]))