1
0

Added pyrate as a direct dependency.

This commit is contained in:
2022-07-11 23:07:33 +02:00
parent 8c4532dad4
commit c99d517f6f
230 changed files with 21114 additions and 0 deletions

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)