mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-23 03:46:37 +00:00
test(e2e): add SyntheticAdapter for harness self-tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
|||||||
|
"""Synthetic dataset — straight-line eastward flight at constant altitude.
|
||||||
|
|
||||||
|
Used for harness self-tests. No image files; image_path is empty so the harness
|
||||||
|
can substitute a generated checkerboard so existing OpenCV readers can open them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from gps_denied.testing.datasets.base import (
|
||||||
|
DatasetAdapter,
|
||||||
|
DatasetCapabilities,
|
||||||
|
DatasetFrame,
|
||||||
|
DatasetIMU,
|
||||||
|
DatasetPose,
|
||||||
|
PlatformClass,
|
||||||
|
)
|
||||||
|
|
||||||
|
GRAVITY = 9.81
|
||||||
|
EARTH_R = 6_378_137.0 # WGS84 semi-major axis, metres
|
||||||
|
|
||||||
|
|
||||||
|
class SyntheticAdapter(DatasetAdapter):
|
||||||
|
"""Deterministic straight-line eastward flight for harness smoke-tests."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
num_frames: int = 100,
|
||||||
|
fps: float = 5.0,
|
||||||
|
imu_rate_hz: float = 100.0,
|
||||||
|
speed_m_s: float = 10.0,
|
||||||
|
altitude_m: float = 100.0,
|
||||||
|
origin_lat: float = 49.0,
|
||||||
|
origin_lon: float = 32.0,
|
||||||
|
) -> None:
|
||||||
|
self._num_frames = num_frames
|
||||||
|
self._fps = fps
|
||||||
|
self._imu_rate_hz = imu_rate_hz
|
||||||
|
self._speed = speed_m_s
|
||||||
|
self._altitude = altitude_m
|
||||||
|
self._origin_lat = origin_lat
|
||||||
|
self._origin_lon = origin_lon
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return "synthetic"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def capabilities(self) -> DatasetCapabilities:
|
||||||
|
return DatasetCapabilities(
|
||||||
|
has_raw_imu=True,
|
||||||
|
has_rtk_gt=True,
|
||||||
|
has_loop_closures=False,
|
||||||
|
platform_class=PlatformClass.SYNTHETIC,
|
||||||
|
)
|
||||||
|
|
||||||
|
def iter_frames(self) -> Iterator[DatasetFrame]:
|
||||||
|
period_ns = int(1e9 / self._fps)
|
||||||
|
for i in range(self._num_frames):
|
||||||
|
yield DatasetFrame(
|
||||||
|
frame_idx=i,
|
||||||
|
timestamp_ns=i * period_ns,
|
||||||
|
image_path="", # harness fills from generator
|
||||||
|
)
|
||||||
|
|
||||||
|
def iter_imu(self) -> Iterator[DatasetIMU]:
|
||||||
|
duration_s = self._num_frames / self._fps
|
||||||
|
n_samples = int(duration_s * self._imu_rate_hz)
|
||||||
|
period_ns = int(1e9 / self._imu_rate_hz)
|
||||||
|
for i in range(n_samples):
|
||||||
|
yield DatasetIMU(
|
||||||
|
timestamp_ns=i * period_ns,
|
||||||
|
accel=(0.0, 0.0, -GRAVITY), # gravity only; constant velocity
|
||||||
|
gyro=(0.0, 0.0, 0.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
def iter_ground_truth(self) -> Iterator[DatasetPose]:
|
||||||
|
period_ns = int(1e9 / self._fps)
|
||||||
|
for i in range(self._num_frames):
|
||||||
|
t_s = i / self._fps
|
||||||
|
east_m = self._speed * t_s
|
||||||
|
# Local tangent plane approximation (small distance)
|
||||||
|
dlon = np.degrees(east_m / (EARTH_R * np.cos(np.radians(self._origin_lat))))
|
||||||
|
yield DatasetPose(
|
||||||
|
timestamp_ns=i * period_ns,
|
||||||
|
lat=self._origin_lat,
|
||||||
|
lon=self._origin_lon + dlon,
|
||||||
|
alt=self._altitude,
|
||||||
|
qx=0.0, qy=0.0, qz=0.0, qw=1.0,
|
||||||
|
)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"""SyntheticAdapter produces a deterministic straight-line trajectory."""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from gps_denied.testing.datasets.synthetic import SyntheticAdapter
|
||||||
|
from gps_denied.testing.datasets.base import PlatformClass
|
||||||
|
|
||||||
|
|
||||||
|
def test_synthetic_has_raw_imu():
|
||||||
|
adapter = SyntheticAdapter(num_frames=10, fps=5.0)
|
||||||
|
assert adapter.capabilities.has_raw_imu is True
|
||||||
|
assert adapter.capabilities.platform_class == PlatformClass.SYNTHETIC
|
||||||
|
|
||||||
|
|
||||||
|
def test_synthetic_iter_frames_count():
|
||||||
|
adapter = SyntheticAdapter(num_frames=10, fps=5.0)
|
||||||
|
frames = list(adapter.iter_frames())
|
||||||
|
assert len(frames) == 10
|
||||||
|
assert frames[0].frame_idx == 0
|
||||||
|
assert frames[-1].frame_idx == 9
|
||||||
|
|
||||||
|
|
||||||
|
def test_synthetic_frame_timestamps_monotonic():
|
||||||
|
adapter = SyntheticAdapter(num_frames=10, fps=5.0)
|
||||||
|
ts = [f.timestamp_ns for f in adapter.iter_frames()]
|
||||||
|
assert ts == sorted(ts)
|
||||||
|
# 5 fps → 200 ms spacing
|
||||||
|
deltas = np.diff(ts)
|
||||||
|
assert np.allclose(deltas, 200_000_000)
|
||||||
|
|
||||||
|
|
||||||
|
def test_synthetic_imu_samples_cover_frames():
|
||||||
|
adapter = SyntheticAdapter(num_frames=10, fps=5.0, imu_rate_hz=100.0)
|
||||||
|
imu = list(adapter.iter_imu())
|
||||||
|
# 10 frames at 5 fps = 2 s of data, at 100 Hz = 200 samples
|
||||||
|
assert len(imu) == 200
|
||||||
|
# Static trajectory → gravity on z, zero gyro
|
||||||
|
assert abs(imu[0].accel[2] + 9.81) < 1e-6
|
||||||
|
assert imu[0].gyro == (0.0, 0.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_synthetic_ground_truth_matches_frames():
|
||||||
|
adapter = SyntheticAdapter(num_frames=10, fps=5.0)
|
||||||
|
poses = list(adapter.iter_ground_truth())
|
||||||
|
assert len(poses) == 10
|
||||||
|
# Straight-line east at 10 m/s → position at t=0.2s is 2m east of origin
|
||||||
|
# (synthetic puts GT at same timestamps as frames)
|
||||||
|
assert abs(poses[1].lat - poses[0].lat) < 1e-9 # no latitude change
|
||||||
|
assert poses[1].lon > poses[0].lon # moving east
|
||||||
Reference in New Issue
Block a user