Added pyrate as a direct dependency.
37
pyrate/tests/__init__.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Contains common helpers for tests."""
|
||||
|
||||
# Standard library
|
||||
from datetime import timedelta
|
||||
import os.path
|
||||
|
||||
# Hypothesis testing
|
||||
from hypothesis import settings
|
||||
|
||||
# Geo libraries
|
||||
import rasterio
|
||||
|
||||
# DataSetAccess getting the path to the example data set
|
||||
from pyrate.common.raster_datasets import DataSetAccess
|
||||
|
||||
# Pyrate
|
||||
from pyrate.common.testing import IS_EXTENDED_TESTING
|
||||
|
||||
|
||||
# do more tests on the CI server as that one has more patience
|
||||
# see: https://hypothesis.readthedocs.io/en/latest/settings.html#settings-profiles
|
||||
settings.register_profile("normal", deadline=timedelta(seconds=5), print_blob=True, max_examples=500)
|
||||
settings.register_profile("extended", parent=settings.get_profile("normal"), max_examples=10_000)
|
||||
if IS_EXTENDED_TESTING:
|
||||
settings.load_profile("extended")
|
||||
else:
|
||||
settings.load_profile("normal")
|
||||
|
||||
|
||||
def _open_test_geo_dataset() -> DataSetAccess:
|
||||
"""Tries to return a Earth2014 20 arc-minute grid resolution dataset."""
|
||||
path = os.path.join(
|
||||
os.path.dirname(__file__), "common/raster_datasets/Earth2014.TBI2014.30min.geod.geo.tif"
|
||||
)
|
||||
|
||||
assert os.path.exists(path), "The downscaled Earth2014 testing dataset is missing"
|
||||
return DataSetAccess(rasterio.open(path))
|
0
pyrate/tests/act/__init__.py
Normal file
66
pyrate/tests/act/test_lqr.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""This module asserts correct runtime behaviour of the pyrate.act.pid module."""
|
||||
|
||||
# Test environment
|
||||
from unittest import TestCase
|
||||
|
||||
# Mathematics
|
||||
from numpy import array
|
||||
from numpy import eye
|
||||
from numpy import Inf
|
||||
from numpy.linalg import norm
|
||||
from numpy import vstack
|
||||
|
||||
# Package under test
|
||||
from pyrate.act.control import Lqr
|
||||
|
||||
|
||||
class TestLqr(TestCase):
|
||||
|
||||
"""Test for correct runtime behaviour in pyrate.act.control.lqr."""
|
||||
|
||||
# In this context, we reproduce a common controller notation
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Setup the LQR specification for testing."""
|
||||
|
||||
# Model specification
|
||||
self.A = array([[0, 1], [0, 0]])
|
||||
self.B = array([0, 1])[:, None]
|
||||
self.C = array([1, 0])[None, :]
|
||||
self.dt = 0.5
|
||||
|
||||
# Cost matrix specification
|
||||
self.Q = eye(2)
|
||||
self.R = array([[1.0]])
|
||||
|
||||
# State and target
|
||||
# Target is already reached in this example
|
||||
self.state = vstack([0.0, 0.0])
|
||||
self.desired = vstack([0.0])
|
||||
|
||||
def test_process_tracking(self) -> None:
|
||||
"""Assert that a pandas.DataFrame with process data is created."""
|
||||
|
||||
# Initialize PID controller
|
||||
lqr = Lqr(self.A, self.B, self.C, self.Q, self.R, self.dt, keep_trace=True)
|
||||
|
||||
# Execute a few control steps
|
||||
state = self.state.copy()
|
||||
previous_error = Inf
|
||||
for _ in range(10):
|
||||
control_signal = lqr.control(desired=self.desired, state=self.state)
|
||||
state += self.A.dot(state) + self.B.dot(control_signal)
|
||||
error = norm(state - self.desired).item() # Convert from numpy scalar to Python float
|
||||
assert error < previous_error or error == 0, "Error did not get lower."
|
||||
previous_error = error
|
||||
|
||||
# Assert correct process tracing with LQR controller
|
||||
assert lqr.process is not None, "LQR did not keep trace of process"
|
||||
assert len(lqr.process.index) == 10, "LQR has not traced enough steps"
|
||||
|
||||
# Reset PID
|
||||
lqr.reset()
|
||||
|
||||
# Assert correct reset to initial state
|
||||
assert len(lqr.process.index) == 0, "LQR has not dropped process trace properly"
|
117
pyrate/tests/act/test_lqr_integral.py
Normal file
@ -0,0 +1,117 @@
|
||||
"""This module asserts correct runtime behaviour of the pyrate.act.pid module."""
|
||||
|
||||
# Test environment
|
||||
from unittest import TestCase
|
||||
|
||||
# Mathematics
|
||||
from numpy import array
|
||||
from numpy import eye
|
||||
from numpy.linalg import eig
|
||||
from numpy import vstack
|
||||
|
||||
# Package under test
|
||||
from pyrate.act.control import AntiWindupLqr
|
||||
|
||||
|
||||
class TestAntiWindupLqr(TestCase):
|
||||
|
||||
"""Test for correct runtime behaviour in pyrate.act.control.anti_windup_lqr."""
|
||||
|
||||
# In this context, we reproduce a common controller notation
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Setup the LQR specification for testing."""
|
||||
|
||||
# Model specification
|
||||
self.A = array([[0, 1], [0, 0]])
|
||||
self.B = array([0, 1])[:, None]
|
||||
self.C = array([1, 0])[None, :]
|
||||
self.max_control = array([1.0])
|
||||
self.dt = 0.5
|
||||
|
||||
# Time discrete model
|
||||
self.Ad = self.dt * eye(2)
|
||||
self.Bd = self.B + self.A @ self.B * self.dt
|
||||
|
||||
# Cost matrix specification
|
||||
self.Q = eye(3)
|
||||
self.R = array([[1.0]])
|
||||
|
||||
# State and target
|
||||
# Target is already reached in this example
|
||||
self.wrong_state = vstack([1.0, 0.0])
|
||||
self.state_small_negative = vstack([-0.001, 0.0])
|
||||
self.state = vstack([0.0, 0.0])
|
||||
self.desired = vstack([0.0])
|
||||
self.desired1 = vstack([1.0])
|
||||
|
||||
def test_lqr_design(self) -> None:
|
||||
"""Assert stable controller dynamics"""
|
||||
|
||||
# Initialize controller
|
||||
lqr = AntiWindupLqr(
|
||||
self.A, self.B, self.C, self.Q, self.R, self.max_control, self.dt, keep_trace=True
|
||||
)
|
||||
# check continous time eigen_values
|
||||
eigen_values, _ = eig(lqr.A - lqr.B @ lqr.K)
|
||||
assert all(ev.real < 0 for ev in eigen_values), "instable controller"
|
||||
|
||||
def test_anti_windup(self) -> None:
|
||||
"""Assert control signal in allowed range, LQR responsive by limited integral part."""
|
||||
|
||||
# Initialize controller
|
||||
lqr = AntiWindupLqr(
|
||||
self.A, self.B, self.C, self.Q, self.R, self.max_control, self.dt, keep_trace=True
|
||||
)
|
||||
|
||||
# Execute a few control steps
|
||||
for _ in range(10):
|
||||
control_signal = lqr.control(desired=self.desired, state=self.wrong_state)
|
||||
assert abs(control_signal) <= self.max_control, "control limits are not applied"
|
||||
|
||||
assert control_signal == -self.max_control, f"control limits not reached {control_signal}"
|
||||
|
||||
# test stationary summed error
|
||||
summed_error = lqr.summed_error
|
||||
control_signal = lqr.control(desired=self.desired, state=self.wrong_state)
|
||||
assert abs(lqr.summed_error - summed_error) < 1e-6, "summed error changes in saturation"
|
||||
|
||||
# test reactiveness
|
||||
control_signal_back = lqr.control(desired=self.desired, state=self.state_small_negative)
|
||||
assert abs(control_signal_back) < self.max_control, "anti wind up is not working"
|
||||
|
||||
def test_process_tracking(self) -> None:
|
||||
"""Assert useful controller behavior and that a pandas.DataFrame with process data is created."""
|
||||
|
||||
# Initialize controller
|
||||
lqr = AntiWindupLqr(
|
||||
self.A, self.B, self.C, self.Q, self.R, self.max_control, self.dt, keep_trace=True
|
||||
)
|
||||
|
||||
# Check zero control
|
||||
control_signal = lqr.control(desired=self.desired, state=self.state)
|
||||
assert control_signal == 0, "Control signal not zero when desired value 0 reached"
|
||||
control_signal = lqr.control(desired=self.desired1, state=self.wrong_state)
|
||||
assert control_signal == 0, "Control signal not zero when desired value 0 reached"
|
||||
lqr.reset()
|
||||
|
||||
# Execute a few control steps
|
||||
state = self.wrong_state.copy()
|
||||
initial_error = abs(self.C @ state - self.desired)
|
||||
for _ in range(10):
|
||||
control_signal = lqr.control(desired=self.desired, state=state)
|
||||
state = self.Ad.dot(state) + self.Bd.dot(control_signal)
|
||||
error = abs(self.C @ state - self.desired)
|
||||
assert error < initial_error or error == 0, "Error exceeds initial - instable controller?"
|
||||
|
||||
# Assert correct process tracing with LQR controller
|
||||
assert lqr.process is not None, "LQR did not keep trace of process"
|
||||
assert len(lqr.process.index) == 10, "LQR has not traced enough steps"
|
||||
|
||||
# Reset controller
|
||||
lqr.reset()
|
||||
|
||||
# Assert correct reset to initial state
|
||||
assert lqr.summed_error == 0.0, "Integral did not reset summed error properly"
|
||||
assert len(lqr.process.index) == 0, "LQR has not dropped process trace properly"
|
65
pyrate/tests/act/test_pid.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""This module asserts correct runtime behaviour of the pyrate.act.pid module."""
|
||||
|
||||
# Test environment
|
||||
from unittest import TestCase
|
||||
|
||||
# Mathematics
|
||||
from numpy import array
|
||||
from numpy import Inf
|
||||
from numpy.linalg import norm
|
||||
from numpy import vstack
|
||||
|
||||
# Package under test
|
||||
from pyrate.act.control import Pid
|
||||
|
||||
|
||||
class TestPid(TestCase):
|
||||
|
||||
"""Test for correct runtime behaviour in pyrate.act.control.pid."""
|
||||
|
||||
# In this context, we reproduce a common PID notation
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Setup the PID specification for testing."""
|
||||
|
||||
# PID specification
|
||||
self.P = array([[1.0]])
|
||||
self.I = array([[1.0]]) # noqa: 741
|
||||
self.D = array([[1.0]])
|
||||
self.dt = 0.5
|
||||
|
||||
# State and target
|
||||
# Target is already reached in this example
|
||||
self.state = vstack([0.0])
|
||||
self.state_derivative = vstack([0.0])
|
||||
self.desired = vstack([0.0])
|
||||
|
||||
def test_process_tracking(self) -> None:
|
||||
"""Assert that a pandas.DataFrame with process data is created."""
|
||||
|
||||
# Initialize PID controller
|
||||
pid = Pid(self.P, self.I, self.D, self.dt, keep_trace=True)
|
||||
|
||||
# Execute a few control steps
|
||||
state = self.state.copy()
|
||||
previous_error = Inf
|
||||
for _ in range(10):
|
||||
control_signal = pid.control(
|
||||
desired=self.desired, state=self.state, state_derivative=self.state_derivative
|
||||
)
|
||||
state += control_signal
|
||||
error = norm(state - self.desired).item() # Convert from numpy scalar to Python float
|
||||
assert error < previous_error or error == 0, "Error did not get lower."
|
||||
previous_error = error
|
||||
|
||||
# Assert correct process tracing with PID controller
|
||||
assert pid.process is not None, "PID did not keep trace of process"
|
||||
assert len(pid.process.index) == 10, "PID has not traced enough steps"
|
||||
|
||||
# Reset PID
|
||||
pid.reset()
|
||||
|
||||
# Assert correct reset to initial state
|
||||
assert pid.summed_error == 0.0, "PID did not reset summed error properly"
|
||||
assert len(pid.process.index) == 0, "PID has not dropped process trace properly"
|
88
pyrate/tests/act/test_pid_anti_windup.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""This module asserts correct runtime behaviour of the pyrate.act.pid module."""
|
||||
|
||||
# Test environment
|
||||
from unittest import TestCase
|
||||
|
||||
# Mathematics
|
||||
from numpy import array
|
||||
from numpy import Inf
|
||||
from numpy.linalg import norm
|
||||
from numpy import vstack
|
||||
|
||||
# Package under test
|
||||
from pyrate.act.control import AntiWindupPid
|
||||
|
||||
|
||||
class TestAntiWindupPid(TestCase):
|
||||
|
||||
"""Test for correct runtime behaviour in pyrate.act.control.anti_windup_pid."""
|
||||
|
||||
# In this context, we reproduce a common PID notation
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Set up the PID specification for testing."""
|
||||
|
||||
# PID specification
|
||||
self.P = array([[1.0]])
|
||||
self.I = array([[1.0]]) # noqa: 741
|
||||
self.D = array([[1.0]])
|
||||
self.max_control = 1.0
|
||||
self.dt = 0.5
|
||||
|
||||
# State and target
|
||||
# Target is already reached in this example
|
||||
self.state = vstack([0.0])
|
||||
self.state_large = vstack([0.5])
|
||||
self.state_small_neg = vstack([-0.01])
|
||||
self.state_derivative = vstack([0.0])
|
||||
self.desired = vstack([0.0])
|
||||
|
||||
def test_anti_windup(self) -> None:
|
||||
"""Assert control signal in allowed range, PID responsive by limited integral part."""
|
||||
# Initialize PID controller
|
||||
pid = AntiWindupPid(self.P, self.I, self.D, self.max_control, self.dt, keep_trace=True)
|
||||
|
||||
# Execute a few control steps
|
||||
for _ in range(10):
|
||||
control_signal = pid.control(
|
||||
desired=self.desired, state=self.state_large, state_derivative=self.state_derivative
|
||||
)
|
||||
assert abs(control_signal) <= self.max_control, "control limits are not applied"
|
||||
assert abs(control_signal) == self.max_control, "control limits not reached"
|
||||
|
||||
# test reactiveness
|
||||
control_signal_back = pid.control(
|
||||
desired=self.desired, state=self.state_small_neg, state_derivative=self.state_derivative
|
||||
)
|
||||
assert abs(control_signal_back) < self.max_control + 1e-4, "anti wind up is not working"
|
||||
|
||||
def test_process_tracking(self) -> None:
|
||||
"""Assert that a pandas.DataFrame with process data is created."""
|
||||
|
||||
# Initialize PID controller
|
||||
pid = AntiWindupPid(self.P, self.I, self.D, self.max_control, self.dt, keep_trace=True)
|
||||
|
||||
# Execute a few control steps
|
||||
# Execute a few control steps
|
||||
state = self.state.copy()
|
||||
previous_error = Inf
|
||||
for _ in range(10):
|
||||
control_signal = pid.control(
|
||||
desired=self.desired, state=self.state, state_derivative=self.state_derivative
|
||||
)
|
||||
state += control_signal
|
||||
error = norm(state - self.desired).item() # Convert from numpy scalar to Python float
|
||||
assert error < previous_error or error == 0, "Error did not decrease"
|
||||
previous_error = error
|
||||
|
||||
# Assert correct process tracing with PID controller
|
||||
assert pid.process is not None, "PID did not keep trace of process"
|
||||
assert len(pid.process.index) == 10, "PID has not traced enough steps"
|
||||
|
||||
# Reset PID
|
||||
pid.reset()
|
||||
|
||||
# Assert correct reset to initial state
|
||||
assert pid.summed_error == 0.0, "PID did not reset summed error properly"
|
||||
assert len(pid.process.index) == 0, "PID has not dropped process trace properly"
|
0
pyrate/tests/common/__init__.py
Normal file
0
pyrate/tests/common/charts/__init__.py
Normal file
1
pyrate/tests/common/charts/example_charts/README.txt
Normal file
@ -0,0 +1 @@
|
||||
See https://gitlab.sailingteam.hg.tu-darmstadt.de/informatik/data/-/tree/master/charts/noaa_vector for the license and more information.
|
@ -0,0 +1,71 @@
|
||||
NOAA ENC<4E>
|
||||
|
||||
NATIONAL OCEANIC AND ATMOSPHERIC ADMINISTRATION
|
||||
|
||||
US1BS04M - BERING SEA NORTHERN PART (EAST)
|
||||
|
||||
|
||||
INDEX:
|
||||
NOTE A
|
||||
AIDS TO NAVIGATION
|
||||
CAUTION - TEMPORARY CHANGES
|
||||
CAUTION - LIMITATIONS
|
||||
NOTE B
|
||||
WARNING - PRUDENT MARINER
|
||||
AUTHORITIES
|
||||
INTERNATIONAL BOUNDARIES
|
||||
POLLUTION REPORTS
|
||||
CAUTION - DANGER
|
||||
ADDITIONAL INFORMATION
|
||||
|
||||
|
||||
NOTES:
|
||||
NOTE A
|
||||
Navigation regulations are published in Chapter 2, U.S. Coast Pilot 9. Additions or revisions to Chapter 2 are published in the Notice to Mariners. Information concerning the regulations may be obtained at the Office of the Commander, 17th Coast Guard District in Juneau, Alaska or at the Office of the District Engineer, Corps of Engineers in Anchorage, Alaska.
|
||||
Refer to charted regulation section numbers.
|
||||
|
||||
|
||||
AIDS TO NAVIGATION
|
||||
Consult U.S. Coast Guard Light List for supplemental information concerning aids to navigation.
|
||||
|
||||
See National Geospatial-Intelligence Agency List of Lights and Fog Signals for information not included in the United States Coast Guard Light List.
|
||||
|
||||
|
||||
CAUTION - TEMPORARY CHANGES
|
||||
Temporary changes or defects in aids to navigation are not indicated. See Local Notice to Mariners.
|
||||
|
||||
|
||||
CAUTION - LIMITATIONS
|
||||
Limitations on the use of radio signals as aids to marine navigation can be found in the U.S. Coast Guard Light Lists and National Geospatial-Intelligence Agency Publication 117.
|
||||
Radio direction-finder bearings to commercial broadcasting stations are subject to error and should be used with caution.
|
||||
|
||||
|
||||
NOTE B
|
||||
Radio navigational aids on the Russian Arctic coast and adjacent islands north of the Arctic Circle have been omitted due to the lack of reliable information.
|
||||
|
||||
|
||||
WARNING - PRUDENT MARINER
|
||||
The prudent mariner will not rely solely on any single aid to navigation, particularly on floating aids. See U.S. Coast Guard Light List and U.S. Coast Pilot for details.
|
||||
|
||||
|
||||
AUTHORITIES
|
||||
Hydrography and topography by the National Ocean Service, Coast Survey, with additional data from the U.S. Coast Guard, National Geospatial Intelligence Agency, and the Japanese Hydrographic Department.
|
||||
|
||||
|
||||
INTERNATIONAL BOUNDARIES
|
||||
International boundaries as shown are approximate.
|
||||
|
||||
|
||||
POLLUTION REPORTS
|
||||
Report all spills of oil and hazardous substances to the National Response Center via 1-800-424-8802 (toll free), or to the nearest U.S. Coast Guard facility if telephone communication is impossible (33 CFR 153).
|
||||
|
||||
|
||||
CAUTION - DANGER
|
||||
Danger, Prohibited, and Restricted Area falling within the limits of the larger scale charts are shown thereon and not repeated.
|
||||
|
||||
|
||||
ADDITIONAL INFORMATION
|
||||
Additional information can be obtained at www.nauticalcharts.noaa.gov
|
||||
|
||||
|
||||
END OF FILE
|
@ -0,0 +1,6 @@
|
||||
Maritime boundary provisionally applied pending formal exchange of instruments of ratification.
|
||||
|
||||
According to Article 3 of the Agreement Between the United States of America and Russia on the Maritime Boundary, signed June 1, 1990:
|
||||
|
||||
"1. In any area east of the maritime boundary that lies within 200 nautical miles of the baseline from which the breadth of the territorial sea of Russia is measured but beyond 200 nautical miles of the baselines from which the breadth of the territorial sea of the United States is measured ("eastern special area"), Russia agrees that henceforth the United States may exercise the sovereign rights and jurisdiction derived from exclusive economic zone jurisdiction that Russia would otherwise be entitled to exercise under international law in the absence of the agreement of the Parties on the maritime boundary...
|
||||
3. to the extent that either Party exercises the sovereign rights or jurisdiction in the special area or areas on its side of the maritime boundary as provided for in this Article, such exercise of sovereign rights or jurisdiction derives from the agreement of the Parties and does not constitute an extension of its exclusive economic zone. To this end, each Party shall take the necessary steps to ensure that any exercise on its part of such rights or jurisdiction in the special area or areas on its side of the maritime boundary shall be so characterized in its relevant laws, regulations, and charts."
|
@ -0,0 +1,2 @@
|
||||
CAUTION - QUALITY OF BATHYMETRIC DATA
|
||||
The areas represented by the object M_QUAL (Quality of data) are approximate due to generalizing for clarity. Caution is advised, particularly for nearshore navigation or voyage planning. M_QUAL represents areas of uniform quality of bathymetric data. The CATZOC (Category of zone of confidence in data) attribute of M_QUAL provides an assessment of the overall zone of confidence.
|
BIN
pyrate/tests/common/charts/example_charts/US1BS04M/US1BS04M.000
Normal file
@ -0,0 +1,97 @@
|
||||
NOAA ENC<4E>
|
||||
|
||||
NATIONAL OCEANIC AND ATMOSPHERIC ADMINISTRATION
|
||||
|
||||
US4AK5GM - PORT MOLLER AND HERENDEEN BAY
|
||||
|
||||
INDEX:
|
||||
|
||||
AIDS TO NAVIGATION
|
||||
POLLUTION REPORTS
|
||||
CAUTION USE OF RADIO SIGNALS (LIMITATIONS)
|
||||
SUPPLEMENTAL INFORMATION
|
||||
CAUTION - TEMPORARY CHANGES
|
||||
WARNING - PRUDENT MARINER
|
||||
ADDITIONAL INFORMATION
|
||||
NOTE A
|
||||
AUTHORITIES
|
||||
CAUTION - LIMITATIONS
|
||||
CAUTION - CHANNELS
|
||||
RADAR REFLECTORS
|
||||
NOAA WEATHER RADIO BROADCASTS
|
||||
TIDAL INFORMATION
|
||||
ADMINISTRATION AREA
|
||||
COLREGS, 82.1705 (see note A)
|
||||
|
||||
|
||||
|
||||
NOTES:
|
||||
|
||||
AIDS TO NAVIGATION
|
||||
Consult U.S. Coast Guard Light List for supplemental information concerning aids to navigation.
|
||||
|
||||
|
||||
POLLUTION REPORTS
|
||||
Report all spills of oil and hazardous substances to the National Response Center via 1-800-424-8802 (toll free), or to the nearest U.S. Coast Guard facility if telephone communication is impossible (33 CFR 153).
|
||||
|
||||
|
||||
CAUTION USE OF RADIO SIGNALS (LIMITATIONS)
|
||||
Limitations on the use of radio signals as aids to marine navigation can be found in the U.S. Coast Guard Light Lists and National Geospatial-Intelligence Agency Publication 117. Radio direction-finder bearings to commercial broadcasting stations are subject to error and should be used with caution.
|
||||
|
||||
|
||||
SUPPLEMENTAL INFORMATION
|
||||
Consult U.S. Coast Pilot 9 for important supplemental information.
|
||||
|
||||
|
||||
CAUTION - TEMPORARY CHANGES
|
||||
Temporary changes or defects in aids to navigation are not indicated. See Local Notice to Mariners
|
||||
|
||||
|
||||
WARNING - PRUDENT MARINER
|
||||
The prudent mariner will not rely solely on any single aid to navigation, particularly on floating aids. See U.S. Coast Guard Light List and U.S. Coast Pilot for details.
|
||||
|
||||
|
||||
ADDITIONAL INFORMATION
|
||||
Additional information can be obtained at www.nauticalcharts.noaa.gov
|
||||
|
||||
|
||||
NOTE A
|
||||
Navigation regulations are published in Chapter 2, U.S. Coast Pilot 9. Additions or revisions to Chapter 2 are published in the Notice to Mariners. Information concerning
|
||||
the regulations may be obtained at the Office of the Commander, 17th Coast Guard District in Juneau, Alaska, or at the Office of the District Engineer, Corps of Engineers in Anchorage, Alaska. Refer to charted regulation section numbers
|
||||
|
||||
|
||||
AUTHORITIES
|
||||
Hydrography and Topography by the National Ocean Service, Coast Survey, with additional data from the U.S. Coast Guard.
|
||||
|
||||
|
||||
CAUTION - LIMITATIONS
|
||||
Limitations on the use of radio signals as aids to marine navigation can be found in the U.S. Coast Guard Light Lists and National Geospatial-Intelligence Agency Publication 117. Radio direction-finder bearings to commercial broadcasting stations are subject to error and should be used with caution.CAUTION - CHANNELS Channels are subject to frequent changes due to very strong tidal currents.
|
||||
|
||||
|
||||
CAUTION - CHANNELS
|
||||
Channels are subject to frequent changes due to very strong tidal currents.
|
||||
|
||||
RADAR REFLECTORS
|
||||
Radar reflectors have been placed on many floating aids to navigation. Individual radar
|
||||
reflector identification on these aids has been omitted from this chart.
|
||||
|
||||
|
||||
NOAA WEATHER RADIO BROADCASTS
|
||||
The NOAA Weather Radio station listed below provides continuous weather broadcasts. The reception range is typically 20 to 40 nautical miles from the antenna site, but can be as much as 100 nautical miles for stations at high elevations.
|
||||
|
||||
Sand Point, AK KSDP 840 AM
|
||||
|
||||
|
||||
TIDAL INFORMATION
|
||||
For tidal information see the NOS Tide Table publication or go to http://co-ops.nos.noaa.gov
|
||||
|
||||
|
||||
ADMINISTRATION AREA
|
||||
The entire extent of this ENC cell falls within the limits of an Administration Area. This area covers land, internal waters, and territorial sea. The territorial sea is a maritime zone which the United States exercises sovereignty extending to the airspace as well as to its bed and subsoil. For more information, please refer to the Coast Pilot.
|
||||
|
||||
|
||||
COLREGS, 82.1705 (see note A)
|
||||
International Regulations for Preventing Collisions at Sea, 1972. The entire area of this chart falls seaward of the COLREGS Demarcation Line.
|
||||
|
||||
|
||||
END OF FILE
|
@ -0,0 +1,2 @@
|
||||
CAUTION - QUALITY OF BATHYMETRIC DATA
|
||||
The areas represented by the object M_QUAL (Quality of data) are approximate due to generalizing for clarity. Caution is advised, particularly for nearshore navigation or voyage planning. M_QUAL represents areas of uniform quality of bathymetric data. The CATZOC (Category of zone of confidence in data) attribute of M_QUAL provides an assessment of the overall zone of confidence.
|
BIN
pyrate/tests/common/charts/example_charts/US4AK5GM/US4AK5GM.000
Normal file
BIN
pyrate/tests/common/charts/example_charts/US4AK5GM/US4AK5GM.001
Normal file
144
pyrate/tests/common/charts/example_charts/US4FL87M/US4FL87A.TXT
Normal file
@ -0,0 +1,144 @@
|
||||
NOAA ENC<4E>
|
||||
|
||||
NATIONAL OCEANIC AND ATMOSPHERIC ADMINISTRATION
|
||||
|
||||
US4FL87M - CAPE CANAVERAL TO BETHEL SHOAL
|
||||
|
||||
INDEX:
|
||||
NOTE A
|
||||
AIDS TO NAVIGATION
|
||||
NOAA WEATHER BROADCASTS
|
||||
CAUTION - TEMPORARY CHANGES
|
||||
CAUTION - DREDGED AREAS
|
||||
SUPPLEMENTAL INFORMATION
|
||||
AUTHORITIES
|
||||
POLLUTION REPORTS
|
||||
RADAR REFLECTORS
|
||||
WARNING - PRUDENT MARINER
|
||||
CAUTION - SUBMARINE PIPELINES AND CABLES
|
||||
HURRICANES AND TROPICAL STORMS
|
||||
CAUTION - LIMITATIONS
|
||||
ADDITIONAL INFORMATION
|
||||
TIDAL INFORMATION
|
||||
CAUTION - USACE HYDROGRAPHIC SURVEYS
|
||||
|
||||
|
||||
NOTES:
|
||||
NOTE A
|
||||
Navigation regulations are published in Chapter 2, U.S.
|
||||
Coast Pilot 4. Additions or revisions to Chapter 2 are pub-
|
||||
lished in the Notice to Mariners. Information concerning the
|
||||
regulations may be obtained at the Office of the Commander,
|
||||
7th Coast Guard District in Miami, Florida, or at the Office
|
||||
of the District Engineer, Corps of Engineers in Jacksonville,
|
||||
Florida.
|
||||
Refer to charted regulation section numbers.
|
||||
|
||||
AIDS TO NAVIGATION
|
||||
Consult U.S. Coast Guard Light List for
|
||||
supplemental information concerning aids to
|
||||
navigation.
|
||||
|
||||
NOAA WEATHER BROADCASTS
|
||||
The NOAA Weather Radio stations listed
|
||||
below provide continuous weather broadcasts.
|
||||
The reception range is typically 20 to 40
|
||||
nautical miles from the antenna site, but can be
|
||||
as much as 100 nautical miles for stations at
|
||||
high elevations.
|
||||
|
||||
Melbourne, FL WXJ-70 162.550 MHz
|
||||
Fort Pierce, FL WWF-69 162.425 MHz
|
||||
|
||||
CAUTION - TEMPORARY CHANGES
|
||||
Temporary changes or defects in aids to
|
||||
navigation are not indicated.
|
||||
See Local Notice to Mariners.
|
||||
|
||||
CAUTION - DREDGED AREAS
|
||||
Improved channels are
|
||||
subject to shoaling, particularly at the edges.
|
||||
|
||||
SUPPLEMENTAL INFORMATION
|
||||
Consult U.S. Coast Pilot 4 for important
|
||||
supplemental information.
|
||||
|
||||
AUTHORITIES
|
||||
Hydrography and topography by the National Ocean Service, Coast
|
||||
Survey, with additional data from the Corps of Engineers, and U.S.
|
||||
Coast Guard.
|
||||
|
||||
POLLUTION REPORTS
|
||||
Report all spills of oil and hazardous sub-
|
||||
stances to the National Response Center via
|
||||
1-800-424-8802 (toll free), or to the nearest U.S.
|
||||
Coast Guard facility if telephone communication
|
||||
is impossible (33 CFR 153).
|
||||
|
||||
RADAR REFLECTORS
|
||||
Radar reflectors have been placed on many
|
||||
floating aids to navigation. Individual radar
|
||||
reflector identification on these aids has been
|
||||
omitted from this chart.
|
||||
|
||||
WARNING - PRUDENT MARINER
|
||||
The prudent mariner will not rely solely on
|
||||
any single aid to navigation, particularly on
|
||||
floating aids. See U.S. Coast Guard Light List
|
||||
and U.S. Coast Pilot for details.
|
||||
|
||||
CAUTION - SUBMARINE PIPELINES AND CABLES
|
||||
Additional uncharted submarine pipelines and
|
||||
submarine cables may exist within the area of
|
||||
this chart. Not all submarine pipelines and sub-
|
||||
marine cables are required to be buried, and
|
||||
those that were originally buried may have
|
||||
become exposed. Mariners should use extreme
|
||||
caution when operating vessels in depths of
|
||||
water comparable to their draft in areas where
|
||||
pipelines and cables may exist, and when
|
||||
anchoring, dragging, or trawling.
|
||||
Covered wells may be marked by lighted or
|
||||
unlighted buoys.
|
||||
|
||||
HURRICANES AND TROPICAL STORMS
|
||||
Hurricanes, tropical storms and other major storms may
|
||||
cause considerable damage to marine structures, aids to
|
||||
navigation and moored vessels, resulting in submerged debris
|
||||
in unknown locations.
|
||||
Charted soundings, channel depths and shoreline may not
|
||||
reflect actual conditions following these storms. Fixed aids to
|
||||
navigation may have been damaged or destroyed. Buoys may
|
||||
have been moved from their charted positions, damaged, sunk,
|
||||
extinguished or otherwise made inoperative. Mariners should
|
||||
not rely upon the position or operation of an aid to navigation.
|
||||
Wrecks and submerged obstructions may have been displaced
|
||||
from charted locations. Pipelines may have become uncovered
|
||||
or moved.
|
||||
Mariners are urged to exercise extreme caution and are
|
||||
requested to report aids to navigation discrepancies and
|
||||
hazards to navigation to the nearest United States Coast Guard
|
||||
unit.
|
||||
|
||||
CAUTION - LIMITATIONS
|
||||
Limitations on the use of radio signals as
|
||||
aids to marine navigation can be found in the
|
||||
U.S. Coast Guard Light Lists and National
|
||||
Geospatial-Intelligence Agency Publication 117.
|
||||
Radio direction-finder bearings to commercial
|
||||
broadcasting stations are subject to error and
|
||||
should be used with caution.
|
||||
|
||||
ADDITIONAL INFORMATION
|
||||
Additional information can be obtained at www.nauticalcharts.noaa.gov
|
||||
|
||||
TIDAL INFORMATION
|
||||
For tidal information see the NOS Tide Table publication or go to http://co-ops.nos.noaa.gov.
|
||||
|
||||
CAUTION - USACE HYDROGRAPHIC SURVEYS
|
||||
USACE conducts hydrographic surveys to monitor navigation conditions.
|
||||
These surveys are not intended to detect underwater features. Uncharted features hazardous to surface navigation are not expected but may exist in federal channels.
|
||||
|
||||
|
||||
|
||||
END OF FILE
|
@ -0,0 +1,16 @@
|
||||
Exclusive Economic Zone (EEZ)
|
||||
The EEZ is a zone beyond and adjacent to the territorial sea within which the U.S. has certain
|
||||
sovereign rights and jurisdiction. Under some U.S. laws, the inner limit of the EEZ extends landward
|
||||
to the seaward limit of the states submerged lands. For more information, please refer to the Coast
|
||||
Pilot.
|
||||
|
||||
Contiguous Zone
|
||||
The Contiguous Zone is a zone contiguous to the territorial sea, in which the United States may
|
||||
exercise the control necessary to prevent and punish infringement within its territory or territorial sea
|
||||
of its customs, fiscal, immigration, cultural heritage or sanitary laws and regulations. For more
|
||||
information, please refer to the Coast Pilot.
|
||||
|
||||
Administration Area
|
||||
This area covers land, internal waters, and territorial sea. The territorial sea is a maritime zone over
|
||||
which the United States exercises sovereignty extending to the airspace as well as to its bed and
|
||||
subsoil. For more information, please refer to the Coast Pilot.
|
@ -0,0 +1,14 @@
|
||||
The Inland Navigational Rules Act of 1980 is in effect for vessels transiting this
|
||||
area. The seaward boundaries of this area are the COLREGS demarcation lines.
|
||||
In the area seaward of the COLREGS demarcation lines, vessels are governed by
|
||||
COLREGS: International Regulations for Preventing Collisions at Sea, 1972.
|
||||
The COLREGS demarcation lines are defined in 33 CFR 80.727b.
|
||||
|
||||
Navigation regulations are published in Chapter 2, U.S.
|
||||
Coast Pilot 4. Additions or revisions to Chapter 2 are pub-
|
||||
lished in the Notice to Mariners. Information concerning the
|
||||
regulations may be obtained at the Office of the Commander,
|
||||
7th Coast Guard District in Miami, Florida, or at the Office
|
||||
of the District Engineer, Corps of Engineers in Jacksonville,
|
||||
Florida.
|
||||
Refer to charted regulation section numbers.
|
@ -0,0 +1 @@
|
||||
Depths from surveys of 2000-2007 - Regulations for Ocean Dumping Sites are contained in 40 CFR, Parts 220-229. Additional information concerning the regulations and requirements for use of the sites may be obtained from the Environmental Protection Agency (EPA). See U.S. Coast Pilots appendix for addresses of EPA offices. Dumping subsequent to the survey dates may have reduced the depths shown.
|
@ -0,0 +1,2 @@
|
||||
This area represents the limits of the Low-Mid Inclination launch hazard areas associated with the majority of launches from Cape Canaveral. Launch debris may fall within these areas. See Notice to Mariners or contact the Coast Guard for launch hazard areas specific to each launch and the times they will be in effect.
|
||||
|
@ -0,0 +1 @@
|
||||
This area represents the limits of the High Inclination launch hazard areas associated with the majority of launches from Cape Canaveral. Launch debris may fall within these areas. See Notice to Mariners or contact the Coast Guard for launch hazard areas specific to each launch and the times they will be in effect.
|
@ -0,0 +1,6 @@
|
||||
CAUTION <20> QUALITY OF BATHYMETRIC DATA
|
||||
The areas represented by the object M_QUAL (Quality of data) are approximate due
|
||||
to generalizing for clarity. Caution is advised, particularly for nearshore navigation
|
||||
or voyage planning. M_QUAL represents areas of uniform quality of bathymetric data.
|
||||
The CATZOC (Category of zone of confidence in data) attribute of M_QUAL provides
|
||||
an assessment of the overall zone of confidence.
|
BIN
pyrate/tests/common/charts/example_charts/US4FL87M/US4FL87M.000
Normal file
BIN
pyrate/tests/common/charts/example_charts/US4FL87M/US4FL87M.001
Normal file
@ -0,0 +1,132 @@
|
||||
NOAA ENC<4E>
|
||||
|
||||
NATIONAL OCEANIC AND ATMOSPHERIC ADMINISTRATION
|
||||
|
||||
US4VA70M - CHINCOTEAGUE INLET TO GREAT MACHIPONGO INLET
|
||||
|
||||
INDEX
|
||||
AUTHORITIES
|
||||
AIDS TO NAVIGATION
|
||||
NOTE A
|
||||
WARNING - PRUDENT MARINER
|
||||
POLLUTION REPORTS
|
||||
SUPPLEMENTAL INFORMATION
|
||||
CAUTION - TEMPORARY CHANGES
|
||||
CAUTION - LIMITATIONS
|
||||
CAUTION - SUBMARINE PIPELINES AND CABLES
|
||||
RADAR REFLECTORS
|
||||
NOAA WEATHER BROADCASTS
|
||||
CAUTION - DREDGED AREAS
|
||||
TIDAL INFORMATION
|
||||
ADDITIONAL INFORMATION
|
||||
|
||||
|
||||
NOTES:
|
||||
AUTHORITIES
|
||||
Hydrography and Topography by the National Ocean Service, Coast
|
||||
Survey, with additional data from the Corps of Engineers, Geological
|
||||
Survey, and U.S. Coast Guard.
|
||||
|
||||
|
||||
AIDS TO NAVIGATION
|
||||
Consult U.S. Coast Guard Light List for
|
||||
supplemental information concerning aids to
|
||||
navigation.
|
||||
|
||||
|
||||
NOTE A
|
||||
Navigation regulations are published in Chapter 2, U.S.
|
||||
Coast Pilot 3. Additions or revisions to Chapter 2 are pub-
|
||||
lished in the Notice to Mariners. Information concerning the
|
||||
regulations may be obtained at the Office of the Commander,
|
||||
5th Coast Guard District in Portsmouth, Virginia or at the
|
||||
Office of the District Engineer, Corps of Engineers in
|
||||
Norfolk, Virginia.
|
||||
Refer to charted regulation section numbers.
|
||||
|
||||
|
||||
WARNING - PRUDENT MARINER
|
||||
The prudent mariner will not rely solely on
|
||||
any single aid to navigation, particularly on
|
||||
floating aids. See U.S. Coast Guard Light List
|
||||
and U.S. Coast Pilot for details.
|
||||
|
||||
|
||||
POLLUTION REPORTS
|
||||
Report all spills of oil and hazardous substances to the
|
||||
National Response Center via 1-800-424-8802 (toll free), or
|
||||
to the nearest U.S. Coast Guard facility if telephone com-
|
||||
munication is impossible (33 CFR 153).
|
||||
|
||||
|
||||
SUPPLEMENTAL INFORMATION
|
||||
Consult U.S. Coast Pilot 3 for important
|
||||
supplemental information.
|
||||
|
||||
|
||||
CAUTION - TEMPORARY CHANGES
|
||||
Temporary changes or defects in aids to
|
||||
navigation are not indicated. See
|
||||
Local Notice to Mariners.
|
||||
|
||||
|
||||
CAUTION - LIMITATIONS
|
||||
Limitations on the use of radio signals as
|
||||
aids to marine navigation can be found in the
|
||||
U.S. Coast Guard Light Lists and National
|
||||
Geospatial-Intelligence Agency Publication 117.
|
||||
Radio direction-finder bearings to commercial
|
||||
broadcasting stations are subject to error and
|
||||
should be used with caution.
|
||||
|
||||
|
||||
CAUTION - SUBMARINE PIPELINES AND CABLES
|
||||
Additional uncharted submarine pipelines and
|
||||
submarine cables may exist within the area of
|
||||
this chart. Not all submarine pipelines and sub-
|
||||
marine cables are required to be buried, and
|
||||
those that were originally buried may have
|
||||
become exposed. Mariners should use extreme
|
||||
caution when operating vessels in depths of
|
||||
water comparable to their draft in areas where
|
||||
pipelines and cables may exist, and when
|
||||
anchoring, dragging, or trawling.
|
||||
Covered wells may be marked by lighted or
|
||||
unlighted buoys.
|
||||
|
||||
|
||||
RADAR REFLECTORS
|
||||
Radar reflectors have been placed on many
|
||||
floating aids to navigation. Individual radar
|
||||
reflector identification on these aids has been
|
||||
omitted from this chart.
|
||||
|
||||
|
||||
NOAA WEATHER RADIO BROADCASTS
|
||||
The NOAA Weather Radio stations listed
|
||||
below provide continuous weather broadcasts.
|
||||
The reception range is typically 37 to 74 kilometers / 20
|
||||
to 40 nautical miles from the antenna site, but can be
|
||||
as much as 100 nautical miles / 185 kilometers for stations at
|
||||
high elevations.
|
||||
|
||||
Norfolk, VA KHB-37 162.550 MHz
|
||||
Salisbury, MD KEC-92 162.475 MHz
|
||||
Heathsville, VA WXM-57 162.400 MHz
|
||||
|
||||
|
||||
CAUTION - DREDGED AREAS
|
||||
Improved channels are
|
||||
subject to shoaling, particularly at the edges.
|
||||
|
||||
|
||||
TIDAL INFORMATION
|
||||
For tidal information see the NOS tide table publication or go to http://co-ops.nos.noaa.gov.
|
||||
|
||||
|
||||
ADDITIONAL INFORMATION
|
||||
Additional information can be obtained at www.nauticalcharts.noaa.gov
|
||||
|
||||
|
||||
END OF FILE
|
||||
|
@ -0,0 +1,8 @@
|
||||
Exclusive Economic Zone (EEZ)
|
||||
The EEZ is a zone beyond and adjacent to the territorial sea within which the U.S. has certain sovereign rights and jurisdiction. Under some U.S. laws, the inner limit of the EEZ extends landward to the seaward limit of the states submerged lands. For more information, please refer to the Coast Pilot.
|
||||
|
||||
Contiguous Zone
|
||||
The Contiguous Zone is a zone contiguous to the territorial sea, in which the United States may exercise the control necessary to prevent and punish infringement within its territory or territorial sea of its customs, fiscal, immigration, cultural heritage or sanitary laws and regulations. For more information, please refer to the Coast Pilot.
|
||||
|
||||
Administration Area
|
||||
This area covers land, internal waters, and territorial sea. The territorial sea is a maritime zone over which the United States exercises sovereignty extending to the airspace as well as to its bed and subsoil. For more information, please refer to the Coast Pilot.
|
@ -0,0 +1 @@
|
||||
Mariners are warned that numerous uncharted duck blinds, stakes, and fishing structures, some submerged, may exist in the fish trap areas. Such structures are not charted unless known to be permanent.
|
@ -0,0 +1,17 @@
|
||||
The Inland Navigational Rules Act of 1980 is in effect for vessels
|
||||
transiting this area. The seaward boundaries of this area are the COLREGS
|
||||
demarcation lines. In the area seaward of the COLREGS demarcation lines, vessels
|
||||
are governed by COLREGS: International Regulations for Prevention of Collisions
|
||||
at Sea, 1972. The COLREGS demarcation lines are defined in 33 CFR 80.505c,
|
||||
33 CFR 80.505d, 33 CFR 80.505e and 33 CFR 505h.
|
||||
|
||||
|
||||
NOTE A
|
||||
Navigation regulations are published in Chapter 2, U.S.
|
||||
Coast Pilot 3. Additions or revisions to Chapter 2 are pub-
|
||||
lished in the Notice to Mariners. Information concerning the
|
||||
regulations may be obtained at the Office of the Commander,
|
||||
5th Coast Guard District in Portsmouth, Virginia or at the
|
||||
Office of the District Engineer, Corps of Engineers in
|
||||
Norfolk, Virginia.
|
||||
Refer to charted regulation section numbers.
|
@ -0,0 +1,6 @@
|
||||
CAUTION - QUALITY OF BATHYMETRIC DATA
|
||||
The areas represented by the object M_QUAL (Quality of data) are approximate
|
||||
due to generalizing for clarity. Caution is advised, particularly for nearshore
|
||||
navigation or voyage planning. M_QUAL represents areas of uniform quality of
|
||||
bathymetric data. The CATZOC (Category of zone of confidence in data) attribute
|
||||
of M_QUAL provides an assessment of the overall zone of confidence.
|
@ -0,0 +1 @@
|
||||
This should not be found by the discovery chart tests.
|
469
pyrate/tests/common/charts/test_db.py
Normal file
@ -0,0 +1,469 @@
|
||||
"""This module tests if the database abstraction is working correctly."""
|
||||
|
||||
# Standard library
|
||||
from io import StringIO
|
||||
from os.path import join
|
||||
import sqlite3
|
||||
from tempfile import TemporaryDirectory
|
||||
import warnings
|
||||
|
||||
# Typing
|
||||
from typing import cast
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
# Unit testing
|
||||
from unittest.mock import patch
|
||||
from unittest import skip
|
||||
from unittest import skipIf
|
||||
from unittest import TestCase
|
||||
|
||||
# Verification
|
||||
from hypothesis import given
|
||||
from hypothesis import HealthCheck
|
||||
from hypothesis import settings
|
||||
import hypothesis.strategies as st
|
||||
import numpy
|
||||
|
||||
# Geometry
|
||||
from shapely.geometry import Point
|
||||
|
||||
# Package under test
|
||||
from pyrate.common.charts.db import to_wkb
|
||||
from pyrate.common.charts import SpatialiteDatabase
|
||||
from pyrate.plan.geometry import CartesianLocation
|
||||
from pyrate.plan.geometry import CartesianPolygon
|
||||
from pyrate.plan.geometry import Geospatial
|
||||
from pyrate.plan.geometry import LocationType
|
||||
from pyrate.plan.geometry import PolarGeometry
|
||||
from pyrate.plan.geometry import PolarLocation
|
||||
from pyrate.plan.geometry import PolarPolygon
|
||||
from pyrate.plan.geometry import PolarRoute
|
||||
|
||||
# Flags and helpers
|
||||
from pyrate.common.testing import IS_CI
|
||||
from pyrate.common.testing import IS_EXTENDED_TESTING
|
||||
from pyrate.common.testing import SPATIALITE_AVAILABLE
|
||||
from pyrate.common.testing.strategies.geometry import location_types
|
||||
from pyrate.common.testing.strategies.geometry import polar_locations
|
||||
from pyrate.common.testing.strategies.geometry import polar_objects
|
||||
from pyrate.common.testing.strategies.geometry import polar_polygons
|
||||
from pyrate.common.testing.strategies.geometry import polar_routes_stable
|
||||
|
||||
|
||||
# force testing this in CI to make sure it is tested regularly at least there
|
||||
SKIP_IF_SPATIALITE_IS_MISSING = skipIf(
|
||||
not SPATIALITE_AVAILABLE and not IS_CI, "allow spatialite to be missing and skip the tests in that case"
|
||||
)
|
||||
|
||||
|
||||
# reduce the example count since else this will take way too long, especially on the extended tests
|
||||
TEST_REDUCED_COUNT = settings(
|
||||
max_examples=500 if IS_EXTENDED_TESTING else 50,
|
||||
deadline=1000,
|
||||
suppress_health_check=(HealthCheck.too_slow,),
|
||||
)
|
||||
|
||||
|
||||
@SKIP_IF_SPATIALITE_IS_MISSING
|
||||
class TestDatabase(TestCase):
|
||||
"""Test for basic use of the database."""
|
||||
|
||||
@staticmethod
|
||||
def _apply_id_if_missing(geometry: Geospatial, identifier: Optional[int]) -> None:
|
||||
if geometry.identifier is None:
|
||||
geometry.identifier = identifier
|
||||
|
||||
def test_empty_creation(self) -> None:
|
||||
"""Tests whether the creation of a new database if not crashing anything."""
|
||||
with SpatialiteDatabase(":memory:") as database:
|
||||
self.assertEqual(len(database), database.count_geometries())
|
||||
self.assertEqual(
|
||||
database.count_geometries(), 0, "a freshly initialized database should contain no polygons"
|
||||
)
|
||||
self.assertEqual(
|
||||
database.count_vertices(), 0, "a freshly initialized database should contain no vertices"
|
||||
)
|
||||
|
||||
def test_disable_issue_create_statement(self) -> None:
|
||||
"""Tests whether initializing with ``issue_create_statement=False`` fails."""
|
||||
with SpatialiteDatabase(":memory:", issue_create_statement=False) as database:
|
||||
with self.assertRaises(sqlite3.OperationalError):
|
||||
database.count_geometries()
|
||||
|
||||
def test_write_invalid_geometry_no_update_exception(self) -> None:
|
||||
"""Tests that an exception is raised if an invalid geometry is attempted to be written.
|
||||
|
||||
This tests the case where ``update=False``.
|
||||
"""
|
||||
with self.assertRaises(ValueError):
|
||||
with SpatialiteDatabase(":memory:") as database:
|
||||
point = PolarLocation(latitude=-70.2, longitude=+120.444, name="Pointy Point")
|
||||
invalid_geometry = PolarPolygon([point, point, point])
|
||||
database.write_geometry(invalid_geometry, update=False)
|
||||
|
||||
def test_write_invalid_geometry_no_update_suppressed(self) -> None:
|
||||
"""Tests that NO exception is raised if an invalid geometry is attempted to be written.
|
||||
|
||||
This tests the case where ``update=False`` and ``raise_on_failure=False``.
|
||||
"""
|
||||
with self.assertRaises(UserWarning):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
with SpatialiteDatabase(":memory:") as database:
|
||||
point = PolarLocation(latitude=-70.2, longitude=+120.444, name="Pointy Point")
|
||||
invalid_geometry = PolarPolygon([point, point, point])
|
||||
database.write_geometry(invalid_geometry, update=False, raise_on_failure=False)
|
||||
|
||||
@skip("SpatialiteDatabase.write_geometry() currently does not detect it when update=True")
|
||||
def test_write_invalid_geometry_with_update_exception(self) -> None:
|
||||
"""Tests that an exception is raised if an invalid geometry is attempted to be written.
|
||||
|
||||
This tests the case where ``update=True``.
|
||||
"""
|
||||
with self.assertRaises(ValueError):
|
||||
with SpatialiteDatabase(":memory:") as database:
|
||||
point = PolarLocation(latitude=-70.2, longitude=+120.444, name="Pointy Point")
|
||||
invalid_geometry = PolarPolygon([point, point, point])
|
||||
database.write_geometry(invalid_geometry, update=True)
|
||||
|
||||
@skip("SpatialiteDatabase.write_geometry() currently does not detect it when update=True")
|
||||
def test_write_invalid_geometry_with_update_suppressed(self) -> None:
|
||||
"""Tests that NO exception is raised if an invalid geometry is attempted to be written.
|
||||
|
||||
This tests the case where ``update=True`` and ``raise_on_failure=False``.
|
||||
"""
|
||||
with self.assertRaises(UserWarning):
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
with SpatialiteDatabase(":memory:") as database:
|
||||
point = PolarLocation(latitude=-70.2, longitude=+120.444, name="Pointy Point")
|
||||
invalid_geometry = PolarPolygon([point, point, point])
|
||||
database.write_geometry(invalid_geometry, update=True, raise_on_failure=False)
|
||||
|
||||
def test_convert_invalid_geometry_type(self) -> None:
|
||||
"""Tests whether converting an unsupported geometry type raises a :class:`NotImplementedError`."""
|
||||
with self.assertRaises(NotImplementedError):
|
||||
# This obviously is a faulty cast, but we need it to trigger the exception
|
||||
polar = cast(PolarLocation, CartesianLocation(55, 55))
|
||||
to_wkb(polar)
|
||||
|
||||
def test_create_twice(self) -> None:
|
||||
"""Tests that opening/creating/initializing a database twice does not cause any problems.
|
||||
|
||||
This method checks for output on stdout and stderr since sqlite will sometimes just print some
|
||||
warnings instead of raising exceptions. This is a regression test.
|
||||
"""
|
||||
# this creates as shared database (within this process); need to pass uri=True later on
|
||||
uri = "file::memory:?cache=shared"
|
||||
|
||||
# capture stdout & stderr
|
||||
with patch("sys.stdout", new=StringIO()) as fake_stdout:
|
||||
with patch("sys.stderr", new=StringIO()) as fake_stderr:
|
||||
# open two databases
|
||||
with SpatialiteDatabase(uri, uri=True):
|
||||
with SpatialiteDatabase(uri, uri=True):
|
||||
pass
|
||||
|
||||
# assert that nothing got printed
|
||||
self.assertEqual(len(fake_stdout.getvalue()), 0)
|
||||
self.assertEqual(len(fake_stderr.getvalue()), 0)
|
||||
|
||||
def test_clear_and_count(self) -> None:
|
||||
"""Tests :meth:`~.SpatialiteDatabase.clear` and the two counting methods."""
|
||||
with SpatialiteDatabase(":memory:") as database:
|
||||
self.assertEqual(len(database), 0)
|
||||
self.assertEqual(database.count_vertices(), 0)
|
||||
|
||||
poly = PolarPolygon([PolarLocation(0, 0), PolarLocation(0, 1), PolarLocation(1, 1)])
|
||||
|
||||
poly.identifier = 1
|
||||
database.write_geometry(poly)
|
||||
|
||||
self.assertEqual(len(database), 1)
|
||||
self.assertEqual(database.count_vertices(), len(poly.locations))
|
||||
|
||||
poly.identifier = 1
|
||||
database.write_geometry(poly, update=True)
|
||||
|
||||
self.assertEqual(len(database), 1)
|
||||
self.assertEqual(database.count_vertices(), len(poly.locations))
|
||||
|
||||
poly.identifier = 2
|
||||
database.write_geometry(poly)
|
||||
|
||||
self.assertEqual(len(database), 2)
|
||||
self.assertEqual(database.count_vertices(), len(poly.locations) * 2)
|
||||
|
||||
database.clear()
|
||||
|
||||
self.assertEqual(len(database), 0)
|
||||
self.assertEqual(database.count_vertices(), 0)
|
||||
|
||||
def test_result_multi_geometry(self) -> None:
|
||||
"""Tests whether the DB can correctly handle query results that are multi-polygons/-routes."""
|
||||
|
||||
# Test with both routes and polygons
|
||||
for geometry_type in (PolarRoute, PolarPolygon):
|
||||
with self.subTest(f"With type {geometry_type.__name__}"):
|
||||
|
||||
with SpatialiteDatabase(":memory:") as database:
|
||||
horseshoe = numpy.array(
|
||||
[
|
||||
[-3.06913376, 47.50722936],
|
||||
[-3.0893898, 47.52566325],
|
||||
[-3.07788849, 47.54640812],
|
||||
[-3.03050995, 47.55278059],
|
||||
[-2.9875946, 47.54675573],
|
||||
[-2.97849655, 47.53006788],
|
||||
[-2.97712326, 47.51801223],
|
||||
[-2.97849655, 47.50908464],
|
||||
[-3.04887772, 47.50653362],
|
||||
[-3.04922104, 47.51047605],
|
||||
[-2.9898262, 47.51349065],
|
||||
[-2.99480438, 47.54084606],
|
||||
[-3.03136826, 47.54698747],
|
||||
[-3.06947708, 47.54327953],
|
||||
[-3.07806015, 47.52763379],
|
||||
[-3.06741714, 47.51198337],
|
||||
[-3.06913376, 47.50722936],
|
||||
]
|
||||
)
|
||||
touch_point = PolarLocation(longitude=-3.0588340759277344, latitude=47.50943249496333)
|
||||
|
||||
database.write_geometry(geometry_type.from_numpy(horseshoe)) # type: ignore
|
||||
result = list(database.read_geometries_around(touch_point, radius=3_000))
|
||||
self.assertTrue(len(result) in {2, 3})
|
||||
|
||||
def _random_insert_and_extract_all_generic(
|
||||
self, geometry: PolarGeometry, location_type: LocationType, update: bool
|
||||
) -> None:
|
||||
"""Tests whether inserting and then reading works in a very basic setting."""
|
||||
|
||||
with SpatialiteDatabase(":memory:") as database:
|
||||
# it should be empty in the beginning
|
||||
self.assertEqual(0, len(database))
|
||||
self.assertEqual(0, database.count_geometries())
|
||||
self.assertEqual(0, database.count_vertices())
|
||||
|
||||
# insert the polygon
|
||||
try:
|
||||
database.write_geometry(geometry, update=update)
|
||||
except ValueError:
|
||||
return # this example is corrupt, try the next one
|
||||
else:
|
||||
if update and len(database) == 0: # Errors cannot be checked if update==True
|
||||
return # this example is corrupt, try the next one
|
||||
|
||||
# now the database should not be empty anymore
|
||||
self.assertEqual(1, len(database))
|
||||
self.assertEqual(1, database.count_geometries())
|
||||
|
||||
if isinstance(geometry, (PolarRoute, PolarPolygon)):
|
||||
# Some repeated points might be removed, so we cannot check the exact number of vertices
|
||||
self.assertGreaterEqual(len(geometry.locations), database.count_vertices())
|
||||
else: # is a PolarLocation
|
||||
self.assertEqual(1, database.count_vertices())
|
||||
|
||||
# the element should be included in "all"
|
||||
read_obstacles = list(database.read_all())
|
||||
self.assertEqual(1, len(read_obstacles))
|
||||
|
||||
# it should only be included if the type matches
|
||||
all_filtered = list(database.read_all(only_location_type=location_type))
|
||||
if geometry.location_type == location_type:
|
||||
self.assertEqual(all_filtered, read_obstacles)
|
||||
else:
|
||||
self.assertEqual(len(all_filtered), 0)
|
||||
|
||||
# and it should be the one that we have written into the database in the first place
|
||||
read_obstacles_single = read_obstacles[0]
|
||||
# the id may be newly generated if polygon has the id None, so
|
||||
# set it to the generated one for the sake of equality testing
|
||||
TestDatabase._apply_id_if_missing(geometry, read_obstacles_single.identifier)
|
||||
|
||||
assert isinstance(read_obstacles_single, type(geometry)) # Make mypy understand this check
|
||||
|
||||
if isinstance(geometry, PolarPolygon):
|
||||
self.assertTrue(geometry.almost_congruent(read_obstacles_single)) # type: ignore
|
||||
else:
|
||||
self.assertTrue(geometry.equals_exact(read_obstacles_single, tolerance=1e-3))
|
||||
|
||||
@given(polar_locations(), location_types(), st.booleans())
|
||||
@TEST_REDUCED_COUNT
|
||||
def test_random_insert_and_extract_all_locations(
|
||||
self, geometry: PolarGeometry, location_type: LocationType, update: bool
|
||||
) -> None:
|
||||
"""Basic test with locations."""
|
||||
self._random_insert_and_extract_all_generic(geometry, location_type, update)
|
||||
|
||||
@given(polar_routes_stable(), location_types(), st.booleans())
|
||||
@TEST_REDUCED_COUNT
|
||||
def test_random_insert_and_extract_all_routes(
|
||||
self, geometry: PolarGeometry, location_type: LocationType, update: bool
|
||||
) -> None:
|
||||
"""Basic test with routes."""
|
||||
self._random_insert_and_extract_all_generic(geometry, location_type, update)
|
||||
|
||||
@given(polar_polygons(), location_types(), st.booleans())
|
||||
@TEST_REDUCED_COUNT
|
||||
def test_random_insert_and_extract_all_polygons(
|
||||
self, geometry: PolarGeometry, location_type: LocationType, update: bool
|
||||
) -> None:
|
||||
"""Basic test with polygons."""
|
||||
self._random_insert_and_extract_all_generic(geometry, location_type, update)
|
||||
|
||||
def test_copy_to_other_database(self) -> None:
|
||||
"""Tests whether database can be copied.
|
||||
|
||||
This test is not performed using hypothesis as it takes too long.
|
||||
"""
|
||||
|
||||
location_1 = PolarLocation(latitude=-76.400, longitude=-171.924)
|
||||
location_2 = PolarLocation(latitude=-70.400, longitude=-171.924)
|
||||
location_3 = PolarLocation(latitude=-76.400, longitude=-170.924)
|
||||
polygons = [
|
||||
PolarPolygon(
|
||||
locations=[location_1, location_2, location_3],
|
||||
name="K",
|
||||
identifier=1234145,
|
||||
location_type=LocationType.SHALLOW_WATER,
|
||||
),
|
||||
PolarPolygon(locations=[location_1, location_2, location_3], identifier=2342),
|
||||
]
|
||||
|
||||
with TemporaryDirectory() as directory_name:
|
||||
database_file_name = join(directory_name, "other_db.sqlite")
|
||||
|
||||
with SpatialiteDatabase(":memory:") as first_database:
|
||||
with first_database.disable_synchronization():
|
||||
first_database.write_geometries(polygons)
|
||||
first_database.copy_contents_to_database(database_file_name)
|
||||
|
||||
with SpatialiteDatabase(database_file_name) as second_database:
|
||||
read = list(second_database.read_all())
|
||||
|
||||
def sorter(geometry: Geospatial) -> int:
|
||||
return geometry.identifier or -1
|
||||
|
||||
read = list(sorted(read, key=sorter))
|
||||
polygons = list(sorted(polygons, key=sorter))
|
||||
|
||||
self.assertEqual(len(read), len(polygons))
|
||||
for polygon_a, polygon_b in zip(read, polygons):
|
||||
assert isinstance(polygon_a, PolarPolygon) # Make mypy understand this check
|
||||
self.assertTrue(polygon_a.equals_almost_congruent(polygon_b, rel_tolerance=1e-15))
|
||||
|
||||
@settings(max_examples=50 if IS_EXTENDED_TESTING else 10, suppress_health_check=(HealthCheck.too_slow,))
|
||||
@given(st.lists(polar_objects(), min_size=0, max_size=3))
|
||||
def test_database_simplification_zero_tolerance(self, geometries: List[PolarGeometry]) -> None:
|
||||
"""Tests whether database objects can be simplified."""
|
||||
|
||||
for index, geometry in enumerate(geometries):
|
||||
geometry.identifier = index
|
||||
|
||||
with SpatialiteDatabase(":memory:") as database:
|
||||
try:
|
||||
database.write_geometries(geometries)
|
||||
except ValueError:
|
||||
return # this example is corrupt, try the next one
|
||||
database.simplify_contents(0.0)
|
||||
read = list(database.read_all())
|
||||
|
||||
# only one polygon should be returned
|
||||
self.assertEqual(len(read), len(geometries))
|
||||
for original, read_back in zip(geometries, read):
|
||||
self.assertTrue(original.equals_exact(read_back, tolerance=1e-3))
|
||||
|
||||
def test_database_simplification(self) -> None:
|
||||
"""Tests whether database objects can be simplified."""
|
||||
|
||||
# set unique identifiers
|
||||
points = numpy.array(Point(0, 0).buffer(10_000, resolution=16).exterior.coords)
|
||||
cartesian_polygon = CartesianPolygon.from_numpy(points, identifier=10042)
|
||||
polygon = cartesian_polygon.to_polar(origin=PolarLocation(-50.111, -30))
|
||||
self.assertEqual(len(polygon.locations), 65)
|
||||
|
||||
with SpatialiteDatabase(":memory:") as database:
|
||||
database.write_geometry(polygon)
|
||||
database.simplify_contents(100)
|
||||
read = list(database.read_all())
|
||||
|
||||
# only one polygon should be returned
|
||||
self.assertEqual(len(read), 1)
|
||||
read_polygon = read[0]
|
||||
assert isinstance(read_polygon, PolarPolygon) # Make mypy understand this check
|
||||
|
||||
# That polygon should be similar
|
||||
self.assertEqual(33, len(read_polygon.locations))
|
||||
self.assertAlmostEqual(read_polygon.area, polygon.area, places=-3)
|
||||
self.assertTrue(read_polygon.equals_almost_congruent(polygon, rel_tolerance=0.01))
|
||||
|
||||
# else, it should be the same
|
||||
polygon.locations = read_polygon.locations
|
||||
self.assertEqual(polygon, read_polygon)
|
||||
|
||||
|
||||
@SKIP_IF_SPATIALITE_IS_MISSING
|
||||
class TestDatabaseReadAroundHandcrafted(TestCase):
|
||||
"""Tests :meth:`pyrate.common.charts.SpatialiteDatabase.read_obstacles_around` with known examples."""
|
||||
|
||||
included_polygon = PolarPolygon(
|
||||
[PolarLocation(1, 1), PolarLocation(1, -1), PolarLocation(-1, -1), PolarLocation(-1, 1)], identifier=1
|
||||
)
|
||||
excluded_polygon = PolarPolygon(
|
||||
[PolarLocation(56, 170), PolarLocation(56.5, 170), PolarLocation(56, 170.2)], identifier=2
|
||||
)
|
||||
both_polygons = [included_polygon, excluded_polygon]
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.database = SpatialiteDatabase(":memory:")
|
||||
self.database.write_geometries(self.both_polygons)
|
||||
super().setUp()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.database.close()
|
||||
super().tearDown()
|
||||
|
||||
def test_read_around_includes_full(self) -> None:
|
||||
"""Reads a complete polygon."""
|
||||
|
||||
around = list(self.database.read_geometries_around(PolarLocation(0, 2), radius=500_000))
|
||||
self.assertEqual(1, len(around))
|
||||
self.assertEqual(1, len(around))
|
||||
read = around[0]
|
||||
assert isinstance(read, PolarPolygon) # Make mypy understand this check
|
||||
self.assertTrue(self.included_polygon.almost_congruent(read))
|
||||
|
||||
def test_read_around_includes_nothing(self) -> None:
|
||||
"""Tests that :meth:`pyrate.common.charts.SpatialiteDatabase.read_obstacles_around` works correctly.
|
||||
|
||||
Reads nothing.
|
||||
"""
|
||||
|
||||
around = list(self.database.read_geometries_around(PolarLocation(0, 2), radius=0))
|
||||
self.assertEqual(0, len(around))
|
||||
|
||||
def test_read_around_includes_partial(self) -> None:
|
||||
"""Tests that :meth:`pyrate.common.charts.SpatialiteDatabase.read_obstacles_around` works correctly.
|
||||
|
||||
Reads about half of the polygon.
|
||||
"""
|
||||
query_point = PolarLocation(latitude=0.0, longitude=5.0)
|
||||
# this is the distance to center of self.included_polygon
|
||||
radius = query_point.distance(PolarLocation(0.0, 0.0))
|
||||
read = list(self.database.read_geometries_around(query_point, radius=radius))
|
||||
self.assertEqual(1, len(read))
|
||||
read_polygon = read[0]
|
||||
assert isinstance(read_polygon, PolarPolygon) # Make mypy understand this check
|
||||
|
||||
# these shall only be very roughly similar, as half of the polygon is missing
|
||||
# thus, we allow for a very large relative tolerance
|
||||
self.assertTrue(read_polygon.equals_almost_congruent(self.included_polygon, rel_tolerance=0.6))
|
||||
|
||||
# this is roughly the part that should be included in the result
|
||||
eastern_half = PolarPolygon(
|
||||
[PolarLocation(1, 0), PolarLocation(-1, 0), PolarLocation(-1, 1), PolarLocation(1, 1)]
|
||||
)
|
||||
# thus, we allow for less relative tolerance
|
||||
self.assertTrue(read_polygon.almost_congruent(eastern_half, rel_tolerance=0.15))
|
96
pyrate/tests/common/charts/test_s57_files.py
Normal file
@ -0,0 +1,96 @@
|
||||
"""Tests whether ``pyrate.common.charts.raw_files`` correctly handles IHO S-57 chart files."""
|
||||
|
||||
# Python standard
|
||||
from pathlib import Path
|
||||
|
||||
# Testing
|
||||
from unittest import skipIf
|
||||
from unittest import TestCase
|
||||
|
||||
# Extra test tooling
|
||||
import pytest
|
||||
|
||||
# Pyrate library
|
||||
from pyrate.common.testing import IS_CI
|
||||
from pyrate.plan.geometry import LocationType
|
||||
|
||||
# Module under test
|
||||
from pyrate.common.charts import ChartFileHandler
|
||||
from pyrate.common.charts.s57_files import _OSGEO_PRESENT
|
||||
from pyrate.common.charts import S57ChartHandler
|
||||
|
||||
|
||||
PATH_TO_EXAMPLES = Path(__file__).parent / "example_charts"
|
||||
CHARTS = [
|
||||
PATH_TO_EXAMPLES / "nested_folder/US4VA70M/US4VA70M.000",
|
||||
PATH_TO_EXAMPLES / "US1BS04M/US1BS04M.000",
|
||||
PATH_TO_EXAMPLES / "US4AK5GM/US4AK5GM.000",
|
||||
PATH_TO_EXAMPLES / "US4FL87M/US4FL87M.000",
|
||||
]
|
||||
CHARTS.sort()
|
||||
|
||||
|
||||
class TestChartDiscovery(TestCase):
|
||||
"""Tests that charts are correctly discovered"""
|
||||
|
||||
def test_discovery(self):
|
||||
"""Also checks for nested folders and npn-chart files being present"""
|
||||
discovered = list(S57ChartHandler.find_chart_files(PATH_TO_EXAMPLES))
|
||||
discovered.sort()
|
||||
|
||||
# check that exactly the expected charts have been found
|
||||
self.assertListEqual(discovered, CHARTS)
|
||||
|
||||
# check that they have the characteristic file extension
|
||||
for chart_path in discovered:
|
||||
self.assertTrue(chart_path.name.endswith(".000"))
|
||||
|
||||
|
||||
# force testing this in CI to make sure it is tested regularly at least there
|
||||
@skipIf(not _OSGEO_PRESENT and not IS_CI, "allow osgeo to be missing and skip the tests in that case")
|
||||
class TestReadCharts(TestCase):
|
||||
"""Tests whether actually reading the charts works"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.handler: ChartFileHandler = S57ChartHandler()
|
||||
|
||||
def test_reading_non_existent_file(self):
|
||||
"""Tests reading a chart file that does not exist."""
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
# Wrapping this in a list causes the generator/iterator to be actually evaluated
|
||||
list(self.handler.read_chart_file("/does/surely/not/exist/and/if/it/does/we/have/a/troll"))
|
||||
with self.assertRaises(FileNotFoundError):
|
||||
# Wrapping this in a list causes the generator/iterator to be actually evaluated
|
||||
list(self.handler.read_chart_file("/does/surely/not/exist/and/if/it/does/we/have/a/troll.000"))
|
||||
|
||||
def test_reading_wrong_file_type(self):
|
||||
"""Tests reading a chart file that is not an S57 file (this Python program file)."""
|
||||
with self.assertRaises(IOError):
|
||||
# This test python file is not a chart
|
||||
not_a_chart_file = __file__
|
||||
# Wrapping this in a list causes the generator/iterator to be actually evaluated
|
||||
list(self.handler.read_chart_file(not_a_chart_file))
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Ignoring LineString geometry in chart")
|
||||
def test_reading_contains_all_types(self):
|
||||
"""Checks for specific types of entries in the charts.
|
||||
|
||||
Note:
|
||||
Only tests for Landmasses as of now
|
||||
"""
|
||||
|
||||
all_obstacles = [obstacle for chart in CHARTS for obstacle in self.handler.read_chart_file(chart)]
|
||||
self.assertGreater(len(all_obstacles), 0, "no obstacles were read")
|
||||
|
||||
relevant_types = (
|
||||
(LocationType.LAND, "Landmass"),
|
||||
(LocationType.SHALLOW_WATER, "Depth"),
|
||||
(LocationType.OBSTRUCTION, "Buoy"),
|
||||
)
|
||||
for location_type, name_component in relevant_types:
|
||||
filtered = [
|
||||
o
|
||||
for o in all_obstacles
|
||||
if (o.name is not None and name_component in o.name and location_type == o.location_type)
|
||||
]
|
||||
self.assertTrue(filtered, f"no obstacle of type {name_component} was found")
|
1
pyrate/tests/common/math/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests ``pyrate.common.math.**``."""
|
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
@ -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
@ -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
|
||||
)
|
0
pyrate/tests/plan/__init__.py
Normal file
0
pyrate/tests/plan/geometry/__init__.py
Normal file
0
pyrate/tests/plan/geometry/helpers/__init__.py
Normal file
171
pyrate/tests/plan/geometry/helpers/test_difference.py
Normal file
@ -0,0 +1,171 @@
|
||||
"""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
|
62
pyrate/tests/plan/geometry/helpers/test_distance.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""This module asserts correct runtime behaviour of the :mod:`pyrate.plan.geometry.helpers` functions
|
||||
for calculating distances.
|
||||
"""
|
||||
|
||||
# Python standard library
|
||||
from datetime import timedelta
|
||||
from math import radians
|
||||
|
||||
# Testing
|
||||
from unittest import TestCase
|
||||
|
||||
# Hypothesis testing
|
||||
from hypothesis import given
|
||||
from hypothesis import settings
|
||||
import hypothesis.strategies as st
|
||||
|
||||
# Scientific (testing)
|
||||
import numpy.testing
|
||||
|
||||
# Module under test
|
||||
from pyrate.plan.geometry.helpers import fast_distance_geo
|
||||
from pyrate.plan.geometry.helpers import haversine_numpy
|
||||
|
||||
# Own geometry
|
||||
from pyrate.plan.geometry.geospatial import MEAN_EARTH_CIRCUMFERENCE
|
||||
from pyrate.plan.geometry import PolarLocation
|
||||
|
||||
# Test helpers
|
||||
from pyrate.common.testing.strategies.geometry import geo_bearings
|
||||
from pyrate.common.testing.strategies.geometry import polar_locations
|
||||
|
||||
|
||||
class TestDistanceCalculation(TestCase):
|
||||
"""Tests the geographic helper methods."""
|
||||
|
||||
@given(polar_locations(), polar_locations())
|
||||
def test_haversine_formula(self, location_1: PolarLocation, location_2: PolarLocation) -> None:
|
||||
"""Test the correctness of the haversine formula."""
|
||||
dist = haversine_numpy(
|
||||
radians(location_1.latitude),
|
||||
radians(location_1.longitude),
|
||||
radians(location_2.latitude),
|
||||
radians(location_2.longitude),
|
||||
)
|
||||
self.assertLessEqual(dist, MEAN_EARTH_CIRCUMFERENCE / 2)
|
||||
numpy.testing.assert_allclose(location_1.distance(location_2), dist, atol=5.0, rtol=0.01)
|
||||
|
||||
@given(polar_locations(), geo_bearings(), st.floats(min_value=0.0, max_value=250_000.0))
|
||||
@settings(deadline=timedelta(seconds=1.0))
|
||||
# pylint: disable=no-self-use
|
||||
def test_fast_distance_geo(self, center: PolarLocation, direction: float, distance: float) -> None:
|
||||
"""Test the correctness of the fast great-circle approximation."""
|
||||
|
||||
other, _ = center.translate(direction, distance)
|
||||
|
||||
distance_calculated = fast_distance_geo(
|
||||
radians(other.latitude),
|
||||
radians(other.longitude),
|
||||
radians(center.latitude),
|
||||
radians(center.longitude),
|
||||
)
|
||||
numpy.testing.assert_allclose(distance, distance_calculated, atol=0.5, rtol=0.05)
|
182
pyrate/tests/plan/geometry/helpers/test_normalize.py
Normal file
@ -0,0 +1,182 @@
|
||||
"""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
|
120
pyrate/tests/plan/geometry/helpers/test_other.py
Normal file
@ -0,0 +1,120 @@
|
||||
"""This module asserts correct runtime behaviour of various additional helpers."""
|
||||
|
||||
# Python Standard Library
|
||||
from math import tau
|
||||
|
||||
# Generic testing
|
||||
from unittest import TestCase
|
||||
|
||||
# Hypothesis testing
|
||||
import hypothesis.extra.numpy as st_numpy
|
||||
from hypothesis import given
|
||||
import hypothesis.strategies as st
|
||||
|
||||
# Scientific
|
||||
import numpy as np
|
||||
from numpy.testing import assert_almost_equal
|
||||
|
||||
# Module under test
|
||||
from pyrate.plan.geometry.helpers import cartesian_to_spherical
|
||||
from pyrate.plan.geometry.helpers import difference_latitude
|
||||
from pyrate.plan.geometry.helpers import difference_longitude
|
||||
from pyrate.plan.geometry.helpers import mean_angle
|
||||
from pyrate.plan.geometry.helpers import mean_coordinate
|
||||
from pyrate.plan.geometry.helpers import meters2rad
|
||||
from pyrate.plan.geometry.helpers import rad2meters
|
||||
|
||||
# Own strategies
|
||||
from pyrate.common.testing.strategies.geometry import geo_bearings
|
||||
|
||||
|
||||
_POSITIVE_FLOATS = st.floats(min_value=0.0, max_value=1e9, allow_infinity=False, allow_nan=False)
|
||||
|
||||
|
||||
class TestRadiansAndMeterConversion(TestCase):
|
||||
"""Makes sure the conversion between meters and radians works."""
|
||||
|
||||
@given(_POSITIVE_FLOATS)
|
||||
def test_is_reversible_float(self, meters: float) -> None:
|
||||
"""Tests that the two functions are the reverse of each other."""
|
||||
self.assertAlmostEqual(meters, rad2meters(meters2rad(meters)), places=5)
|
||||
|
||||
@given(st_numpy.arrays(dtype=float, shape=st_numpy.array_shapes(), elements=_POSITIVE_FLOATS))
|
||||
def test_is_reversible_numpy(self, meters: np.ndarray) -> None: # pylint: disable=no-self-use
|
||||
"""Tests that the two functions are the reverse of each other."""
|
||||
assert_almost_equal(meters, rad2meters(meters2rad(meters)), decimal=5)
|
||||
|
||||
|
||||
class TestCartesianToSpherical(TestCase):
|
||||
"""Makes sure the conversion from cartesian to spherical coordinates works."""
|
||||
|
||||
def test_raises_if_not_on_unit_sphere(self) -> None:
|
||||
"""Asserts that an exception is raised if values are not on the unit sphere."""
|
||||
with self.assertRaises(AssertionError):
|
||||
cartesian_to_spherical(np.array([(10, 20, 30)]))
|
||||
|
||||
def test_specific_values(self) -> None: # pylint: disable=no-self-use
|
||||
"""Asserts that an exception is raised if values are not on the unit sphere."""
|
||||
data_in = np.array([(1, 0, 0), (0, 1, 0), (0, 0, 1), (0.5, 0.5, np.sqrt(1 - 0.5**2 - 0.5**2))])
|
||||
expected_data_out = np.array([(0, 0), (0, np.pi / 2), (-np.pi / 2, 0), (-np.pi / 4, np.pi / 4)]).T
|
||||
|
||||
assert_almost_equal(cartesian_to_spherical(data_in), expected_data_out)
|
||||
|
||||
|
||||
class TestAngleAndCoordinateMean(TestCase):
|
||||
"""Makes sure the mean computation and angles and coordinates works correctly."""
|
||||
|
||||
@given(geo_bearings(), st.floats(min_value=0.0, max_value=1e-9))
|
||||
def test_raises_if_ambiguous(self, angle: float, noise: float) -> None:
|
||||
"""Asserts that an exception is raised if no sensible mean can be calculated."""
|
||||
|
||||
ambiguous_pair = np.array([angle, (angle + 180 + noise) % 360])
|
||||
with self.assertRaises(ValueError):
|
||||
mean_angle(np.radians(ambiguous_pair))
|
||||
with self.assertRaises(ValueError):
|
||||
mean_coordinate(np.array([0.0, 67.2]), ambiguous_pair)
|
||||
|
||||
# But the methods should recover from an exception on the latitude mean computation
|
||||
latitude, _ = mean_coordinate(ambiguous_pair, np.array([0.0, 67.2]))
|
||||
self.assertAlmostEqual(latitude, 0.0)
|
||||
|
||||
@given(
|
||||
st_numpy.arrays(
|
||||
elements=st.floats(min_value=0.0, max_value=np.pi), dtype=float, shape=st_numpy.array_shapes()
|
||||
)
|
||||
)
|
||||
def test_mean_angle_is_in_valid_range(self, data: np.ndarray) -> None:
|
||||
"""Asserts that means are never negative and always between ``0°`` and ``360°``."""
|
||||
|
||||
try:
|
||||
mean = mean_angle(data)
|
||||
self.assertGreaterEqual(mean, 0.0)
|
||||
self.assertLessEqual(mean, np.pi)
|
||||
|
||||
except ValueError:
|
||||
pass # this might happen with the generated values and is okay
|
||||
|
||||
@given(geo_bearings(), st.floats(min_value=0.0, max_value=170))
|
||||
def test_obvious_values_angle(self, angle: float, difference: float) -> None:
|
||||
"""Asserts that the result is sensible for known values."""
|
||||
|
||||
mean = mean_angle(np.radians(np.array([angle, (angle + difference) % 360])))
|
||||
self.assertAlmostEqual(mean, np.radians((angle + difference / 2)) % tau, delta=1e-6)
|
||||
|
||||
@given(
|
||||
st.floats(min_value=-80.0, max_value=+80.0),
|
||||
st.floats(min_value=-170.0, max_value=+170.0),
|
||||
st.floats(min_value=-9.0, max_value=9.0),
|
||||
st.floats(min_value=-9.0, max_value=9.0),
|
||||
)
|
||||
def test_obvious_values_coordinate(
|
||||
self, latitude: float, longitude: float, lat_delta: float, lon_delta: float
|
||||
) -> None:
|
||||
"""Asserts that the result is sensible for known values."""
|
||||
|
||||
lat_mean, lon_mean = mean_coordinate(
|
||||
latitudes=np.array([latitude, latitude + lat_delta]),
|
||||
longitudes=np.array([longitude, longitude + lon_delta]),
|
||||
)
|
||||
self.assertLessEqual(difference_latitude(lat_mean, (latitude + lat_delta / 2)), 1e-6)
|
||||
self.assertLessEqual(difference_longitude(lon_mean, (longitude + lon_delta / 2)), 1e-6)
|
34
pyrate/tests/plan/geometry/helpers/test_translate.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""This module asserts correct runtime behaviour of the :mod:`pyrate.plan.geometry.helpers` functions
|
||||
for translation.
|
||||
|
||||
Note that most of the correctness is asserted by the use in
|
||||
:meth:`pyrate.plan.geometry.PolarPolygon.translate` and :meth:`pyrate.plan.geometry.PolarRoute.translate`.
|
||||
Also, no extensive tests are needed since we trust the underlying library due to its widespread adoption and
|
||||
maturity.
|
||||
We only need to check that the conversion of parameters and results works as expcted.
|
||||
"""
|
||||
|
||||
# Testing
|
||||
from unittest import TestCase
|
||||
|
||||
# Scientific (testing)
|
||||
from numpy import array
|
||||
|
||||
# Module under test
|
||||
from pyrate.plan.geometry.helpers import translate_numpy
|
||||
|
||||
|
||||
class TestTranslate(TestCase):
|
||||
"""Tests the translation helpers."""
|
||||
|
||||
COORDINATES = array([[1.0, 2.0], [3.0, -4.0], [-5.0, 6.0]])
|
||||
DIRECTIONS = array([0.0, 90.0, -90.0])
|
||||
DISTANCES = array([1.0, 100.0, 10000.0])
|
||||
|
||||
def test_translate_numpy(self) -> None: # pylint: disable=no-self-use
|
||||
"""Test that any combination of types of input are accepted."""
|
||||
|
||||
translate_numpy(TestTranslate.COORDINATES, TestTranslate.DIRECTIONS, TestTranslate.DISTANCES)
|
||||
translate_numpy(TestTranslate.COORDINATES, 90, TestTranslate.DISTANCES)
|
||||
translate_numpy(TestTranslate.COORDINATES, TestTranslate.DIRECTIONS, 100)
|
||||
translate_numpy(TestTranslate.COORDINATES, 90, 100)
|
53
pyrate/tests/plan/geometry/primitives/__init__.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""This module asserts correct runtime behaviour of the :mod:`pyrate.plan.geometry` primitives for
|
||||
locations, polygons and trajectories.
|
||||
|
||||
Quite a few tests are marked with ``@settings(max_examples=<some small count>)`` since this test suite makes
|
||||
up a very large part of the total testing time and some tests just don't justify wasting many resources on
|
||||
them due to very simple code being tested.
|
||||
"""
|
||||
|
||||
# Python standard math
|
||||
from math import isclose
|
||||
|
||||
# Typing
|
||||
from typing import Union
|
||||
|
||||
# Hypothesis testing
|
||||
from hypothesis import HealthCheck
|
||||
from hypothesis import settings
|
||||
|
||||
# Package under test
|
||||
from pyrate.plan.geometry import PolarLocation
|
||||
from pyrate.plan.geometry import PolarPolygon
|
||||
from pyrate.plan.geometry import PolarRoute
|
||||
|
||||
|
||||
#: Tests that require the generation of cartesian routes are slow since the generation of examples is slow.
|
||||
#: As polar routes, cartesian polygons and polar polygons depend on this, they are also run at reduced rate.
|
||||
slow_route_max_examples = settings(
|
||||
max_examples=int(settings().max_examples * 0.1), suppress_health_check=(HealthCheck.too_slow,)
|
||||
)
|
||||
|
||||
|
||||
#: A test that only tests very few examples since the property to be tested is rather trivial and we do not
|
||||
#: want to invest significant amounts of time into it.
|
||||
simple_property_only_few_examples = settings(
|
||||
max_examples=int(max(5, settings().max_examples * 0.001)), suppress_health_check=(HealthCheck.too_slow,)
|
||||
)
|
||||
|
||||
|
||||
def is_near_special_point(polar_location: PolarLocation, tolerance: float = 1e-6) -> bool:
|
||||
"""Checks if the given ``polar_location`` is within ``tolerance`` of the poles or +/- 180° longitude."""
|
||||
return (
|
||||
isclose(polar_location.latitude, -90, abs_tol=tolerance)
|
||||
or isclose(polar_location.latitude, +90, abs_tol=tolerance)
|
||||
or isclose(polar_location.longitude, -180, abs_tol=tolerance)
|
||||
or isclose(polar_location.longitude, +180, abs_tol=tolerance)
|
||||
)
|
||||
|
||||
|
||||
def is_any_near_special_point(
|
||||
polar_line_object: Union[PolarPolygon, PolarRoute], tolerance: float = 1e-6
|
||||
) -> bool:
|
||||
"""Checks if any point in in the given geometry ``is_near_special_point`` within the ``tolerance``."""
|
||||
return any(is_near_special_point(location, tolerance) for location in polar_line_object.locations)
|
44
pyrate/tests/plan/geometry/primitives/test_common.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Tests some general properties of geometries."""
|
||||
|
||||
# Generic testing
|
||||
from unittest import TestCase
|
||||
|
||||
# Scientific testing
|
||||
from numpy.testing import assert_array_almost_equal
|
||||
|
||||
# Hypothesis testing
|
||||
from hypothesis import given
|
||||
import hypothesis.strategies as st
|
||||
|
||||
# Package under test
|
||||
from pyrate.plan.geometry import CartesianGeometry
|
||||
|
||||
# Test helpers
|
||||
from pyrate.common.testing.strategies.geometry import cartesian_objects
|
||||
from pyrate.common.testing.strategies.geometry import geo_bearings
|
||||
|
||||
|
||||
class TestCartesianGeometries(TestCase):
|
||||
"""Asserts general properties of the cartesian geometries."""
|
||||
|
||||
@given(
|
||||
cartesian_objects(),
|
||||
geo_bearings(),
|
||||
st.floats(min_value=1.0, max_value=100_000.0),
|
||||
)
|
||||
def test_translation_is_invertible(
|
||||
self,
|
||||
original: CartesianGeometry,
|
||||
direction: float,
|
||||
distance: float,
|
||||
) -> None:
|
||||
"""Tests that translation is invertible and a valid backwards vector is returned."""
|
||||
|
||||
# translate & translate back
|
||||
translated, back_vector = original.translate(direction, distance)
|
||||
back_direction = (direction + 180) % 360
|
||||
translated_translated, back_back_vector = translated.translate(back_direction, distance)
|
||||
|
||||
# check the result
|
||||
assert_array_almost_equal(back_vector, -back_back_vector, decimal=9)
|
||||
self.assertTrue(original.equals_exact(translated_translated, tolerance=1e-9))
|
217
pyrate/tests/plan/geometry/primitives/test_geospatial.py
Normal file
@ -0,0 +1,217 @@
|
||||
"""Tests that the geometry base classes in :mod:`pyrate.plan.geometry.geospatial` work correctly."""
|
||||
|
||||
# Python standard
|
||||
from copy import copy
|
||||
from copy import deepcopy
|
||||
from json import loads
|
||||
|
||||
# Typing
|
||||
from typing import Any
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
|
||||
# Generic testing
|
||||
from unittest import TestCase
|
||||
|
||||
# Hypothesis testing
|
||||
from hypothesis import given
|
||||
from hypothesis import HealthCheck
|
||||
from hypothesis import Phase
|
||||
from hypothesis import settings
|
||||
import hypothesis.strategies as st
|
||||
|
||||
# Package under test
|
||||
from pyrate.plan.geometry import CartesianLocation
|
||||
from pyrate.plan.geometry import CartesianPolygon
|
||||
from pyrate.plan.geometry import CartesianRoute
|
||||
from pyrate.plan.geometry import Direction
|
||||
from pyrate.plan.geometry import Geospatial
|
||||
from pyrate.plan.geometry import PolarLocation
|
||||
from pyrate.plan.geometry import PolarPolygon
|
||||
from pyrate.plan.geometry import PolarRoute
|
||||
|
||||
# Hypothesis testing
|
||||
from pyrate.common.testing.strategies.geometry import geospatial_objects
|
||||
|
||||
|
||||
_CARTESIAN_LOCATION_1 = CartesianLocation(5003.0, 139.231)
|
||||
_CARTESIAN_LOCATION_2 = CartesianLocation(600.1, 139.231)
|
||||
_POLAR_LOCATION_1 = PolarLocation(65.01, -180.0)
|
||||
_POLAR_LOCATION_2 = PolarLocation(-80.3, -180.0)
|
||||
|
||||
|
||||
class TestStringRepresentations(TestCase):
|
||||
"""Makes sure that the string conversion with ``__str__`` and ``__repr__`` works."""
|
||||
|
||||
_GROUND_TRUTH: Sequence[Tuple[Geospatial, str]] = [
|
||||
(
|
||||
_CARTESIAN_LOCATION_1,
|
||||
"CartesianLocation(east=5003.0, north=139.231)",
|
||||
),
|
||||
(
|
||||
PolarLocation(65.01, -180.0),
|
||||
"PolarLocation(latitude=65.00999999999999, longitude=-180.0)",
|
||||
),
|
||||
(
|
||||
CartesianPolygon([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1]),
|
||||
"CartesianPolygon(locations=[(5003.0, 139.231), (5003.0, 139.231), (5003.0, 139.231), "
|
||||
"(5003.0, 139.231)])",
|
||||
),
|
||||
(
|
||||
PolarPolygon([_POLAR_LOCATION_1, _POLAR_LOCATION_1, _POLAR_LOCATION_1]),
|
||||
"PolarPolygon(locations=[PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
|
||||
"PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
|
||||
"PolarLocation(latitude=65.00999999999999, longitude=-180.0)])",
|
||||
),
|
||||
(
|
||||
CartesianRoute([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_2, _CARTESIAN_LOCATION_1]),
|
||||
"CartesianRoute(locations=[(5003.0, 139.231), (600.1, 139.231), (5003.0, 139.231)])",
|
||||
),
|
||||
(
|
||||
PolarRoute([_POLAR_LOCATION_1, _POLAR_LOCATION_2, _POLAR_LOCATION_1, _POLAR_LOCATION_1]),
|
||||
"PolarRoute(locations=[PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
|
||||
"PolarLocation(latitude=-80.3, longitude=-180.0), "
|
||||
"PolarLocation(latitude=65.00999999999999, longitude=-180.0), "
|
||||
"PolarLocation(latitude=65.00999999999999, longitude=-180.0)])",
|
||||
),
|
||||
]
|
||||
|
||||
def test_conversions(self) -> None:
|
||||
"""Makes sure that all given geospatial objects can be converted."""
|
||||
|
||||
for geospatial, desired_str in TestStringRepresentations._GROUND_TRUTH:
|
||||
|
||||
with self.subTest(f"{type(geospatial)}.__str__"):
|
||||
self.assertEqual(str(geospatial), desired_str)
|
||||
|
||||
with self.subTest(f"{type(geospatial)}.__repr__"):
|
||||
self.assertEqual(repr(geospatial), desired_str)
|
||||
|
||||
|
||||
class TestGeoJsonRepresentations(TestCase):
|
||||
"""Makes sure that the conversion to GeoJSON via the common property ``__geo_interface__`` works."""
|
||||
|
||||
_GROUND_TRUTH: Sequence[Tuple[Geospatial, str]] = [
|
||||
(
|
||||
_CARTESIAN_LOCATION_1,
|
||||
'{"type": "Feature", "geometry": {"type": "Point", '
|
||||
'"coordinates": [5003.0, 139.231]}, "properties": {}}',
|
||||
),
|
||||
(
|
||||
PolarLocation(65.01, -180.0),
|
||||
'{"type": "Feature", "geometry": {"type": "Point", "coordinates": [-180.0, 65.01]}, '
|
||||
'"properties": {}}',
|
||||
),
|
||||
(
|
||||
CartesianPolygon([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_1]),
|
||||
'{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": '
|
||||
"[[[5003.0, 139.231], [5003.0, 139.231], [5003.0, 139.231], [5003.0, 139.231]]]}, "
|
||||
'"properties": {}}',
|
||||
),
|
||||
(
|
||||
PolarPolygon([_POLAR_LOCATION_1, _POLAR_LOCATION_1, _POLAR_LOCATION_1]),
|
||||
'{"type": "Feature", "geometry": {"type": "Polygon", '
|
||||
'"coordinates": [[[-180.0, 65.01], [-180.0, 65.01], [-180.0, 65.01]]]}, "properties": {}}',
|
||||
),
|
||||
(
|
||||
CartesianRoute([_CARTESIAN_LOCATION_1, _CARTESIAN_LOCATION_2, _CARTESIAN_LOCATION_1]),
|
||||
'{"type": "Feature", "geometry": {"type": "LineString", "coordinates": '
|
||||
'[[5003.0, 139.231], [600.1, 139.231], [5003.0, 139.231]]}, "properties": {}}',
|
||||
),
|
||||
(
|
||||
PolarRoute([_POLAR_LOCATION_1, _POLAR_LOCATION_2, _POLAR_LOCATION_1]),
|
||||
'{"type": "Feature", "geometry": {"type": "LineString", "coordinates": '
|
||||
'[[-180.0, 65.01], [-180.0, -80.3], [-180.0, 65.01]]}, "properties": {}}',
|
||||
),
|
||||
]
|
||||
|
||||
def test_conversions(self) -> None:
|
||||
"""Makes sure that all given geospatial objects can be converted."""
|
||||
|
||||
for geospatial, desired_geojson in TestGeoJsonRepresentations._GROUND_TRUTH:
|
||||
for indent in (None, 1, 8):
|
||||
with self.subTest(f"{type(geospatial)} with indent={indent}"):
|
||||
geojson = geospatial.to_geo_json(indent=indent)
|
||||
# load as JSON get get better error messages and become whitespace independent
|
||||
self.assertDictEqual(loads(geojson), loads(desired_geojson))
|
||||
|
||||
|
||||
class TestIdentifiers(TestCase):
|
||||
"""Makes sure that identifiers are validated correctly.
|
||||
|
||||
The test is only performed on polar locations for simplicity and because validation is handled in the
|
||||
abstract common parent class :class:`pyrate.plan.geometry.Geospatial` anyway.
|
||||
"""
|
||||
|
||||
@given(st.integers(min_value=0, max_value=(2**63) - 1))
|
||||
def test_on_locations_success(self, integer: int) -> None: # pylint: disable=no-self-use
|
||||
"""Tests that valid identifiers are accepted."""
|
||||
PolarLocation(latitude=0.0, longitude=0.0, identifier=integer)
|
||||
|
||||
@given(
|
||||
st.one_of(
|
||||
st.integers(max_value=-1), # negative numbers
|
||||
st.integers(min_value=2**63), # very large numbers
|
||||
)
|
||||
)
|
||||
def test_on_locations_rejected(self, integer: int) -> None:
|
||||
"""Tests that invalid identifiers are rejected."""
|
||||
with self.assertRaises(AssertionError):
|
||||
PolarLocation(latitude=0.0, longitude=0.0, identifier=integer)
|
||||
|
||||
|
||||
class TestEqualityMethods(TestCase):
|
||||
"""Test the various equality methods."""
|
||||
|
||||
@given(geospatial_objects(stable=True))
|
||||
@settings(
|
||||
max_examples=200,
|
||||
suppress_health_check=(HealthCheck.data_too_large,),
|
||||
phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target), # Do not shrink as it takes long
|
||||
)
|
||||
def test_equality_after_translation(self, geospatial: Any) -> None:
|
||||
"""Tests that translated objects are only equal under sufficient tolerance."""
|
||||
|
||||
# We discard the second output since it differs between cartesian and polar objects
|
||||
translated, _ = geospatial.translate(direction=Direction.North, distance=0.5)
|
||||
|
||||
# Try since generated primitives might cause an exception to be thrown
|
||||
# e.g. if projected routes become length 0
|
||||
try:
|
||||
# They should not be equal
|
||||
self.assertNotEqual(geospatial, translated)
|
||||
self.assertFalse(geospatial.equals(translated))
|
||||
self.assertFalse(geospatial.equals_exact(translated, tolerance=0.0))
|
||||
if hasattr(geospatial, "equals_almost_congruent"):
|
||||
self.assertFalse(
|
||||
geospatial.equals_almost_congruent(translated, abs_tolerance=0.0, rel_tolerance=0.0)
|
||||
)
|
||||
|
||||
# They should be equal within some tolerance (the tolerance needs to be large for the polar
|
||||
# variants)
|
||||
# TODO(Someone): re-enable; see #114
|
||||
# self.assertTrue(geospatial.equals_exact(translated, tolerance=5))
|
||||
# self.assertTrue(geospatial.almost_equals(translated, decimal=-1))
|
||||
|
||||
# We do not use `equals_almost_congruent` as it is not a per-coordinate difference and might cause
|
||||
# a very large symmetric difference on very large objects
|
||||
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
class TestCopyAndDeepcopy(TestCase):
|
||||
"""Tests that all geometric objects can be deep-copied."""
|
||||
|
||||
@given(geospatial_objects())
|
||||
@settings(max_examples=500)
|
||||
def test_is_copyable(self, geospatial: Any) -> None:
|
||||
"""Tests that copies can be made and are equal to the original."""
|
||||
|
||||
# Check copy
|
||||
copied = copy(geospatial)
|
||||
self.assertEqual(geospatial, copied)
|
||||
|
||||
# Check deepcopy
|
||||
deep_copied = deepcopy(geospatial)
|
||||
self.assertEqual(geospatial, deep_copied)
|
173
pyrate/tests/plan/geometry/primitives/test_locations.py
Normal file
@ -0,0 +1,173 @@
|
||||
"""Tests that the location classes in :mod:`pyrate.plan.geometry.location` work correctly."""
|
||||
|
||||
# Python standard math
|
||||
from math import isclose
|
||||
|
||||
# Typing
|
||||
from typing import cast
|
||||
|
||||
# Generic testing
|
||||
from unittest import TestCase
|
||||
|
||||
# Geometry
|
||||
from shapely.geometry import Point
|
||||
|
||||
# Hypothesis testing
|
||||
from hypothesis import given
|
||||
from hypothesis import HealthCheck
|
||||
from hypothesis import settings
|
||||
import hypothesis.strategies as st
|
||||
|
||||
# Package under test
|
||||
from pyrate.plan.geometry import CartesianLocation
|
||||
from pyrate.plan.geometry import PolarLocation
|
||||
|
||||
# Test helpers
|
||||
from pyrate.common.testing.strategies.geometry import cartesian_locations
|
||||
from pyrate.common.testing.strategies.geometry import geo_bearings
|
||||
from pyrate.common.testing.strategies.geometry import polar_locations
|
||||
|
||||
# Local test helpers
|
||||
from . import is_near_special_point
|
||||
from . import simple_property_only_few_examples
|
||||
|
||||
|
||||
class TestLocationConversion(TestCase):
|
||||
"""Test for correct runtime behaviour in :mod:`pyrate.plan` location and shape primitives."""
|
||||
|
||||
@given(cartesian_locations(origin=polar_locations()))
|
||||
@settings(max_examples=20, suppress_health_check=(HealthCheck.data_too_large,)) # this is a slow test
|
||||
def test_projection_and_back_projection_origin_in_route(
|
||||
self, cartesian_location: CartesianLocation
|
||||
) -> None:
|
||||
"""Test the projection with an origin already being present in the geometry."""
|
||||
recreated = cartesian_location.to_polar().to_cartesian(cast(PolarLocation, cartesian_location.origin))
|
||||
self.assertTrue(recreated.equals_exact(recreated, tolerance=1e-6))
|
||||
|
||||
@given(cartesian_locations(origin=st.none()), polar_locations())
|
||||
@simple_property_only_few_examples # this only checks very simple additional logic
|
||||
def test_projection_and_back_projection_origin_given_extra(
|
||||
self, cartesian_location: CartesianLocation, origin: PolarLocation
|
||||
) -> None:
|
||||
"""Test the projection with an origin being provided."""
|
||||
recreated = cartesian_location.to_polar(origin).to_cartesian(origin)
|
||||
self.assertTrue(recreated.equals_exact(recreated, tolerance=1e-6))
|
||||
|
||||
@given(cartesian_locations(origin=st.none()))
|
||||
@simple_property_only_few_examples # this only checks very simple additional logic
|
||||
def test_projection_and_back_projection_origin_not_given(
|
||||
self, cartesian_location: CartesianLocation
|
||||
) -> None:
|
||||
"""Test the projection with no origin being given."""
|
||||
with self.assertRaises(ValueError):
|
||||
cartesian_location.to_polar()
|
||||
|
||||
@given(cartesian_locations(origin=polar_locations()), polar_locations())
|
||||
@simple_property_only_few_examples # this only checks very simple additional logic
|
||||
def test_projection_and_back_projection_origin_given_twice(
|
||||
self, cartesian_location: CartesianLocation, origin: PolarLocation
|
||||
) -> None:
|
||||
"""Test the projection with ambiguous origin being provided."""
|
||||
with self.assertRaises(ValueError):
|
||||
cartesian_location.to_polar(origin)
|
||||
|
||||
def test_distance_measuring_specific(self) -> None:
|
||||
"""Tests a specific input/output pair."""
|
||||
|
||||
location_a = PolarLocation(latitude=55.6544, longitude=139.74477)
|
||||
location_b = PolarLocation(latitude=21.4225, longitude=39.8261)
|
||||
distance = location_a.distance(location_b, approximate=False)
|
||||
self.assertAlmostEqual(distance, 8_665_850.116876071)
|
||||
|
||||
@given(
|
||||
polar_locations(),
|
||||
geo_bearings(),
|
||||
st.floats(min_value=1.0, max_value=100_000.0, allow_nan=False, allow_infinity=False),
|
||||
)
|
||||
def test_translation_is_invertible(
|
||||
self, original: PolarLocation, direction: float, distance: float
|
||||
) -> None:
|
||||
"""Tests that translation is invertible and a valid bearing is returned.
|
||||
|
||||
Warning:
|
||||
Only tests in-depth in the case where latitudes and longitudes are not near the poles.
|
||||
"""
|
||||
|
||||
# translate
|
||||
translated, back_direction = original.translate(direction, distance)
|
||||
self.assertGreaterEqual(back_direction, 0.0)
|
||||
self.assertLess(back_direction, 360.0)
|
||||
|
||||
# translate back
|
||||
translated_translated, back_back_direction = translated.translate(back_direction, distance)
|
||||
self.assertGreaterEqual(back_back_direction, 0.0)
|
||||
self.assertLess(back_back_direction, 360.0)
|
||||
|
||||
# the method seems to have problems at poles
|
||||
if not is_near_special_point(original) and not is_near_special_point(translated):
|
||||
# the method is rather rough, so we want to add larger tolerances than usual while checking
|
||||
self.assertTrue(isclose(direction, back_back_direction, abs_tol=1e-6))
|
||||
self.assertTrue(original.equals_exact(translated_translated, 1e-6))
|
||||
|
||||
@given(cartesian_locations())
|
||||
def test_from_shapely_conversion(self, cartesian_location: CartesianLocation) -> None:
|
||||
"""Test that :meth:`pyrate.plan.geometry.location.CartesianLocation.from_shapely` works."""
|
||||
|
||||
# we only want to compare the coordinates, so create a new instance without the identifier, name, etc.
|
||||
bare = CartesianLocation(cartesian_location.x, cartesian_location.y)
|
||||
bare_shapely = Point(cartesian_location.x, cartesian_location.y)
|
||||
recreated = CartesianLocation.from_shapely(bare_shapely)
|
||||
self.assertEqual(recreated, bare)
|
||||
|
||||
|
||||
class TestPolarLocationDistanceIsAMetric(TestCase):
|
||||
"""Makes sure that :meth:`~pyrate.plan.geometry.location.PolarLocation.distance` is a metric.
|
||||
|
||||
This should always succeed since we use a very stable external library for this.
|
||||
|
||||
See `Wikipedia <https://en.wikipedia.org/wiki/Metric_(mathematics)#Definition>`__ for the axioms.
|
||||
"""
|
||||
|
||||
@given(polar_locations(), polar_locations(), st.booleans())
|
||||
def test_distance_measuring_commutes_and_sanity_checks(
|
||||
self, location_a: PolarLocation, location_b: PolarLocation, approximate: bool
|
||||
) -> None:
|
||||
"""Assures flipping the sides when calculating distances does not make a significant difference."""
|
||||
|
||||
distance_1 = location_a.distance(location_b, approximate)
|
||||
distance_2 = location_b.distance(location_a, approximate)
|
||||
|
||||
# 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)
|
||||
|
||||
@given(polar_locations(), polar_locations(), polar_locations(), st.booleans())
|
||||
def test_distance_measuring_triangle_inequality(
|
||||
self,
|
||||
location_a: PolarLocation,
|
||||
location_b: PolarLocation,
|
||||
location_c: PolarLocation,
|
||||
approximate: bool,
|
||||
) -> None:
|
||||
"""Assures flipping the sides when calculating distances does not make a significant difference."""
|
||||
|
||||
distance_a_b = location_a.distance(location_b, approximate)
|
||||
distance_b_c = location_b.distance(location_c, approximate)
|
||||
distance_a_c = location_a.distance(location_c, approximate)
|
||||
|
||||
# allow for floating point errors
|
||||
abs_tolerance = 1e-6 # 1 micro meter
|
||||
self.assertGreaterEqual(distance_a_b + distance_b_c + abs_tolerance, distance_a_c)
|
||||
|
||||
@given(polar_locations(), st.booleans())
|
||||
def test_distance_measuring_to_itself_is_zero(self, location: PolarLocation, approximate: bool) -> None:
|
||||
"""Assures flipping the sides when calculating distances does not make a significant difference."""
|
||||
|
||||
distance = location.distance(location, approximate)
|
||||
|
||||
# make sure the distance is always positive and very close to zero
|
||||
self.assertGreaterEqual(distance, 0.0)
|
||||
self.assertAlmostEqual(distance, 0.0)
|
266
pyrate/tests/plan/geometry/primitives/test_polygons.py
Normal file
@ -0,0 +1,266 @@
|
||||
"""Tests that the polygon classes in :mod:`pyrate.plan.geometry.polygon` work correctly."""
|
||||
|
||||
# Python standard math
|
||||
from math import sqrt
|
||||
|
||||
# Typing
|
||||
from typing import cast
|
||||
|
||||
# Generic testing
|
||||
from unittest import TestCase
|
||||
|
||||
# Geometry
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
# Scientific
|
||||
from numpy import array
|
||||
|
||||
# Scientific testing
|
||||
from numpy.testing import assert_array_less
|
||||
|
||||
# Hypothesis testing
|
||||
from hypothesis import given
|
||||
import hypothesis.strategies as st
|
||||
|
||||
# Package under test
|
||||
from pyrate.plan.geometry import CartesianPolygon
|
||||
from pyrate.plan.geometry import LocationType
|
||||
from pyrate.plan.geometry import PolarLocation
|
||||
from pyrate.plan.geometry import PolarPolygon
|
||||
|
||||
# Test helpers
|
||||
from pyrate.common.testing.strategies.geometry import cartesian_polygons
|
||||
from pyrate.common.testing.strategies.geometry import geo_bearings
|
||||
from pyrate.common.testing.strategies.geometry import polar_locations
|
||||
from pyrate.common.testing.strategies.geometry import polar_polygons
|
||||
|
||||
# Local test helpers
|
||||
from . import is_any_near_special_point
|
||||
from . import simple_property_only_few_examples
|
||||
from . import slow_route_max_examples
|
||||
|
||||
|
||||
class TestPolarPolygons(TestCase):
|
||||
"""Asserts general properties of the polar polygons."""
|
||||
|
||||
@given(polar_polygons())
|
||||
@slow_route_max_examples
|
||||
def test_area_is_non_negative(self, polar_polygon: PolarPolygon) -> None:
|
||||
"""Tests that all areas are non-negative."""
|
||||
self.assertGreaterEqual(polar_polygon.area, 0, "areas must be non-negative")
|
||||
|
||||
@given(polar_polygons())
|
||||
@slow_route_max_examples
|
||||
def test_is_valid(self, polygon: PolarPolygon) -> None:
|
||||
"""Test that the generated polygons are valid."""
|
||||
|
||||
self.assertTrue(polygon.is_valid)
|
||||
|
||||
def test_is_not_valid(self) -> None:
|
||||
"""Test that a known invalid polygon is detected as such."""
|
||||
|
||||
location = PolarLocation(12, 23.999)
|
||||
polygon = PolarPolygon([location, location, location])
|
||||
self.assertFalse(polygon.is_valid)
|
||||
|
||||
@given(polar_polygons(), polar_locations(), st.booleans())
|
||||
@slow_route_max_examples
|
||||
def test_distance_to_vertices_is_non_negative(
|
||||
self, polar_polygon: PolarPolygon, polar_location: PolarLocation, approximate: bool
|
||||
) -> None:
|
||||
"""Tests that all distances to vertices are non-negative."""
|
||||
distance = polar_polygon.distance_to_vertices(polar_location, approximate)
|
||||
self.assertGreaterEqual(distance, 0, "distances must be non-negative")
|
||||
|
||||
@given(polar_polygons(max_vertices=50))
|
||||
@slow_route_max_examples
|
||||
def test_simplification(self, original: PolarPolygon) -> None:
|
||||
"""Checks the the area change is valid and the rough position is preserved."""
|
||||
|
||||
simplified = original.simplify(tolerance=sqrt(original.area) / 10)
|
||||
|
||||
self.assertLessEqual(len(simplified.locations), len(original.locations))
|
||||
self.assertTrue(original.almost_congruent(simplified, rel_tolerance=0.3))
|
||||
|
||||
@given(polar_polygons())
|
||||
@slow_route_max_examples
|
||||
def test_simplification_artificial(self, original: PolarPolygon) -> None:
|
||||
"""This duplicates the first point and looks whether it is removed."""
|
||||
|
||||
locations = original.locations
|
||||
original.locations = [locations[0]] + locations
|
||||
|
||||
simplified = original.simplify(tolerance=sqrt(original.area) / 1000)
|
||||
|
||||
# strictly less, as opposed to test_simplification()
|
||||
self.assertLess(len(simplified.locations), len(original.locations))
|
||||
self.assertTrue(original.almost_congruent(simplified, rel_tolerance=0.05))
|
||||
|
||||
@given(polar_polygons())
|
||||
@simple_property_only_few_examples # this only checks the call signatures so no need for many examples
|
||||
def test_numpy_conversion_invertible(self, polar_polygon: PolarPolygon) -> None:
|
||||
"""Tests that the polygon conversion can be inverted."""
|
||||
recreated = PolarPolygon.from_numpy(
|
||||
polar_polygon.to_numpy(),
|
||||
name=polar_polygon.name,
|
||||
location_type=polar_polygon.location_type,
|
||||
identifier=polar_polygon.identifier,
|
||||
)
|
||||
self.assertEqual(polar_polygon, recreated)
|
||||
|
||||
@given(
|
||||
st.sampled_from(
|
||||
[
|
||||
PolarPolygon(
|
||||
locations=[
|
||||
PolarLocation(latitude=-76.40057132099628, longitude=-171.92454675519284),
|
||||
PolarLocation(latitude=-76.40057132099628, longitude=-171.92454675519284),
|
||||
PolarLocation(latitude=-76.40057132099628, longitude=-171.92454675519284),
|
||||
],
|
||||
name="K",
|
||||
),
|
||||
PolarPolygon(
|
||||
locations=[
|
||||
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
||||
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
||||
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
||||
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
||||
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
||||
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
||||
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
||||
],
|
||||
location_type=LocationType.TESTING,
|
||||
name="_1",
|
||||
),
|
||||
PolarPolygon(
|
||||
locations=[
|
||||
PolarLocation(latitude=0.0, longitude=0.029771743643124182),
|
||||
PolarLocation(latitude=0.0, longitude=0.029771743643124182),
|
||||
PolarLocation(latitude=0.0, longitude=0.029771743643124182),
|
||||
PolarLocation(latitude=0.0, longitude=0.029771743643124182),
|
||||
],
|
||||
name="",
|
||||
),
|
||||
]
|
||||
),
|
||||
geo_bearings(),
|
||||
st.floats(min_value=1.0, max_value=100_000.0, allow_nan=False, allow_infinity=False),
|
||||
)
|
||||
def test_translation_is_invertible(
|
||||
self, original: PolarPolygon, direction: float, distance: float
|
||||
) -> None:
|
||||
"""Tests that translation is invertible and a valid bearing is returned.
|
||||
|
||||
Warning:
|
||||
Only tests in-depth in the case where latitudes and longitudes are not near the poles.
|
||||
Since the tests are quite flaky due to the underling library, we only test specific polygons.
|
||||
"""
|
||||
# translate
|
||||
translated, back_direction = original.translate(direction, distance)
|
||||
assert_array_less(0.0 - 1e-12, back_direction)
|
||||
assert_array_less(back_direction, 360.0 + 1e-12)
|
||||
|
||||
# translate back
|
||||
translated_translated, back_back_direction = translated.translate(back_direction[0], distance)
|
||||
assert_array_less(0.0 - 1e-12, back_back_direction)
|
||||
assert_array_less(back_back_direction, 360.0 + 1e-12)
|
||||
|
||||
# the method seems to have problems at poles
|
||||
if not is_any_near_special_point(original) and not is_any_near_special_point(translated):
|
||||
# the method is rather rough, so we want to add larger tolerances than usual while checking
|
||||
self.assertAlmostEqual(direction, back_back_direction[0], delta=0.1)
|
||||
self.assertTrue(original.equals_exact(translated_translated, tolerance=0.1))
|
||||
|
||||
def test_non_finite_from_numpy_raises(self) -> None:
|
||||
"""Tests that invalid parameter to :meth:`~PolarPolygon.from_numpy` warn about it."""
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
PolarPolygon.from_numpy(array([(1, 2), (2, 4), (4, float("NaN"))]))
|
||||
with self.assertRaises(AssertionError):
|
||||
PolarPolygon.from_numpy(array([(float("Inf"), 2), (2, 4), (4, 1)]))
|
||||
with self.assertRaises(AssertionError):
|
||||
PolarPolygon.from_numpy(array([(1, 2), (2, float("-Inf")), (4, 4)]))
|
||||
|
||||
|
||||
class TestCartesianPolygons(TestCase):
|
||||
"""Asserts general properties of the cartesian polygons."""
|
||||
|
||||
@given(cartesian_polygons())
|
||||
@simple_property_only_few_examples # this only checks the call signatures so no need for many examples
|
||||
def test_numpy_conversion_invertible(self, cartesian_polygon: CartesianPolygon) -> None:
|
||||
"""Tests that the polygon conversion can be inverted."""
|
||||
recreated = CartesianPolygon.from_numpy(
|
||||
cartesian_polygon.to_numpy(),
|
||||
origin=cartesian_polygon.origin,
|
||||
name=cartesian_polygon.name,
|
||||
location_type=cartesian_polygon.location_type,
|
||||
identifier=cartesian_polygon.identifier,
|
||||
)
|
||||
|
||||
self.assertEqual(cartesian_polygon, recreated)
|
||||
|
||||
@given(cartesian_polygons(origin=polar_locations()))
|
||||
@slow_route_max_examples
|
||||
def test_projection_and_back_projection_origin_in_route(
|
||||
self, cartesian_polygon: CartesianPolygon
|
||||
) -> None:
|
||||
"""Test the projection with an origin already being present in the geometry."""
|
||||
recreated = cartesian_polygon.to_polar().to_cartesian(cast(PolarLocation, cartesian_polygon.origin))
|
||||
self.assertTrue(recreated.equals_exact(recreated, tolerance=1e-6))
|
||||
|
||||
@given(cartesian_polygons(origin=st.none()), polar_locations())
|
||||
@simple_property_only_few_examples # this only checks very simple additional logic
|
||||
def test_projection_and_back_projection_origin_given_extra(
|
||||
self, cartesian_polygon: CartesianPolygon, origin: PolarLocation
|
||||
) -> None:
|
||||
"""Test the projection with an origin being provided."""
|
||||
recreated = cartesian_polygon.to_polar(origin).to_cartesian(origin)
|
||||
self.assertTrue(recreated.equals_exact(recreated, tolerance=1e-6))
|
||||
|
||||
@given(cartesian_polygons(origin=st.none()))
|
||||
@simple_property_only_few_examples # this only checks very simple additional logic
|
||||
def test_projection_and_back_projection_origin_not_given(
|
||||
self, cartesian_polygon: CartesianPolygon
|
||||
) -> None:
|
||||
"""Test the projection with no origin being given."""
|
||||
with self.assertRaises(ValueError):
|
||||
cartesian_polygon.to_polar()
|
||||
|
||||
@given(cartesian_polygons(origin=polar_locations()), polar_locations())
|
||||
@simple_property_only_few_examples # this only checks very simple additional logic
|
||||
def test_projection_and_back_projection_origin_given_twice(
|
||||
self, cartesian_polygon: CartesianPolygon, origin: PolarLocation
|
||||
) -> None:
|
||||
"""Test the projection with ambiguous origin being provided."""
|
||||
with self.assertRaises(ValueError):
|
||||
cartesian_polygon.to_polar(origin)
|
||||
|
||||
@given(cartesian_polygons())
|
||||
@slow_route_max_examples
|
||||
def test_locations_property_attributes(self, cartesian_polygon: CartesianPolygon) -> None:
|
||||
"""Test that all contained locations share the same attributes."""
|
||||
for location in cartesian_polygon.locations:
|
||||
self.assertEqual(location.location_type, cartesian_polygon.location_type)
|
||||
self.assertEqual(location.name, cartesian_polygon.name)
|
||||
self.assertEqual(location.identifier, cartesian_polygon.identifier)
|
||||
self.assertEqual(location.origin, cartesian_polygon.origin)
|
||||
|
||||
@given(cartesian_polygons())
|
||||
@slow_route_max_examples
|
||||
def test_from_shapely_conversion(self, cartesian_polygon: CartesianPolygon) -> None:
|
||||
"""Test that :meth:`pyrate.plan.geometry.polygon.CartesianPolygon.from_shapely` works."""
|
||||
# we only want to compare the coordinates, so create a new instance without the identifier, name, etc.
|
||||
bare = CartesianPolygon.from_numpy(cartesian_polygon.to_numpy())
|
||||
bare_shapely = Polygon(cartesian_polygon.to_numpy())
|
||||
recreated = CartesianPolygon.from_shapely(bare_shapely)
|
||||
self.assertEqual(recreated, bare)
|
||||
|
||||
def test_non_finite_from_numpy_raises(self) -> None:
|
||||
"""Tests that invalid parameter to :meth:`~CartesianPolygon.from_numpy` warn about it."""
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
CartesianPolygon.from_numpy(array([(1, 2), (2, 4), (4, float("NaN"))]))
|
||||
with self.assertRaises(AssertionError):
|
||||
CartesianPolygon.from_numpy(array([(float("Inf"), 2), (2, 4), (4, 1)]))
|
||||
with self.assertRaises(AssertionError):
|
||||
CartesianPolygon.from_numpy(array([(1, 2), (2, float("-Inf")), (4, 4)]))
|
266
pyrate/tests/plan/geometry/primitives/test_routes.py
Normal file
@ -0,0 +1,266 @@
|
||||
"""Tests that the route classes in :mod:`pyrate.plan.geometry.route` work correctly."""
|
||||
|
||||
# Typing
|
||||
from typing import cast
|
||||
|
||||
# Generic testing
|
||||
from unittest import TestCase
|
||||
|
||||
# Geometry
|
||||
from shapely.geometry import LineString
|
||||
|
||||
# Scientific
|
||||
from numpy import array
|
||||
|
||||
# Scientific testing
|
||||
from numpy.testing import assert_array_less
|
||||
|
||||
# Hypothesis testing
|
||||
from hypothesis import given
|
||||
import hypothesis.strategies as st
|
||||
|
||||
# Package under test
|
||||
from pyrate.plan.geometry import CartesianLocation
|
||||
from pyrate.plan.geometry import CartesianRoute
|
||||
from pyrate.plan.geometry import LocationType
|
||||
from pyrate.plan.geometry import PolarLocation
|
||||
from pyrate.plan.geometry import PolarRoute
|
||||
|
||||
# Test helpers
|
||||
from pyrate.common.testing.strategies.geometry import cartesian_routes
|
||||
from pyrate.common.testing.strategies.geometry import geo_bearings
|
||||
from pyrate.common.testing.strategies.geometry import polar_locations
|
||||
from pyrate.common.testing.strategies.geometry import polar_routes
|
||||
|
||||
# Local test helpers
|
||||
from . import is_any_near_special_point
|
||||
from . import simple_property_only_few_examples
|
||||
from . import slow_route_max_examples
|
||||
|
||||
|
||||
class TestPolarRoutes(TestCase):
|
||||
"""Asserts general properties of the polar routes."""
|
||||
|
||||
@given(polar_routes())
|
||||
@simple_property_only_few_examples # this only checks the call signatures so no need for many examples
|
||||
def test_numpy_conversion_invertible(self, polar_route: PolarRoute) -> None:
|
||||
"""Tests that the route conversion can be inverted."""
|
||||
recreated = PolarRoute.from_numpy(
|
||||
polar_route.to_numpy(),
|
||||
name=polar_route.name,
|
||||
location_type=polar_route.location_type,
|
||||
identifier=polar_route.identifier,
|
||||
)
|
||||
self.assertEqual(polar_route, recreated)
|
||||
|
||||
@given(polar_routes(), polar_locations(), st.booleans())
|
||||
@slow_route_max_examples
|
||||
def test_distance_to_vertices_is_non_negative(
|
||||
self, polar_route: PolarRoute, polar_location: PolarLocation, approximate: bool
|
||||
) -> None:
|
||||
"""Tests that all distances to vertices are non-negative."""
|
||||
distance = polar_route.distance_to_vertices(polar_location, approximate)
|
||||
self.assertGreaterEqual(distance, 0, "distances must be non-negative")
|
||||
|
||||
@given(polar_routes())
|
||||
@slow_route_max_examples
|
||||
def test_length_is_non_negative(self, polar_route: PolarRoute) -> None:
|
||||
"""Tests that the length of a route is always non-negative."""
|
||||
self.assertGreaterEqual(polar_route.length(), 0, "lengths must be non-negative")
|
||||
|
||||
@given(polar_routes(min_vertices=3, max_vertices=3))
|
||||
@slow_route_max_examples
|
||||
def test_length_values(self, polar_route: PolarRoute) -> None:
|
||||
"""Tests that the length of a route with three locations is plausible."""
|
||||
location_a, location_b, location_c = polar_route.locations
|
||||
distance = location_a.distance(location_b) + location_b.distance(location_c)
|
||||
self.assertAlmostEqual(polar_route.length(), distance, msg="lengths must be non-negative")
|
||||
|
||||
@given(
|
||||
st.sampled_from(
|
||||
[
|
||||
PolarRoute(
|
||||
locations=[
|
||||
PolarLocation(latitude=-76.40057132099628, longitude=-171.92454675519284),
|
||||
PolarLocation(latitude=-76, longitude=-171),
|
||||
],
|
||||
name="K",
|
||||
),
|
||||
PolarRoute(
|
||||
locations=[
|
||||
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
||||
PolarLocation(latitude=-33, longitude=89),
|
||||
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
||||
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
||||
PolarLocation(latitude=-33.68964326163993, longitude=89.95053943144632),
|
||||
],
|
||||
location_type=LocationType.TESTING,
|
||||
name="_1",
|
||||
),
|
||||
PolarRoute(
|
||||
locations=[
|
||||
PolarLocation(latitude=0.0, longitude=0.029771743643124182),
|
||||
PolarLocation(latitude=0.0, longitude=0.0),
|
||||
PolarLocation(latitude=0.0, longitude=0.029771743643124182),
|
||||
PolarLocation(latitude=0.0, longitude=0.029771743643124182),
|
||||
],
|
||||
name="",
|
||||
),
|
||||
]
|
||||
),
|
||||
geo_bearings(),
|
||||
st.floats(min_value=1.0, max_value=100_000.0),
|
||||
)
|
||||
def test_translation_is_invertible(self, original: PolarRoute, direction: float, distance: float) -> None:
|
||||
"""Tests that translation is invertible and a valid bearing is returned.
|
||||
|
||||
Warning:
|
||||
Only tests in-depth in the case where latitudes and longitudes are not near the poles.
|
||||
Since the tests are quite flaky due to the underling library, we only test specific routes.
|
||||
"""
|
||||
|
||||
# translate
|
||||
translated, back_direction = original.translate(direction, distance)
|
||||
assert_array_less(0.0 - 1e-12, back_direction)
|
||||
assert_array_less(back_direction, 360.0 + 1e-12)
|
||||
|
||||
# translate back
|
||||
translated_translated, back_back_direction = translated.translate(back_direction[0], distance)
|
||||
assert_array_less(0.0 - 1e-12, back_back_direction)
|
||||
assert_array_less(back_back_direction, 360.0 + 1e-12)
|
||||
|
||||
# the method seems to have problems at poles
|
||||
if not is_any_near_special_point(original) and not is_any_near_special_point(translated):
|
||||
# the method is rather rough, so we want to add larger tolerances than usual while checking
|
||||
self.assertAlmostEqual(direction, back_back_direction[0], delta=0.1)
|
||||
self.assertTrue(original.equals_exact(translated_translated, tolerance=1e-2 * distance))
|
||||
|
||||
def test_zero_length_route(self) -> None:
|
||||
"""Test that :meth:`pyrate.plan.geometry.route.PolarRoute.__init__` raises an exception."""
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
PolarRoute(locations=[PolarLocation(0.0, 0.0)] * 2)
|
||||
|
||||
def test_non_finite_from_numpy_raises(self) -> None:
|
||||
"""Tests that invalid parameter to :meth:`~PolarRoute.from_numpy` warn about it."""
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
PolarRoute.from_numpy(array([(1, 2), (2, 4), (4, float("NaN"))]))
|
||||
with self.assertRaises(AssertionError):
|
||||
PolarRoute.from_numpy(array([(float("Inf"), 2), (2, 4), (4, 1)]))
|
||||
with self.assertRaises(AssertionError):
|
||||
PolarRoute.from_numpy(array([(1, 2), (2, float("-Inf")), (4, 4)]))
|
||||
|
||||
|
||||
class TestCartesianRoutes(TestCase):
|
||||
"""Asserts general properties of the cartesian routes."""
|
||||
|
||||
@given(cartesian_routes())
|
||||
@simple_property_only_few_examples # this only checks the call signatures so no need for many examples
|
||||
def test_numpy_conversion_invertible(self, cartesian_route: CartesianRoute) -> None:
|
||||
"""Tests that the polygon conversion can be inverted."""
|
||||
|
||||
recreated = CartesianRoute.from_numpy(
|
||||
cartesian_route.to_numpy(),
|
||||
origin=cartesian_route.origin,
|
||||
name=cartesian_route.name,
|
||||
location_type=cartesian_route.location_type,
|
||||
identifier=cartesian_route.identifier,
|
||||
)
|
||||
|
||||
self.assertEqual(cartesian_route, recreated)
|
||||
|
||||
@given(cartesian_routes(origin=polar_locations()))
|
||||
@slow_route_max_examples
|
||||
def test_projection_and_back_projection_origin_in_route(self, cartesian_route: CartesianRoute) -> None:
|
||||
"""Test the projection with an origin already being present in the geometry."""
|
||||
|
||||
# Try since generated primitives might cause an exception to be thrown
|
||||
# e.g. if projected routes become length 0
|
||||
try:
|
||||
recreated = cartesian_route.to_polar().to_cartesian(cast(PolarLocation, cartesian_route.origin))
|
||||
self.assertTrue(recreated.equals_exact(recreated, tolerance=1e-6))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@given(cartesian_routes(origin=st.none()), polar_locations())
|
||||
@simple_property_only_few_examples # this only checks very simple additional logic
|
||||
def test_projection_and_back_projection_origin_given_extra(
|
||||
self, cartesian_route: CartesianRoute, origin: PolarLocation
|
||||
) -> None:
|
||||
"""Test the projection with an origin being provided."""
|
||||
|
||||
# Try since generated primitives might cause an exception to be thrown
|
||||
# e.g. if projected routes become length 0
|
||||
try:
|
||||
recreated = cartesian_route.to_polar(origin).to_cartesian(origin)
|
||||
self.assertTrue(recreated.equals_exact(recreated, tolerance=1e-6))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@given(cartesian_routes(origin=st.none()))
|
||||
@simple_property_only_few_examples # this only checks very simple additional logic
|
||||
def test_projection_and_back_projection_origin_not_given(self, cartesian_route: CartesianRoute) -> None:
|
||||
"""Test the projection with no origin being given."""
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
cartesian_route.to_polar()
|
||||
|
||||
@given(cartesian_routes(origin=polar_locations()), polar_locations())
|
||||
@simple_property_only_few_examples # this only checks very simple additional logic
|
||||
def test_projection_and_back_projection_origin_given_twice(
|
||||
self, cartesian_route: CartesianRoute, origin: PolarLocation
|
||||
) -> None:
|
||||
"""Test the projection with ambiguous origin being provided."""
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
cartesian_route.to_polar(origin)
|
||||
|
||||
@given(cartesian_routes())
|
||||
@slow_route_max_examples
|
||||
def test_locations_property_attributes(self, cartesian_route: CartesianRoute) -> None:
|
||||
"""Test that all contained locations share the same attributes."""
|
||||
|
||||
for location in cartesian_route.locations:
|
||||
self.assertEqual(location.location_type, cartesian_route.location_type)
|
||||
self.assertEqual(location.name, cartesian_route.name)
|
||||
self.assertEqual(location.identifier, cartesian_route.identifier)
|
||||
self.assertEqual(location.origin, cartesian_route.origin)
|
||||
|
||||
@given(cartesian_routes())
|
||||
@slow_route_max_examples
|
||||
def test_from_shapely_conversion(self, cartesian_route: CartesianRoute) -> None:
|
||||
"""Test that :meth:`pyrate.plan.geometry.route.CartesianRoute.from_shapely` works."""
|
||||
|
||||
# we only want to compare the coordinates, so create a new instance without the identifier, name, etc.
|
||||
bare = CartesianRoute.from_numpy(cartesian_route.to_numpy())
|
||||
bare_shapely = LineString(cartesian_route.to_numpy())
|
||||
recreated = CartesianRoute.from_shapely(bare_shapely)
|
||||
self.assertEqual(recreated, bare)
|
||||
|
||||
@given(cartesian_routes())
|
||||
@simple_property_only_few_examples # this only checks very simple additional logic
|
||||
def test_locations_property(self, cartesian_route: CartesianRoute) -> None:
|
||||
"""Test that :meth:`pyrate.plan.geometry.route.CartesianRoute.locations` works."""
|
||||
|
||||
locations = cartesian_route.locations
|
||||
self.assertEqual(len(cartesian_route.coords), len(locations))
|
||||
for i, (x, y) in enumerate(cartesian_route.coords):
|
||||
self.assertEqual(x, locations[i].x)
|
||||
self.assertEqual(y, locations[i].y)
|
||||
|
||||
def test_zero_length_route(self) -> None:
|
||||
"""Test that :meth:`pyrate.plan.geometry.route.CartesianRoute.__init__` raises an exception."""
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
CartesianRoute(locations=[CartesianLocation(0.0, 0.0)] * 2)
|
||||
|
||||
def test_non_finite_from_numpy_raises(self) -> None:
|
||||
"""Tests that invalid parameter to :meth:`~CartesianRoute.from_numpy` warn about it."""
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
CartesianRoute.from_numpy(array([(1, 2), (2, 4), (4, float("NaN"))]))
|
||||
with self.assertRaises(AssertionError):
|
||||
CartesianRoute.from_numpy(array([(float("Inf"), 2), (2, 4), (4, 1)]))
|
||||
with self.assertRaises(AssertionError):
|
||||
CartesianRoute.from_numpy(array([(1, 2), (2, float("-Inf")), (4, 4)]))
|
0
pyrate/tests/plan/graph/__init__.py
Normal file
0
pyrate/tests/plan/graph/generate/__init__.py
Normal file
@ -0,0 +1,376 @@
|
||||
OFF
|
||||
122 252 0
|
||||
0 0.5257311121191336 0.85065080835204
|
||||
0 0.5257311121191336 -0.85065080835204
|
||||
0 -0.5257311121191336 0.85065080835204
|
||||
0 -0.5257311121191336 -0.85065080835204
|
||||
0.5257311121191336 0.85065080835204 0
|
||||
0.5257311121191336 -0.85065080835204 0
|
||||
-0.5257311121191336 0.85065080835204 0
|
||||
-0.5257311121191336 -0.85065080835204 0
|
||||
0.85065080835204 0 0.5257311121191336
|
||||
0.85065080835204 0 -0.5257311121191336
|
||||
-0.85065080835204 0 0.5257311121191336
|
||||
-0.85065080835204 0 -0.5257311121191336
|
||||
2.175242402100701e-16 -1.643460219210441e-32 1
|
||||
0.3090169943749475 0.8090169943749472 0.5000000000000002
|
||||
-0.3090169943749475 0.8090169943749472 0.5000000000000002
|
||||
0.4999999999999999 0.3090169943749474 0.8090169943749472
|
||||
-0.5000000000000001 0.3090169943749475 0.8090169943749472
|
||||
2.175242402100701e-16 1.643460219210441e-32 -1
|
||||
0.3090169943749475 0.8090169943749472 -0.5000000000000002
|
||||
-0.3090169943749475 0.8090169943749472 -0.5000000000000002
|
||||
0.5 0.3090169943749473 -0.8090169943749475
|
||||
-0.4999999999999999 0.3090169943749474 -0.8090169943749472
|
||||
0.3090169943749473 -0.8090169943749475 0.5
|
||||
-0.3090169943749475 -0.8090169943749472 0.5000000000000002
|
||||
0.5 -0.3090169943749473 0.8090169943749475
|
||||
-0.4999999999999999 -0.3090169943749474 0.8090169943749472
|
||||
0.3090169943749473 -0.8090169943749475 -0.5
|
||||
-0.3090169943749473 -0.8090169943749475 -0.5
|
||||
0.5 -0.3090169943749472 -0.8090169943749475
|
||||
-0.5000000000000001 -0.3090169943749475 -0.8090169943749472
|
||||
0 1 4.350484804201401e-17
|
||||
0.8090169943749475 0.5 0.3090169943749472
|
||||
0.8090169943749472 0.4999999999999999 -0.3090169943749473
|
||||
0 -1 -4.350484804201401e-17
|
||||
0.8090169943749472 -0.4999999999999999 0.3090169943749473
|
||||
0.8090169943749475 -0.5 -0.3090169943749472
|
||||
-0.8090169943749472 0.4999999999999999 0.3090169943749473
|
||||
-0.8090169943749472 0.4999999999999999 -0.3090169943749475
|
||||
-0.8090169943749475 -0.5 0.3090169943749472
|
||||
-0.8090169943749472 -0.4999999999999999 -0.3090169943749473
|
||||
1 2.175242402100701e-16 -1.643460219210441e-32
|
||||
-1 -2.175242402100701e-16 -1.643460219210441e-32
|
||||
-0.1803319730021167 0.289241011911498 -0.9401170227910867
|
||||
-0.35682208977309 -3.124513936890529e-17 -0.9341723589627157
|
||||
-0.1803319730021166 -0.2892410119114981 -0.9401170227910867
|
||||
-0.6483337612153338 -5.436311068297173e-17 -0.7613562464893677
|
||||
-0.1803319730021166 0.2892410119114981 0.9401170227910867
|
||||
-0.35682208977309 3.09531117213564e-17 0.9341723589627158
|
||||
-0.6483337612153338 5.402340711901317e-17 0.7613562464893677
|
||||
-0.1803319730021167 -0.289241011911498 0.9401170227910867
|
||||
0.291783261575753 -0.5810242734872509 0.7597850497889703
|
||||
0.5773502691896258 -0.5773502691896256 0.5773502691896258
|
||||
0.5810242734872511 -0.7597850497889701 0.291783261575753
|
||||
0.7597850497889702 -0.291783261575753 0.5810242734872511
|
||||
-0.291783261575753 -0.5810242734872509 -0.7597850497889703
|
||||
-0.5773502691896258 -0.5773502691896256 -0.5773502691896258
|
||||
-0.5810242734872511 -0.7597850497889701 -0.291783261575753
|
||||
-0.7597850497889702 -0.291783261575753 -0.5810242734872511
|
||||
-2.313323858849861e-18 0.7613562464893674 -0.6483337612153339
|
||||
3.124513936890529e-17 0.9341723589627158 -0.3568220897730901
|
||||
-0.2892410119114981 0.9401170227910867 -0.1803319730021165
|
||||
0.2892410119114981 0.9401170227910867 -0.1803319730021165
|
||||
-2.313323858849861e-18 -0.7613562464893674 0.6483337612153339
|
||||
3.124513936890529e-17 -0.9341723589627158 0.3568220897730901
|
||||
-0.2892410119114981 -0.9401170227910867 0.1803319730021165
|
||||
0.2892410119114981 -0.9401170227910867 0.1803319730021165
|
||||
0.2917832615757529 -0.5810242734872509 -0.7597850497889704
|
||||
0.5773502691896258 -0.5773502691896257 -0.5773502691896258
|
||||
0.7597850497889701 -0.2917832615757531 -0.5810242734872512
|
||||
0.5810242734872511 -0.7597850497889701 -0.291783261575753
|
||||
2.313323858849861e-18 0.7613562464893674 0.6483337612153339
|
||||
-3.124513936890529e-17 0.9341723589627158 0.3568220897730901
|
||||
0.2892410119114981 0.9401170227910867 0.1803319730021165
|
||||
-0.2892410119114981 0.9401170227910867 0.1803319730021165
|
||||
-0.2917832615757529 -0.5810242734872509 0.7597850497889704
|
||||
-0.5773502691896258 -0.5773502691896257 0.5773502691896258
|
||||
-0.7597850497889701 -0.2917832615757531 0.5810242734872512
|
||||
-0.5810242734872511 -0.7597850497889701 0.291783261575753
|
||||
2.313323858849861e-18 -0.7613562464893674 -0.6483337612153339
|
||||
-3.124513936890529e-17 -0.9341723589627158 -0.3568220897730901
|
||||
0.2892410119114981 -0.9401170227910867 -0.1803319730021165
|
||||
-0.2892410119114981 -0.9401170227910867 -0.1803319730021165
|
||||
0.1803319730021167 0.289241011911498 0.9401170227910867
|
||||
0.35682208977309 -3.124513936890529e-17 0.9341723589627157
|
||||
0.1803319730021166 -0.2892410119114981 0.9401170227910867
|
||||
0.6483337612153338 -5.436311068297173e-17 0.7613562464893677
|
||||
0.2917832615757529 0.5810242734872509 0.7597850497889704
|
||||
0.5773502691896258 0.5773502691896257 0.5773502691896258
|
||||
0.7597850497889701 0.2917832615757531 0.5810242734872512
|
||||
0.5810242734872511 0.7597850497889701 0.291783261575753
|
||||
0.7613562464893677 -0.6483337612153338 5.436311068297173e-17
|
||||
0.9341723589627157 -0.35682208977309 3.124513936890529e-17
|
||||
0.9401170227910867 -0.1803319730021167 -0.289241011911498
|
||||
0.9401170227910867 -0.1803319730021166 0.2892410119114981
|
||||
0.291783261575753 0.5810242734872509 -0.7597850497889703
|
||||
0.5773502691896258 0.5773502691896256 -0.5773502691896258
|
||||
0.5810242734872511 0.7597850497889701 -0.291783261575753
|
||||
0.7597850497889702 0.291783261575753 -0.5810242734872511
|
||||
0.1803319730021166 0.2892410119114981 -0.9401170227910867
|
||||
0.35682208977309 3.09531117213564e-17 -0.9341723589627158
|
||||
0.6483337612153338 5.402340711901317e-17 -0.7613562464893677
|
||||
0.1803319730021167 -0.289241011911498 -0.9401170227910867
|
||||
0.7613562464893677 0.6483337612153338 -5.436311068297173e-17
|
||||
0.9341723589627157 0.35682208977309 -3.124513936890529e-17
|
||||
0.9401170227910867 0.1803319730021167 0.289241011911498
|
||||
0.9401170227910867 0.1803319730021166 -0.2892410119114981
|
||||
-0.291783261575753 0.5810242734872509 0.7597850497889703
|
||||
-0.5773502691896258 0.5773502691896256 0.5773502691896258
|
||||
-0.5810242734872511 0.7597850497889701 0.291783261575753
|
||||
-0.7597850497889702 0.291783261575753 0.5810242734872511
|
||||
-0.7613562464893677 0.6483337612153338 5.436311068297173e-17
|
||||
-0.9341723589627157 0.35682208977309 3.124513936890529e-17
|
||||
-0.9401170227910867 0.1803319730021167 -0.289241011911498
|
||||
-0.9401170227910867 0.1803319730021166 0.2892410119114981
|
||||
-0.2917832615757529 0.5810242734872509 -0.7597850497889704
|
||||
-0.5773502691896258 0.5773502691896257 -0.5773502691896258
|
||||
-0.7597850497889701 0.2917832615757531 -0.5810242734872512
|
||||
-0.5810242734872511 0.7597850497889701 -0.291783261575753
|
||||
-0.7613562464893677 -0.6483337612153338 -5.436311068297173e-17
|
||||
-0.9341723589627157 -0.35682208977309 -3.124513936890529e-17
|
||||
-0.9401170227910867 -0.1803319730021167 0.289241011911498
|
||||
-0.9401170227910867 -0.1803319730021166 -0.2892410119114981
|
||||
3 42 1 98 0.90196 0.45098 0.00000
|
||||
3 21 114 42 0.90196 0.45098 0.00000
|
||||
3 42 43 21 0.90196 0.45098 0.00000
|
||||
3 43 42 17 0.90196 0.45098 0.00000
|
||||
3 17 44 43 0.90196 0.45098 0.00000
|
||||
3 44 17 101 0.90196 0.45098 0.00000
|
||||
3 45 21 43 0.90196 0.45098 0.00000
|
||||
3 43 29 45 0.90196 0.45098 0.00000
|
||||
3 29 43 44 0.90196 0.45098 0.00000
|
||||
3 54 44 3 0.90196 0.45098 0.00000
|
||||
3 11 116 45 0.90196 0.45098 0.00000
|
||||
3 57 45 29 0.90196 0.45098 0.00000
|
||||
3 46 0 106 0.90196 0.45098 0.00000
|
||||
3 12 82 46 0.90196 0.45098 0.00000
|
||||
3 46 47 12 0.90196 0.45098 0.00000
|
||||
3 47 46 16 0.90196 0.45098 0.00000
|
||||
3 16 48 47 0.90196 0.45098 0.00000
|
||||
3 48 16 109 0.90196 0.45098 0.00000
|
||||
3 49 12 47 0.90196 0.45098 0.00000
|
||||
3 47 25 49 0.90196 0.45098 0.00000
|
||||
3 25 47 48 0.90196 0.45098 0.00000
|
||||
3 76 48 10 0.90196 0.45098 0.00000
|
||||
3 2 84 49 0.90196 0.45098 0.00000
|
||||
3 74 49 25 0.90196 0.45098 0.00000
|
||||
3 50 2 62 0.90196 0.45098 0.00000
|
||||
3 24 84 50 0.90196 0.45098 0.00000
|
||||
3 50 51 24 0.90196 0.45098 0.00000
|
||||
3 51 50 22 0.90196 0.45098 0.00000
|
||||
3 22 52 51 0.90196 0.45098 0.00000
|
||||
3 52 22 65 0.90196 0.45098 0.00000
|
||||
3 53 24 51 0.90196 0.45098 0.00000
|
||||
3 51 34 53 0.90196 0.45098 0.00000
|
||||
3 34 51 52 0.90196 0.45098 0.00000
|
||||
3 90 52 5 0.90196 0.45098 0.00000
|
||||
3 8 85 53 0.90196 0.45098 0.00000
|
||||
3 93 53 34 0.90196 0.45098 0.00000
|
||||
3 54 3 78 0.90196 0.45098 0.00000
|
||||
3 29 44 54 0.90196 0.45098 0.00000
|
||||
3 54 55 29 0.90196 0.45098 0.00000
|
||||
3 55 54 27 0.90196 0.45098 0.00000
|
||||
3 27 56 55 0.90196 0.45098 0.00000
|
||||
3 56 27 81 0.90196 0.45098 0.00000
|
||||
3 57 29 55 0.90196 0.45098 0.00000
|
||||
3 55 39 57 0.90196 0.45098 0.00000
|
||||
3 39 55 56 0.90196 0.45098 0.00000
|
||||
3 118 56 7 0.90196 0.45098 0.00000
|
||||
3 11 45 57 0.90196 0.45098 0.00000
|
||||
3 121 57 39 0.90196 0.45098 0.00000
|
||||
3 58 1 114 0.90196 0.45098 0.00000
|
||||
3 18 94 58 0.90196 0.45098 0.00000
|
||||
3 58 59 18 0.90196 0.45098 0.00000
|
||||
3 59 58 19 0.90196 0.45098 0.00000
|
||||
3 19 60 59 0.90196 0.45098 0.00000
|
||||
3 60 19 117 0.90196 0.45098 0.00000
|
||||
3 61 18 59 0.90196 0.45098 0.00000
|
||||
3 59 30 61 0.90196 0.45098 0.00000
|
||||
3 30 59 60 0.90196 0.45098 0.00000
|
||||
3 73 60 6 0.90196 0.45098 0.00000
|
||||
3 4 96 61 0.90196 0.45098 0.00000
|
||||
3 72 61 30 0.90196 0.45098 0.00000
|
||||
3 62 2 74 0.90196 0.45098 0.00000
|
||||
3 22 50 62 0.90196 0.45098 0.00000
|
||||
3 62 63 22 0.90196 0.45098 0.00000
|
||||
3 63 62 23 0.90196 0.45098 0.00000
|
||||
3 23 64 63 0.90196 0.45098 0.00000
|
||||
3 64 23 77 0.90196 0.45098 0.00000
|
||||
3 65 22 63 0.90196 0.45098 0.00000
|
||||
3 63 33 65 0.90196 0.45098 0.00000
|
||||
3 33 63 64 0.90196 0.45098 0.00000
|
||||
3 81 64 7 0.90196 0.45098 0.00000
|
||||
3 5 52 65 0.90196 0.45098 0.00000
|
||||
3 80 65 33 0.90196 0.45098 0.00000
|
||||
3 66 3 101 0.90196 0.45098 0.00000
|
||||
3 26 78 66 0.90196 0.45098 0.00000
|
||||
3 66 67 26 0.90196 0.45098 0.00000
|
||||
3 67 66 28 0.90196 0.45098 0.00000
|
||||
3 28 68 67 0.90196 0.45098 0.00000
|
||||
3 68 28 100 0.90196 0.45098 0.00000
|
||||
3 69 26 67 0.90196 0.45098 0.00000
|
||||
3 67 35 69 0.90196 0.45098 0.00000
|
||||
3 35 67 68 0.90196 0.45098 0.00000
|
||||
3 92 68 9 0.90196 0.45098 0.00000
|
||||
3 5 80 69 0.90196 0.45098 0.00000
|
||||
3 90 69 35 0.90196 0.45098 0.00000
|
||||
3 70 0 86 0.90196 0.45098 0.00000
|
||||
3 14 106 70 0.90196 0.45098 0.00000
|
||||
3 70 71 14 0.90196 0.45098 0.00000
|
||||
3 71 70 13 0.90196 0.45098 0.00000
|
||||
3 13 72 71 0.90196 0.45098 0.00000
|
||||
3 72 13 89 0.90196 0.45098 0.00000
|
||||
3 73 14 71 0.90196 0.45098 0.00000
|
||||
3 71 30 73 0.90196 0.45098 0.00000
|
||||
3 30 71 72 0.90196 0.45098 0.00000
|
||||
3 61 72 4 0.90196 0.45098 0.00000
|
||||
3 6 108 73 0.90196 0.45098 0.00000
|
||||
3 60 73 30 0.90196 0.45098 0.00000
|
||||
3 74 2 49 0.90196 0.45098 0.00000
|
||||
3 23 62 74 0.90196 0.45098 0.00000
|
||||
3 74 75 23 0.90196 0.45098 0.00000
|
||||
3 75 74 25 0.90196 0.45098 0.00000
|
||||
3 25 76 75 0.90196 0.45098 0.00000
|
||||
3 76 25 48 0.90196 0.45098 0.00000
|
||||
3 77 23 75 0.90196 0.45098 0.00000
|
||||
3 75 38 77 0.90196 0.45098 0.00000
|
||||
3 38 75 76 0.90196 0.45098 0.00000
|
||||
3 120 76 10 0.90196 0.45098 0.00000
|
||||
3 7 64 77 0.90196 0.45098 0.00000
|
||||
3 118 77 38 0.90196 0.45098 0.00000
|
||||
3 78 3 66 0.90196 0.45098 0.00000
|
||||
3 27 54 78 0.90196 0.45098 0.00000
|
||||
3 78 79 27 0.90196 0.45098 0.00000
|
||||
3 79 78 26 0.90196 0.45098 0.00000
|
||||
3 26 80 79 0.90196 0.45098 0.00000
|
||||
3 80 26 69 0.90196 0.45098 0.00000
|
||||
3 81 27 79 0.90196 0.45098 0.00000
|
||||
3 79 33 81 0.90196 0.45098 0.00000
|
||||
3 33 79 80 0.90196 0.45098 0.00000
|
||||
3 65 80 5 0.90196 0.45098 0.00000
|
||||
3 7 56 81 0.90196 0.45098 0.00000
|
||||
3 64 81 33 0.90196 0.45098 0.00000
|
||||
3 82 0 46 0.90196 0.45098 0.00000
|
||||
3 15 86 82 0.90196 0.45098 0.00000
|
||||
3 82 83 15 0.90196 0.45098 0.00000
|
||||
3 83 82 12 0.90196 0.45098 0.00000
|
||||
3 12 84 83 0.90196 0.45098 0.00000
|
||||
3 84 12 49 0.90196 0.45098 0.00000
|
||||
3 85 15 83 0.90196 0.45098 0.00000
|
||||
3 83 24 85 0.90196 0.45098 0.00000
|
||||
3 24 83 84 0.90196 0.45098 0.00000
|
||||
3 50 84 2 0.90196 0.45098 0.00000
|
||||
3 8 88 85 0.90196 0.45098 0.00000
|
||||
3 53 85 24 0.90196 0.45098 0.00000
|
||||
3 86 0 82 0.90196 0.45098 0.00000
|
||||
3 13 70 86 0.90196 0.45098 0.00000
|
||||
3 86 87 13 0.90196 0.45098 0.00000
|
||||
3 87 86 15 0.90196 0.45098 0.00000
|
||||
3 15 88 87 0.90196 0.45098 0.00000
|
||||
3 88 15 85 0.90196 0.45098 0.00000
|
||||
3 89 13 87 0.90196 0.45098 0.00000
|
||||
3 87 31 89 0.90196 0.45098 0.00000
|
||||
3 31 87 88 0.90196 0.45098 0.00000
|
||||
3 104 88 8 0.90196 0.45098 0.00000
|
||||
3 4 72 89 0.90196 0.45098 0.00000
|
||||
3 102 89 31 0.90196 0.45098 0.00000
|
||||
3 90 5 69 0.90196 0.45098 0.00000
|
||||
3 34 52 90 0.90196 0.45098 0.00000
|
||||
3 90 91 34 0.90196 0.45098 0.00000
|
||||
3 91 90 35 0.90196 0.45098 0.00000
|
||||
3 35 92 91 0.90196 0.45098 0.00000
|
||||
3 92 35 68 0.90196 0.45098 0.00000
|
||||
3 93 34 91 0.90196 0.45098 0.00000
|
||||
3 91 40 93 0.90196 0.45098 0.00000
|
||||
3 40 91 92 0.90196 0.45098 0.00000
|
||||
3 105 92 9 0.90196 0.45098 0.00000
|
||||
3 8 53 93 0.90196 0.45098 0.00000
|
||||
3 104 93 40 0.90196 0.45098 0.00000
|
||||
3 94 1 58 0.90196 0.45098 0.00000
|
||||
3 20 98 94 0.90196 0.45098 0.00000
|
||||
3 94 95 20 0.90196 0.45098 0.00000
|
||||
3 95 94 18 0.90196 0.45098 0.00000
|
||||
3 18 96 95 0.90196 0.45098 0.00000
|
||||
3 96 18 61 0.90196 0.45098 0.00000
|
||||
3 97 20 95 0.90196 0.45098 0.00000
|
||||
3 95 32 97 0.90196 0.45098 0.00000
|
||||
3 32 95 96 0.90196 0.45098 0.00000
|
||||
3 102 96 4 0.90196 0.45098 0.00000
|
||||
3 9 100 97 0.90196 0.45098 0.00000
|
||||
3 105 97 32 0.90196 0.45098 0.00000
|
||||
3 98 1 94 0.90196 0.45098 0.00000
|
||||
3 17 42 98 0.90196 0.45098 0.00000
|
||||
3 98 99 17 0.90196 0.45098 0.00000
|
||||
3 99 98 20 0.90196 0.45098 0.00000
|
||||
3 20 100 99 0.90196 0.45098 0.00000
|
||||
3 100 20 97 0.90196 0.45098 0.00000
|
||||
3 101 17 99 0.90196 0.45098 0.00000
|
||||
3 99 28 101 0.90196 0.45098 0.00000
|
||||
3 28 99 100 0.90196 0.45098 0.00000
|
||||
3 68 100 9 0.90196 0.45098 0.00000
|
||||
3 3 44 101 0.90196 0.45098 0.00000
|
||||
3 66 101 28 0.90196 0.45098 0.00000
|
||||
3 102 4 89 0.90196 0.45098 0.00000
|
||||
3 32 96 102 0.90196 0.45098 0.00000
|
||||
3 102 103 32 0.90196 0.45098 0.00000
|
||||
3 103 102 31 0.90196 0.45098 0.00000
|
||||
3 31 104 103 0.90196 0.45098 0.00000
|
||||
3 104 31 88 0.90196 0.45098 0.00000
|
||||
3 105 32 103 0.90196 0.45098 0.00000
|
||||
3 103 40 105 0.90196 0.45098 0.00000
|
||||
3 40 103 104 0.90196 0.45098 0.00000
|
||||
3 93 104 8 0.90196 0.45098 0.00000
|
||||
3 9 97 105 0.90196 0.45098 0.00000
|
||||
3 92 105 40 0.90196 0.45098 0.00000
|
||||
3 106 0 70 0.90196 0.45098 0.00000
|
||||
3 16 46 106 0.90196 0.45098 0.00000
|
||||
3 106 107 16 0.90196 0.45098 0.00000
|
||||
3 107 106 14 0.90196 0.45098 0.00000
|
||||
3 14 108 107 0.90196 0.45098 0.00000
|
||||
3 108 14 73 0.90196 0.45098 0.00000
|
||||
3 109 16 107 0.90196 0.45098 0.00000
|
||||
3 107 36 109 0.90196 0.45098 0.00000
|
||||
3 36 107 108 0.90196 0.45098 0.00000
|
||||
3 110 108 6 0.90196 0.45098 0.00000
|
||||
3 10 48 109 0.90196 0.45098 0.00000
|
||||
3 113 109 36 0.90196 0.45098 0.00000
|
||||
3 110 6 117 0.90196 0.45098 0.00000
|
||||
3 36 108 110 0.90196 0.45098 0.00000
|
||||
3 110 111 36 0.90196 0.45098 0.00000
|
||||
3 111 110 37 0.90196 0.45098 0.00000
|
||||
3 37 112 111 0.90196 0.45098 0.00000
|
||||
3 112 37 116 0.90196 0.45098 0.00000
|
||||
3 113 36 111 0.90196 0.45098 0.00000
|
||||
3 111 41 113 0.90196 0.45098 0.00000
|
||||
3 41 111 112 0.90196 0.45098 0.00000
|
||||
3 121 112 11 0.90196 0.45098 0.00000
|
||||
3 10 109 113 0.90196 0.45098 0.00000
|
||||
3 120 113 41 0.90196 0.45098 0.00000
|
||||
3 114 1 42 0.90196 0.45098 0.00000
|
||||
3 19 58 114 0.90196 0.45098 0.00000
|
||||
3 114 115 19 0.90196 0.45098 0.00000
|
||||
3 115 114 21 0.90196 0.45098 0.00000
|
||||
3 21 116 115 0.90196 0.45098 0.00000
|
||||
3 116 21 45 0.90196 0.45098 0.00000
|
||||
3 117 19 115 0.90196 0.45098 0.00000
|
||||
3 115 37 117 0.90196 0.45098 0.00000
|
||||
3 37 115 116 0.90196 0.45098 0.00000
|
||||
3 112 116 11 0.90196 0.45098 0.00000
|
||||
3 6 60 117 0.90196 0.45098 0.00000
|
||||
3 110 117 37 0.90196 0.45098 0.00000
|
||||
3 118 7 77 0.90196 0.45098 0.00000
|
||||
3 39 56 118 0.90196 0.45098 0.00000
|
||||
3 118 119 39 0.90196 0.45098 0.00000
|
||||
3 119 118 38 0.90196 0.45098 0.00000
|
||||
3 38 120 119 0.90196 0.45098 0.00000
|
||||
3 120 38 76 0.90196 0.45098 0.00000
|
||||
3 121 39 119 0.90196 0.45098 0.00000
|
||||
3 119 41 121 0.90196 0.45098 0.00000
|
||||
3 41 119 120 0.90196 0.45098 0.00000
|
||||
3 113 120 10 0.90196 0.45098 0.00000
|
||||
3 11 57 121 0.90196 0.45098 0.00000
|
||||
3 112 121 41 0.90196 0.45098 0.00000
|
||||
1 0 0.38824 0.60000 0.30196
|
||||
1 1 0.38824 0.60000 0.30196
|
||||
1 2 0.38824 0.60000 0.30196
|
||||
1 3 0.38824 0.60000 0.30196
|
||||
1 4 0.38824 0.60000 0.30196
|
||||
1 5 0.38824 0.60000 0.30196
|
||||
1 6 0.38824 0.60000 0.30196
|
||||
1 7 0.38824 0.60000 0.30196
|
||||
1 8 0.38824 0.60000 0.30196
|
||||
1 9 0.38824 0.60000 0.30196
|
||||
1 10 0.38824 0.60000 0.30196
|
||||
1 11 0.38824 0.60000 0.30196
|
@ -0,0 +1,826 @@
|
||||
OFF
|
||||
272 552 0
|
||||
0 0.5257311121191336 0.85065080835204
|
||||
0 0.5257311121191336 -0.85065080835204
|
||||
0 -0.5257311121191336 0.85065080835204
|
||||
0 -0.5257311121191336 -0.85065080835204
|
||||
0.5257311121191336 0.85065080835204 0
|
||||
0.5257311121191336 -0.85065080835204 0
|
||||
-0.5257311121191336 0.85065080835204 0
|
||||
-0.5257311121191336 -0.85065080835204 0
|
||||
0.85065080835204 0 0.5257311121191336
|
||||
0.85065080835204 0 -0.5257311121191336
|
||||
-0.85065080835204 0 0.5257311121191336
|
||||
-0.85065080835204 0 -0.5257311121191336
|
||||
2.267469933117213e-16 0.1834794080019837 0.9830235535526306
|
||||
1.92245706721902e-16 -0.1834794080019837 0.9830235535526306
|
||||
0.2120312799176223 0.7385845055044615 0.6399497359677749
|
||||
0.395510687919606 0.8519810158853969 0.3430738175848559
|
||||
-0.2120312799176222 0.7385845055044614 0.639949735967775
|
||||
-0.395510687919606 0.851981015885397 0.3430738175848558
|
||||
0.3430738175848558 0.395510687919606 0.851981015885397
|
||||
0.6399497359677748 0.2120312799176223 0.7385845055044618
|
||||
-0.3430738175848559 0.3955106879196059 0.851981015885397
|
||||
-0.6399497359677748 0.2120312799176223 0.7385845055044615
|
||||
1.922457067219021e-16 0.1834794080019837 -0.9830235535526306
|
||||
2.267469933117213e-16 -0.1834794080019837 -0.9830235535526306
|
||||
0.2120312799176222 0.7385845055044614 -0.639949735967775
|
||||
0.395510687919606 0.851981015885397 -0.3430738175848558
|
||||
-0.2120312799176223 0.7385845055044615 -0.6399497359677749
|
||||
-0.395510687919606 0.8519810158853969 -0.3430738175848559
|
||||
0.3430738175848558 0.3955106879196059 -0.8519810158853971
|
||||
0.6399497359677748 0.2120312799176222 -0.7385845055044618
|
||||
-0.3430738175848558 0.395510687919606 -0.851981015885397
|
||||
-0.6399497359677748 0.2120312799176223 -0.7385845055044618
|
||||
0.2120312799176222 -0.7385845055044615 0.639949735967775
|
||||
0.3955106879196058 -0.851981015885397 0.3430738175848558
|
||||
-0.2120312799176223 -0.7385845055044615 0.6399497359677749
|
||||
-0.395510687919606 -0.8519810158853969 0.3430738175848559
|
||||
0.3430738175848558 -0.3955106879196059 0.8519810158853971
|
||||
0.6399497359677748 -0.2120312799176222 0.7385845055044618
|
||||
-0.3430738175848558 -0.395510687919606 0.851981015885397
|
||||
-0.6399497359677748 -0.2120312799176223 0.7385845055044618
|
||||
0.2120312799176222 -0.7385845055044615 -0.639949735967775
|
||||
0.3955106879196059 -0.851981015885397 -0.3430738175848558
|
||||
-0.2120312799176222 -0.7385845055044615 -0.639949735967775
|
||||
-0.3955106879196058 -0.851981015885397 -0.3430738175848558
|
||||
0.3430738175848558 -0.3955106879196056 -0.851981015885397
|
||||
0.6399497359677748 -0.2120312799176221 -0.7385845055044618
|
||||
-0.3430738175848559 -0.3955106879196059 -0.851981015885397
|
||||
-0.6399497359677748 -0.2120312799176223 -0.7385845055044615
|
||||
0.1834794080019837 0.9830235535526306 3.4096087162373e-17
|
||||
-0.1834794080019837 0.9830235535526306 3.4096087162373e-17
|
||||
0.7385845055044618 0.6399497359677748 0.2120312799176221
|
||||
0.8519810158853971 0.3430738175848559 0.3955106879196058
|
||||
0.7385845055044618 0.6399497359677748 -0.2120312799176222
|
||||
0.851981015885397 0.3430738175848557 -0.3955106879196058
|
||||
0.1834794080019837 -0.9830235535526306 -3.4096087162373e-17
|
||||
-0.1834794080019837 -0.9830235535526306 -3.4096087162373e-17
|
||||
0.7385845055044618 -0.6399497359677748 0.2120312799176222
|
||||
0.851981015885397 -0.3430738175848557 0.3955106879196058
|
||||
0.7385845055044618 -0.6399497359677748 -0.2120312799176221
|
||||
0.8519810158853971 -0.3430738175848559 -0.3955106879196058
|
||||
-0.7385845055044618 0.6399497359677748 0.2120312799176222
|
||||
-0.851981015885397 0.3430738175848557 0.3955106879196058
|
||||
-0.7385845055044615 0.6399497359677748 -0.2120312799176224
|
||||
-0.8519810158853969 0.3430738175848559 -0.395510687919606
|
||||
-0.7385845055044618 -0.6399497359677748 0.2120312799176221
|
||||
-0.8519810158853971 -0.3430738175848559 0.3955106879196058
|
||||
-0.7385845055044618 -0.6399497359677748 -0.2120312799176222
|
||||
-0.851981015885397 -0.3430738175848557 -0.3955106879196058
|
||||
0.9830235535526306 2.267469933117213e-16 0.1834794080019836
|
||||
0.9830235535526306 1.92245706721902e-16 -0.1834794080019837
|
||||
-0.9830235535526306 -2.267469933117213e-16 0.1834794080019836
|
||||
-0.9830235535526306 -1.92245706721902e-16 -0.1834794080019837
|
||||
-0.1194960329361959 0.374843742971558 -0.919354592345948
|
||||
-0.2408723836637745 0.1975414971827028 -0.9502409440131212
|
||||
-0.1235792137159473 -4.626953174201564e-17 -0.9923347106381738
|
||||
-0.4579792781293658 0.2066706056476467 -0.8646052518724022
|
||||
-0.35682208977309 -3.124513936890529e-17 -0.9341723589627157
|
||||
-0.2408723836637744 -0.1975414971827028 -0.9502409440131212
|
||||
-0.1194960329361959 -0.3748437429715579 -0.919354592345948
|
||||
-0.5605012402939293 1.966702600323942e-17 -0.8281535845656691
|
||||
-0.4579792781293658 -0.2066706056476469 -0.8646052518724022
|
||||
-0.7260059495344062 -6.939177849948745e-18 -0.6876884187192956
|
||||
-0.1194960329361959 0.3748437429715579 0.919354592345948
|
||||
-0.2408723836637744 0.1975414971827029 0.9502409440131212
|
||||
-0.4579792781293657 0.2066706056476468 0.8646052518724022
|
||||
-0.1235792137159473 4.574573400785218e-17 0.9923347106381738
|
||||
-0.35682208977309 3.09531117213564e-17 0.9341723589627158
|
||||
-0.5605012402939293 -1.986949146301587e-17 0.8281535845656691
|
||||
-0.7260059495344062 7.000242466867724e-18 0.6876884187192956
|
||||
-0.2408723836637744 -0.1975414971827029 0.9502409440131212
|
||||
-0.4579792781293658 -0.2066706056476467 0.8646052518724022
|
||||
-0.1194960329361959 -0.374843742971558 0.919354592345948
|
||||
0.1933486428115418 -0.5681923857830995 0.7998585594097523
|
||||
0.3897397037191919 -0.5872812009018947 0.7093685603493469
|
||||
0.4066259737430365 -0.741026038156455 0.5343554325088083
|
||||
0.5343554325088083 -0.4066259737430364 0.7410260381564551
|
||||
0.5773502691896258 -0.5773502691896256 0.5773502691896258
|
||||
0.5872812009018947 -0.7093685603493469 0.389739703719192
|
||||
0.5681923857830998 -0.799858559409752 0.1933486428115418
|
||||
0.7093685603493468 -0.3897397037191919 0.5872812009018948
|
||||
0.741026038156455 -0.5343554325088083 0.4066259737430364
|
||||
0.799858559409752 -0.1933486428115418 0.5681923857830997
|
||||
-0.1933486428115418 -0.5681923857830995 -0.7998585594097523
|
||||
-0.3897397037191919 -0.5872812009018947 -0.7093685603493469
|
||||
-0.4066259737430365 -0.741026038156455 -0.5343554325088083
|
||||
-0.5343554325088083 -0.4066259737430364 -0.7410260381564551
|
||||
-0.5773502691896258 -0.5773502691896256 -0.5773502691896258
|
||||
-0.5872812009018947 -0.7093685603493469 -0.389739703719192
|
||||
-0.5681923857830998 -0.799858559409752 -0.1933486428115418
|
||||
-0.7093685603493468 -0.3897397037191919 -0.5872812009018948
|
||||
-0.741026038156455 -0.5343554325088083 -0.4066259737430364
|
||||
-0.799858559409752 -0.1933486428115418 -0.5681923857830997
|
||||
2.949150586228217e-17 0.6876884187192956 -0.7260059495344064
|
||||
-1.966702600323942e-17 0.8281535845656691 -0.5605012402939293
|
||||
-0.2066706056476468 0.8646052518724022 -0.4579792781293658
|
||||
0.2066706056476469 0.8646052518724022 -0.4579792781293658
|
||||
3.124513936890529e-17 0.9341723589627158 -0.3568220897730901
|
||||
-0.1975414971827029 0.9502409440131212 -0.2408723836637746
|
||||
-0.374843742971558 0.919354592345948 -0.1194960329361958
|
||||
0.1975414971827029 0.9502409440131212 -0.2408723836637745
|
||||
2.197802757745743e-17 0.9923347106381738 -0.1235792137159472
|
||||
0.374843742971558 0.919354592345948 -0.1194960329361959
|
||||
2.949150586228217e-17 -0.6876884187192956 0.7260059495344064
|
||||
-1.966702600323942e-17 -0.8281535845656691 0.5605012402939293
|
||||
-0.2066706056476468 -0.8646052518724022 0.4579792781293658
|
||||
0.2066706056476469 -0.8646052518724022 0.4579792781293658
|
||||
3.124513936890529e-17 -0.9341723589627158 0.3568220897730901
|
||||
-0.1975414971827029 -0.9502409440131212 0.2408723836637746
|
||||
-0.374843742971558 -0.919354592345948 0.1194960329361958
|
||||
0.1975414971827029 -0.9502409440131212 0.2408723836637745
|
||||
2.197802757745743e-17 -0.9923347106381738 0.1235792137159472
|
||||
0.374843742971558 -0.919354592345948 0.1194960329361959
|
||||
0.1933486428115418 -0.5681923857830995 -0.7998585594097523
|
||||
0.3897397037191918 -0.5872812009018947 -0.7093685603493468
|
||||
0.5343554325088081 -0.4066259737430364 -0.7410260381564551
|
||||
0.4066259737430364 -0.741026038156455 -0.5343554325088083
|
||||
0.5773502691896258 -0.5773502691896257 -0.5773502691896258
|
||||
0.7093685603493467 -0.389739703719192 -0.5872812009018948
|
||||
0.799858559409752 -0.1933486428115418 -0.5681923857830997
|
||||
0.5872812009018947 -0.7093685603493468 -0.3897397037191921
|
||||
0.7410260381564551 -0.5343554325088083 -0.4066259737430366
|
||||
0.5681923857830998 -0.799858559409752 -0.1933486428115418
|
||||
-2.949150586228217e-17 0.6876884187192956 0.7260059495344064
|
||||
1.966702600323942e-17 0.8281535845656691 0.5605012402939293
|
||||
0.2066706056476468 0.8646052518724022 0.4579792781293658
|
||||
-0.2066706056476469 0.8646052518724022 0.4579792781293658
|
||||
-3.124513936890529e-17 0.9341723589627158 0.3568220897730901
|
||||
0.1975414971827029 0.9502409440131212 0.2408723836637746
|
||||
0.374843742971558 0.919354592345948 0.1194960329361958
|
||||
-0.1975414971827029 0.9502409440131212 0.2408723836637745
|
||||
-2.197802757745743e-17 0.9923347106381738 0.1235792137159472
|
||||
-0.374843742971558 0.919354592345948 0.1194960329361959
|
||||
-0.1933486428115418 -0.5681923857830995 0.7998585594097523
|
||||
-0.3897397037191918 -0.5872812009018947 0.7093685603493468
|
||||
-0.5343554325088081 -0.4066259737430364 0.7410260381564551
|
||||
-0.4066259737430364 -0.741026038156455 0.5343554325088083
|
||||
-0.5773502691896258 -0.5773502691896257 0.5773502691896258
|
||||
-0.7093685603493467 -0.389739703719192 0.5872812009018948
|
||||
-0.799858559409752 -0.1933486428115418 0.5681923857830997
|
||||
-0.5872812009018947 -0.7093685603493468 0.3897397037191921
|
||||
-0.7410260381564551 -0.5343554325088083 0.4066259737430366
|
||||
-0.5681923857830998 -0.799858559409752 0.1933486428115418
|
||||
-2.949150586228217e-17 -0.6876884187192956 -0.7260059495344064
|
||||
1.966702600323942e-17 -0.8281535845656691 -0.5605012402939293
|
||||
0.2066706056476468 -0.8646052518724022 -0.4579792781293658
|
||||
-0.2066706056476469 -0.8646052518724022 -0.4579792781293658
|
||||
-3.124513936890529e-17 -0.9341723589627158 -0.3568220897730901
|
||||
0.1975414971827029 -0.9502409440131212 -0.2408723836637746
|
||||
0.374843742971558 -0.919354592345948 -0.1194960329361958
|
||||
-0.1975414971827029 -0.9502409440131212 -0.2408723836637745
|
||||
-2.197802757745743e-17 -0.9923347106381738 -0.1235792137159472
|
||||
-0.374843742971558 -0.919354592345948 -0.1194960329361959
|
||||
0.1194960329361959 0.374843742971558 0.919354592345948
|
||||
0.2408723836637745 0.1975414971827028 0.9502409440131212
|
||||
0.1235792137159473 -4.626953174201564e-17 0.9923347106381738
|
||||
0.4579792781293658 0.2066706056476467 0.8646052518724022
|
||||
0.35682208977309 -3.124513936890529e-17 0.9341723589627157
|
||||
0.2408723836637744 -0.1975414971827028 0.9502409440131212
|
||||
0.1194960329361959 -0.3748437429715579 0.919354592345948
|
||||
0.5605012402939293 1.966702600323942e-17 0.8281535845656691
|
||||
0.4579792781293658 -0.2066706056476469 0.8646052518724022
|
||||
0.7260059495344062 -6.939177849948745e-18 0.6876884187192956
|
||||
0.1933486428115418 0.5681923857830995 0.7998585594097523
|
||||
0.3897397037191918 0.5872812009018947 0.7093685603493468
|
||||
0.5343554325088081 0.4066259737430364 0.7410260381564551
|
||||
0.4066259737430364 0.741026038156455 0.5343554325088083
|
||||
0.5773502691896258 0.5773502691896257 0.5773502691896258
|
||||
0.7093685603493467 0.389739703719192 0.5872812009018948
|
||||
0.799858559409752 0.1933486428115418 0.5681923857830997
|
||||
0.5872812009018947 0.7093685603493468 0.3897397037191921
|
||||
0.7410260381564551 0.5343554325088083 0.4066259737430366
|
||||
0.5681923857830998 0.799858559409752 0.1933486428115418
|
||||
0.6876884187192954 -0.7260059495344061 6.939177849948745e-18
|
||||
0.8281535845656691 -0.5605012402939293 -1.966702600323942e-17
|
||||
0.8646052518724022 -0.4579792781293658 -0.2066706056476467
|
||||
0.8646052518724022 -0.4579792781293658 0.2066706056476468
|
||||
0.9341723589627157 -0.35682208977309 3.124513936890529e-17
|
||||
0.9502409440131212 -0.2408723836637745 -0.1975414971827029
|
||||
0.919354592345948 -0.1194960329361959 -0.374843742971558
|
||||
0.9502409440131212 -0.2408723836637745 0.1975414971827029
|
||||
0.992334710638174 -0.1235792137159473 4.511279344846526e-17
|
||||
0.919354592345948 -0.1194960329361959 0.3748437429715579
|
||||
0.1933486428115418 0.5681923857830995 -0.7998585594097523
|
||||
0.3897397037191919 0.5872812009018947 -0.7093685603493469
|
||||
0.4066259737430365 0.741026038156455 -0.5343554325088083
|
||||
0.5343554325088083 0.4066259737430364 -0.7410260381564551
|
||||
0.5773502691896258 0.5773502691896256 -0.5773502691896258
|
||||
0.5872812009018947 0.7093685603493469 -0.389739703719192
|
||||
0.5681923857830998 0.799858559409752 -0.1933486428115418
|
||||
0.7093685603493468 0.3897397037191919 -0.5872812009018948
|
||||
0.741026038156455 0.5343554325088083 -0.4066259737430364
|
||||
0.799858559409752 0.1933486428115418 -0.5681923857830997
|
||||
0.1194960329361959 0.3748437429715579 -0.919354592345948
|
||||
0.2408723836637744 0.1975414971827029 -0.9502409440131212
|
||||
0.4579792781293657 0.2066706056476468 -0.8646052518724022
|
||||
0.1235792137159473 4.574573400785218e-17 -0.9923347106381738
|
||||
0.35682208977309 3.09531117213564e-17 -0.9341723589627158
|
||||
0.5605012402939293 -1.986949146301587e-17 -0.8281535845656691
|
||||
0.7260059495344062 7.000242466867724e-18 -0.6876884187192956
|
||||
0.2408723836637744 -0.1975414971827029 -0.9502409440131212
|
||||
0.4579792781293658 -0.2066706056476467 -0.8646052518724022
|
||||
0.1194960329361959 -0.374843742971558 -0.919354592345948
|
||||
0.6876884187192954 0.7260059495344061 -6.939177849948745e-18
|
||||
0.8281535845656691 0.5605012402939293 1.966702600323942e-17
|
||||
0.8646052518724022 0.4579792781293658 0.2066706056476467
|
||||
0.8646052518724022 0.4579792781293658 -0.2066706056476468
|
||||
0.9341723589627157 0.35682208977309 -3.124513936890529e-17
|
||||
0.9502409440131212 0.2408723836637745 0.1975414971827029
|
||||
0.919354592345948 0.1194960329361959 0.374843742971558
|
||||
0.9502409440131212 0.2408723836637745 -0.1975414971827029
|
||||
0.992334710638174 0.1235792137159473 -4.511279344846526e-17
|
||||
0.919354592345948 0.1194960329361959 -0.3748437429715579
|
||||
-0.1933486428115418 0.5681923857830995 0.7998585594097523
|
||||
-0.3897397037191919 0.5872812009018947 0.7093685603493469
|
||||
-0.4066259737430365 0.741026038156455 0.5343554325088083
|
||||
-0.5343554325088083 0.4066259737430364 0.7410260381564551
|
||||
-0.5773502691896258 0.5773502691896256 0.5773502691896258
|
||||
-0.5872812009018947 0.7093685603493469 0.389739703719192
|
||||
-0.5681923857830998 0.799858559409752 0.1933486428115418
|
||||
-0.7093685603493468 0.3897397037191919 0.5872812009018948
|
||||
-0.741026038156455 0.5343554325088083 0.4066259737430364
|
||||
-0.799858559409752 0.1933486428115418 0.5681923857830997
|
||||
-0.6876884187192954 0.7260059495344061 6.939177849948745e-18
|
||||
-0.8281535845656691 0.5605012402939293 -1.966702600323942e-17
|
||||
-0.8646052518724022 0.4579792781293658 -0.2066706056476467
|
||||
-0.8646052518724022 0.4579792781293658 0.2066706056476468
|
||||
-0.9341723589627157 0.35682208977309 3.124513936890529e-17
|
||||
-0.9502409440131212 0.2408723836637745 -0.1975414971827029
|
||||
-0.919354592345948 0.1194960329361959 -0.374843742971558
|
||||
-0.9502409440131212 0.2408723836637745 0.1975414971827029
|
||||
-0.992334710638174 0.1235792137159473 4.511279344846526e-17
|
||||
-0.919354592345948 0.1194960329361959 0.3748437429715579
|
||||
-0.1933486428115418 0.5681923857830995 -0.7998585594097523
|
||||
-0.3897397037191918 0.5872812009018947 -0.7093685603493468
|
||||
-0.5343554325088081 0.4066259737430364 -0.7410260381564551
|
||||
-0.4066259737430364 0.741026038156455 -0.5343554325088083
|
||||
-0.5773502691896258 0.5773502691896257 -0.5773502691896258
|
||||
-0.7093685603493467 0.389739703719192 -0.5872812009018948
|
||||
-0.799858559409752 0.1933486428115418 -0.5681923857830997
|
||||
-0.5872812009018947 0.7093685603493468 -0.3897397037191921
|
||||
-0.7410260381564551 0.5343554325088083 -0.4066259737430366
|
||||
-0.5681923857830998 0.799858559409752 -0.1933486428115418
|
||||
-0.6876884187192954 -0.7260059495344061 -6.939177849948745e-18
|
||||
-0.8281535845656691 -0.5605012402939293 1.966702600323942e-17
|
||||
-0.8646052518724022 -0.4579792781293658 0.2066706056476467
|
||||
-0.8646052518724022 -0.4579792781293658 -0.2066706056476468
|
||||
-0.9341723589627157 -0.35682208977309 -3.124513936890529e-17
|
||||
-0.9502409440131212 -0.2408723836637745 0.1975414971827029
|
||||
-0.919354592345948 -0.1194960329361959 0.374843742971558
|
||||
-0.9502409440131212 -0.2408723836637745 -0.1975414971827029
|
||||
-0.992334710638174 -0.1235792137159473 -4.511279344846526e-17
|
||||
-0.919354592345948 -0.1194960329361959 -0.3748437429715579
|
||||
3 72 1 212 0.90196 0.45098 0.00000
|
||||
3 30 252 72 0.90196 0.45098 0.00000
|
||||
3 72 73 30 0.90196 0.45098 0.00000
|
||||
3 73 72 22 0.90196 0.45098 0.00000
|
||||
3 22 74 73 0.90196 0.45098 0.00000
|
||||
3 74 22 215 0.90196 0.45098 0.00000
|
||||
3 75 30 73 0.90196 0.45098 0.00000
|
||||
3 73 76 75 0.90196 0.45098 0.00000
|
||||
3 76 73 74 0.90196 0.45098 0.00000
|
||||
3 74 77 76 0.90196 0.45098 0.00000
|
||||
3 77 74 23 0.90196 0.45098 0.00000
|
||||
3 23 78 77 0.90196 0.45098 0.00000
|
||||
3 78 23 221 0.90196 0.45098 0.00000
|
||||
3 31 254 75 0.90196 0.45098 0.00000
|
||||
3 75 79 31 0.90196 0.45098 0.00000
|
||||
3 79 75 76 0.90196 0.45098 0.00000
|
||||
3 76 80 79 0.90196 0.45098 0.00000
|
||||
3 80 76 77 0.90196 0.45098 0.00000
|
||||
3 77 46 80 0.90196 0.45098 0.00000
|
||||
3 46 77 78 0.90196 0.45098 0.00000
|
||||
3 102 78 3 0.90196 0.45098 0.00000
|
||||
3 81 31 79 0.90196 0.45098 0.00000
|
||||
3 79 47 81 0.90196 0.45098 0.00000
|
||||
3 47 79 80 0.90196 0.45098 0.00000
|
||||
3 105 80 46 0.90196 0.45098 0.00000
|
||||
3 11 258 81 0.90196 0.45098 0.00000
|
||||
3 111 81 47 0.90196 0.45098 0.00000
|
||||
3 82 0 232 0.90196 0.45098 0.00000
|
||||
3 12 172 82 0.90196 0.45098 0.00000
|
||||
3 82 83 12 0.90196 0.45098 0.00000
|
||||
3 83 82 20 0.90196 0.45098 0.00000
|
||||
3 20 84 83 0.90196 0.45098 0.00000
|
||||
3 84 20 235 0.90196 0.45098 0.00000
|
||||
3 85 12 83 0.90196 0.45098 0.00000
|
||||
3 83 86 85 0.90196 0.45098 0.00000
|
||||
3 86 83 84 0.90196 0.45098 0.00000
|
||||
3 84 87 86 0.90196 0.45098 0.00000
|
||||
3 87 84 21 0.90196 0.45098 0.00000
|
||||
3 21 88 87 0.90196 0.45098 0.00000
|
||||
3 88 21 241 0.90196 0.45098 0.00000
|
||||
3 13 174 85 0.90196 0.45098 0.00000
|
||||
3 85 89 13 0.90196 0.45098 0.00000
|
||||
3 89 85 86 0.90196 0.45098 0.00000
|
||||
3 86 90 89 0.90196 0.45098 0.00000
|
||||
3 90 86 87 0.90196 0.45098 0.00000
|
||||
3 87 39 90 0.90196 0.45098 0.00000
|
||||
3 39 87 88 0.90196 0.45098 0.00000
|
||||
3 158 88 10 0.90196 0.45098 0.00000
|
||||
3 91 13 89 0.90196 0.45098 0.00000
|
||||
3 89 38 91 0.90196 0.45098 0.00000
|
||||
3 38 89 90 0.90196 0.45098 0.00000
|
||||
3 154 90 39 0.90196 0.45098 0.00000
|
||||
3 2 178 91 0.90196 0.45098 0.00000
|
||||
3 152 91 38 0.90196 0.45098 0.00000
|
||||
3 92 2 122 0.90196 0.45098 0.00000
|
||||
3 36 178 92 0.90196 0.45098 0.00000
|
||||
3 92 93 36 0.90196 0.45098 0.00000
|
||||
3 93 92 32 0.90196 0.45098 0.00000
|
||||
3 32 94 93 0.90196 0.45098 0.00000
|
||||
3 94 32 125 0.90196 0.45098 0.00000
|
||||
3 95 36 93 0.90196 0.45098 0.00000
|
||||
3 93 96 95 0.90196 0.45098 0.00000
|
||||
3 96 93 94 0.90196 0.45098 0.00000
|
||||
3 94 97 96 0.90196 0.45098 0.00000
|
||||
3 97 94 33 0.90196 0.45098 0.00000
|
||||
3 33 98 97 0.90196 0.45098 0.00000
|
||||
3 98 33 131 0.90196 0.45098 0.00000
|
||||
3 37 180 95 0.90196 0.45098 0.00000
|
||||
3 95 99 37 0.90196 0.45098 0.00000
|
||||
3 99 95 96 0.90196 0.45098 0.00000
|
||||
3 96 100 99 0.90196 0.45098 0.00000
|
||||
3 100 96 97 0.90196 0.45098 0.00000
|
||||
3 97 56 100 0.90196 0.45098 0.00000
|
||||
3 56 97 98 0.90196 0.45098 0.00000
|
||||
3 192 98 5 0.90196 0.45098 0.00000
|
||||
3 101 37 99 0.90196 0.45098 0.00000
|
||||
3 99 57 101 0.90196 0.45098 0.00000
|
||||
3 57 99 100 0.90196 0.45098 0.00000
|
||||
3 195 100 56 0.90196 0.45098 0.00000
|
||||
3 8 181 101 0.90196 0.45098 0.00000
|
||||
3 201 101 57 0.90196 0.45098 0.00000
|
||||
3 102 3 162 0.90196 0.45098 0.00000
|
||||
3 46 78 102 0.90196 0.45098 0.00000
|
||||
3 102 103 46 0.90196 0.45098 0.00000
|
||||
3 103 102 42 0.90196 0.45098 0.00000
|
||||
3 42 104 103 0.90196 0.45098 0.00000
|
||||
3 104 42 165 0.90196 0.45098 0.00000
|
||||
3 105 46 103 0.90196 0.45098 0.00000
|
||||
3 103 106 105 0.90196 0.45098 0.00000
|
||||
3 106 103 104 0.90196 0.45098 0.00000
|
||||
3 104 107 106 0.90196 0.45098 0.00000
|
||||
3 107 104 43 0.90196 0.45098 0.00000
|
||||
3 43 108 107 0.90196 0.45098 0.00000
|
||||
3 108 43 171 0.90196 0.45098 0.00000
|
||||
3 47 80 105 0.90196 0.45098 0.00000
|
||||
3 105 109 47 0.90196 0.45098 0.00000
|
||||
3 109 105 106 0.90196 0.45098 0.00000
|
||||
3 106 110 109 0.90196 0.45098 0.00000
|
||||
3 110 106 107 0.90196 0.45098 0.00000
|
||||
3 107 66 110 0.90196 0.45098 0.00000
|
||||
3 66 107 108 0.90196 0.45098 0.00000
|
||||
3 262 108 7 0.90196 0.45098 0.00000
|
||||
3 111 47 109 0.90196 0.45098 0.00000
|
||||
3 109 67 111 0.90196 0.45098 0.00000
|
||||
3 67 109 110 0.90196 0.45098 0.00000
|
||||
3 265 110 66 0.90196 0.45098 0.00000
|
||||
3 11 81 111 0.90196 0.45098 0.00000
|
||||
3 271 111 67 0.90196 0.45098 0.00000
|
||||
3 112 1 252 0.90196 0.45098 0.00000
|
||||
3 24 202 112 0.90196 0.45098 0.00000
|
||||
3 112 113 24 0.90196 0.45098 0.00000
|
||||
3 113 112 26 0.90196 0.45098 0.00000
|
||||
3 26 114 113 0.90196 0.45098 0.00000
|
||||
3 114 26 255 0.90196 0.45098 0.00000
|
||||
3 115 24 113 0.90196 0.45098 0.00000
|
||||
3 113 116 115 0.90196 0.45098 0.00000
|
||||
3 116 113 114 0.90196 0.45098 0.00000
|
||||
3 114 117 116 0.90196 0.45098 0.00000
|
||||
3 117 114 27 0.90196 0.45098 0.00000
|
||||
3 27 118 117 0.90196 0.45098 0.00000
|
||||
3 118 27 261 0.90196 0.45098 0.00000
|
||||
3 25 204 115 0.90196 0.45098 0.00000
|
||||
3 115 119 25 0.90196 0.45098 0.00000
|
||||
3 119 115 116 0.90196 0.45098 0.00000
|
||||
3 116 120 119 0.90196 0.45098 0.00000
|
||||
3 120 116 117 0.90196 0.45098 0.00000
|
||||
3 117 49 120 0.90196 0.45098 0.00000
|
||||
3 49 117 118 0.90196 0.45098 0.00000
|
||||
3 151 118 6 0.90196 0.45098 0.00000
|
||||
3 121 25 119 0.90196 0.45098 0.00000
|
||||
3 119 48 121 0.90196 0.45098 0.00000
|
||||
3 48 119 120 0.90196 0.45098 0.00000
|
||||
3 150 120 49 0.90196 0.45098 0.00000
|
||||
3 4 208 121 0.90196 0.45098 0.00000
|
||||
3 148 121 48 0.90196 0.45098 0.00000
|
||||
3 122 2 152 0.90196 0.45098 0.00000
|
||||
3 32 92 122 0.90196 0.45098 0.00000
|
||||
3 122 123 32 0.90196 0.45098 0.00000
|
||||
3 123 122 34 0.90196 0.45098 0.00000
|
||||
3 34 124 123 0.90196 0.45098 0.00000
|
||||
3 124 34 155 0.90196 0.45098 0.00000
|
||||
3 125 32 123 0.90196 0.45098 0.00000
|
||||
3 123 126 125 0.90196 0.45098 0.00000
|
||||
3 126 123 124 0.90196 0.45098 0.00000
|
||||
3 124 127 126 0.90196 0.45098 0.00000
|
||||
3 127 124 35 0.90196 0.45098 0.00000
|
||||
3 35 128 127 0.90196 0.45098 0.00000
|
||||
3 128 35 161 0.90196 0.45098 0.00000
|
||||
3 33 94 125 0.90196 0.45098 0.00000
|
||||
3 125 129 33 0.90196 0.45098 0.00000
|
||||
3 129 125 126 0.90196 0.45098 0.00000
|
||||
3 126 130 129 0.90196 0.45098 0.00000
|
||||
3 130 126 127 0.90196 0.45098 0.00000
|
||||
3 127 55 130 0.90196 0.45098 0.00000
|
||||
3 55 127 128 0.90196 0.45098 0.00000
|
||||
3 171 128 7 0.90196 0.45098 0.00000
|
||||
3 131 33 129 0.90196 0.45098 0.00000
|
||||
3 129 54 131 0.90196 0.45098 0.00000
|
||||
3 54 129 130 0.90196 0.45098 0.00000
|
||||
3 170 130 55 0.90196 0.45098 0.00000
|
||||
3 5 98 131 0.90196 0.45098 0.00000
|
||||
3 168 131 54 0.90196 0.45098 0.00000
|
||||
3 132 3 221 0.90196 0.45098 0.00000
|
||||
3 40 162 132 0.90196 0.45098 0.00000
|
||||
3 132 133 40 0.90196 0.45098 0.00000
|
||||
3 133 132 44 0.90196 0.45098 0.00000
|
||||
3 44 134 133 0.90196 0.45098 0.00000
|
||||
3 134 44 220 0.90196 0.45098 0.00000
|
||||
3 135 40 133 0.90196 0.45098 0.00000
|
||||
3 133 136 135 0.90196 0.45098 0.00000
|
||||
3 136 133 134 0.90196 0.45098 0.00000
|
||||
3 134 137 136 0.90196 0.45098 0.00000
|
||||
3 137 134 45 0.90196 0.45098 0.00000
|
||||
3 45 138 137 0.90196 0.45098 0.00000
|
||||
3 138 45 218 0.90196 0.45098 0.00000
|
||||
3 41 164 135 0.90196 0.45098 0.00000
|
||||
3 135 139 41 0.90196 0.45098 0.00000
|
||||
3 139 135 136 0.90196 0.45098 0.00000
|
||||
3 136 140 139 0.90196 0.45098 0.00000
|
||||
3 140 136 137 0.90196 0.45098 0.00000
|
||||
3 137 59 140 0.90196 0.45098 0.00000
|
||||
3 59 137 138 0.90196 0.45098 0.00000
|
||||
3 198 138 9 0.90196 0.45098 0.00000
|
||||
3 141 41 139 0.90196 0.45098 0.00000
|
||||
3 139 58 141 0.90196 0.45098 0.00000
|
||||
3 58 139 140 0.90196 0.45098 0.00000
|
||||
3 194 140 59 0.90196 0.45098 0.00000
|
||||
3 5 168 141 0.90196 0.45098 0.00000
|
||||
3 192 141 58 0.90196 0.45098 0.00000
|
||||
3 142 0 182 0.90196 0.45098 0.00000
|
||||
3 16 232 142 0.90196 0.45098 0.00000
|
||||
3 142 143 16 0.90196 0.45098 0.00000
|
||||
3 143 142 14 0.90196 0.45098 0.00000
|
||||
3 14 144 143 0.90196 0.45098 0.00000
|
||||
3 144 14 185 0.90196 0.45098 0.00000
|
||||
3 145 16 143 0.90196 0.45098 0.00000
|
||||
3 143 146 145 0.90196 0.45098 0.00000
|
||||
3 146 143 144 0.90196 0.45098 0.00000
|
||||
3 144 147 146 0.90196 0.45098 0.00000
|
||||
3 147 144 15 0.90196 0.45098 0.00000
|
||||
3 15 148 147 0.90196 0.45098 0.00000
|
||||
3 148 15 191 0.90196 0.45098 0.00000
|
||||
3 17 234 145 0.90196 0.45098 0.00000
|
||||
3 145 149 17 0.90196 0.45098 0.00000
|
||||
3 149 145 146 0.90196 0.45098 0.00000
|
||||
3 146 150 149 0.90196 0.45098 0.00000
|
||||
3 150 146 147 0.90196 0.45098 0.00000
|
||||
3 147 48 150 0.90196 0.45098 0.00000
|
||||
3 48 147 148 0.90196 0.45098 0.00000
|
||||
3 121 148 4 0.90196 0.45098 0.00000
|
||||
3 151 17 149 0.90196 0.45098 0.00000
|
||||
3 149 49 151 0.90196 0.45098 0.00000
|
||||
3 49 149 150 0.90196 0.45098 0.00000
|
||||
3 120 150 48 0.90196 0.45098 0.00000
|
||||
3 6 238 151 0.90196 0.45098 0.00000
|
||||
3 118 151 49 0.90196 0.45098 0.00000
|
||||
3 152 2 91 0.90196 0.45098 0.00000
|
||||
3 34 122 152 0.90196 0.45098 0.00000
|
||||
3 152 153 34 0.90196 0.45098 0.00000
|
||||
3 153 152 38 0.90196 0.45098 0.00000
|
||||
3 38 154 153 0.90196 0.45098 0.00000
|
||||
3 154 38 90 0.90196 0.45098 0.00000
|
||||
3 155 34 153 0.90196 0.45098 0.00000
|
||||
3 153 156 155 0.90196 0.45098 0.00000
|
||||
3 156 153 154 0.90196 0.45098 0.00000
|
||||
3 154 157 156 0.90196 0.45098 0.00000
|
||||
3 157 154 39 0.90196 0.45098 0.00000
|
||||
3 39 158 157 0.90196 0.45098 0.00000
|
||||
3 158 39 88 0.90196 0.45098 0.00000
|
||||
3 35 124 155 0.90196 0.45098 0.00000
|
||||
3 155 159 35 0.90196 0.45098 0.00000
|
||||
3 159 155 156 0.90196 0.45098 0.00000
|
||||
3 156 160 159 0.90196 0.45098 0.00000
|
||||
3 160 156 157 0.90196 0.45098 0.00000
|
||||
3 157 65 160 0.90196 0.45098 0.00000
|
||||
3 65 157 158 0.90196 0.45098 0.00000
|
||||
3 268 158 10 0.90196 0.45098 0.00000
|
||||
3 161 35 159 0.90196 0.45098 0.00000
|
||||
3 159 64 161 0.90196 0.45098 0.00000
|
||||
3 64 159 160 0.90196 0.45098 0.00000
|
||||
3 264 160 65 0.90196 0.45098 0.00000
|
||||
3 7 128 161 0.90196 0.45098 0.00000
|
||||
3 262 161 64 0.90196 0.45098 0.00000
|
||||
3 162 3 132 0.90196 0.45098 0.00000
|
||||
3 42 102 162 0.90196 0.45098 0.00000
|
||||
3 162 163 42 0.90196 0.45098 0.00000
|
||||
3 163 162 40 0.90196 0.45098 0.00000
|
||||
3 40 164 163 0.90196 0.45098 0.00000
|
||||
3 164 40 135 0.90196 0.45098 0.00000
|
||||
3 165 42 163 0.90196 0.45098 0.00000
|
||||
3 163 166 165 0.90196 0.45098 0.00000
|
||||
3 166 163 164 0.90196 0.45098 0.00000
|
||||
3 164 167 166 0.90196 0.45098 0.00000
|
||||
3 167 164 41 0.90196 0.45098 0.00000
|
||||
3 41 168 167 0.90196 0.45098 0.00000
|
||||
3 168 41 141 0.90196 0.45098 0.00000
|
||||
3 43 104 165 0.90196 0.45098 0.00000
|
||||
3 165 169 43 0.90196 0.45098 0.00000
|
||||
3 169 165 166 0.90196 0.45098 0.00000
|
||||
3 166 170 169 0.90196 0.45098 0.00000
|
||||
3 170 166 167 0.90196 0.45098 0.00000
|
||||
3 167 54 170 0.90196 0.45098 0.00000
|
||||
3 54 167 168 0.90196 0.45098 0.00000
|
||||
3 131 168 5 0.90196 0.45098 0.00000
|
||||
3 171 43 169 0.90196 0.45098 0.00000
|
||||
3 169 55 171 0.90196 0.45098 0.00000
|
||||
3 55 169 170 0.90196 0.45098 0.00000
|
||||
3 130 170 54 0.90196 0.45098 0.00000
|
||||
3 7 108 171 0.90196 0.45098 0.00000
|
||||
3 128 171 55 0.90196 0.45098 0.00000
|
||||
3 172 0 82 0.90196 0.45098 0.00000
|
||||
3 18 182 172 0.90196 0.45098 0.00000
|
||||
3 172 173 18 0.90196 0.45098 0.00000
|
||||
3 173 172 12 0.90196 0.45098 0.00000
|
||||
3 12 174 173 0.90196 0.45098 0.00000
|
||||
3 174 12 85 0.90196 0.45098 0.00000
|
||||
3 175 18 173 0.90196 0.45098 0.00000
|
||||
3 173 176 175 0.90196 0.45098 0.00000
|
||||
3 176 173 174 0.90196 0.45098 0.00000
|
||||
3 174 177 176 0.90196 0.45098 0.00000
|
||||
3 177 174 13 0.90196 0.45098 0.00000
|
||||
3 13 178 177 0.90196 0.45098 0.00000
|
||||
3 178 13 91 0.90196 0.45098 0.00000
|
||||
3 19 184 175 0.90196 0.45098 0.00000
|
||||
3 175 179 19 0.90196 0.45098 0.00000
|
||||
3 179 175 176 0.90196 0.45098 0.00000
|
||||
3 176 180 179 0.90196 0.45098 0.00000
|
||||
3 180 176 177 0.90196 0.45098 0.00000
|
||||
3 177 36 180 0.90196 0.45098 0.00000
|
||||
3 36 177 178 0.90196 0.45098 0.00000
|
||||
3 92 178 2 0.90196 0.45098 0.00000
|
||||
3 181 19 179 0.90196 0.45098 0.00000
|
||||
3 179 37 181 0.90196 0.45098 0.00000
|
||||
3 37 179 180 0.90196 0.45098 0.00000
|
||||
3 95 180 36 0.90196 0.45098 0.00000
|
||||
3 8 188 181 0.90196 0.45098 0.00000
|
||||
3 101 181 37 0.90196 0.45098 0.00000
|
||||
3 182 0 172 0.90196 0.45098 0.00000
|
||||
3 14 142 182 0.90196 0.45098 0.00000
|
||||
3 182 183 14 0.90196 0.45098 0.00000
|
||||
3 183 182 18 0.90196 0.45098 0.00000
|
||||
3 18 184 183 0.90196 0.45098 0.00000
|
||||
3 184 18 175 0.90196 0.45098 0.00000
|
||||
3 185 14 183 0.90196 0.45098 0.00000
|
||||
3 183 186 185 0.90196 0.45098 0.00000
|
||||
3 186 183 184 0.90196 0.45098 0.00000
|
||||
3 184 187 186 0.90196 0.45098 0.00000
|
||||
3 187 184 19 0.90196 0.45098 0.00000
|
||||
3 19 188 187 0.90196 0.45098 0.00000
|
||||
3 188 19 181 0.90196 0.45098 0.00000
|
||||
3 15 144 185 0.90196 0.45098 0.00000
|
||||
3 185 189 15 0.90196 0.45098 0.00000
|
||||
3 189 185 186 0.90196 0.45098 0.00000
|
||||
3 186 190 189 0.90196 0.45098 0.00000
|
||||
3 190 186 187 0.90196 0.45098 0.00000
|
||||
3 187 51 190 0.90196 0.45098 0.00000
|
||||
3 51 187 188 0.90196 0.45098 0.00000
|
||||
3 228 188 8 0.90196 0.45098 0.00000
|
||||
3 191 15 189 0.90196 0.45098 0.00000
|
||||
3 189 50 191 0.90196 0.45098 0.00000
|
||||
3 50 189 190 0.90196 0.45098 0.00000
|
||||
3 224 190 51 0.90196 0.45098 0.00000
|
||||
3 4 148 191 0.90196 0.45098 0.00000
|
||||
3 222 191 50 0.90196 0.45098 0.00000
|
||||
3 192 5 141 0.90196 0.45098 0.00000
|
||||
3 56 98 192 0.90196 0.45098 0.00000
|
||||
3 192 193 56 0.90196 0.45098 0.00000
|
||||
3 193 192 58 0.90196 0.45098 0.00000
|
||||
3 58 194 193 0.90196 0.45098 0.00000
|
||||
3 194 58 140 0.90196 0.45098 0.00000
|
||||
3 195 56 193 0.90196 0.45098 0.00000
|
||||
3 193 196 195 0.90196 0.45098 0.00000
|
||||
3 196 193 194 0.90196 0.45098 0.00000
|
||||
3 194 197 196 0.90196 0.45098 0.00000
|
||||
3 197 194 59 0.90196 0.45098 0.00000
|
||||
3 59 198 197 0.90196 0.45098 0.00000
|
||||
3 198 59 138 0.90196 0.45098 0.00000
|
||||
3 57 100 195 0.90196 0.45098 0.00000
|
||||
3 195 199 57 0.90196 0.45098 0.00000
|
||||
3 199 195 196 0.90196 0.45098 0.00000
|
||||
3 196 200 199 0.90196 0.45098 0.00000
|
||||
3 200 196 197 0.90196 0.45098 0.00000
|
||||
3 197 69 200 0.90196 0.45098 0.00000
|
||||
3 69 197 198 0.90196 0.45098 0.00000
|
||||
3 231 198 9 0.90196 0.45098 0.00000
|
||||
3 201 57 199 0.90196 0.45098 0.00000
|
||||
3 199 68 201 0.90196 0.45098 0.00000
|
||||
3 68 199 200 0.90196 0.45098 0.00000
|
||||
3 230 200 69 0.90196 0.45098 0.00000
|
||||
3 8 101 201 0.90196 0.45098 0.00000
|
||||
3 228 201 68 0.90196 0.45098 0.00000
|
||||
3 202 1 112 0.90196 0.45098 0.00000
|
||||
3 28 212 202 0.90196 0.45098 0.00000
|
||||
3 202 203 28 0.90196 0.45098 0.00000
|
||||
3 203 202 24 0.90196 0.45098 0.00000
|
||||
3 24 204 203 0.90196 0.45098 0.00000
|
||||
3 204 24 115 0.90196 0.45098 0.00000
|
||||
3 205 28 203 0.90196 0.45098 0.00000
|
||||
3 203 206 205 0.90196 0.45098 0.00000
|
||||
3 206 203 204 0.90196 0.45098 0.00000
|
||||
3 204 207 206 0.90196 0.45098 0.00000
|
||||
3 207 204 25 0.90196 0.45098 0.00000
|
||||
3 25 208 207 0.90196 0.45098 0.00000
|
||||
3 208 25 121 0.90196 0.45098 0.00000
|
||||
3 29 214 205 0.90196 0.45098 0.00000
|
||||
3 205 209 29 0.90196 0.45098 0.00000
|
||||
3 209 205 206 0.90196 0.45098 0.00000
|
||||
3 206 210 209 0.90196 0.45098 0.00000
|
||||
3 210 206 207 0.90196 0.45098 0.00000
|
||||
3 207 52 210 0.90196 0.45098 0.00000
|
||||
3 52 207 208 0.90196 0.45098 0.00000
|
||||
3 222 208 4 0.90196 0.45098 0.00000
|
||||
3 211 29 209 0.90196 0.45098 0.00000
|
||||
3 209 53 211 0.90196 0.45098 0.00000
|
||||
3 53 209 210 0.90196 0.45098 0.00000
|
||||
3 225 210 52 0.90196 0.45098 0.00000
|
||||
3 9 218 211 0.90196 0.45098 0.00000
|
||||
3 231 211 53 0.90196 0.45098 0.00000
|
||||
3 212 1 202 0.90196 0.45098 0.00000
|
||||
3 22 72 212 0.90196 0.45098 0.00000
|
||||
3 212 213 22 0.90196 0.45098 0.00000
|
||||
3 213 212 28 0.90196 0.45098 0.00000
|
||||
3 28 214 213 0.90196 0.45098 0.00000
|
||||
3 214 28 205 0.90196 0.45098 0.00000
|
||||
3 215 22 213 0.90196 0.45098 0.00000
|
||||
3 213 216 215 0.90196 0.45098 0.00000
|
||||
3 216 213 214 0.90196 0.45098 0.00000
|
||||
3 214 217 216 0.90196 0.45098 0.00000
|
||||
3 217 214 29 0.90196 0.45098 0.00000
|
||||
3 29 218 217 0.90196 0.45098 0.00000
|
||||
3 218 29 211 0.90196 0.45098 0.00000
|
||||
3 23 74 215 0.90196 0.45098 0.00000
|
||||
3 215 219 23 0.90196 0.45098 0.00000
|
||||
3 219 215 216 0.90196 0.45098 0.00000
|
||||
3 216 220 219 0.90196 0.45098 0.00000
|
||||
3 220 216 217 0.90196 0.45098 0.00000
|
||||
3 217 45 220 0.90196 0.45098 0.00000
|
||||
3 45 217 218 0.90196 0.45098 0.00000
|
||||
3 138 218 9 0.90196 0.45098 0.00000
|
||||
3 221 23 219 0.90196 0.45098 0.00000
|
||||
3 219 44 221 0.90196 0.45098 0.00000
|
||||
3 44 219 220 0.90196 0.45098 0.00000
|
||||
3 134 220 45 0.90196 0.45098 0.00000
|
||||
3 3 78 221 0.90196 0.45098 0.00000
|
||||
3 132 221 44 0.90196 0.45098 0.00000
|
||||
3 222 4 191 0.90196 0.45098 0.00000
|
||||
3 52 208 222 0.90196 0.45098 0.00000
|
||||
3 222 223 52 0.90196 0.45098 0.00000
|
||||
3 223 222 50 0.90196 0.45098 0.00000
|
||||
3 50 224 223 0.90196 0.45098 0.00000
|
||||
3 224 50 190 0.90196 0.45098 0.00000
|
||||
3 225 52 223 0.90196 0.45098 0.00000
|
||||
3 223 226 225 0.90196 0.45098 0.00000
|
||||
3 226 223 224 0.90196 0.45098 0.00000
|
||||
3 224 227 226 0.90196 0.45098 0.00000
|
||||
3 227 224 51 0.90196 0.45098 0.00000
|
||||
3 51 228 227 0.90196 0.45098 0.00000
|
||||
3 228 51 188 0.90196 0.45098 0.00000
|
||||
3 53 210 225 0.90196 0.45098 0.00000
|
||||
3 225 229 53 0.90196 0.45098 0.00000
|
||||
3 229 225 226 0.90196 0.45098 0.00000
|
||||
3 226 230 229 0.90196 0.45098 0.00000
|
||||
3 230 226 227 0.90196 0.45098 0.00000
|
||||
3 227 68 230 0.90196 0.45098 0.00000
|
||||
3 68 227 228 0.90196 0.45098 0.00000
|
||||
3 201 228 8 0.90196 0.45098 0.00000
|
||||
3 231 53 229 0.90196 0.45098 0.00000
|
||||
3 229 69 231 0.90196 0.45098 0.00000
|
||||
3 69 229 230 0.90196 0.45098 0.00000
|
||||
3 200 230 68 0.90196 0.45098 0.00000
|
||||
3 9 211 231 0.90196 0.45098 0.00000
|
||||
3 198 231 69 0.90196 0.45098 0.00000
|
||||
3 232 0 142 0.90196 0.45098 0.00000
|
||||
3 20 82 232 0.90196 0.45098 0.00000
|
||||
3 232 233 20 0.90196 0.45098 0.00000
|
||||
3 233 232 16 0.90196 0.45098 0.00000
|
||||
3 16 234 233 0.90196 0.45098 0.00000
|
||||
3 234 16 145 0.90196 0.45098 0.00000
|
||||
3 235 20 233 0.90196 0.45098 0.00000
|
||||
3 233 236 235 0.90196 0.45098 0.00000
|
||||
3 236 233 234 0.90196 0.45098 0.00000
|
||||
3 234 237 236 0.90196 0.45098 0.00000
|
||||
3 237 234 17 0.90196 0.45098 0.00000
|
||||
3 17 238 237 0.90196 0.45098 0.00000
|
||||
3 238 17 151 0.90196 0.45098 0.00000
|
||||
3 21 84 235 0.90196 0.45098 0.00000
|
||||
3 235 239 21 0.90196 0.45098 0.00000
|
||||
3 239 235 236 0.90196 0.45098 0.00000
|
||||
3 236 240 239 0.90196 0.45098 0.00000
|
||||
3 240 236 237 0.90196 0.45098 0.00000
|
||||
3 237 60 240 0.90196 0.45098 0.00000
|
||||
3 60 237 238 0.90196 0.45098 0.00000
|
||||
3 242 238 6 0.90196 0.45098 0.00000
|
||||
3 241 21 239 0.90196 0.45098 0.00000
|
||||
3 239 61 241 0.90196 0.45098 0.00000
|
||||
3 61 239 240 0.90196 0.45098 0.00000
|
||||
3 245 240 60 0.90196 0.45098 0.00000
|
||||
3 10 88 241 0.90196 0.45098 0.00000
|
||||
3 251 241 61 0.90196 0.45098 0.00000
|
||||
3 242 6 261 0.90196 0.45098 0.00000
|
||||
3 60 238 242 0.90196 0.45098 0.00000
|
||||
3 242 243 60 0.90196 0.45098 0.00000
|
||||
3 243 242 62 0.90196 0.45098 0.00000
|
||||
3 62 244 243 0.90196 0.45098 0.00000
|
||||
3 244 62 260 0.90196 0.45098 0.00000
|
||||
3 245 60 243 0.90196 0.45098 0.00000
|
||||
3 243 246 245 0.90196 0.45098 0.00000
|
||||
3 246 243 244 0.90196 0.45098 0.00000
|
||||
3 244 247 246 0.90196 0.45098 0.00000
|
||||
3 247 244 63 0.90196 0.45098 0.00000
|
||||
3 63 248 247 0.90196 0.45098 0.00000
|
||||
3 248 63 258 0.90196 0.45098 0.00000
|
||||
3 61 240 245 0.90196 0.45098 0.00000
|
||||
3 245 249 61 0.90196 0.45098 0.00000
|
||||
3 249 245 246 0.90196 0.45098 0.00000
|
||||
3 246 250 249 0.90196 0.45098 0.00000
|
||||
3 250 246 247 0.90196 0.45098 0.00000
|
||||
3 247 71 250 0.90196 0.45098 0.00000
|
||||
3 71 247 248 0.90196 0.45098 0.00000
|
||||
3 271 248 11 0.90196 0.45098 0.00000
|
||||
3 251 61 249 0.90196 0.45098 0.00000
|
||||
3 249 70 251 0.90196 0.45098 0.00000
|
||||
3 70 249 250 0.90196 0.45098 0.00000
|
||||
3 270 250 71 0.90196 0.45098 0.00000
|
||||
3 10 241 251 0.90196 0.45098 0.00000
|
||||
3 268 251 70 0.90196 0.45098 0.00000
|
||||
3 252 1 72 0.90196 0.45098 0.00000
|
||||
3 26 112 252 0.90196 0.45098 0.00000
|
||||
3 252 253 26 0.90196 0.45098 0.00000
|
||||
3 253 252 30 0.90196 0.45098 0.00000
|
||||
3 30 254 253 0.90196 0.45098 0.00000
|
||||
3 254 30 75 0.90196 0.45098 0.00000
|
||||
3 255 26 253 0.90196 0.45098 0.00000
|
||||
3 253 256 255 0.90196 0.45098 0.00000
|
||||
3 256 253 254 0.90196 0.45098 0.00000
|
||||
3 254 257 256 0.90196 0.45098 0.00000
|
||||
3 257 254 31 0.90196 0.45098 0.00000
|
||||
3 31 258 257 0.90196 0.45098 0.00000
|
||||
3 258 31 81 0.90196 0.45098 0.00000
|
||||
3 27 114 255 0.90196 0.45098 0.00000
|
||||
3 255 259 27 0.90196 0.45098 0.00000
|
||||
3 259 255 256 0.90196 0.45098 0.00000
|
||||
3 256 260 259 0.90196 0.45098 0.00000
|
||||
3 260 256 257 0.90196 0.45098 0.00000
|
||||
3 257 63 260 0.90196 0.45098 0.00000
|
||||
3 63 257 258 0.90196 0.45098 0.00000
|
||||
3 248 258 11 0.90196 0.45098 0.00000
|
||||
3 261 27 259 0.90196 0.45098 0.00000
|
||||
3 259 62 261 0.90196 0.45098 0.00000
|
||||
3 62 259 260 0.90196 0.45098 0.00000
|
||||
3 244 260 63 0.90196 0.45098 0.00000
|
||||
3 6 118 261 0.90196 0.45098 0.00000
|
||||
3 242 261 62 0.90196 0.45098 0.00000
|
||||
3 262 7 161 0.90196 0.45098 0.00000
|
||||
3 66 108 262 0.90196 0.45098 0.00000
|
||||
3 262 263 66 0.90196 0.45098 0.00000
|
||||
3 263 262 64 0.90196 0.45098 0.00000
|
||||
3 64 264 263 0.90196 0.45098 0.00000
|
||||
3 264 64 160 0.90196 0.45098 0.00000
|
||||
3 265 66 263 0.90196 0.45098 0.00000
|
||||
3 263 266 265 0.90196 0.45098 0.00000
|
||||
3 266 263 264 0.90196 0.45098 0.00000
|
||||
3 264 267 266 0.90196 0.45098 0.00000
|
||||
3 267 264 65 0.90196 0.45098 0.00000
|
||||
3 65 268 267 0.90196 0.45098 0.00000
|
||||
3 268 65 158 0.90196 0.45098 0.00000
|
||||
3 67 110 265 0.90196 0.45098 0.00000
|
||||
3 265 269 67 0.90196 0.45098 0.00000
|
||||
3 269 265 266 0.90196 0.45098 0.00000
|
||||
3 266 270 269 0.90196 0.45098 0.00000
|
||||
3 270 266 267 0.90196 0.45098 0.00000
|
||||
3 267 70 270 0.90196 0.45098 0.00000
|
||||
3 70 267 268 0.90196 0.45098 0.00000
|
||||
3 251 268 10 0.90196 0.45098 0.00000
|
||||
3 271 67 269 0.90196 0.45098 0.00000
|
||||
3 269 71 271 0.90196 0.45098 0.00000
|
||||
3 71 269 270 0.90196 0.45098 0.00000
|
||||
3 250 270 70 0.90196 0.45098 0.00000
|
||||
3 11 111 271 0.90196 0.45098 0.00000
|
||||
3 248 271 71 0.90196 0.45098 0.00000
|
||||
1 0 0.38824 0.60000 0.30196
|
||||
1 1 0.38824 0.60000 0.30196
|
||||
1 2 0.38824 0.60000 0.30196
|
||||
1 3 0.38824 0.60000 0.30196
|
||||
1 4 0.38824 0.60000 0.30196
|
||||
1 5 0.38824 0.60000 0.30196
|
||||
1 6 0.38824 0.60000 0.30196
|
||||
1 7 0.38824 0.60000 0.30196
|
||||
1 8 0.38824 0.60000 0.30196
|
||||
1 9 0.38824 0.60000 0.30196
|
||||
1 10 0.38824 0.60000 0.30196
|
||||
1 11 0.38824 0.60000 0.30196
|
@ -0,0 +1,376 @@
|
||||
OFF
|
||||
122 252 0
|
||||
0 0.5257311121191336 0.85065080835204
|
||||
0 0.5257311121191336 -0.85065080835204
|
||||
0 -0.5257311121191336 0.85065080835204
|
||||
0 -0.5257311121191336 -0.85065080835204
|
||||
0.5257311121191336 0.85065080835204 0
|
||||
0.5257311121191336 -0.85065080835204 0
|
||||
-0.5257311121191336 0.85065080835204 0
|
||||
-0.5257311121191336 -0.85065080835204 0
|
||||
0.85065080835204 0 0.5257311121191336
|
||||
0.85065080835204 0 -0.5257311121191336
|
||||
-0.85065080835204 0 0.5257311121191336
|
||||
-0.85065080835204 0 -0.5257311121191336
|
||||
2.175242402100701e-16 -1.643460219210441e-32 1
|
||||
0.3090169943749475 0.8090169943749472 0.5000000000000002
|
||||
-0.3090169943749475 0.8090169943749472 0.5000000000000002
|
||||
0.4999999999999999 0.3090169943749474 0.8090169943749472
|
||||
-0.5000000000000001 0.3090169943749475 0.8090169943749472
|
||||
2.175242402100701e-16 1.643460219210441e-32 -1
|
||||
0.3090169943749475 0.8090169943749472 -0.5000000000000002
|
||||
-0.3090169943749475 0.8090169943749472 -0.5000000000000002
|
||||
0.5 0.3090169943749473 -0.8090169943749475
|
||||
-0.4999999999999999 0.3090169943749474 -0.8090169943749472
|
||||
0.3090169943749473 -0.8090169943749475 0.5
|
||||
-0.3090169943749475 -0.8090169943749472 0.5000000000000002
|
||||
0.5 -0.3090169943749473 0.8090169943749475
|
||||
-0.4999999999999999 -0.3090169943749474 0.8090169943749472
|
||||
0.3090169943749473 -0.8090169943749475 -0.5
|
||||
-0.3090169943749473 -0.8090169943749475 -0.5
|
||||
0.5 -0.3090169943749472 -0.8090169943749475
|
||||
-0.5000000000000001 -0.3090169943749475 -0.8090169943749472
|
||||
0 1 4.350484804201401e-17
|
||||
0.8090169943749475 0.5 0.3090169943749472
|
||||
0.8090169943749472 0.4999999999999999 -0.3090169943749473
|
||||
0 -1 -4.350484804201401e-17
|
||||
0.8090169943749472 -0.4999999999999999 0.3090169943749473
|
||||
0.8090169943749475 -0.5 -0.3090169943749472
|
||||
-0.8090169943749472 0.4999999999999999 0.3090169943749473
|
||||
-0.8090169943749472 0.4999999999999999 -0.3090169943749475
|
||||
-0.8090169943749475 -0.5 0.3090169943749472
|
||||
-0.8090169943749472 -0.4999999999999999 -0.3090169943749473
|
||||
1 2.175242402100701e-16 -1.643460219210441e-32
|
||||
-1 -2.175242402100701e-16 -1.643460219210441e-32
|
||||
-0.1803319730021167 0.289241011911498 -0.9401170227910867
|
||||
-0.35682208977309 -3.124513936890529e-17 -0.9341723589627157
|
||||
-0.1803319730021166 -0.2892410119114981 -0.9401170227910867
|
||||
-0.6483337612153338 -5.436311068297173e-17 -0.7613562464893677
|
||||
-0.1803319730021166 0.2892410119114981 0.9401170227910867
|
||||
-0.35682208977309 3.09531117213564e-17 0.9341723589627158
|
||||
-0.6483337612153338 5.402340711901317e-17 0.7613562464893677
|
||||
-0.1803319730021167 -0.289241011911498 0.9401170227910867
|
||||
0.291783261575753 -0.5810242734872509 0.7597850497889703
|
||||
0.5773502691896258 -0.5773502691896256 0.5773502691896258
|
||||
0.5810242734872511 -0.7597850497889701 0.291783261575753
|
||||
0.7597850497889702 -0.291783261575753 0.5810242734872511
|
||||
-0.291783261575753 -0.5810242734872509 -0.7597850497889703
|
||||
-0.5773502691896258 -0.5773502691896256 -0.5773502691896258
|
||||
-0.5810242734872511 -0.7597850497889701 -0.291783261575753
|
||||
-0.7597850497889702 -0.291783261575753 -0.5810242734872511
|
||||
-2.313323858849861e-18 0.7613562464893674 -0.6483337612153339
|
||||
3.124513936890529e-17 0.9341723589627158 -0.3568220897730901
|
||||
-0.2892410119114981 0.9401170227910867 -0.1803319730021165
|
||||
0.2892410119114981 0.9401170227910867 -0.1803319730021165
|
||||
-2.313323858849861e-18 -0.7613562464893674 0.6483337612153339
|
||||
3.124513936890529e-17 -0.9341723589627158 0.3568220897730901
|
||||
-0.2892410119114981 -0.9401170227910867 0.1803319730021165
|
||||
0.2892410119114981 -0.9401170227910867 0.1803319730021165
|
||||
0.2917832615757529 -0.5810242734872509 -0.7597850497889704
|
||||
0.5773502691896258 -0.5773502691896257 -0.5773502691896258
|
||||
0.7597850497889701 -0.2917832615757531 -0.5810242734872512
|
||||
0.5810242734872511 -0.7597850497889701 -0.291783261575753
|
||||
2.313323858849861e-18 0.7613562464893674 0.6483337612153339
|
||||
-3.124513936890529e-17 0.9341723589627158 0.3568220897730901
|
||||
0.2892410119114981 0.9401170227910867 0.1803319730021165
|
||||
-0.2892410119114981 0.9401170227910867 0.1803319730021165
|
||||
-0.2917832615757529 -0.5810242734872509 0.7597850497889704
|
||||
-0.5773502691896258 -0.5773502691896257 0.5773502691896258
|
||||
-0.7597850497889701 -0.2917832615757531 0.5810242734872512
|
||||
-0.5810242734872511 -0.7597850497889701 0.291783261575753
|
||||
2.313323858849861e-18 -0.7613562464893674 -0.6483337612153339
|
||||
-3.124513936890529e-17 -0.9341723589627158 -0.3568220897730901
|
||||
0.2892410119114981 -0.9401170227910867 -0.1803319730021165
|
||||
-0.2892410119114981 -0.9401170227910867 -0.1803319730021165
|
||||
0.1803319730021167 0.289241011911498 0.9401170227910867
|
||||
0.35682208977309 -3.124513936890529e-17 0.9341723589627157
|
||||
0.1803319730021166 -0.2892410119114981 0.9401170227910867
|
||||
0.6483337612153338 -5.436311068297173e-17 0.7613562464893677
|
||||
0.2917832615757529 0.5810242734872509 0.7597850497889704
|
||||
0.5773502691896258 0.5773502691896257 0.5773502691896258
|
||||
0.7597850497889701 0.2917832615757531 0.5810242734872512
|
||||
0.5810242734872511 0.7597850497889701 0.291783261575753
|
||||
0.7613562464893677 -0.6483337612153338 5.436311068297173e-17
|
||||
0.9341723589627157 -0.35682208977309 3.124513936890529e-17
|
||||
0.9401170227910867 -0.1803319730021167 -0.289241011911498
|
||||
0.9401170227910867 -0.1803319730021166 0.2892410119114981
|
||||
0.291783261575753 0.5810242734872509 -0.7597850497889703
|
||||
0.5773502691896258 0.5773502691896256 -0.5773502691896258
|
||||
0.5810242734872511 0.7597850497889701 -0.291783261575753
|
||||
0.7597850497889702 0.291783261575753 -0.5810242734872511
|
||||
0.1803319730021166 0.2892410119114981 -0.9401170227910867
|
||||
0.35682208977309 3.09531117213564e-17 -0.9341723589627158
|
||||
0.6483337612153338 5.402340711901317e-17 -0.7613562464893677
|
||||
0.1803319730021167 -0.289241011911498 -0.9401170227910867
|
||||
0.7613562464893677 0.6483337612153338 -5.436311068297173e-17
|
||||
0.9341723589627157 0.35682208977309 -3.124513936890529e-17
|
||||
0.9401170227910867 0.1803319730021167 0.289241011911498
|
||||
0.9401170227910867 0.1803319730021166 -0.2892410119114981
|
||||
-0.291783261575753 0.5810242734872509 0.7597850497889703
|
||||
-0.5773502691896258 0.5773502691896256 0.5773502691896258
|
||||
-0.5810242734872511 0.7597850497889701 0.291783261575753
|
||||
-0.7597850497889702 0.291783261575753 0.5810242734872511
|
||||
-0.7613562464893677 0.6483337612153338 5.436311068297173e-17
|
||||
-0.9341723589627157 0.35682208977309 3.124513936890529e-17
|
||||
-0.9401170227910867 0.1803319730021167 -0.289241011911498
|
||||
-0.9401170227910867 0.1803319730021166 0.2892410119114981
|
||||
-0.2917832615757529 0.5810242734872509 -0.7597850497889704
|
||||
-0.5773502691896258 0.5773502691896257 -0.5773502691896258
|
||||
-0.7597850497889701 0.2917832615757531 -0.5810242734872512
|
||||
-0.5810242734872511 0.7597850497889701 -0.291783261575753
|
||||
-0.7613562464893677 -0.6483337612153338 -5.436311068297173e-17
|
||||
-0.9341723589627157 -0.35682208977309 -3.124513936890529e-17
|
||||
-0.9401170227910867 -0.1803319730021167 0.289241011911498
|
||||
-0.9401170227910867 -0.1803319730021166 -0.2892410119114981
|
||||
3 42 1 98 0.90196 0.45098 0.00000
|
||||
3 21 114 42 0.90196 0.45098 0.00000
|
||||
3 42 43 21 0.90196 0.45098 0.00000
|
||||
3 43 42 17 0.90196 0.45098 0.00000
|
||||
3 17 44 43 0.90196 0.45098 0.00000
|
||||
3 44 17 101 0.90196 0.45098 0.00000
|
||||
3 45 21 43 0.90196 0.45098 0.00000
|
||||
3 43 29 45 0.90196 0.45098 0.00000
|
||||
3 29 43 44 0.90196 0.45098 0.00000
|
||||
3 54 44 3 0.90196 0.45098 0.00000
|
||||
3 11 116 45 0.90196 0.45098 0.00000
|
||||
3 57 45 29 0.90196 0.45098 0.00000
|
||||
3 46 0 106 0.90196 0.45098 0.00000
|
||||
3 12 82 46 0.90196 0.45098 0.00000
|
||||
3 46 47 12 0.90196 0.45098 0.00000
|
||||
3 47 46 16 0.90196 0.45098 0.00000
|
||||
3 16 48 47 0.90196 0.45098 0.00000
|
||||
3 48 16 109 0.90196 0.45098 0.00000
|
||||
3 49 12 47 0.90196 0.45098 0.00000
|
||||
3 47 25 49 0.90196 0.45098 0.00000
|
||||
3 25 47 48 0.90196 0.45098 0.00000
|
||||
3 76 48 10 0.90196 0.45098 0.00000
|
||||
3 2 84 49 0.90196 0.45098 0.00000
|
||||
3 74 49 25 0.90196 0.45098 0.00000
|
||||
3 50 2 62 0.90196 0.45098 0.00000
|
||||
3 24 84 50 0.90196 0.45098 0.00000
|
||||
3 50 51 24 0.90196 0.45098 0.00000
|
||||
3 51 50 22 0.90196 0.45098 0.00000
|
||||
3 22 52 51 0.90196 0.45098 0.00000
|
||||
3 52 22 65 0.90196 0.45098 0.00000
|
||||
3 53 24 51 0.90196 0.45098 0.00000
|
||||
3 51 34 53 0.90196 0.45098 0.00000
|
||||
3 34 51 52 0.90196 0.45098 0.00000
|
||||
3 90 52 5 0.90196 0.45098 0.00000
|
||||
3 8 85 53 0.90196 0.45098 0.00000
|
||||
3 93 53 34 0.90196 0.45098 0.00000
|
||||
3 54 3 78 0.90196 0.45098 0.00000
|
||||
3 29 44 54 0.90196 0.45098 0.00000
|
||||
3 54 55 29 0.90196 0.45098 0.00000
|
||||
3 55 54 27 0.90196 0.45098 0.00000
|
||||
3 27 56 55 0.90196 0.45098 0.00000
|
||||
3 56 27 81 0.90196 0.45098 0.00000
|
||||
3 57 29 55 0.90196 0.45098 0.00000
|
||||
3 55 39 57 0.90196 0.45098 0.00000
|
||||
3 39 55 56 0.90196 0.45098 0.00000
|
||||
3 118 56 7 0.90196 0.45098 0.00000
|
||||
3 11 45 57 0.90196 0.45098 0.00000
|
||||
3 121 57 39 0.90196 0.45098 0.00000
|
||||
3 58 1 114 0.90196 0.45098 0.00000
|
||||
3 18 94 58 0.90196 0.45098 0.00000
|
||||
3 58 59 18 0.90196 0.45098 0.00000
|
||||
3 59 58 19 0.90196 0.45098 0.00000
|
||||
3 19 60 59 0.90196 0.45098 0.00000
|
||||
3 60 19 117 0.90196 0.45098 0.00000
|
||||
3 61 18 59 0.90196 0.45098 0.00000
|
||||
3 59 30 61 0.90196 0.45098 0.00000
|
||||
3 30 59 60 0.90196 0.45098 0.00000
|
||||
3 73 60 6 0.90196 0.45098 0.00000
|
||||
3 4 96 61 0.90196 0.45098 0.00000
|
||||
3 72 61 30 0.90196 0.45098 0.00000
|
||||
3 62 2 74 0.90196 0.45098 0.00000
|
||||
3 22 50 62 0.90196 0.45098 0.00000
|
||||
3 62 63 22 0.90196 0.45098 0.00000
|
||||
3 63 62 23 0.90196 0.45098 0.00000
|
||||
3 23 64 63 0.90196 0.45098 0.00000
|
||||
3 64 23 77 0.90196 0.45098 0.00000
|
||||
3 65 22 63 0.90196 0.45098 0.00000
|
||||
3 63 33 65 0.90196 0.45098 0.00000
|
||||
3 33 63 64 0.90196 0.45098 0.00000
|
||||
3 81 64 7 0.90196 0.45098 0.00000
|
||||
3 5 52 65 0.90196 0.45098 0.00000
|
||||
3 80 65 33 0.90196 0.45098 0.00000
|
||||
3 66 3 101 0.90196 0.45098 0.00000
|
||||
3 26 78 66 0.90196 0.45098 0.00000
|
||||
3 66 67 26 0.90196 0.45098 0.00000
|
||||
3 67 66 28 0.90196 0.45098 0.00000
|
||||
3 28 68 67 0.90196 0.45098 0.00000
|
||||
3 68 28 100 0.90196 0.45098 0.00000
|
||||
3 69 26 67 0.90196 0.45098 0.00000
|
||||
3 67 35 69 0.90196 0.45098 0.00000
|
||||
3 35 67 68 0.90196 0.45098 0.00000
|
||||
3 92 68 9 0.90196 0.45098 0.00000
|
||||
3 5 80 69 0.90196 0.45098 0.00000
|
||||
3 90 69 35 0.90196 0.45098 0.00000
|
||||
3 70 0 86 0.90196 0.45098 0.00000
|
||||
3 14 106 70 0.90196 0.45098 0.00000
|
||||
3 70 71 14 0.90196 0.45098 0.00000
|
||||
3 71 70 13 0.90196 0.45098 0.00000
|
||||
3 13 72 71 0.90196 0.45098 0.00000
|
||||
3 72 13 89 0.90196 0.45098 0.00000
|
||||
3 73 14 71 0.90196 0.45098 0.00000
|
||||
3 71 30 73 0.90196 0.45098 0.00000
|
||||
3 30 71 72 0.90196 0.45098 0.00000
|
||||
3 61 72 4 0.90196 0.45098 0.00000
|
||||
3 6 108 73 0.90196 0.45098 0.00000
|
||||
3 60 73 30 0.90196 0.45098 0.00000
|
||||
3 74 2 49 0.90196 0.45098 0.00000
|
||||
3 23 62 74 0.90196 0.45098 0.00000
|
||||
3 74 75 23 0.90196 0.45098 0.00000
|
||||
3 75 74 25 0.90196 0.45098 0.00000
|
||||
3 25 76 75 0.90196 0.45098 0.00000
|
||||
3 76 25 48 0.90196 0.45098 0.00000
|
||||
3 77 23 75 0.90196 0.45098 0.00000
|
||||
3 75 38 77 0.90196 0.45098 0.00000
|
||||
3 38 75 76 0.90196 0.45098 0.00000
|
||||
3 120 76 10 0.90196 0.45098 0.00000
|
||||
3 7 64 77 0.90196 0.45098 0.00000
|
||||
3 118 77 38 0.90196 0.45098 0.00000
|
||||
3 78 3 66 0.90196 0.45098 0.00000
|
||||
3 27 54 78 0.90196 0.45098 0.00000
|
||||
3 78 79 27 0.90196 0.45098 0.00000
|
||||
3 79 78 26 0.90196 0.45098 0.00000
|
||||
3 26 80 79 0.90196 0.45098 0.00000
|
||||
3 80 26 69 0.90196 0.45098 0.00000
|
||||
3 81 27 79 0.90196 0.45098 0.00000
|
||||
3 79 33 81 0.90196 0.45098 0.00000
|
||||
3 33 79 80 0.90196 0.45098 0.00000
|
||||
3 65 80 5 0.90196 0.45098 0.00000
|
||||
3 7 56 81 0.90196 0.45098 0.00000
|
||||
3 64 81 33 0.90196 0.45098 0.00000
|
||||
3 82 0 46 0.90196 0.45098 0.00000
|
||||
3 15 86 82 0.90196 0.45098 0.00000
|
||||
3 82 83 15 0.90196 0.45098 0.00000
|
||||
3 83 82 12 0.90196 0.45098 0.00000
|
||||
3 12 84 83 0.90196 0.45098 0.00000
|
||||
3 84 12 49 0.90196 0.45098 0.00000
|
||||
3 85 15 83 0.90196 0.45098 0.00000
|
||||
3 83 24 85 0.90196 0.45098 0.00000
|
||||
3 24 83 84 0.90196 0.45098 0.00000
|
||||
3 50 84 2 0.90196 0.45098 0.00000
|
||||
3 8 88 85 0.90196 0.45098 0.00000
|
||||
3 53 85 24 0.90196 0.45098 0.00000
|
||||
3 86 0 82 0.90196 0.45098 0.00000
|
||||
3 13 70 86 0.90196 0.45098 0.00000
|
||||
3 86 87 13 0.90196 0.45098 0.00000
|
||||
3 87 86 15 0.90196 0.45098 0.00000
|
||||
3 15 88 87 0.90196 0.45098 0.00000
|
||||
3 88 15 85 0.90196 0.45098 0.00000
|
||||
3 89 13 87 0.90196 0.45098 0.00000
|
||||
3 87 31 89 0.90196 0.45098 0.00000
|
||||
3 31 87 88 0.90196 0.45098 0.00000
|
||||
3 104 88 8 0.90196 0.45098 0.00000
|
||||
3 4 72 89 0.90196 0.45098 0.00000
|
||||
3 102 89 31 0.90196 0.45098 0.00000
|
||||
3 90 5 69 0.90196 0.45098 0.00000
|
||||
3 34 52 90 0.90196 0.45098 0.00000
|
||||
3 90 91 34 0.90196 0.45098 0.00000
|
||||
3 91 90 35 0.90196 0.45098 0.00000
|
||||
3 35 92 91 0.90196 0.45098 0.00000
|
||||
3 92 35 68 0.90196 0.45098 0.00000
|
||||
3 93 34 91 0.90196 0.45098 0.00000
|
||||
3 91 40 93 0.90196 0.45098 0.00000
|
||||
3 40 91 92 0.90196 0.45098 0.00000
|
||||
3 105 92 9 0.90196 0.45098 0.00000
|
||||
3 8 53 93 0.90196 0.45098 0.00000
|
||||
3 104 93 40 0.90196 0.45098 0.00000
|
||||
3 94 1 58 0.90196 0.45098 0.00000
|
||||
3 20 98 94 0.90196 0.45098 0.00000
|
||||
3 94 95 20 0.90196 0.45098 0.00000
|
||||
3 95 94 18 0.90196 0.45098 0.00000
|
||||
3 18 96 95 0.90196 0.45098 0.00000
|
||||
3 96 18 61 0.90196 0.45098 0.00000
|
||||
3 97 20 95 0.90196 0.45098 0.00000
|
||||
3 95 32 97 0.90196 0.45098 0.00000
|
||||
3 32 95 96 0.90196 0.45098 0.00000
|
||||
3 102 96 4 0.90196 0.45098 0.00000
|
||||
3 9 100 97 0.90196 0.45098 0.00000
|
||||
3 105 97 32 0.90196 0.45098 0.00000
|
||||
3 98 1 94 0.90196 0.45098 0.00000
|
||||
3 17 42 98 0.90196 0.45098 0.00000
|
||||
3 98 99 17 0.90196 0.45098 0.00000
|
||||
3 99 98 20 0.90196 0.45098 0.00000
|
||||
3 20 100 99 0.90196 0.45098 0.00000
|
||||
3 100 20 97 0.90196 0.45098 0.00000
|
||||
3 101 17 99 0.90196 0.45098 0.00000
|
||||
3 99 28 101 0.90196 0.45098 0.00000
|
||||
3 28 99 100 0.90196 0.45098 0.00000
|
||||
3 68 100 9 0.90196 0.45098 0.00000
|
||||
3 3 44 101 0.90196 0.45098 0.00000
|
||||
3 66 101 28 0.90196 0.45098 0.00000
|
||||
3 102 4 89 0.90196 0.45098 0.00000
|
||||
3 32 96 102 0.90196 0.45098 0.00000
|
||||
3 102 103 32 0.90196 0.45098 0.00000
|
||||
3 103 102 31 0.90196 0.45098 0.00000
|
||||
3 31 104 103 0.90196 0.45098 0.00000
|
||||
3 104 31 88 0.90196 0.45098 0.00000
|
||||
3 105 32 103 0.90196 0.45098 0.00000
|
||||
3 103 40 105 0.90196 0.45098 0.00000
|
||||
3 40 103 104 0.90196 0.45098 0.00000
|
||||
3 93 104 8 0.90196 0.45098 0.00000
|
||||
3 9 97 105 0.90196 0.45098 0.00000
|
||||
3 92 105 40 0.90196 0.45098 0.00000
|
||||
3 106 0 70 0.90196 0.45098 0.00000
|
||||
3 16 46 106 0.90196 0.45098 0.00000
|
||||
3 106 107 16 0.90196 0.45098 0.00000
|
||||
3 107 106 14 0.90196 0.45098 0.00000
|
||||
3 14 108 107 0.90196 0.45098 0.00000
|
||||
3 108 14 73 0.90196 0.45098 0.00000
|
||||
3 109 16 107 0.90196 0.45098 0.00000
|
||||
3 107 36 109 0.90196 0.45098 0.00000
|
||||
3 36 107 108 0.90196 0.45098 0.00000
|
||||
3 110 108 6 0.90196 0.45098 0.00000
|
||||
3 10 48 109 0.90196 0.45098 0.00000
|
||||
3 113 109 36 0.90196 0.45098 0.00000
|
||||
3 110 6 117 0.90196 0.45098 0.00000
|
||||
3 36 108 110 0.90196 0.45098 0.00000
|
||||
3 110 111 36 0.90196 0.45098 0.00000
|
||||
3 111 110 37 0.90196 0.45098 0.00000
|
||||
3 37 112 111 0.90196 0.45098 0.00000
|
||||
3 112 37 116 0.90196 0.45098 0.00000
|
||||
3 113 36 111 0.90196 0.45098 0.00000
|
||||
3 111 41 113 0.90196 0.45098 0.00000
|
||||
3 41 111 112 0.90196 0.45098 0.00000
|
||||
3 121 112 11 0.90196 0.45098 0.00000
|
||||
3 10 109 113 0.90196 0.45098 0.00000
|
||||
3 120 113 41 0.90196 0.45098 0.00000
|
||||
3 114 1 42 0.90196 0.45098 0.00000
|
||||
3 19 58 114 0.90196 0.45098 0.00000
|
||||
3 114 115 19 0.90196 0.45098 0.00000
|
||||
3 115 114 21 0.90196 0.45098 0.00000
|
||||
3 21 116 115 0.90196 0.45098 0.00000
|
||||
3 116 21 45 0.90196 0.45098 0.00000
|
||||
3 117 19 115 0.90196 0.45098 0.00000
|
||||
3 115 37 117 0.90196 0.45098 0.00000
|
||||
3 37 115 116 0.90196 0.45098 0.00000
|
||||
3 112 116 11 0.90196 0.45098 0.00000
|
||||
3 6 60 117 0.90196 0.45098 0.00000
|
||||
3 110 117 37 0.90196 0.45098 0.00000
|
||||
3 118 7 77 0.90196 0.45098 0.00000
|
||||
3 39 56 118 0.90196 0.45098 0.00000
|
||||
3 118 119 39 0.90196 0.45098 0.00000
|
||||
3 119 118 38 0.90196 0.45098 0.00000
|
||||
3 38 120 119 0.90196 0.45098 0.00000
|
||||
3 120 38 76 0.90196 0.45098 0.00000
|
||||
3 121 39 119 0.90196 0.45098 0.00000
|
||||
3 119 41 121 0.90196 0.45098 0.00000
|
||||
3 41 119 120 0.90196 0.45098 0.00000
|
||||
3 113 120 10 0.90196 0.45098 0.00000
|
||||
3 11 57 121 0.90196 0.45098 0.00000
|
||||
3 112 121 41 0.90196 0.45098 0.00000
|
||||
1 0 0.38824 0.60000 0.30196
|
||||
1 1 0.38824 0.60000 0.30196
|
||||
1 2 0.38824 0.60000 0.30196
|
||||
1 3 0.38824 0.60000 0.30196
|
||||
1 4 0.38824 0.60000 0.30196
|
||||
1 5 0.38824 0.60000 0.30196
|
||||
1 6 0.38824 0.60000 0.30196
|
||||
1 7 0.38824 0.60000 0.30196
|
||||
1 8 0.38824 0.60000 0.30196
|
||||
1 9 0.38824 0.60000 0.30196
|
||||
1 10 0.38824 0.60000 0.30196
|
||||
1 11 0.38824 0.60000 0.30196
|
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)
|
56
pyrate/tests/plan/graph/generate/test_off_handler.py
Normal file
@ -0,0 +1,56 @@
|
||||
"""Tests the Aptiprism OFF file handler."""
|
||||
|
||||
# Standard library
|
||||
import os.path
|
||||
|
||||
# Testing
|
||||
import unittest
|
||||
|
||||
# Scientific
|
||||
import numpy as np
|
||||
|
||||
# Module under test
|
||||
from pyrate.plan.graph.generate import _parse_off_file
|
||||
|
||||
|
||||
TEST_FILES_DIR = os.path.realpath(os.path.join(os.path.dirname(__file__), "example_files/"))
|
||||
TEST_FILES = [
|
||||
os.path.join(TEST_FILES_DIR, "geodestic_file_1.off"),
|
||||
os.path.join(TEST_FILES_DIR, "geodestic_file_2.off"),
|
||||
os.path.join(TEST_FILES_DIR, "geodesic_-M_s_-c_2_-f_2_ico.off"),
|
||||
]
|
||||
|
||||
|
||||
class TestOffHandler(unittest.TestCase):
|
||||
"""Tests the Aptiprism OFF file handler using some examples."""
|
||||
|
||||
def test_with_example_files(self):
|
||||
"""Tests the Aptiprism OFF file handler using three example files."""
|
||||
|
||||
for test_file in TEST_FILES:
|
||||
with self.subTest(f'Test file "{test_file}"'):
|
||||
|
||||
# test that it does not crash
|
||||
with open(test_file, "r", encoding="utf-8") as myfile:
|
||||
source = myfile.read()
|
||||
latitudes, longitudes, edges = _parse_off_file(source)
|
||||
|
||||
if "geodesic_-M_s_-c_2_-f_2_ico" in test_file:
|
||||
self.assertEqual(
|
||||
len(latitudes), 122, f"wrong total number of nodes: {len(latitudes)} instead of 122"
|
||||
)
|
||||
self.assertEqual(
|
||||
edges.shape[0], 360, f"wrong total number of edges: {edges.shape[0]} instead of 360"
|
||||
)
|
||||
|
||||
# the shapes of the returned arrays must match
|
||||
self.assertEqual(
|
||||
latitudes.shape, longitudes.shape, "latitude and longitude must have the same shape"
|
||||
)
|
||||
self.assertGreater(len(latitudes), 0, "no points found")
|
||||
|
||||
# the edges must be valid indices into the edges
|
||||
self.assertTrue(
|
||||
np.all(edges[:, :] >= 0) and np.all(edges[:, :] < len(latitudes)),
|
||||
"some edges reference non-existent points",
|
||||
)
|
166
pyrate/tests/plan/graph/test_geo_graph.py
Normal file
@ -0,0 +1,166 @@
|
||||
"""Asserts correct behaviour of the geo-referenced graph navigation.
|
||||
|
||||
See Also:
|
||||
tests/common/raster_datasets/test_transformers_concrete.py
|
||||
"""
|
||||
|
||||
# Standard library
|
||||
from copy import deepcopy
|
||||
import os.path
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest import TestCase
|
||||
|
||||
# Scientific
|
||||
import numpy
|
||||
from numpy import arange
|
||||
from numpy import array
|
||||
from numpy import empty
|
||||
from numpy.testing import assert_array_equal
|
||||
from pandas import DataFrame
|
||||
|
||||
# Graph generation / Module under test
|
||||
from pyrate.common.raster_datasets import transformers_concrete
|
||||
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
|
||||
|
||||
|
||||
from .generate.test_graph_generation import EXAMPLE_DISTANCES_KILOMETERS
|
||||
|
||||
|
||||
class TestGeoNavigationGraph(TestCase):
|
||||
"""Tests properties specific to :class:`pyrate.plan.graph.GeoNavigationGraph`."""
|
||||
|
||||
def test_create_invalid_duplicate_argument_nodes(self) -> None:
|
||||
"""Tests supplying nodes to from_coordinates_radians/from_coordinates_degrees raises an Exception."""
|
||||
for function in [
|
||||
GeoNavigationGraph.from_coordinates_degrees,
|
||||
GeoNavigationGraph.from_coordinates_radians,
|
||||
]:
|
||||
with self.subTest(msg=f"function {str(function)}"):
|
||||
with self.assertRaises(Exception): # noqa: H202
|
||||
function( # type: ignore
|
||||
latitudes=empty((0,)), longitudes=empty((0,)), edges=empty((0, 2)), nodes=DataFrame()
|
||||
)
|
||||
|
||||
def test_node_radius_constructor(self) -> None:
|
||||
"""Tests that only invalid inputs to node_radius raise exceptions."""
|
||||
GeoNavigationGraph.from_coordinates_degrees(
|
||||
latitudes=empty((0,)), longitudes=empty((0,)), edges=empty((0, 2)), node_radius=0
|
||||
)
|
||||
GeoNavigationGraph.from_coordinates_degrees(
|
||||
latitudes=empty((0,)), longitudes=empty((0,)), edges=empty((0, 2)), node_radius=100_000
|
||||
)
|
||||
|
||||
with self.assertRaises(Exception): # noqa: H202
|
||||
GeoNavigationGraph.from_coordinates_degrees(
|
||||
latitudes=empty((0,)), longitudes=empty((0,)), edges=empty((0, 2)), node_radius=-1e-9
|
||||
)
|
||||
|
||||
def test_set_node_properties(self) -> None:
|
||||
"""Tests that passing ``node_properties`` works."""
|
||||
graph = GeoNavigationGraph.from_coordinates_radians(
|
||||
latitudes=array([42]),
|
||||
longitudes=array([21]),
|
||||
edges=empty((0, 2)),
|
||||
node_radius=100,
|
||||
node_properties=DataFrame(data={"col1": [99], "col2": ["text"]}),
|
||||
)
|
||||
self.assertEqual(graph.node_radius, 100)
|
||||
assert_array_equal(graph.node_properties["col1"], [99])
|
||||
assert_array_equal(graph.node_properties["col2"], ["text"])
|
||||
|
||||
def test_read_write(self) -> None:
|
||||
"""Tests that a *geo* navigation graph can be serialized and deserialized again."""
|
||||
latitudes = array([49.8725144])
|
||||
longitudes = array([8.6528707])
|
||||
edges = empty((0, 2))
|
||||
|
||||
# `graph.neighbors` is cached, so we want to try it with and without the cached neighbors being set
|
||||
for set_neighbors in [True, False]:
|
||||
with self.subTest(f"neighbors set = {set_neighbors}"):
|
||||
graph = GeoNavigationGraph.from_coordinates_degrees(
|
||||
latitudes, longitudes, edges=edges, max_neighbors=42, node_radius=1000
|
||||
)
|
||||
if set_neighbors:
|
||||
_ = graph.neighbors
|
||||
|
||||
with TemporaryDirectory() as directory:
|
||||
path = os.path.join(directory, "some_file.hdf5")
|
||||
graph.to_disk(path)
|
||||
new_graph = GeoNavigationGraph.from_disk(path)
|
||||
|
||||
self.assertEqual(graph, new_graph)
|
||||
assert_array_equal(new_graph.neighbors, graph.neighbors)
|
||||
|
||||
|
||||
class TestNavigationGraphPruningGeo(TestCase):
|
||||
"""Tests that navigation graphs can be pruned by testing it with earth graphs."""
|
||||
|
||||
def test_pruning_artificial(self) -> None:
|
||||
"""Tests that pruning half of the points works as expected."""
|
||||
|
||||
for distance_km in EXAMPLE_DISTANCES_KILOMETERS:
|
||||
with self.subTest(f"Test with distance {distance_km} km"):
|
||||
# create a grid
|
||||
graph = create_earth_graph(min_required_frequency(distance_km * 1000, in_meters=True))
|
||||
|
||||
# keep all nodes at even latitudes
|
||||
keep_condition = arange(0, len(graph)) % 2 == 0
|
||||
pruned_graph = deepcopy(graph)
|
||||
pruned_graph.prune_nodes(keep_condition)
|
||||
|
||||
self.assertGreater(len(pruned_graph), 0, "some node must remain")
|
||||
|
||||
# test the reduction ratio
|
||||
delta_nodes = len(pruned_graph) / len(graph)
|
||||
delta_edges = pruned_graph.num_edges / graph.num_edges
|
||||
self.assertAlmostEqual(delta_nodes, 0.5, msg="suspicious node count reduction")
|
||||
# about a fifth of all edges should be removed since each of the removed nodes removed five
|
||||
# edges
|
||||
self.assertAlmostEqual(delta_edges, 1 / 5, delta=0.15, msg="suspicious edge count reduction")
|
||||
|
||||
# test the values in the edges, since they were rewritten as they point to new indices
|
||||
self.assertTrue(numpy.all(pruned_graph.edges[:, :] >= 0), "indices must be non-negative")
|
||||
self.assertTrue(
|
||||
numpy.all(pruned_graph.edges[:, :] < len(pruned_graph)),
|
||||
"some filtered edges reference (now) non-existent points",
|
||||
)
|
||||
|
||||
def test_pruning_depth(self) -> None:
|
||||
"""Supplements :meth`~test_pruning_artificial` by a real-world application.
|
||||
|
||||
Only checks application-specific properties and not, for example, the general shapes of the result.
|
||||
"""
|
||||
# create a grid
|
||||
distance_meters = 500_000
|
||||
graph = create_earth_graph(min_required_frequency(distance_meters, in_meters=True))
|
||||
|
||||
# fetch properties
|
||||
mode = transformers_concrete.BathymetricTransformer.Modes.AVERAGE_DEPTH
|
||||
graph.append_property(transformers_concrete.BathymetricTransformer(_open_test_geo_dataset(), [mode]))
|
||||
|
||||
# keep all nodes that are below sea level
|
||||
keep_condition = (graph.node_properties[mode.column_name] < 0.0).to_numpy()
|
||||
|
||||
# Remove the now useless property
|
||||
graph.clear_node_properties()
|
||||
|
||||
# perform pruning
|
||||
pruned_graph = deepcopy(graph)
|
||||
pruned_graph.prune_nodes(keep_condition)
|
||||
|
||||
# test the reduction ratio
|
||||
delta_nodes = len(pruned_graph) / len(graph)
|
||||
delta_edges = pruned_graph.num_edges / graph.num_edges
|
||||
earth_fraction_water = 0.708 # see https://en.wikipedia.org/wiki/World_Ocean
|
||||
# although we go by topography and not water coverage, this should still be fairly correct
|
||||
self.assertAlmostEqual(
|
||||
delta_nodes, earth_fraction_water, delta=0.1, msg="suspicious node count reduction"
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
delta_edges, earth_fraction_water, delta=0.1, msg="suspicious edge count reduction"
|
||||
)
|
120
pyrate/tests/plan/graph/test_graph.py
Normal file
@ -0,0 +1,120 @@
|
||||
"""Asserts correct behaviour of the base classes for graph navigation.
|
||||
|
||||
See Also:
|
||||
tests/common/raster_datasets/test_transformers_concrete.py
|
||||
"""
|
||||
|
||||
# Standard library
|
||||
from copy import deepcopy
|
||||
import os.path
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest import TestCase
|
||||
|
||||
# Scientific
|
||||
from numpy import array
|
||||
from numpy import empty
|
||||
from numpy import full
|
||||
from numpy.testing import assert_array_equal
|
||||
from pandas import DataFrame
|
||||
from pandas.testing import assert_frame_equal
|
||||
|
||||
# Module under test
|
||||
from pyrate.plan.graph import NavigationGraph
|
||||
|
||||
|
||||
# Some examples:
|
||||
_NODES = DataFrame(data={"property_1": [1, 2, 3], "property_2": [10, 20, 30]})
|
||||
_EDGES = array([[0, 1], [1, 2]])
|
||||
_NEIGHBORS = array([[1, -1], [0, 2], [1, -1]])
|
||||
|
||||
|
||||
class TestNavigationGraph(TestCase):
|
||||
"""Tests the very basic functionality like initialization, (de)serialization and finding neighbors."""
|
||||
|
||||
def test_empty(self) -> None:
|
||||
"""Tests that a new instance can be created with and without neighbors."""
|
||||
graph = NavigationGraph(DataFrame(), empty((0, 2)))
|
||||
self.assertEqual(len(graph), 0)
|
||||
self.assertEqual(graph.num_edges, 0)
|
||||
|
||||
# check that the correct neighbor table is returned
|
||||
self.assertEqual(graph.neighbors.shape, (0, 0))
|
||||
|
||||
def test_create(self) -> None:
|
||||
"""Tests that a new instance can be created with and without neighbors."""
|
||||
|
||||
for given_neighbors in [_NEIGHBORS, None]:
|
||||
with self.subTest(f"neighbors given = {given_neighbors is not None}"):
|
||||
graph = NavigationGraph(_NODES, _EDGES, given_neighbors)
|
||||
assert_array_equal(graph.neighbors, _NEIGHBORS)
|
||||
|
||||
# repeated queries should return the same neighbors
|
||||
assert_array_equal(graph.neighbors, graph.neighbors)
|
||||
|
||||
def test_read_write(self) -> None:
|
||||
"""Tests that a navigation graph can be serialized and deserialized again."""
|
||||
|
||||
# `graph.neighbors` is cached, so we want to try it with and without the cached neighbors being set
|
||||
for set_neighbors in [True, False]:
|
||||
with self.subTest(f"neighbors set = {set_neighbors}"):
|
||||
graph = NavigationGraph(_NODES, _EDGES, max_neighbors=42)
|
||||
if set_neighbors:
|
||||
_ = graph.neighbors
|
||||
|
||||
with TemporaryDirectory() as directory:
|
||||
path = os.path.join(directory, "some_file.hdf5")
|
||||
graph.to_disk(path)
|
||||
new_graph = NavigationGraph.from_disk(path)
|
||||
|
||||
self.assertEqual(graph, new_graph)
|
||||
assert_array_equal(new_graph.neighbors, _NEIGHBORS)
|
||||
|
||||
def test_max_neighbors_constructor(self) -> None:
|
||||
"""Tests that only invalid inputs to max_neighbors raise exceptions."""
|
||||
NavigationGraph(DataFrame(), empty((0, 2)), max_neighbors=0)
|
||||
NavigationGraph(DataFrame(), empty((0, 2)), max_neighbors=10)
|
||||
|
||||
with self.assertRaises(Exception): # noqa: H202
|
||||
NavigationGraph(DataFrame(), empty((0, 2)), max_neighbors=-2)
|
||||
|
||||
|
||||
class TestNavigationGraphPruningArtificial(TestCase):
|
||||
"""Tests that simple toy navigation graphs can be pruned."""
|
||||
|
||||
def test_pruning_no_nodes(self) -> None:
|
||||
"""Tests that pruning no nodes works."""
|
||||
old_graph = NavigationGraph(_NODES, _EDGES, _NEIGHBORS)
|
||||
|
||||
pruned_graph = deepcopy(old_graph)
|
||||
retain_all = full((len(_NODES),), True)
|
||||
pruned_graph.prune_nodes(retain_all)
|
||||
|
||||
self.assertEqual(old_graph, pruned_graph)
|
||||
|
||||
def test_pruning_all(self) -> None:
|
||||
"""Tests that pruning all nodes works."""
|
||||
old_graph = NavigationGraph(_NODES, _EDGES, _NEIGHBORS)
|
||||
|
||||
pruned_graph = deepcopy(old_graph)
|
||||
retain_all = full((len(_NODES),), False)
|
||||
pruned_graph.prune_nodes(retain_all)
|
||||
|
||||
self.assertNotEqual(old_graph, pruned_graph)
|
||||
self.assertEqual(len(pruned_graph.nodes), 0)
|
||||
self.assertEqual(len(pruned_graph.nodes.columns), 2, "the properties must be retained")
|
||||
self.assertEqual(pruned_graph.edges.shape, (0, 2))
|
||||
self.assertEqual(pruned_graph.neighbors.shape, (0, 0))
|
||||
|
||||
def test_pruning_very_simple(self) -> None:
|
||||
"""Tests that pruning some nodes works as expected."""
|
||||
|
||||
old_graph = NavigationGraph(_NODES, _EDGES, _NEIGHBORS)
|
||||
|
||||
pruned_graph = deepcopy(old_graph)
|
||||
keep_condition = array([True, True, False]) # only prune the last node
|
||||
pruned_graph.prune_nodes(keep_condition)
|
||||
|
||||
self.assertNotEqual(old_graph, pruned_graph)
|
||||
assert_frame_equal(pruned_graph.nodes, _NODES[:2])
|
||||
assert_array_equal(pruned_graph.edges, _EDGES[:1])
|
||||
assert_array_equal(pruned_graph.neighbors, _NEIGHBORS[:2, :1])
|
0
pyrate/tests/sense/__init__.py
Normal file
0
pyrate/tests/sense/filters/__init__.py
Normal file
194
pyrate/tests/sense/filters/test_kalman.py
Normal file
@ -0,0 +1,194 @@
|
||||
"""This module asserts correct runtime behaviour of the pyrate.sense.filter subpackage."""
|
||||
|
||||
# Python standard library
|
||||
from datetime import timedelta
|
||||
from typing import cast
|
||||
|
||||
# Test environment
|
||||
from unittest import TestCase
|
||||
|
||||
# Mathematics
|
||||
from numpy import array
|
||||
from numpy import eye
|
||||
from numpy import Inf
|
||||
from numpy.linalg import norm
|
||||
from numpy import ndarray
|
||||
from numpy import vstack
|
||||
|
||||
# Hypothesis testing
|
||||
from hypothesis import given
|
||||
from hypothesis import settings
|
||||
|
||||
# Package under test
|
||||
from pyrate.common.math import Gaussian
|
||||
from pyrate.sense.filters import ExtendedKalman
|
||||
from pyrate.sense.filters import Kalman
|
||||
from pyrate.sense.filters import UnscentedKalman
|
||||
|
||||
# Helpers
|
||||
from pyrate.common.testing.strategies.dynamic_system import linear_model
|
||||
from pyrate.common.testing.strategies.dynamic_system import nonlinear_model
|
||||
|
||||
# Flags from Pyrate
|
||||
from pyrate.common.testing import IS_EXTENDED_TESTING
|
||||
|
||||
|
||||
class TestKalman(TestCase):
|
||||
|
||||
"""Test for correct runtime behaviour of Kalman filters pyrate.sense.filter."""
|
||||
|
||||
# In this context, we reproduce a common filter notation
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
@given(linear_model())
|
||||
@settings(max_examples=1000 if IS_EXTENDED_TESTING else 10, deadline=timedelta(seconds=1.0))
|
||||
def test_kalman(self, model):
|
||||
"""Assert the correct functionality of the standard Kalman filter."""
|
||||
|
||||
# Unpack generated model
|
||||
estimate, F, B, H, Q, R, measurements, inputs = model
|
||||
|
||||
# Initialize filter
|
||||
kalman = Kalman(F=F, estimate=estimate, H=H, Q=Q, R=R, B=B, keep_trace=True)
|
||||
|
||||
# Apply the Kalman filter a few times
|
||||
for i, measurement in enumerate(measurements):
|
||||
kalman.predict(u=inputs[i])
|
||||
kalman.correct(z=measurement)
|
||||
|
||||
# Assert correct tracing with Kalman
|
||||
self.assertIsNotNone(kalman.predictions, "Kalman filter did not keep trace of predictions")
|
||||
self.assertIsNotNone(kalman.estimates, "Kalman filter did not keep trace of estimates")
|
||||
self.assertEqual(
|
||||
len(kalman.predictions.index), len(measurements), "Kalman filter has not traced all predictions"
|
||||
)
|
||||
self.assertEqual(
|
||||
len(kalman.estimates.index), len(measurements), "Kalman filter has not traced all estimates"
|
||||
)
|
||||
|
||||
# Apply the Kalman filter with a callable H
|
||||
callable_H = lambda dummy: H # noqa: E731
|
||||
kalman.H = callable_H
|
||||
kalman.correct(z=measurements[0], dummy=None)
|
||||
|
||||
@given(nonlinear_model())
|
||||
@settings(max_examples=1000 if IS_EXTENDED_TESTING else 10, deadline=timedelta(seconds=1.0))
|
||||
def test_extended_kalman(self, model):
|
||||
"""Assert the correct functionality of the extended Kalman filter."""
|
||||
|
||||
# Unpack generated model
|
||||
estimate, f, _, Jf, h, _, Jh, Q, R, measurements = model
|
||||
|
||||
# Initialize filter
|
||||
extended = ExtendedKalman(F=Jf, f=f, estimate=estimate, H=Jh, h=h, Q=Q, R=R, keep_trace=True)
|
||||
|
||||
# Apply the Kalman filter a few times
|
||||
for z in measurements:
|
||||
extended.predict()
|
||||
extended.correct(z)
|
||||
|
||||
# Assert correct tracing with Kalman
|
||||
self.assertIsNotNone(extended.predictions, "Extended Kalman filter did not keep trace of predictions")
|
||||
self.assertIsNotNone(extended.estimates, "Extended Kalman filter did not keep trace of estimates")
|
||||
self.assertEqual(
|
||||
len(extended.predictions.index),
|
||||
len(measurements),
|
||||
"Extended Kalman filter has not traced all predictions",
|
||||
)
|
||||
self.assertEqual(
|
||||
len(extended.estimates.index),
|
||||
len(measurements),
|
||||
"Extended Kalman filter has not traced all estimates",
|
||||
)
|
||||
|
||||
@given(nonlinear_model())
|
||||
@settings(max_examples=1000 if IS_EXTENDED_TESTING else 10, deadline=timedelta(seconds=1.0))
|
||||
def test_unscented_kalman(self, model):
|
||||
"""Assert the correct functionality of the extended Kalman filter."""
|
||||
|
||||
# Unpack generated model
|
||||
estimate, f, _, _, h, _, _, Q, R, measurements = model
|
||||
|
||||
# Initialize filter
|
||||
unscented = UnscentedKalman(f=f, estimate=estimate, h=h, Q=Q, R=R, keep_trace=True)
|
||||
|
||||
# Apply the Kalman filter a few times
|
||||
for z in measurements:
|
||||
unscented.predict()
|
||||
unscented.correct(z)
|
||||
|
||||
# Assert correct tracing with Kalman
|
||||
self.assertIsNotNone(
|
||||
unscented.predictions, "Uncented Kalman filter did not keep trace of predictions"
|
||||
)
|
||||
self.assertIsNotNone(unscented.estimates, "Uncented Kalman filter did not keep trace of estimates")
|
||||
self.assertEqual(
|
||||
len(unscented.predictions.index),
|
||||
len(measurements),
|
||||
"Uncented Kalman filter has not traced all predictions",
|
||||
)
|
||||
self.assertEqual(
|
||||
len(unscented.estimates.index),
|
||||
len(measurements),
|
||||
"Uncented Kalman filter has not traced all estimates",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def test_estimation():
|
||||
"""Assert that the filter estimates tend towards the true state over time."""
|
||||
|
||||
# Define model of a constant value
|
||||
F = H = Q = R = eye(1)
|
||||
|
||||
# Initial belief
|
||||
estimate = Gaussian(vstack([0.0]), eye(1))
|
||||
|
||||
# Initialize filters
|
||||
kalman = Kalman(F=F, estimate=estimate, H=H, Q=Q, R=R)
|
||||
extended = ExtendedKalman(
|
||||
F=lambda _: F,
|
||||
f=lambda x: cast(ndarray, x),
|
||||
estimate=estimate,
|
||||
H=lambda _: H,
|
||||
h=lambda x: cast(ndarray, x),
|
||||
Q=Q,
|
||||
R=R,
|
||||
)
|
||||
unscented = UnscentedKalman(
|
||||
f=lambda x: cast(ndarray, x),
|
||||
estimate=estimate,
|
||||
h=lambda x: cast(ndarray, x),
|
||||
Q=Q,
|
||||
R=R,
|
||||
)
|
||||
|
||||
# Apply the Kalman filter a few times
|
||||
true_state = array([20.0])
|
||||
previous_kalman_error = Inf
|
||||
previous_extended_error = Inf
|
||||
previous_unscented_error = Inf
|
||||
for _ in range(10):
|
||||
# Check error going down for Kalman
|
||||
kalman.predict()
|
||||
kalman.correct(z=true_state)
|
||||
error = norm(kalman.estimate.x - true_state).item() # Convert from numpy scalar to Python float
|
||||
assert (
|
||||
error < previous_kalman_error or error == 0
|
||||
), "Kalman estimate did not get better over time."
|
||||
previous_kalman_error = error
|
||||
|
||||
# Check error going down for EKF
|
||||
extended.predict()
|
||||
extended.correct(z=true_state)
|
||||
error = norm(extended.estimate.x - true_state).item()
|
||||
assert error < previous_extended_error or error == 0, "EKF estimate did not get better over time."
|
||||
previous_extended_error = error
|
||||
|
||||
# Check error going down for UKF
|
||||
unscented.predict()
|
||||
unscented.correct(z=true_state)
|
||||
error = norm(unscented.estimate.x - true_state).item()
|
||||
assert (
|
||||
error < previous_unscented_error or error == 0
|
||||
), "UKF estimate did not get better over time."
|
||||
previous_unscented_error = error
|
145
pyrate/tests/sense/filters/test_phd.py
Normal file
@ -0,0 +1,145 @@
|
||||
"""This module asserts correct runtime behaviour of the pyrate.sense.filter subpackage."""
|
||||
|
||||
# Python standard library
|
||||
from datetime import timedelta
|
||||
|
||||
# Test environment
|
||||
from unittest import TestCase
|
||||
|
||||
# Hypothesis testing
|
||||
from hypothesis import given
|
||||
from hypothesis import settings
|
||||
|
||||
# Package under test
|
||||
from pyrate.sense.filters import ExtendedGaussianMixturePHD
|
||||
from pyrate.sense.filters import GaussianMixturePHD
|
||||
|
||||
# Helpers
|
||||
from pyrate.common.testing.strategies.dynamic_system import linear_model
|
||||
from pyrate.common.testing.strategies.dynamic_system import nonlinear_model
|
||||
|
||||
# Flags from Pyrate
|
||||
from pyrate.common.testing import IS_EXTENDED_TESTING
|
||||
|
||||
|
||||
class TestPHD(TestCase):
|
||||
|
||||
"""Test for correct runtime behaviour in pyrate.sense.filter."""
|
||||
|
||||
# In this context, we reproduce a common filter notation
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Setup the linear motion model for the filter tests."""
|
||||
|
||||
# Survival and detection rate of targets
|
||||
self.survival_rate = 0.99
|
||||
self.detection_rate = 0.99
|
||||
|
||||
# Clutter intensity
|
||||
self.intensity = 0.01
|
||||
|
||||
# PHD pruning
|
||||
self.threshold = 0.1
|
||||
self.merge_distance = 0.5
|
||||
self.max_components = 0.5
|
||||
|
||||
@given(linear_model())
|
||||
@settings(max_examples=1000 if IS_EXTENDED_TESTING else 10, deadline=timedelta(seconds=1.0))
|
||||
def test_gmphd(self, model):
|
||||
"""Assert the correct functionality of the gaussian mixture PHD filter."""
|
||||
|
||||
# Unpack generated model
|
||||
estimate, F, _, H, Q, R, measurements, _ = model
|
||||
|
||||
# Initialize filter
|
||||
gmphd = GaussianMixturePHD(
|
||||
birth_belief=[estimate],
|
||||
survival_rate=self.survival_rate,
|
||||
detection_rate=self.detection_rate,
|
||||
intensity=self.intensity,
|
||||
F=F,
|
||||
H=H,
|
||||
Q=Q,
|
||||
R=R,
|
||||
)
|
||||
|
||||
self.assertEqual(len(gmphd.gmm), 0, "Mixture model should be initialy empty")
|
||||
|
||||
# Predict with a callable F and check number of components
|
||||
callable_F = lambda dummy: F # noqa: E731
|
||||
gmphd.F = callable_F
|
||||
gmphd.predict(dummy=None)
|
||||
gmphd.F = F
|
||||
self.assertEqual(
|
||||
len(gmphd.gmm), len(gmphd.birth_belief), "Mixture model is not the right size after prediction"
|
||||
)
|
||||
|
||||
# Apply the PHD filter with a callable H and check number of components
|
||||
callable_H = lambda dummy: H # noqa: E731
|
||||
gmphd.H = callable_H
|
||||
gmphd.correct(measurements=measurements, dummy=None)
|
||||
gmphd.H = H
|
||||
self.assertEqual(
|
||||
len(gmphd.gmm), len(measurements) + 1, "Mixture model is not the right size after correction"
|
||||
)
|
||||
|
||||
# Removing all components
|
||||
gmphd.prune(self.threshold, self.merge_distance, 0)
|
||||
self.assertEqual(len(gmphd.extract()), 0, "Pruning did not remove all components")
|
||||
|
||||
# Apply the filter multiple times
|
||||
for _, _ in enumerate(measurements):
|
||||
gmphd.predict()
|
||||
gmphd.correct(measurements=measurements)
|
||||
|
||||
# Extract states
|
||||
gmphd.extract(self.threshold)
|
||||
|
||||
@given(nonlinear_model())
|
||||
@settings(max_examples=1000 if IS_EXTENDED_TESTING else 10, deadline=timedelta(seconds=1.0))
|
||||
def test_extended_gmphd(self, model):
|
||||
"""Assert the correct functionality of the gaussian mixture PHD filter."""
|
||||
|
||||
# Unpack generated model
|
||||
estimate, f, _, Jf, h, _, Jh, Q, R, measurements = model
|
||||
|
||||
# Initialize filter
|
||||
gmphd = ExtendedGaussianMixturePHD(
|
||||
birth_belief=[estimate],
|
||||
survival_rate=self.survival_rate,
|
||||
detection_rate=self.detection_rate,
|
||||
intensity=self.intensity,
|
||||
F=Jf,
|
||||
f=f,
|
||||
H=Jh,
|
||||
h=h,
|
||||
Q=Q,
|
||||
R=R,
|
||||
)
|
||||
|
||||
self.assertEqual(len(gmphd.gmm), 0, "Mixture model should be initialy empty")
|
||||
|
||||
# Predict and check number of components
|
||||
gmphd.predict()
|
||||
self.assertEqual(
|
||||
len(gmphd.gmm), len(gmphd.birth_belief), "Mixture model is not the right size after prediction"
|
||||
)
|
||||
|
||||
# Apply the PHD filter and check number of components
|
||||
gmphd.correct(measurements=measurements)
|
||||
self.assertEqual(
|
||||
len(gmphd.gmm), len(measurements) + 1, "Mixture model is not the right size after correction"
|
||||
)
|
||||
|
||||
# Removing all components
|
||||
gmphd.prune(self.threshold, self.merge_distance, 0)
|
||||
self.assertEqual(len(gmphd.extract()), 0, "Pruning did not remove all components")
|
||||
|
||||
# Apply the filter multiple times
|
||||
for _, _ in enumerate(measurements):
|
||||
gmphd.predict()
|
||||
gmphd.correct(measurements=measurements)
|
||||
|
||||
# Extract states
|
||||
gmphd.extract(self.threshold)
|
0
pyrate/tests/sense/vision/__init__.py
Normal file
After Width: | Height: | Size: 45 KiB |
@ -0,0 +1,3 @@
|
||||
19,237
|
||||
432,242
|
||||
0.69
|
@ -0,0 +1,3 @@
|
||||
17,262
|
||||
630,273
|
||||
1.03
|
@ -0,0 +1,3 @@
|
||||
14,277
|
||||
389,279
|
||||
0.31
|
@ -0,0 +1,3 @@
|
||||
153,286
|
||||
638,291
|
||||
0.59
|
@ -0,0 +1,3 @@
|
||||
160,286
|
||||
623,290
|
||||
0.49
|
@ -0,0 +1,3 @@
|
||||
48,271
|
||||
411,273
|
||||
0.32
|
@ -0,0 +1,3 @@
|
||||
21,253
|
||||
598,261
|
||||
0.79
|
@ -0,0 +1,3 @@
|
||||
67,266
|
||||
415,274
|
||||
1.32
|
@ -0,0 +1,3 @@
|
||||
7,257
|
||||
242,262
|
||||
1.22
|
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 70 KiB |
61
pyrate/tests/sense/vision/test_image_line.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""This test suite runs additional tests for ``ImageLine`` that are not covered in ``TestObstacleLocator``"""
|
||||
|
||||
# Testing
|
||||
from unittest import TestCase
|
||||
|
||||
# Hypothesis
|
||||
from hypothesis import given
|
||||
from hypothesis.strategies import composite
|
||||
from hypothesis.strategies import floats
|
||||
from hypothesis.strategies import integers
|
||||
from hypothesis.strategies import just
|
||||
from hypothesis.strategies import tuples
|
||||
|
||||
# Scientific
|
||||
from numpy import pi
|
||||
|
||||
# Module under test
|
||||
from pyrate.sense.vision.image_line import ImageLine
|
||||
|
||||
|
||||
@composite
|
||||
def image_dimensions_and_points(draw):
|
||||
"""Generate image dimensions and points left and right on that image"""
|
||||
|
||||
image_dims = draw(tuples(integers(1, 10000), integers(1, 10000)))
|
||||
point_a = draw(tuples(just(0), integers(0, image_dims[1] - 1)))
|
||||
point_b = draw(tuples(just(image_dims[0] - 1), integers(0, image_dims[1] - 1)))
|
||||
|
||||
return image_dims, point_a, point_b
|
||||
|
||||
|
||||
class TestImageLine(TestCase):
|
||||
|
||||
"""Tests the remaining methods of ``ImageLine`` not covered by testing ``ObstacleLocator``"""
|
||||
|
||||
@given(floats(1, 10000), floats(1, 10000), floats(-5000, 5000), floats(0, 2 * pi))
|
||||
def test_from_height_angle(self, image_width, image_height, height, angle):
|
||||
"""Test that creates (from height and angle) and tests ``ImageLine``s"""
|
||||
|
||||
image_line = ImageLine.from_height_angle((image_width, image_height), height, angle)
|
||||
|
||||
self.assertTrue(image_line.image_width == image_width and image_line.image_height == image_height)
|
||||
self.assertTrue(image_line.angle == angle)
|
||||
self.assertAlmostEqual(image_line.height, int(height + image_height / 2))
|
||||
|
||||
end_points = image_line.end_points
|
||||
self.assertTrue(
|
||||
end_points[0][0] == 0 and end_points[1][0] == image_width,
|
||||
msg=f"x1={end_points[0][0]} x2={end_points[1][0]}",
|
||||
)
|
||||
|
||||
@given(test_input=image_dimensions_and_points())
|
||||
def test_indices(self, test_input):
|
||||
"""Test that tests the ``indices`` property of ``ImageLine``"""
|
||||
|
||||
image_dims, point1, point2 = test_input
|
||||
|
||||
image_line = ImageLine.from_points(image_dims, (point1, point2))
|
||||
x_coords, y_coords = image_line.indices
|
||||
self.assertTrue(((50 <= x_coords) <= 50).all())
|
||||
self.assertTrue(((0 <= y_coords) <= 200).all())
|
102
pyrate/tests/sense/vision/test_image_rectangle.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""This test suite evaluates and tests behavior of the ``ImageRectangle`` class"""
|
||||
|
||||
# Testing
|
||||
from unittest import TestCase
|
||||
|
||||
# Hypothesis
|
||||
from hypothesis import given
|
||||
from hypothesis.strategies import integers
|
||||
|
||||
# Module under test
|
||||
from pyrate.sense.vision.image_rectangle import ImageRectangle
|
||||
|
||||
|
||||
class TestImageRectangle(TestCase):
|
||||
|
||||
"""Tests functionality of the ``ImageRectangle`` class"""
|
||||
|
||||
@given(
|
||||
integers(0, 10000),
|
||||
integers(0, 10000),
|
||||
integers(0, 10000),
|
||||
integers(0, 10000),
|
||||
integers(0, 10000),
|
||||
integers(0, 10000),
|
||||
)
|
||||
# pylint: disable=too-many-arguments
|
||||
def test_bottom_center(self, position_x, position_y, width, height, offset_x, offset_y):
|
||||
"""Parametrized test that tests correct functionality of the bottom_center property
|
||||
|
||||
Args:
|
||||
position_x: x position of the rectangle
|
||||
position_y: y position of the rectangle
|
||||
width: width of the rectangle
|
||||
height: height of the rectangle
|
||||
offset_x: x component of the offset
|
||||
offset_y: y component of the offset
|
||||
"""
|
||||
|
||||
offset = (offset_x, offset_y)
|
||||
|
||||
rectangle_without_offset = ImageRectangle((position_x, position_y, width, height))
|
||||
self.assertTupleEqual(rectangle_without_offset.offset, (0, 0))
|
||||
self.assertAlmostEqual(rectangle_without_offset.bottom_center[0], position_x + (width / 2), delta=0.5)
|
||||
self.assertAlmostEqual(rectangle_without_offset.bottom_center[1], position_y + height)
|
||||
|
||||
rectangle_with_offset = ImageRectangle((position_x, position_y, width, height), offset=offset)
|
||||
self.assertTupleEqual(rectangle_with_offset.offset, offset)
|
||||
self.assertAlmostEqual(
|
||||
rectangle_with_offset.bottom_center[0], position_x + offset_x + (width / 2), delta=0.5
|
||||
)
|
||||
self.assertAlmostEqual(rectangle_with_offset.bottom_center[1], position_y + offset_y + height)
|
||||
|
||||
@given(
|
||||
integers(0, 10000),
|
||||
integers(0, 10000),
|
||||
integers(0, 10000),
|
||||
integers(0, 10000),
|
||||
integers(0, 10000),
|
||||
integers(0, 10000),
|
||||
)
|
||||
# pylint: disable=too-many-arguments
|
||||
def test_rectangle_to_corner(self, position_x, position_y, width, height, offset_x, offset_y):
|
||||
"""Parametrized test that tests correct functionality of the rectangle_to_corner method
|
||||
|
||||
Args:
|
||||
position_x: x position of the rectangle
|
||||
position_y: y position of the rectangle
|
||||
width: width of the rectangle
|
||||
height: height of the rectangle
|
||||
offset_x: x component of the offset
|
||||
offset_y: y component of the offset
|
||||
"""
|
||||
|
||||
offset = (offset_x, offset_y)
|
||||
|
||||
# rectangle without offset
|
||||
rectangle_without_offset = ImageRectangle((position_x, position_y, width, height))
|
||||
self.assertTupleEqual(rectangle_without_offset.offset, (0, 0))
|
||||
cornerlu, cornerrb = rectangle_without_offset.rectangle_to_corner(offset=False)
|
||||
|
||||
self.assertTrue(
|
||||
cornerlu[0] == position_x and cornerlu[1] == position_y,
|
||||
msg=f"Left upper corner: {cornerlu}",
|
||||
)
|
||||
self.assertTrue(
|
||||
cornerrb[0] == position_x + width and cornerrb[1] == position_y + height,
|
||||
msg=f"Right bottom corner: {cornerrb}",
|
||||
)
|
||||
|
||||
# rectangle with offset
|
||||
rectangle_with_offset = ImageRectangle((position_x, position_y, width, height), offset)
|
||||
self.assertTupleEqual(rectangle_with_offset.offset, offset)
|
||||
cornerlu, cornerrb = rectangle_with_offset.rectangle_to_corner(offset=True)
|
||||
|
||||
self.assertTrue(
|
||||
cornerlu[0] == position_x + offset_x and cornerlu[1] == position_y + offset_y,
|
||||
msg=f"Left upper corner: {cornerlu}",
|
||||
)
|
||||
self.assertTrue(
|
||||
cornerrb[0] == position_x + offset_x + width and cornerrb[1] == position_y + offset_y + height,
|
||||
msg=f"Right bottom corner: {cornerrb}",
|
||||
)
|
103
pyrate/tests/sense/vision/test_obstacle_locator.py
Normal file
@ -0,0 +1,103 @@
|
||||
"""This test suite evaluates and tests behavior of the ``ObstacleLocator`` class"""
|
||||
|
||||
# Standard library
|
||||
from csv import reader
|
||||
from math import radians
|
||||
from pathlib import Path
|
||||
|
||||
# Typing
|
||||
from typing import cast
|
||||
from typing import Tuple
|
||||
|
||||
# Testing
|
||||
from unittest import TestCase
|
||||
|
||||
# Scientific
|
||||
from cv2 import imread
|
||||
|
||||
# Module under test
|
||||
from pyrate.sense.vision.image_line import ImageLine
|
||||
from pyrate.sense.vision.obstacle_locator import ObstacleLocator
|
||||
|
||||
|
||||
PATH_TO_DATASET = Path(__file__).parent / "resources" / "testing_dataset_successful"
|
||||
DATASET_IMAGES_PATHS = sorted(list((PATH_TO_DATASET / "testims").glob("*.jpg")))
|
||||
DATASET_ANNOTATIONS_PATHS = sorted(list((PATH_TO_DATASET / "annotations").glob("*.txt")))
|
||||
|
||||
PATH_TO_FAILING = (
|
||||
Path(__file__).parent / "resources" / "testing_dataset_no_horizon" / "testims" / "Preprocessed_test_0.jpg"
|
||||
)
|
||||
IMAGE_HEIGHT, IMAGE_WIDTH = imread(PATH_TO_FAILING.as_posix()).shape[:2]
|
||||
|
||||
|
||||
class TestObstacleLocator(TestCase):
|
||||
|
||||
"""Test for correct predictions made by ``ObstacleLocator``"""
|
||||
|
||||
@staticmethod
|
||||
def parse_annotation(file_path: str, obstacle_locator: ObstacleLocator) -> Tuple[ImageLine, float]:
|
||||
"""Helper function to parse the ground truth labels from the dataset.
|
||||
|
||||
Args:
|
||||
file_path: Label file path
|
||||
obstacle_locator: the ObstacleLocator that returns the ImageLine that should be
|
||||
compared to the returned ImageLine of this function
|
||||
|
||||
Returns:
|
||||
ImageLine as described in the annotation, angle read from annotation
|
||||
(for testing correct angle calculation)
|
||||
"""
|
||||
|
||||
with open(file_path, "rt", encoding="UTF-8") as label_file:
|
||||
content = label_file.read().split("\n")
|
||||
|
||||
csvreader = reader(content, delimiter=",")
|
||||
point_a = cast(Tuple[int, int], tuple(int(x) for x in next(csvreader)))
|
||||
point_b = cast(Tuple[int, int], tuple(int(x) for x in next(csvreader)))
|
||||
label_angle = radians(float(next(csvreader)[0]))
|
||||
|
||||
line = ImageLine.from_points(
|
||||
image_shape=(obstacle_locator.image_width, obstacle_locator.image_height),
|
||||
points=(point_a, point_b),
|
||||
)
|
||||
|
||||
return line, label_angle
|
||||
|
||||
def test_horizon_angle(self):
|
||||
"""Compares ``ObstacleLocator`` horizon estimates to ground truth annotations"""
|
||||
|
||||
uut_ol = ObstacleLocator(image_width=IMAGE_WIDTH, image_height=IMAGE_HEIGHT) # unit/module under test
|
||||
|
||||
for image_path, label_path in zip(DATASET_IMAGES_PATHS, DATASET_ANNOTATIONS_PATHS):
|
||||
with self.subTest(image=image_path.name):
|
||||
# Assert that we have the correct label for the test image
|
||||
self.assertEqual(
|
||||
image_path.name.split(".")[0],
|
||||
label_path.name.split(".")[0],
|
||||
msg="That isn't the right label for the image. This shouldn't happen.",
|
||||
)
|
||||
|
||||
image = imread(image_path.as_posix())
|
||||
|
||||
# read annotation and test if ImageLine calculates the line's angle correctly
|
||||
label_image_line, label_angle = self.parse_annotation(label_path.as_posix(), uut_ol)
|
||||
self.assertAlmostEqual(label_angle, label_image_line.angle, places=2)
|
||||
|
||||
result = uut_ol.detect_horizon(image)
|
||||
horizons = result[0]
|
||||
|
||||
# Test that a) a horizon is detected and b) it has the correct angle
|
||||
self.assertTrue(len(horizons) > 0, msg="No horizon was detected.")
|
||||
self.assertAlmostEqual(
|
||||
horizons[0].angle, label_image_line.angle, places=1, msg="Horizon angle mismatch."
|
||||
)
|
||||
|
||||
def test_missing_lines(self):
|
||||
"""Tests the branch when no horizon line is detected in the image"""
|
||||
|
||||
uut_ol = ObstacleLocator(image_width=IMAGE_WIDTH, image_height=IMAGE_HEIGHT) # unit/module under test
|
||||
|
||||
image = imread(PATH_TO_FAILING.as_posix())
|
||||
# ObstacleLocator does not find a horizon line in this image
|
||||
result = uut_ol.detect_horizon(image)
|
||||
self.assertFalse(result[0])
|
33
pyrate/tests/test_the_scripts.py
Normal file
@ -0,0 +1,33 @@
|
||||
"""Tests the bundled scripts."""
|
||||
|
||||
# Standard library
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
import sys
|
||||
|
||||
# Typing
|
||||
from typing import Set
|
||||
|
||||
# Generic testing
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
class TestScripts(TestCase):
|
||||
"""Tests ``pyrate/scripts/*``."""
|
||||
|
||||
BASE_DIR = Path(__file__).parent.parent / "scripts"
|
||||
EXCLUDE: Set[Path] = set()
|
||||
|
||||
def test_invoke_help(self) -> None:
|
||||
"""Tests that the help can be invoked, which makes sure that at least all imports work."""
|
||||
|
||||
for script in set(TestScripts.BASE_DIR.iterdir()) - TestScripts.EXCLUDE:
|
||||
if not script.name.startswith(".") and not script.name.startswith("__"):
|
||||
with self.subTest(script.name):
|
||||
run(
|
||||
[sys.executable, str(script), "--help"],
|
||||
capture_output=True,
|
||||
timeout=30, # Sometimes caches are generated, so this is a long timeout
|
||||
text=True,
|
||||
check=True,
|
||||
)
|