202 lines
8.8 KiB
Python

"""Tests :class:`pyrate.common.raster_datasets.geo_datasets.DataSetAccess`."""
# Standard library
from math import degrees
from math import pi
from math import radians
# Generic testing
from unittest import TestCase
# Geometry
from rasterio.windows import intersect
# Hypothesis testing
from hypothesis import given
import hypothesis.strategies as st
# Numeric testing
from numpy import rad2deg
from numpy.testing import assert_array_almost_equal
# Own Geometry
from pyrate.plan.geometry.helpers import meters2rad
from pyrate.plan.geometry.helpers import rad2meters
from pyrate.plan.geometry import PolarLocation
# Test environment helper
from ... import _open_test_geo_dataset
class TestGeoDataset(TestCase):
"""Ensure that the :class:`pyrate.plan.graph.generate.geo_datasets.DataSetAccess` works correctly.
Uses the *Earth2014* dataset.
"""
#: The resolution of the dataset in arc-minutes
DATASET_RESOLUTION = 30
#: The maximal distance of two data points in the dataset in degrees
MAX_POINT_DISTANCE_DEG = DATASET_RESOLUTION / 60
#: The maximal distance of two data points in the dataset in meters (at the equator)
MAX_POINT_DISTANCE = rad2meters(radians(MAX_POINT_DISTANCE_DEG))
# Handle the context manager, see https://stackoverflow.com/a/11180583/3753684
def run(self, result=None) -> None:
with _open_test_geo_dataset() as dataset:
self.dataset = dataset # pylint: disable=attribute-defined-outside-init
super().run(result)
@given(
st.floats(min_value=-pi / 2 * 0.75, max_value=+pi / 2 * 0.75),
st.floats(min_value=-pi * 0.75, max_value=+pi * 0.75),
st.floats(min_value=0.001, max_value=1000_000.0),
)
def test_bounding_window_center_of_dataset(
self, latitude: float, longitude: float, radius: float
) -> None:
"""Tests that the bounding box is correctly calculated if the query point is not at the border."""
# pylint: disable=too-many-locals
win_1, win_2 = self.dataset.get_bounding_windows_around(latitude, longitude, radius)
self.assertIsNone(win_2, "only one window shall be returned")
# Get the geographical extends in degrees, not radians:
left, bottom, right, top = self.dataset.dataset.window_bounds(win_1)
# Check the position of the window
latitude_deg, longitude_deg = degrees(latitude), degrees(longitude)
self.assertLessEqual(bottom, latitude_deg + 1e-12)
self.assertGreaterEqual(top, latitude_deg - 1e-12)
self.assertAlmostEqual(
(top + bottom) / 2,
latitude_deg,
delta=self.MAX_POINT_DISTANCE_DEG,
msg="window should be vertically centered around the given center point",
)
self.assertLessEqual(left, longitude_deg + 1e-12)
self.assertGreaterEqual(right, longitude_deg - 1e-12)
self.assertAlmostEqual(
(right + left) / 2,
longitude_deg,
delta=self.MAX_POINT_DISTANCE_DEG,
msg="window should be horizontally centered around the given center point",
)
# Check the size of the window
self.assertLessEqual(left, right)
self.assertLessEqual(bottom, top)
# Distances are uniform along longitudes
radius_deg = degrees(meters2rad(radius))
self.assertGreaterEqual(top - bottom, 2 * radius_deg - 1e-12)
self.assertLessEqual(top - bottom, 2 * radius_deg + 3 * self.MAX_POINT_DISTANCE_DEG)
# Distances are more complicated along the latitudes
left_side_center = PolarLocation(latitude=(top + bottom) / 2, longitude=left)
right_side_center = PolarLocation(latitude=(top + bottom) / 2, longitude=right)
# Use approximate=True for a spherical model
distance_horizontal = left_side_center.distance(right_side_center, approximate=True)
# The rough checking of the bounding boxes is very coarse and was determined by fiddling until it
# works
# This part of the test guarantees that the windows is roughly the right size, but pinning it down to
# exact number is hard since we round the discrete window bounds, map them to geographical coordinates
# and then have to deal with floating point inaccuracies
self.assertGreaterEqual(distance_horizontal, 2 * radius - 6 * self.MAX_POINT_DISTANCE)
self.assertLessEqual(distance_horizontal, 2 * radius + 4 * self.MAX_POINT_DISTANCE)
@given(
st.floats(min_value=-pi / 2 * 0.95, max_value=+pi / 2 * 0.95),
st.one_of(
[st.floats(min_value=-pi, max_value=-pi * 0.995), st.floats(min_value=+pi * 0.995, max_value=+pi)]
),
st.floats(min_value=200_000.0, max_value=1_000_000.0),
)
def test_bounding_window_left_and_right_side(
self, latitude: float, longitude: float, radius: float
) -> None:
"""Tests that the bounding box is correctly calculated if the query point is at the border.
Very high latitudes (near the poles) are not tested as there might be a single (albeit very wide)
window being returned. For the same reason, only moderate radii are tested.
"""
window_1, window_2 = self.dataset.get_bounding_windows_around(latitude, longitude, radius)
# We need a plain assert here for type checking
assert window_2 is not None, "two windows should be returned"
self.assertFalse(intersect(window_1, window_2), "windows may never overlap")
# Also test the same as in :meth:`~test_bounding_window_intersection_empty`
self.assertEqual(window_1.height, window_2.height)
self.assertGreaterEqual(window_1.height, 1)
self.assertGreaterEqual(window_1.width, 1)
self.assertGreaterEqual(window_2.width, 1)
self.assertLessEqual(window_1.height, self.dataset.dataset.height)
self.assertLessEqual(window_1.width + window_2.width, self.dataset.dataset.width)
@given(
st.floats(min_value=-pi / 2, max_value=+pi / 2),
st.floats(min_value=-pi, max_value=+pi),
st.floats(min_value=0.001, max_value=100_000_000.0),
)
def test_bounding_window_general_properties(
self, latitude: float, longitude: float, radius: float
) -> None:
"""Tests some more general properties that should hold for all windows and window pairs.
In particular, it makes sure that even if a window pair is very wide, the intersection is always
empty.
"""
window_1, window_2 = self.dataset.get_bounding_windows_around(latitude, longitude, radius)
# Make sure that everything in window_1 is rounded
self.assertIsInstance(window_1.col_off, int)
self.assertIsInstance(window_1.row_off, int)
self.assertIsInstance(window_1.height, int)
self.assertIsInstance(window_1.width, int)
# Test some general properties of window_1
self.assertTrue(radius == 0 or window_1.height >= 1)
self.assertLessEqual(window_1.height, self.dataset.dataset.height)
self.assertTrue(radius == 0 or window_1.width >= 1)
self.assertLessEqual(window_1.width, self.dataset.dataset.width)
if window_2 is not None:
self.assertGreater(radius, 0)
# Make sure that everything in window_2 is rounded
self.assertIsInstance(window_2.col_off, int)
self.assertIsInstance(window_2.row_off, int)
self.assertIsInstance(window_2.height, int)
self.assertIsInstance(window_2.width, int)
# Test some general properties of window_2 in relation to window_1
self.assertEqual(window_1.height, window_2.height)
self.assertGreaterEqual(window_2.width, 1)
self.assertLessEqual(window_1.width + window_2.width, self.dataset.dataset.width)
self.assertFalse(intersect(window_1, window_2), "windows may never overlap")
@given(
st.floats(min_value=-pi / 2 * 0.75, max_value=+pi / 2 * 0.75),
st.floats(min_value=-pi * 0.75, max_value=+pi * 0.75),
st.floats(min_value=0.001, max_value=1000_000.0),
)
def test_meshgrid_generation(self, latitude: float, longitude: float, radius: float) -> None:
"""Tests that msehgrids are generated correctly no matter whether radians are used or not.
Uses the data generation of :meth:`~test_bounding_window_center_of_dataset`.
"""
window, window_empty = self.dataset.get_bounding_windows_around(latitude, longitude, radius)
self.assertIsNone(window_empty, "only one window shall be returned")
lats_deg, lons_deg = self.dataset.lat_lon_meshgrid_for(window, window_empty, radians=False)
lats_rad, lons_rad = self.dataset.lat_lon_meshgrid_for(window, window_empty, radians=True)
assert_array_almost_equal(rad2deg(lats_rad), lats_deg)
assert_array_almost_equal(rad2deg(lons_rad), lons_deg)