1
0

Added pyrate as a direct dependency.

This commit is contained in:
2022-07-11 23:07:33 +02:00
parent 8c4532dad4
commit c99d517f6f
230 changed files with 21114 additions and 0 deletions

37
pyrate/tests/__init__.py Normal file
View 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))

View File

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

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

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

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

View File

View File

View 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.

View File

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

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

View 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.

View 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

View 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.

View 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

View 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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View 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

View 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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1 @@
This should not be found by the discovery chart tests.

View 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))

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

View File

@ -0,0 +1 @@
"""Tests ``pyrate.common.math.**``."""

View 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>`__.
"""

View 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)

View 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
)

View File

View File

View 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

View 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)

View 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

View 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)

View 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)

View 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)

View 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))

View 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)

View 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)

View 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)]))

View 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)]))

View File

View 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

View File

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

View 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

View 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)

View 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",
)

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

View 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])

View File

View File

View 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

View 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)

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View 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())

View 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}",
)

View 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])

View 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,
)