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

View File

@ -0,0 +1,171 @@
"""This module asserts correct runtime behaviour of the :mod:`pyrate.plan.geometry.helpers` functions
for calculating differences.
"""
# Python standard
from abc import ABC
from abc import abstractmethod
from math import isfinite
from math import isnan
import warnings
# Typing
from typing import Callable
from typing import Sequence
from typing import Tuple
# Generic testing
from unittest import TestCase
# Numeric testing
from numpy import allclose
from numpy import array
# Hypothesis testing
from hypothesis import given
import hypothesis.strategies as st
# Test helpers
from pyrate.plan.geometry.helpers import difference_direction
from pyrate.plan.geometry.helpers import difference_latitude
from pyrate.plan.geometry.helpers import difference_longitude
from pyrate.plan.geometry.helpers import ScalarOrArray
class TestDifference(TestCase, ABC):
"""Makes sure the distance measure is well-behaved.
Keep in mind that it is formally not a metric since the triangle inequality does not hold.
"""
@abstractmethod
def _get_difference_function(self) -> Callable[[ScalarOrArray, ScalarOrArray], ScalarOrArray]:
"""Get the function to be tested."""
@abstractmethod
def _get_max(self) -> float:
"""Get the desired maximum value (inclusive)."""
@abstractmethod
def _get_concrete_examples(self) -> Sequence[Tuple[float, float, float]]:
"""Get some concrete values to be tested as a sequence of ``(value a, value b, distance between)``."""
@given(st.floats(), st.floats())
def test_distance_measuring_commutes_and_is_in_bounds(self, first: float, second: float) -> None:
"""Assures flipping the sides when calculating distances does not make a significant difference."""
with warnings.catch_warnings():
warnings.simplefilter("ignore")
distance_1 = self._get_difference_function()(first, second)
distance_2 = self._get_difference_function()(second, first)
if isfinite(distance_1) and isfinite(distance_1):
# 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)
# make sure the distance is within bounds
self.assertLessEqual(distance_1, self._get_max())
self.assertLessEqual(distance_2, self._get_max())
else:
self.assertTrue(isnan(distance_1))
self.assertTrue(isnan(distance_2))
@given(st.floats())
def test_distance_measuring_to_itself_is_zero(self, thing: float) -> None:
"""Assures flipping the sides when calculating distances does not make a significant difference."""
with warnings.catch_warnings():
warnings.simplefilter("ignore")
distance = self._get_difference_function()(thing, thing)
# make sure the distance is always positive and very close to zero
if isfinite(distance):
self.assertGreaterEqual(distance, 0.0)
self.assertAlmostEqual(distance, 0.0)
else:
self.assertTrue(isnan(distance))
def test_concrete_examples(self) -> None:
"""Checks the result for the concrete examples given in :meth:`~_get_concrete_examples`."""
function = self._get_difference_function()
for index, (value_a, value_b, expected_result) in enumerate(self._get_concrete_examples()):
with self.subTest(f"example triple #{index}"):
self.assertAlmostEqual(function(value_a, value_b), expected_result, delta=1e-12)
def test_concrete_examples_as_array(self) -> None:
"""Checks the result for the concrete examples given in :meth:`~_get_concrete_examples`."""
function = self._get_difference_function()
data = array(self._get_concrete_examples()).T
self.assertTrue(allclose(function(data[0, :], data[1, :]), data[2, :]))
class TestDifferenceLatitude(TestDifference):
"""Tests :func:`pyrate.plan.geometry.helpers.difference_latitude`."""
def _get_difference_function(self) -> Callable[[ScalarOrArray, ScalarOrArray], ScalarOrArray]:
return difference_latitude
def _get_max(self) -> float:
return 180.0
def _get_concrete_examples(self) -> Sequence[Tuple[float, float, float]]:
return [
(0, 0, 0),
(-90, 90, 180),
(-89.5, 0, 89.5),
(-89.5, 0.5, 90),
(-89.5, -0.5, 89),
(-45, 45, 90),
]
class TestDifferenceLongitude(TestDifference):
"""Tests :func:`pyrate.plan.geometry.helpers.difference_longitude`."""
def _get_difference_function(self) -> Callable[[ScalarOrArray, ScalarOrArray], ScalarOrArray]:
return difference_longitude
def _get_max(self) -> float:
return 180.0
def _get_concrete_examples(self) -> Sequence[Tuple[float, float, float]]:
return [
(0, 0, 0),
(-90, 90, 180),
(-89.5, 0, 89.5),
(-89.5, 0.5, 90),
(-89.5, -0.5, 89),
(180, -180, 0),
(100, -100, 160),
(-45, 45, 90),
]
class TestDifferenceDirection(TestDifference):
"""Tests :func:`pyrate.plan.geometry.helpers.difference_direction`."""
def _get_difference_function(self) -> Callable[[ScalarOrArray, ScalarOrArray], ScalarOrArray]:
return difference_direction
def _get_max(self) -> float:
return 180.0
def _get_concrete_examples(self) -> Sequence[Tuple[float, float, float]]:
return [
(0, 0, 0),
(-90, 90, 180),
(0, 360, 0),
(10, -10, 20),
(10, 350, 20),
(370, 20, 10),
]
# Do not execute the base class as a test, see https://stackoverflow.com/a/43353680/3753684
del TestDifference

View File

@ -0,0 +1,62 @@
"""This module asserts correct runtime behaviour of the :mod:`pyrate.plan.geometry.helpers` functions
for calculating distances.
"""
# Python standard library
from datetime import timedelta
from math import radians
# Testing
from unittest import TestCase
# Hypothesis testing
from hypothesis import given
from hypothesis import settings
import hypothesis.strategies as st
# Scientific (testing)
import numpy.testing
# Module under test
from pyrate.plan.geometry.helpers import fast_distance_geo
from pyrate.plan.geometry.helpers import haversine_numpy
# Own geometry
from pyrate.plan.geometry.geospatial import MEAN_EARTH_CIRCUMFERENCE
from pyrate.plan.geometry import PolarLocation
# Test helpers
from pyrate.common.testing.strategies.geometry import geo_bearings
from pyrate.common.testing.strategies.geometry import polar_locations
class TestDistanceCalculation(TestCase):
"""Tests the geographic helper methods."""
@given(polar_locations(), polar_locations())
def test_haversine_formula(self, location_1: PolarLocation, location_2: PolarLocation) -> None:
"""Test the correctness of the haversine formula."""
dist = haversine_numpy(
radians(location_1.latitude),
radians(location_1.longitude),
radians(location_2.latitude),
radians(location_2.longitude),
)
self.assertLessEqual(dist, MEAN_EARTH_CIRCUMFERENCE / 2)
numpy.testing.assert_allclose(location_1.distance(location_2), dist, atol=5.0, rtol=0.01)
@given(polar_locations(), geo_bearings(), st.floats(min_value=0.0, max_value=250_000.0))
@settings(deadline=timedelta(seconds=1.0))
# pylint: disable=no-self-use
def test_fast_distance_geo(self, center: PolarLocation, direction: float, distance: float) -> None:
"""Test the correctness of the fast great-circle approximation."""
other, _ = center.translate(direction, distance)
distance_calculated = fast_distance_geo(
radians(other.latitude),
radians(other.longitude),
radians(center.latitude),
radians(center.longitude),
)
numpy.testing.assert_allclose(distance, distance_calculated, atol=0.5, rtol=0.05)

View File

@ -0,0 +1,182 @@
"""This module asserts correct runtime behaviour of the :mod:`pyrate.plan.geometry.helpers` functions
for normalization.
"""
# Python standard
from abc import ABC
from abc import abstractmethod
# Typing
from typing import Callable
from typing import Sequence
from typing import Tuple
# Generic testing
from unittest import TestCase
# Numeric testing
from numpy import allclose
from numpy import array
# Hypothesis testing
from hypothesis import given
import hypothesis.strategies as st
# Test helpers
from pyrate.plan.geometry.helpers import normalize_direction
from pyrate.plan.geometry.helpers import normalize_latitude
from pyrate.plan.geometry.helpers import normalize_longitude
from pyrate.plan.geometry.helpers import ScalarOrArray
class TestNormalize(TestCase, ABC):
"""Makes sure the normalizations are well-behaved."""
@abstractmethod
def _get_normalization_function(self) -> Callable[[ScalarOrArray], ScalarOrArray]:
"""Get the function to be tested."""
@abstractmethod
def _get_min(self) -> float:
"""Get the desired minimum value (inclusive)."""
@abstractmethod
def _get_max(self) -> float:
"""Get the desired maximum value, see :meth:`TestNormalize._max_is_inclusive`."""
def _max_is_inclusive(self) -> bool: # pylint: disable=no-self-use
"""If :meth:`TestNormalize._get_max` is to be seen as inclusive or exclusive"""
return False
@abstractmethod
def _get_concrete_examples(self) -> Sequence[Tuple[float, float]]:
"""Get some concrete values to be tested as a sequence of ``(non-normalized, normalized)``."""
@given(st.floats(allow_infinity=False, allow_nan=False))
def test_bounds(self, value: float) -> None:
"""Assures that the normalized value is within its bounds."""
normalized = self._get_normalization_function()(value)
# make sure the normalized value is within bounds
self.assertGreaterEqual(normalized, self._get_min())
if self._max_is_inclusive():
self.assertLessEqual(normalized, self._get_max())
else:
self.assertLess(normalized, self._get_max())
@given(st.floats(allow_infinity=False, allow_nan=False))
def test_normalizing_twice(self, value: float) -> None:
"""Assures that normalizing twice does not really change the value."""
normalized = self._get_normalization_function()(value)
normalized_twice = self._get_normalization_function()(normalized)
self.assertAlmostEqual(normalized, normalized_twice, places=10)
@given(st.floats(min_value=-400, max_value=+400))
def test_already_normalized_values(self, value: float) -> None:
"""Assures that values stay unchanged if and only if are already normalized (i.e. within bounds)."""
below_max = value < self._get_max() or (self._max_is_inclusive() and value == self._get_max())
if self._get_min() <= value and below_max:
self.assertAlmostEqual(self._get_normalization_function()(value), value, delta=1e-12)
else:
self.assertNotEqual(self._get_normalization_function()(value), value)
def test_concrete_examples(self) -> None:
"""Checks the result for the concrete examples given in :meth:`~_get_concrete_examples`."""
function = self._get_normalization_function()
for index, (non_normalized, normalized) in enumerate(self._get_concrete_examples()):
with self.subTest(f"example triple #{index}"):
self.assertAlmostEqual(function(non_normalized), normalized, delta=1e-12)
def test_concrete_examples_as_array(self) -> None:
"""Checks the result for the concrete examples given in :meth:`~_get_concrete_examples`."""
function = self._get_normalization_function()
data = array(self._get_concrete_examples()).T
self.assertTrue(allclose(function(data[0, :]), data[1, :]))
class TestNormalizeLatitude(TestNormalize):
"""Tests :func:`pyrate.plan.geometry.helpers.normalize_latitude`."""
def _get_normalization_function(self) -> Callable[[ScalarOrArray], ScalarOrArray]:
return normalize_latitude
def _get_min(self) -> float:
return -90.0
def _get_max(self) -> float:
return 90.0
def _max_is_inclusive(self) -> bool:
return True
def _get_concrete_examples(self) -> Sequence[Tuple[float, float]]:
return [
(0, 0),
(90, 90),
(-90, -90),
(100, 80),
(180, 0),
(270, -90),
(-180, 0),
(-270, 90),
(-10, -10),
]
class TestNormalizeLongitude(TestNormalize):
"""Tests :func:`pyrate.plan.geometry.helpers.normalize_longitude`."""
def _get_normalization_function(self) -> Callable[[ScalarOrArray], ScalarOrArray]:
return normalize_longitude
def _get_min(self) -> float:
return -180.0
def _get_max(self) -> float:
return 180.0
def _get_concrete_examples(self) -> Sequence[Tuple[float, float]]:
return [
(0, 0),
(90, 90),
(-90, -90),
(100, 100),
(180, -180),
(-180, -180),
(270, -90),
(-10, -10),
]
class TestNormalizeDirection(TestNormalize):
"""Tests :func:`pyrate.plan.geometry.helpers.normalize_direction`."""
def _get_normalization_function(self) -> Callable[[ScalarOrArray], ScalarOrArray]:
return normalize_direction
def _get_min(self) -> float:
return 0.0
def _get_max(self) -> float:
return 360.0
def _get_concrete_examples(self) -> Sequence[Tuple[float, float]]:
return [
(0, 0),
(90, 90),
(-90, 270),
(100, 100),
(180, 180),
(-180, 180),
(270, 270),
(-10, 350),
]
# Do not execute the base class as a test, see https://stackoverflow.com/a/43353680/3753684
del TestNormalize

View File

@ -0,0 +1,120 @@
"""This module asserts correct runtime behaviour of various additional helpers."""
# Python Standard Library
from math import tau
# Generic testing
from unittest import TestCase
# Hypothesis testing
import hypothesis.extra.numpy as st_numpy
from hypothesis import given
import hypothesis.strategies as st
# Scientific
import numpy as np
from numpy.testing import assert_almost_equal
# Module under test
from pyrate.plan.geometry.helpers import cartesian_to_spherical
from pyrate.plan.geometry.helpers import difference_latitude
from pyrate.plan.geometry.helpers import difference_longitude
from pyrate.plan.geometry.helpers import mean_angle
from pyrate.plan.geometry.helpers import mean_coordinate
from pyrate.plan.geometry.helpers import meters2rad
from pyrate.plan.geometry.helpers import rad2meters
# Own strategies
from pyrate.common.testing.strategies.geometry import geo_bearings
_POSITIVE_FLOATS = st.floats(min_value=0.0, max_value=1e9, allow_infinity=False, allow_nan=False)
class TestRadiansAndMeterConversion(TestCase):
"""Makes sure the conversion between meters and radians works."""
@given(_POSITIVE_FLOATS)
def test_is_reversible_float(self, meters: float) -> None:
"""Tests that the two functions are the reverse of each other."""
self.assertAlmostEqual(meters, rad2meters(meters2rad(meters)), places=5)
@given(st_numpy.arrays(dtype=float, shape=st_numpy.array_shapes(), elements=_POSITIVE_FLOATS))
def test_is_reversible_numpy(self, meters: np.ndarray) -> None: # pylint: disable=no-self-use
"""Tests that the two functions are the reverse of each other."""
assert_almost_equal(meters, rad2meters(meters2rad(meters)), decimal=5)
class TestCartesianToSpherical(TestCase):
"""Makes sure the conversion from cartesian to spherical coordinates works."""
def test_raises_if_not_on_unit_sphere(self) -> None:
"""Asserts that an exception is raised if values are not on the unit sphere."""
with self.assertRaises(AssertionError):
cartesian_to_spherical(np.array([(10, 20, 30)]))
def test_specific_values(self) -> None: # pylint: disable=no-self-use
"""Asserts that an exception is raised if values are not on the unit sphere."""
data_in = np.array([(1, 0, 0), (0, 1, 0), (0, 0, 1), (0.5, 0.5, np.sqrt(1 - 0.5**2 - 0.5**2))])
expected_data_out = np.array([(0, 0), (0, np.pi / 2), (-np.pi / 2, 0), (-np.pi / 4, np.pi / 4)]).T
assert_almost_equal(cartesian_to_spherical(data_in), expected_data_out)
class TestAngleAndCoordinateMean(TestCase):
"""Makes sure the mean computation and angles and coordinates works correctly."""
@given(geo_bearings(), st.floats(min_value=0.0, max_value=1e-9))
def test_raises_if_ambiguous(self, angle: float, noise: float) -> None:
"""Asserts that an exception is raised if no sensible mean can be calculated."""
ambiguous_pair = np.array([angle, (angle + 180 + noise) % 360])
with self.assertRaises(ValueError):
mean_angle(np.radians(ambiguous_pair))
with self.assertRaises(ValueError):
mean_coordinate(np.array([0.0, 67.2]), ambiguous_pair)
# But the methods should recover from an exception on the latitude mean computation
latitude, _ = mean_coordinate(ambiguous_pair, np.array([0.0, 67.2]))
self.assertAlmostEqual(latitude, 0.0)
@given(
st_numpy.arrays(
elements=st.floats(min_value=0.0, max_value=np.pi), dtype=float, shape=st_numpy.array_shapes()
)
)
def test_mean_angle_is_in_valid_range(self, data: np.ndarray) -> None:
"""Asserts that means are never negative and always between ``0°`` and ``360°``."""
try:
mean = mean_angle(data)
self.assertGreaterEqual(mean, 0.0)
self.assertLessEqual(mean, np.pi)
except ValueError:
pass # this might happen with the generated values and is okay
@given(geo_bearings(), st.floats(min_value=0.0, max_value=170))
def test_obvious_values_angle(self, angle: float, difference: float) -> None:
"""Asserts that the result is sensible for known values."""
mean = mean_angle(np.radians(np.array([angle, (angle + difference) % 360])))
self.assertAlmostEqual(mean, np.radians((angle + difference / 2)) % tau, delta=1e-6)
@given(
st.floats(min_value=-80.0, max_value=+80.0),
st.floats(min_value=-170.0, max_value=+170.0),
st.floats(min_value=-9.0, max_value=9.0),
st.floats(min_value=-9.0, max_value=9.0),
)
def test_obvious_values_coordinate(
self, latitude: float, longitude: float, lat_delta: float, lon_delta: float
) -> None:
"""Asserts that the result is sensible for known values."""
lat_mean, lon_mean = mean_coordinate(
latitudes=np.array([latitude, latitude + lat_delta]),
longitudes=np.array([longitude, longitude + lon_delta]),
)
self.assertLessEqual(difference_latitude(lat_mean, (latitude + lat_delta / 2)), 1e-6)
self.assertLessEqual(difference_longitude(lon_mean, (longitude + lon_delta / 2)), 1e-6)

View File

@ -0,0 +1,34 @@
"""This module asserts correct runtime behaviour of the :mod:`pyrate.plan.geometry.helpers` functions
for translation.
Note that most of the correctness is asserted by the use in
:meth:`pyrate.plan.geometry.PolarPolygon.translate` and :meth:`pyrate.plan.geometry.PolarRoute.translate`.
Also, no extensive tests are needed since we trust the underlying library due to its widespread adoption and
maturity.
We only need to check that the conversion of parameters and results works as expcted.
"""
# Testing
from unittest import TestCase
# Scientific (testing)
from numpy import array
# Module under test
from pyrate.plan.geometry.helpers import translate_numpy
class TestTranslate(TestCase):
"""Tests the translation helpers."""
COORDINATES = array([[1.0, 2.0], [3.0, -4.0], [-5.0, 6.0]])
DIRECTIONS = array([0.0, 90.0, -90.0])
DISTANCES = array([1.0, 100.0, 10000.0])
def test_translate_numpy(self) -> None: # pylint: disable=no-self-use
"""Test that any combination of types of input are accepted."""
translate_numpy(TestTranslate.COORDINATES, TestTranslate.DIRECTIONS, TestTranslate.DISTANCES)
translate_numpy(TestTranslate.COORDINATES, 90, TestTranslate.DISTANCES)
translate_numpy(TestTranslate.COORDINATES, TestTranslate.DIRECTIONS, 100)
translate_numpy(TestTranslate.COORDINATES, 90, 100)

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)]))