Removed the subdir.
This commit is contained in:
Binary file not shown.
6
pyrate/tests/common/raster_datasets/__init__.py
Normal file
6
pyrate/tests/common/raster_datasets/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Tests the raster datasets.
|
||||
|
||||
The test file ``Earth2014.TBI2014.30min.geod.geo.tif`` is the *Earth 2014* dataset, exported from the
|
||||
`data repository
|
||||
<https://gitlab.sailingteam.hg.tu-darmstadt.de/informatik/data/-/tree/master/topography/earth2014>`__.
|
||||
"""
|
201
pyrate/tests/common/raster_datasets/test_geo_datasets.py
Normal file
201
pyrate/tests/common/raster_datasets/test_geo_datasets.py
Normal file
@ -0,0 +1,201 @@
|
||||
"""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)
|
132
pyrate/tests/common/raster_datasets/test_transformers.py
Normal file
132
pyrate/tests/common/raster_datasets/test_transformers.py
Normal file
@ -0,0 +1,132 @@
|
||||
"""
|
||||
Tests the transformers in :mod:`pyrate.common.raster_datasets.transformer_base` and in
|
||||
:mod:`pyrate.common.raster_datasets.transformers_concrete`.
|
||||
"""
|
||||
|
||||
# Testing
|
||||
from unittest import TestCase
|
||||
|
||||
# Scientific
|
||||
from numpy import array
|
||||
from numpy import empty
|
||||
from numpy import float32
|
||||
from numpy import int16
|
||||
from numpy.testing import assert_array_equal
|
||||
from numpy import uint16
|
||||
from numpy import uint32
|
||||
from pandas import DataFrame
|
||||
from pandas import Series
|
||||
from pandas.testing import assert_frame_equal
|
||||
from pandas.testing import assert_series_equal
|
||||
|
||||
# Module under test
|
||||
from pyrate.common.raster_datasets.transformers_concrete import BathymetricTransformer
|
||||
from pyrate.common.raster_datasets.transformers_concrete import ConstantTransformer
|
||||
|
||||
# Graph generation
|
||||
from pyrate.plan.graph import create_earth_graph
|
||||
from pyrate.plan.graph import GeoNavigationGraph
|
||||
from pyrate.plan.graph import min_required_frequency
|
||||
|
||||
# CI/Testing helpers
|
||||
from ... import _open_test_geo_dataset
|
||||
|
||||
|
||||
class TestGetNodePropertiesWithConstantTransformer(TestCase):
|
||||
"""Ensure that the :meth:`pyrate.plan.graph.GeoNavigationGraph.append_properties` works correctly."""
|
||||
|
||||
def test_get_node_properties_empty_coordinates(self) -> None:
|
||||
"""Tests getting properties for a graph without nodes."""
|
||||
graph = GeoNavigationGraph.from_coordinates_degrees(
|
||||
latitudes=empty((0,)), longitudes=empty((0,)), edges=empty((0, 2)), node_radius=111.111
|
||||
)
|
||||
transformers = [ConstantTransformer(42, uint32, "prop_1"), ConstantTransformer(43, uint16, "prop_2")]
|
||||
graph.append_properties(transformers)
|
||||
self.assertEqual(len(graph.node_properties), 0)
|
||||
assert_array_equal(graph.node_properties.columns, ["prop_1", "prop_2"])
|
||||
|
||||
def test_get_node_properties_no_transformers(self) -> None:
|
||||
"""Tests getting properties without a transformer."""
|
||||
graph = GeoNavigationGraph.from_coordinates_degrees(
|
||||
latitudes=array([0, 1]), longitudes=array([0, 0]), edges=array([[0, 1]]), node_radius=111.111
|
||||
)
|
||||
graph.append_properties([]) # empty!
|
||||
self.assertEqual(len(graph.node_properties), 2)
|
||||
assert_array_equal(graph.node_properties.columns, [])
|
||||
|
||||
def test_get_node_properties_single_transformer(self) -> None:
|
||||
"""Tests getting properties using only a single transformer."""
|
||||
graph = GeoNavigationGraph.from_coordinates_degrees(
|
||||
latitudes=array([0, 1]),
|
||||
longitudes=array([0, 0]),
|
||||
edges=array([[0, 1]]),
|
||||
node_radius=0.0, # some weird radius
|
||||
)
|
||||
# now we use `append_property` to append a single one
|
||||
graph.append_property(ConstantTransformer(33, uint32, "prop_1"))
|
||||
self.assertEqual(len(graph.node_properties), 2)
|
||||
assert_frame_equal(graph.node_properties, DataFrame(data={"prop_1": [33, 33]}, dtype=uint32))
|
||||
|
||||
def test_get_node_properties_single_transformer_str_datatype(self) -> None:
|
||||
"""Tests getting properties using only a single transformer and a string datatype."""
|
||||
graph = GeoNavigationGraph.from_coordinates_degrees(
|
||||
latitudes=array([0]),
|
||||
longitudes=array([0]),
|
||||
edges=array([[0, 0]]), # edge to itself
|
||||
node_radius=111.111,
|
||||
)
|
||||
# now we use `append_property` to append a single one
|
||||
data_type = "U10" # must give string data type explicitly and not with np.str or "U"
|
||||
graph.append_property(ConstantTransformer("content", data_type, "prop_1"))
|
||||
self.assertEqual(len(graph.node_properties), 1)
|
||||
assert_frame_equal(graph.node_properties, DataFrame(data={"prop_1": ["content"]}, dtype=data_type))
|
||||
|
||||
def test_get_node_properties_multiple_transformers(self) -> None:
|
||||
"""Tests getting properties using multiple transformers."""
|
||||
graph = GeoNavigationGraph.from_coordinates_degrees(
|
||||
latitudes=array([0, 1]), longitudes=array([0, 0]), edges=array([[0, 1]]), node_radius=111.111
|
||||
)
|
||||
# now we use `append_property` to append a single one
|
||||
graph.append_properties(
|
||||
[ConstantTransformer(33, uint32, "prop_1"), ConstantTransformer(99, int16, "prop_2")]
|
||||
)
|
||||
self.assertEqual(len(graph.node_properties), 2)
|
||||
assert_array_equal(graph.node_properties.columns, ["prop_1", "prop_2"])
|
||||
assert_series_equal(
|
||||
graph.node_properties["prop_1"], Series(data=[33, 33], dtype=uint32, name="prop_1")
|
||||
)
|
||||
assert_series_equal(
|
||||
graph.node_properties["prop_2"], Series(data=[99, 99], dtype=int16, name="prop_2")
|
||||
)
|
||||
|
||||
|
||||
class TestBathymetricTransformer(TestCase):
|
||||
"""Tests :class:`pyrate.common.raster_datasets.transformers_concrete.BathymetricTransformer`."""
|
||||
|
||||
def test_all_modes(self) -> None:
|
||||
"""Tests all modes at once."""
|
||||
|
||||
# create a coarse grid
|
||||
distance_meters = 1000_000
|
||||
graph = create_earth_graph(min_required_frequency(distance_meters, in_meters=True))
|
||||
|
||||
# fetch properties
|
||||
modes = list(BathymetricTransformer.Modes)
|
||||
graph.append_property(BathymetricTransformer(_open_test_geo_dataset(), modes))
|
||||
properties = graph.node_properties
|
||||
|
||||
# check that the returned properties are all floats
|
||||
self.assertTrue((properties.dtypes == float32).all())
|
||||
|
||||
def test_no_data(self) -> None:
|
||||
"""Tests that querying for data where there are no data points in the result range raises an error."""
|
||||
for mode in list(BathymetricTransformer.Modes):
|
||||
with self.subTest(mode.name), self.assertRaises(ValueError):
|
||||
with BathymetricTransformer(_open_test_geo_dataset(), [mode]) as transformer:
|
||||
# This works by querying for a point (at 1e-3°N 1e-3°E), where there is no data point
|
||||
# within 1e-9 meters in the underlying dataset
|
||||
# This should trigger an exception (e.g. because the average depth over zero data
|
||||
# points is not clearly)
|
||||
transformer.get_transformed_at_nodes(
|
||||
latitudes=array([1e-3]), longitudes=array([1e-3]), radius=1e-9
|
||||
)
|
Reference in New Issue
Block a user