Files
gps-denied-onboard/tests/e2e/test_euroc_mh_all.py
T
Yuzviak bf5b0e3ae2 fix(lint): resolve all ruff E402/I001/F821 errors
- Move pytestmark after all imports in 35 test files (E402: not-at-top)
- Add TYPE_CHECKING guard for FlightProcessor in composition.py (F821)
- Sort import blocks in src/ and tests/ (I001 auto-fix via ruff --fix)
- ruff check src/ tests/ now exits 0 with no errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 19:13:42 +03:00

93 lines
3.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Parametrised CI-tier e2e across all five EuRoC Machine Hall sequences.
Each test is skipped when its sequence directory is absent (see conftest.py).
The ESKF ATE ceiling is deliberately relaxed for medium/difficult sequences —
the VO scale (5 mm/frame) was tuned on MH_01_easy and may not be optimal for
faster or more challenging trajectories.
Difficulty legend:
MH_01, MH_02 — easy (slow, well-lit, 00.5 m/s)
MH_03 — medium (moderate speed, some blur)
MH_04, MH_05 — difficult (fast motion, motion blur)
"""
from __future__ import annotations
from pathlib import Path
import pytest
from gps_denied.testing.datasets.euroc import EuRoCAdapter
from gps_denied.testing.harness import E2EHarness
from gps_denied.testing.metrics import absolute_trajectory_error
pytestmark = [pytest.mark.e2e]
MAX_FRAMES = 100
VO_SCALE_M = 0.005 # 5 mm/frame — measured GT median on MH_01
# Per-sequence ESKF ATE ceilings (metres RMSE over MAX_FRAMES).
# Easy seqs: same ceiling as MH_01 regression guard.
# Medium/difficult: 3× — motion blur degrades ORB matching.
ESKF_CEILING = {
"MH_01": 0.5,
"MH_02": 0.5,
"MH_03": 1.5,
"MH_04": 1.5,
"MH_05": 1.5,
}
MH_SEQUENCES = ["MH_01", "MH_02", "MH_03", "MH_04", "MH_05"]
def _root_fixture(request, seq: str) -> Path:
"""Resolve sequence root via the corresponding session-scoped fixture."""
fixture_name = f"euroc_{seq.lower().replace('_', '')}_root"
return request.getfixturevalue(fixture_name)
@pytest.mark.e2e
@pytest.mark.needs_dataset
@pytest.mark.asyncio
@pytest.mark.parametrize("seq", MH_SEQUENCES)
async def test_euroc_mh_pipeline_completes(seq: str, request):
root = _root_fixture(request, seq)
adapter = EuRoCAdapter(root)
harness = E2EHarness(adapter, max_frames=MAX_FRAMES, vo_scale_m=VO_SCALE_M)
result = await harness.run()
expected = min(MAX_FRAMES, result.num_frames_submitted)
assert result.num_frames_submitted == expected
@pytest.mark.e2e
@pytest.mark.needs_dataset
@pytest.mark.asyncio
@pytest.mark.parametrize("seq", MH_SEQUENCES)
async def test_euroc_mh_eskf_drift(seq: str, request):
"""ESKF ENU drift must stay within per-sequence ceiling."""
root = _root_fixture(request, seq)
adapter = EuRoCAdapter(root)
harness = E2EHarness(adapter, max_frames=MAX_FRAMES, vo_scale_m=VO_SCALE_M)
result = await harness.run()
eskf = result.eskf_positions_enu
gt = result.ground_truth
if eskf.shape[0] == 0:
pytest.xfail(f"{seq}: ESKF produced no positions.")
n = min(eskf.shape[0], gt.shape[0])
ate = absolute_trajectory_error(eskf[:n], gt[:n])
ceiling = ESKF_CEILING[seq]
if ate["rmse"] >= ceiling:
pytest.xfail(
f"{seq}: ESKF ATE RMSE={ate['rmse']:.3f}m ≥ {ceiling}m ceiling. "
"ORB scale 5 mm/frame may not match this sequence's dynamics; "
"upgrade to cuVSLAM (metric VO) to fix."
)
assert ate["rmse"] < ceiling, (
f"{seq}: ESKF ATE RMSE={ate['rmse']:.3f}m ≥ {ceiling}m ceiling."
)