1
0

Removed the subdir.

This commit is contained in:
2022-07-20 15:01:07 +02:00
parent aee5e8bbd9
commit e03f1c8be8
234 changed files with 21562 additions and 456 deletions

View File

@ -0,0 +1,53 @@
"""This module asserts correct runtime behaviour of the :mod:`pyrate.plan.geometry` primitives for
locations, polygons and trajectories.
Quite a few tests are marked with ``@settings(max_examples=<some small count>)`` since this test suite makes
up a very large part of the total testing time and some tests just don't justify wasting many resources on
them due to very simple code being tested.
"""
# Python standard math
from math import isclose
# Typing
from typing import Union
# Hypothesis testing
from hypothesis import HealthCheck
from hypothesis import settings
# Package under test
from pyrate.plan.geometry import PolarLocation
from pyrate.plan.geometry import PolarPolygon
from pyrate.plan.geometry import PolarRoute
#: Tests that require the generation of cartesian routes are slow since the generation of examples is slow.
#: As polar routes, cartesian polygons and polar polygons depend on this, they are also run at reduced rate.
slow_route_max_examples = settings(
max_examples=int(settings().max_examples * 0.1), suppress_health_check=(HealthCheck.too_slow,)
)
#: A test that only tests very few examples since the property to be tested is rather trivial and we do not
#: want to invest significant amounts of time into it.
simple_property_only_few_examples = settings(
max_examples=int(max(5, settings().max_examples * 0.001)), suppress_health_check=(HealthCheck.too_slow,)
)
def is_near_special_point(polar_location: PolarLocation, tolerance: float = 1e-6) -> bool:
"""Checks if the given ``polar_location`` is within ``tolerance`` of the poles or +/- 180° longitude."""
return (
isclose(polar_location.latitude, -90, abs_tol=tolerance)
or isclose(polar_location.latitude, +90, abs_tol=tolerance)
or isclose(polar_location.longitude, -180, abs_tol=tolerance)
or isclose(polar_location.longitude, +180, abs_tol=tolerance)
)
def is_any_near_special_point(
polar_line_object: Union[PolarPolygon, PolarRoute], tolerance: float = 1e-6
) -> bool:
"""Checks if any point in in the given geometry ``is_near_special_point`` within the ``tolerance``."""
return any(is_near_special_point(location, tolerance) for location in polar_line_object.locations)

View File

@ -0,0 +1,44 @@
"""Tests some general properties of geometries."""
# Generic testing
from unittest import TestCase
# Scientific testing
from numpy.testing import assert_array_almost_equal
# Hypothesis testing
from hypothesis import given
import hypothesis.strategies as st
# Package under test
from pyrate.plan.geometry import CartesianGeometry
# Test helpers
from pyrate.common.testing.strategies.geometry import cartesian_objects
from pyrate.common.testing.strategies.geometry import geo_bearings
class TestCartesianGeometries(TestCase):
"""Asserts general properties of the cartesian geometries."""
@given(
cartesian_objects(),
geo_bearings(),
st.floats(min_value=1.0, max_value=100_000.0),
)
def test_translation_is_invertible(
self,
original: CartesianGeometry,
direction: float,
distance: float,
) -> None:
"""Tests that translation is invertible and a valid backwards vector is returned."""
# translate & translate back
translated, back_vector = original.translate(direction, distance)
back_direction = (direction + 180) % 360
translated_translated, back_back_vector = translated.translate(back_direction, distance)
# check the result
assert_array_almost_equal(back_vector, -back_back_vector, decimal=9)
self.assertTrue(original.equals_exact(translated_translated, tolerance=1e-9))

View File

@ -0,0 +1,217 @@
"""Tests that the geometry base classes in :mod:`pyrate.plan.geometry.geospatial` work correctly."""
# Python standard
from copy import copy
from copy import deepcopy
from json import loads
# Typing
from typing import Any
from typing import Sequence
from typing import Tuple
# Generic testing
from unittest import TestCase
# Hypothesis testing
from hypothesis import given
from hypothesis import HealthCheck
from hypothesis import Phase
from hypothesis import settings
import hypothesis.strategies as st
# Package under test
from pyrate.plan.geometry import CartesianLocation
from pyrate.plan.geometry import CartesianPolygon
from pyrate.plan.geometry import CartesianRoute
from pyrate.plan.geometry import Direction
from pyrate.plan.geometry import Geospatial
from pyrate.plan.geometry import PolarLocation
from pyrate.plan.geometry import PolarPolygon
from pyrate.plan.geometry import PolarRoute
# Hypothesis testing
from pyrate.common.testing.strategies.geometry import geospatial_objects
_CARTESIAN_LOCATION_1 = CartesianLocation(5003.0, 139.231)
_CARTESIAN_LOCATION_2 = CartesianLocation(600.1, 139.231)
_POLAR_LOCATION_1 = PolarLocation(65.01, -180.0)
_POLAR_LOCATION_2 = PolarLocation(-80.3, -180.0)
class TestStringRepresentations(TestCase):
"""Makes sure that the string conversion with ``__str__`` and ``__repr__`` works."""
_GROUND_TRUTH: Sequence[Tuple[Geospatial, str]] = [
(
_CARTESIAN_LOCATION_1,
"CartesianLocation(east=5003.0, north=139.231)",
),
(
PolarLocation(65.01, -180.0),
"PolarLocation(latitude=65.00999999999999, longitude=-180.0)",
),
(
CartesianPolygon([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1]),
"CartesianPolygon(locations=[(5003.0, 139.231), (5003.0, 139.231), (5003.0, 139.231), "
"(5003.0, 139.231)])",
),
(
PolarPolygon([_POLAR_LOCATION_1, _POLAR_LOCATION_1, _POLAR_LOCATION_1]),
"PolarPolygon(locations=[PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
"PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
"PolarLocation(latitude=65.00999999999999, longitude=-180.0)])",
),
(
CartesianRoute([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_2, _CARTESIAN_LOCATION_1]),
"CartesianRoute(locations=[(5003.0, 139.231), (600.1, 139.231), (5003.0, 139.231)])",
),
(
PolarRoute([_POLAR_LOCATION_1, _POLAR_LOCATION_2, _POLAR_LOCATION_1, _POLAR_LOCATION_1]),
"PolarRoute(locations=[PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
"PolarLocation(latitude=-80.3, longitude=-180.0), "
"PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
"PolarLocation(latitude=65.00999999999999, longitude=-180.0)])",
),
]
def test_conversions(self) -> None:
"""Makes sure that all given geospatial objects can be converted."""
for geospatial, desired_str in TestStringRepresentations._GROUND_TRUTH:
with self.subTest(f"{type(geospatial)}.__str__"):
self.assertEqual(str(geospatial), desired_str)
with self.subTest(f"{type(geospatial)}.__repr__"):
self.assertEqual(repr(geospatial), desired_str)
class TestGeoJsonRepresentations(TestCase):
"""Makes sure that the conversion to GeoJSON via the common property ``__geo_interface__`` works."""
_GROUND_TRUTH: Sequence[Tuple[Geospatial, str]] = [
(
_CARTESIAN_LOCATION_1,
'{"type": "Feature", "geometry": {"type": "Point", '
'"coordinates": [5003.0, 139.231]}, "properties": {}}',
),
(
PolarLocation(65.01, -180.0),
'{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-180.0, 65.01]}, '
'"properties": {}}',
),
(
CartesianPolygon([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1]),
'{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": '
"[[[5003.0, 139.231], [5003.0, 139.231], [5003.0, 139.231], [5003.0, 139.231]]]}, "
'"properties": {}}',
),
(
PolarPolygon([_POLAR_LOCATION_1, _POLAR_LOCATION_1, _POLAR_LOCATION_1]),
'{"type": "Feature", "geometry": {"type": "Polygon", '
'"coordinates": [[[-180.0, 65.01], [-180.0, 65.01], [-180.0, 65.01]]]}, "properties": {}}',
),
(
CartesianRoute([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_2, _CARTESIAN_LOCATION_1]),
'{"type": "Feature", "geometry": {"type": "LineString", "coordinates": '
'[[5003.0, 139.231], [600.1, 139.231], [5003.0, 139.231]]}, "properties": {}}',
),
(
PolarRoute([_POLAR_LOCATION_1, _POLAR_LOCATION_2, _POLAR_LOCATION_1]),
'{"type": "Feature", "geometry": {"type": "LineString", "coordinates": '
'[[-180.0, 65.01], [-180.0, -80.3], [-180.0, 65.01]]}, "properties": {}}',
),
]
def test_conversions(self) -> None:
"""Makes sure that all given geospatial objects can be converted."""
for geospatial, desired_geojson in TestGeoJsonRepresentations._GROUND_TRUTH:
for indent in (None, 1, 8):
with self.subTest(f"{type(geospatial)} with indent={indent}"):
geojson = geospatial.to_geo_json(indent=indent)
# load as JSON get get better error messages and become whitespace independent
self.assertDictEqual(loads(geojson), loads(desired_geojson))
class TestIdentifiers(TestCase):
"""Makes sure that identifiers are validated correctly.
The test is only performed on polar locations for simplicity and because validation is handled in the
abstract common parent class :class:`pyrate.plan.geometry.Geospatial` anyway.
"""
@given(st.integers(min_value=0, max_value=(2**63) - 1))
def test_on_locations_success(self, integer: int) -> None: # pylint: disable=no-self-use
"""Tests that valid identifiers are accepted."""
PolarLocation(latitude=0.0, longitude=0.0, identifier=integer)
@given(
st.one_of(
st.integers(max_value=-1), # negative numbers
st.integers(min_value=2**63), # very large numbers
)
)
def test_on_locations_rejected(self, integer: int) -> None:
"""Tests that invalid identifiers are rejected."""
with self.assertRaises(AssertionError):
PolarLocation(latitude=0.0, longitude=0.0, identifier=integer)
class TestEqualityMethods(TestCase):
"""Test the various equality methods."""
@given(geospatial_objects(stable=True))
@settings(
max_examples=200,
suppress_health_check=(HealthCheck.data_too_large,),
phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target), # Do not shrink as it takes long
)
def test_equality_after_translation(self, geospatial: Any) -> None:
"""Tests that translated objects are only equal under sufficient tolerance."""
# We discard the second output since it differs between cartesian and polar objects
translated, _ = geospatial.translate(direction=Direction.North, distance=0.5)
# Try since generated primitives might cause an exception to be thrown
# e.g. if projected routes become length 0
try:
# They should not be equal
self.assertNotEqual(geospatial, translated)
self.assertFalse(geospatial.equals(translated))
self.assertFalse(geospatial.equals_exact(translated, tolerance=0.0))
if hasattr(geospatial, "equals_almost_congruent"):
self.assertFalse(
geospatial.equals_almost_congruent(translated, abs_tolerance=0.0, rel_tolerance=0.0)
)
# They should be equal within some tolerance (the tolerance needs to be large for the polar
# variants)
# TODO(Someone): re-enable; see #114
# self.assertTrue(geospatial.equals_exact(translated, tolerance=5))
# self.assertTrue(geospatial.almost_equals(translated, decimal=-1))
# We do not use `equals_almost_congruent` as it is not a per-coordinate difference and might cause
# a very large symmetric difference on very large objects
except ValueError:
pass
class TestCopyAndDeepcopy(TestCase):
"""Tests that all geometric objects can be deep-copied."""
@given(geospatial_objects())
@settings(max_examples=500)
def test_is_copyable(self, geospatial: Any) -> None:
"""Tests that copies can be made and are equal to the original."""
# Check copy
copied = copy(geospatial)
self.assertEqual(geospatial, copied)
# Check deepcopy
deep_copied = deepcopy(geospatial)
self.assertEqual(geospatial, deep_copied)

View File

@ -0,0 +1,173 @@
"""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 <https://en.wikipedia.org/wiki/Metric_(mathematics)#Definition>`__ 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)

View File

@ -0,0 +1,266 @@
"""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)]))

View File

@ -0,0 +1,266 @@
"""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)]))