"""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