mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-22 21:56:38 +00:00
f66b266219
Four I001 violations surfaced when running ruff over the full src/ tests/ tree (the CI command) rather than just the testing subpath: - src/gps_denied/testing/coord.py - src/gps_denied/testing/datasets/vpair.py - tests/e2e/test_coord.py - tests/e2e/test_vpair_adapter.py All auto-fixable; no behavioural change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
104 lines
3.7 KiB
Python
104 lines
3.7 KiB
Python
"""VPAIRAdapter unit tests with a fabricated layout matching the real VPAIR sample.
|
|
|
|
Real VPAIR poses_query.txt columns:
|
|
filepath,x,y,z,undulation,roll,pitch,yaw,landcover
|
|
x/y/z = ECEF metres; roll/pitch/yaw = radians; landcover = int tag (ignored).
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from gps_denied.testing.datasets.base import (
|
|
DatasetNotAvailableError,
|
|
PlatformClass,
|
|
)
|
|
from gps_denied.testing.datasets.vpair import VPAIRAdapter
|
|
|
|
# ECEF for a point at roughly lat=50.737°, lon=7.095°, alt=350m (Bonn/Eifel region).
|
|
# Chosen to hit the real VPAIR geographic area so the adapter's conversion
|
|
# produces plausible numbers the tests can assert on.
|
|
_ECEF_ROW0 = (4023518.0, 510303.75, 4906569.65)
|
|
_ECEF_ROW1 = (4023484.45, 510291.89, 4906597.63)
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_vpair_root(tmp_path: Path) -> Path:
|
|
(tmp_path / "queries").mkdir()
|
|
for fn in ("00001.png", "00002.png"):
|
|
# minimal PNG header bytes; OpenCV won't actually need to read these in
|
|
# unit tests (adapter only stores paths).
|
|
(tmp_path / "queries" / fn).write_bytes(b"\x89PNG\r\n\x1a\n")
|
|
(tmp_path / "poses_query.txt").write_text(
|
|
"filepath,x,y,z,undulation,roll,pitch,yaw,landcover\n"
|
|
f"queries/00001.png,{_ECEF_ROW0[0]},{_ECEF_ROW0[1]},{_ECEF_ROW0[2]},"
|
|
"47.8,-0.0073764682747423,0.0095759807154536,-0.0762864127755165,1\n"
|
|
f"queries/00002.png,{_ECEF_ROW1[0]},{_ECEF_ROW1[1]},{_ECEF_ROW1[2]},"
|
|
"47.8,0.0266015380620956,-0.0029512757901102,-0.0540984831750392,3\n"
|
|
)
|
|
return tmp_path
|
|
|
|
|
|
def test_raises_when_missing(tmp_path: Path):
|
|
with pytest.raises(DatasetNotAvailableError):
|
|
VPAIRAdapter(tmp_path / "nope")
|
|
|
|
|
|
def test_raises_when_poses_file_missing(tmp_path: Path):
|
|
(tmp_path / "queries").mkdir()
|
|
# no poses_query.txt
|
|
with pytest.raises(DatasetNotAvailableError):
|
|
VPAIRAdapter(tmp_path)
|
|
|
|
|
|
def test_capabilities_no_raw_imu(fake_vpair_root: Path):
|
|
adapter = VPAIRAdapter(fake_vpair_root)
|
|
cap = adapter.capabilities
|
|
assert cap.has_raw_imu is False
|
|
assert cap.platform_class == PlatformClass.FIXED_WING
|
|
|
|
|
|
def test_iter_frames_indices_and_paths(fake_vpair_root: Path):
|
|
adapter = VPAIRAdapter(fake_vpair_root)
|
|
frames = list(adapter.iter_frames())
|
|
assert len(frames) == 2
|
|
assert frames[0].frame_idx == 0
|
|
assert Path(frames[0].image_path).name == "00001.png"
|
|
assert frames[1].frame_idx == 1
|
|
|
|
|
|
def test_iter_frames_synthesizes_timestamps_at_5hz(fake_vpair_root: Path):
|
|
adapter = VPAIRAdapter(fake_vpair_root)
|
|
frames = list(adapter.iter_frames())
|
|
# 5 Hz → 200 ms = 200_000_000 ns
|
|
assert frames[0].timestamp_ns == 0
|
|
assert frames[1].timestamp_ns == 200_000_000
|
|
|
|
|
|
def test_iter_imu_empty(fake_vpair_root: Path):
|
|
adapter = VPAIRAdapter(fake_vpair_root)
|
|
assert list(adapter.iter_imu()) == []
|
|
|
|
|
|
def test_iter_ground_truth_converts_ecef_to_wgs84(fake_vpair_root: Path):
|
|
adapter = VPAIRAdapter(fake_vpair_root)
|
|
gt = list(adapter.iter_ground_truth())
|
|
assert len(gt) == 2
|
|
# Bonn/Eifel area — rough bounds
|
|
assert 50.0 < gt[0].lat < 51.5
|
|
assert 6.5 < gt[0].lon < 8.0
|
|
# Altitude above ellipsoid, hundreds of metres
|
|
assert 100.0 < gt[0].alt < 700.0
|
|
|
|
|
|
def test_iter_ground_truth_converts_euler_to_unit_quaternion(fake_vpair_root: Path):
|
|
adapter = VPAIRAdapter(fake_vpair_root)
|
|
gt = list(adapter.iter_ground_truth())
|
|
for pose in gt:
|
|
norm = np.sqrt(pose.qx**2 + pose.qy**2 + pose.qz**2 + pose.qw**2)
|
|
assert norm == pytest.approx(1.0, abs=1e-10)
|
|
# First row roll/pitch/yaw are small angles ≈0 → quaternion close to identity
|
|
# qw should be close to 1 (but not exactly; roll/pitch/yaw != 0)
|
|
assert gt[0].qw > 0.99
|