diff --git a/tests/e2e/test_vpair.py b/tests/e2e/test_vpair.py new file mode 100644 index 0000000..00646e4 --- /dev/null +++ b/tests/e2e/test_vpair.py @@ -0,0 +1,51 @@ +"""VPAIR nominal e2e — fixed-wing, downward, no raw IMU. + +Tests that require full ESKF path are skipped because VPAIR ships poses only. +The VO + GPR + graph path is still exercised. +""" + +from pathlib import Path + +import pytest + +from gps_denied.testing.datasets.vpair import VPAIRAdapter +from gps_denied.testing.harness import E2EHarness +from gps_denied.testing.metrics import absolute_trajectory_error + + +VPAIR_SAMPLE_RMSE_CEILING_M = 20.0 # initial target, calibrate after first runs + + +@pytest.mark.e2e +@pytest.mark.e2e_slow +@pytest.mark.needs_dataset +@pytest.mark.asyncio +async def test_vpair_sample_pipeline_completes(vpair_sample_root: Path): + adapter = VPAIRAdapter(vpair_sample_root) + if not adapter.capabilities.has_raw_imu: + # ESKF path is skipped automatically inside the product because IMU + # callbacks are never fired; we only check completion here. + pass + harness = E2EHarness(adapter) + result = await harness.run() + assert result.num_frames_submitted > 0 + + +@pytest.mark.e2e +@pytest.mark.e2e_slow +@pytest.mark.needs_dataset +@pytest.mark.asyncio +async def test_vpair_sample_trajectory_bounded(vpair_sample_root: Path): + adapter = VPAIRAdapter(vpair_sample_root) + harness = E2EHarness(adapter) + result = await harness.run() + if result.estimated_positions_enu.shape[0] == 0: + pytest.xfail( + "Pipeline produced no GPS estimates on VPAIR sample. " + "Expected until VO + GPR tuning for 300-400m nadir imagery is validated." + ) + n = min(result.estimated_positions_enu.shape[0], result.ground_truth.shape[0]) + ate = absolute_trajectory_error( + result.estimated_positions_enu[:n], result.ground_truth[:n] + ) + assert ate["rmse"] < VPAIR_SAMPLE_RMSE_CEILING_M, f"ATE RMSE={ate['rmse']:.2f}m"