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