Removed the subdir.
This commit is contained in:
205
pyrate/tests/plan/graph/generate/test_graph_generation.py
Normal file
205
pyrate/tests/plan/graph/generate/test_graph_generation.py
Normal file
@ -0,0 +1,205 @@
|
||||
"""Tests the generated graphs are well-formed."""
|
||||
|
||||
# Standard Library
|
||||
from contextlib import redirect_stdout
|
||||
from io import StringIO
|
||||
from math import isclose
|
||||
|
||||
# Testing
|
||||
import unittest
|
||||
|
||||
# Typing
|
||||
from typing import cast
|
||||
|
||||
# Hypothesis testing
|
||||
from hypothesis import given
|
||||
import hypothesis.strategies as st
|
||||
|
||||
# Scientific (testing)
|
||||
import numpy as np
|
||||
import numpy.testing
|
||||
|
||||
# Own geography
|
||||
from pyrate.plan.geometry.geospatial import MEAN_EARTH_CIRCUMFERENCE
|
||||
from pyrate.plan.geometry.helpers import haversine_numpy
|
||||
|
||||
# Module under test
|
||||
from pyrate.plan.graph.generate import angular_distance_for
|
||||
from pyrate.plan.graph.generate import create_earth_graph
|
||||
from pyrate.plan.graph.generate import great_circle_distance_distance_for
|
||||
from pyrate.plan.graph.generate import min_required_frequency
|
||||
|
||||
|
||||
EXAMPLE_DISTANCES_KILOMETERS = [100000, 100000.0, 5000, 250] # smaller values take too long
|
||||
|
||||
|
||||
class TestGridGeneration(unittest.TestCase):
|
||||
"""Tests that a grid can be created and pruned."""
|
||||
|
||||
@staticmethod
|
||||
def _calculate_distances(latitudes: np.ndarray, longitudes: np.ndarray, edges: np.ndarray) -> np.ndarray:
|
||||
"""Calculates the distance of all edges. The `edges` index into the coordinate arrays."""
|
||||
entries = [
|
||||
(latitudes[node_1], longitudes[node_1], latitudes[node_2], longitudes[node_2])
|
||||
for node_1, node_2 in edges
|
||||
]
|
||||
return haversine_numpy(*np.transpose(entries))
|
||||
|
||||
def test_create_earth_grid(self) -> None:
|
||||
"""Ensures that the generated earth grids are formed correctly."""
|
||||
for distance_km in EXAMPLE_DISTANCES_KILOMETERS:
|
||||
with self.subTest(f"Test with distance {distance_km} km"):
|
||||
distance = distance_km * 1000
|
||||
|
||||
# create a grid
|
||||
graph = create_earth_graph(min_required_frequency(distance, in_meters=True))
|
||||
self.assertIsNotNone(graph.node_radius)
|
||||
actual_distance: float = cast(float, graph.node_radius) * 2
|
||||
|
||||
# the actual_distance must be a upper-bounded by he requested distance
|
||||
self.assertLessEqual(actual_distance, distance)
|
||||
self.assertLessEqual(actual_distance, MEAN_EARTH_CIRCUMFERENCE / 2)
|
||||
|
||||
# the shapes of the returned arrays must match
|
||||
self.assertEqual(
|
||||
graph.latitudes_radians.shape,
|
||||
graph.longitudes_radians.shape,
|
||||
"latitude and longitude must have the same shape",
|
||||
)
|
||||
self.assertEqual(
|
||||
graph.latitudes_degrees.shape,
|
||||
graph.longitudes_degrees.shape,
|
||||
"latitude and longitude must have the same shape",
|
||||
)
|
||||
self.assertEqual(
|
||||
graph.latitudes_radians.shape,
|
||||
graph.longitudes_degrees.shape,
|
||||
"radians and degrees must have the same shape",
|
||||
)
|
||||
self.assertGreaterEqual(len(graph), 12) # as it is based on slicing an icosahedron
|
||||
|
||||
# the edges must be valid indices into the edges
|
||||
self.assertTrue(
|
||||
np.all(graph.edges[:, :] >= 0) and np.all(graph.edges[:, :] < len(graph)),
|
||||
"some edges reference non-existent points",
|
||||
)
|
||||
|
||||
# check the actual coordinate value
|
||||
if (
|
||||
np.any(graph.latitudes_radians < -np.pi / 2)
|
||||
or np.any(graph.longitudes_radians < -np.pi)
|
||||
or np.any(graph.latitudes_radians >= +np.pi / 2)
|
||||
or np.any(graph.longitudes_radians >= +np.pi)
|
||||
):
|
||||
print(
|
||||
"latitude < min / 2:",
|
||||
np.compress(graph.latitudes_radians < -np.pi / 2, graph.latitudes_radians),
|
||||
)
|
||||
print(
|
||||
"longitude < min:",
|
||||
np.compress(graph.longitudes_radians < -np.pi, graph.longitudes_radians),
|
||||
)
|
||||
print(
|
||||
"latitude >= max / 2:",
|
||||
np.compress(graph.latitudes_radians >= +np.pi / 2, graph.latitudes_radians),
|
||||
)
|
||||
print(
|
||||
"longitude >= max:",
|
||||
np.compress(graph.longitudes_radians >= +np.pi, graph.longitudes_radians),
|
||||
)
|
||||
self.fail("some points are outside of the allowed range")
|
||||
|
||||
# check the distances along the edges
|
||||
distances = TestGridGeneration._calculate_distances(
|
||||
graph.latitudes_radians, graph.longitudes_radians, graph.edges
|
||||
)
|
||||
|
||||
numpy.testing.assert_allclose(distances, actual_distance, atol=10, rtol=0.2)
|
||||
|
||||
mean = np.mean(distances)
|
||||
self.assertTrue(isclose(mean, actual_distance, rel_tol=0.1, abs_tol=10.0))
|
||||
standard_deviation = np.std(distances)
|
||||
self.assertLessEqual(standard_deviation / mean, 0.075)
|
||||
|
||||
def test_print_status(self) -> None:
|
||||
"""This tests that logging being enabled actually logs something and does not crash."""
|
||||
stdout_logging = StringIO()
|
||||
with redirect_stdout(stdout_logging):
|
||||
create_earth_graph(6, print_status=True)
|
||||
logged_lines = list(stdout_logging.getvalue().splitlines())
|
||||
self.assertEqual(len(logged_lines), 6, "we expect 6 lines of messages")
|
||||
|
||||
def test_find_neighbors(self) -> None:
|
||||
"""Tests that result of the neighbor search is correct."""
|
||||
for distance_km in EXAMPLE_DISTANCES_KILOMETERS:
|
||||
with self.subTest(f"Test with distance {distance_km} km"):
|
||||
|
||||
# create a grid & determine neighbors
|
||||
graph = create_earth_graph(min_required_frequency(distance_km * 1000, in_meters=True))
|
||||
neighbors = graph.neighbors
|
||||
count_per_node = np.count_nonzero(neighbors >= 0, axis=1)
|
||||
|
||||
# check the resulting number of entries
|
||||
self.assertEqual(
|
||||
np.sum(count_per_node),
|
||||
graph.edges.shape[0] * 2,
|
||||
"each edge must generate two entries in the neighbor table",
|
||||
)
|
||||
self.assertEqual(
|
||||
np.count_nonzero(count_per_node == 5),
|
||||
12,
|
||||
"exactly twelve nodes must have exactly five neighbors "
|
||||
"(the corners of the original icosahedron)",
|
||||
)
|
||||
self.assertEqual(
|
||||
np.count_nonzero(count_per_node == 6),
|
||||
len(graph) - 12,
|
||||
"all but twelve nodes must have exactly six neighbors",
|
||||
)
|
||||
|
||||
# check the range of values
|
||||
valid_index = np.logical_and(neighbors >= 0, neighbors < len(graph))
|
||||
self.assertTrue(
|
||||
np.all(np.logical_xor(neighbors == -1, valid_index)),
|
||||
"any value i may either be -1 (=null) or a valid index with 0 <= i < num_nodes",
|
||||
)
|
||||
|
||||
|
||||
class TestHelperMethods(unittest.TestCase):
|
||||
"""Tests that the helpers (e.g. for computing minimum required frequencies) work correctly."""
|
||||
|
||||
@given(st.floats(min_value=1e-6, allow_infinity=False, allow_nan=False), st.booleans())
|
||||
def test_right_order_of_magnitude(self, desired_distance: float, in_meters: bool) -> None:
|
||||
"""Asserts that commuting a frequency and converting it to units is correct w.r.t. to each other."""
|
||||
frequency = min_required_frequency(desired_distance, in_meters)
|
||||
|
||||
if in_meters:
|
||||
actual_distance = great_circle_distance_distance_for(frequency)
|
||||
else:
|
||||
actual_distance = angular_distance_for(frequency)
|
||||
self.assertLessEqual(actual_distance, desired_distance)
|
||||
|
||||
if frequency > 1:
|
||||
if in_meters:
|
||||
actual_distance_one_rougher = great_circle_distance_distance_for(frequency - 1)
|
||||
else:
|
||||
actual_distance_one_rougher = angular_distance_for(frequency - 1)
|
||||
self.assertGreaterEqual(actual_distance_one_rougher, desired_distance)
|
||||
|
||||
def test_specific_values(self) -> None:
|
||||
"""Asserts that commuting a frequency works correct for specific hand-chosen values."""
|
||||
|
||||
# Taken from the implementation:
|
||||
# The approximate angle between two edges on an icosahedron, in radians, about 63.4°
|
||||
alpha = 1.1071487
|
||||
|
||||
# Contains pairs: (angular distance in radians, frequency)
|
||||
table = [
|
||||
(alpha + 1e-6, 1),
|
||||
(alpha - 1e-9, 2),
|
||||
(alpha / 9000.005, 9001),
|
||||
]
|
||||
|
||||
for desired_angular_distance, desired_frequency in table:
|
||||
computed_frequency = min_required_frequency(desired_angular_distance, in_meters=False)
|
||||
self.assertEqual(desired_frequency, computed_frequency)
|
Reference in New Issue
Block a user