Added pyrate as a direct dependency.
This commit is contained in:
0
pyrate/tests/plan/geometry/__init__.py
Normal file
0
pyrate/tests/plan/geometry/__init__.py
Normal file
0
pyrate/tests/plan/geometry/helpers/__init__.py
Normal file
0
pyrate/tests/plan/geometry/helpers/__init__.py
Normal file
171
pyrate/tests/plan/geometry/helpers/test_difference.py
Normal file
171
pyrate/tests/plan/geometry/helpers/test_difference.py
Normal 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
|
62
pyrate/tests/plan/geometry/helpers/test_distance.py
Normal file
62
pyrate/tests/plan/geometry/helpers/test_distance.py
Normal 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)
|
182
pyrate/tests/plan/geometry/helpers/test_normalize.py
Normal file
182
pyrate/tests/plan/geometry/helpers/test_normalize.py
Normal 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
|
120
pyrate/tests/plan/geometry/helpers/test_other.py
Normal file
120
pyrate/tests/plan/geometry/helpers/test_other.py
Normal 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)
|
34
pyrate/tests/plan/geometry/helpers/test_translate.py
Normal file
34
pyrate/tests/plan/geometry/helpers/test_translate.py
Normal 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)
|
53
pyrate/tests/plan/geometry/primitives/__init__.py
Normal file
53
pyrate/tests/plan/geometry/primitives/__init__.py
Normal 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)
|
44
pyrate/tests/plan/geometry/primitives/test_common.py
Normal file
44
pyrate/tests/plan/geometry/primitives/test_common.py
Normal 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))
|
217
pyrate/tests/plan/geometry/primitives/test_geospatial.py
Normal file
217
pyrate/tests/plan/geometry/primitives/test_geospatial.py
Normal 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)
|
173
pyrate/tests/plan/geometry/primitives/test_locations.py
Normal file
173
pyrate/tests/plan/geometry/primitives/test_locations.py
Normal 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)
|
266
pyrate/tests/plan/geometry/primitives/test_polygons.py
Normal file
266
pyrate/tests/plan/geometry/primitives/test_polygons.py
Normal 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)]))
|
266
pyrate/tests/plan/geometry/primitives/test_routes.py
Normal file
266
pyrate/tests/plan/geometry/primitives/test_routes.py
Normal 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)]))
|
Reference in New Issue
Block a user