From 759766d737d3582b21e7ff7b474a602f4eebdb12 Mon Sep 17 00:00:00 2001 From: Yuzviak Date: Sat, 18 Apr 2026 16:29:00 +0300 Subject: [PATCH] =?UTF-8?q?refactor(vo):=20address=20final=20review=20?= =?UTF-8?q?=E2=80=94=20accurate=20docstring=20+=20update=5Fdepth=5Fhint=20?= =?UTF-8?q?tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final review findings (Important): - I1: e2e test docstring overclaimed — harness always uses ORBVisualOdometry. Rewrite docstring to describe the actual scope: smoke test + ORB regression guard. Wiring Mono-Depth wrapper through the harness is a sprint 2 task. - I2: update_depth_hint had no tests. Add 2 tests: clamp at 1.0m for bogus values, and verify next compute_relative_pose uses the updated scale. - I3: add TODO marker for sprint 2 deduplication with CuVSLAMVisualOdometry. No behavior change — only docstrings, TODO markers, and test coverage. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/gps_denied/core/vo.py | 3 +++ tests/e2e/test_euroc.py | 25 +++++++++++++++--------- tests/test_vo.py | 40 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/gps_denied/core/vo.py b/src/gps_denied/core/vo.py index 5950698..f0831c0 100644 --- a/src/gps_denied/core/vo.py +++ b/src/gps_denied/core/vo.py @@ -395,6 +395,9 @@ class CuVSLAMVisualOdometry(ISequentialVisualOdometry): # --------------------------------------------------------------------------- # CuVSLAMMonoDepthVisualOdometry — cuVSLAM Mono-Depth mode (sprint 1 production) # --------------------------------------------------------------------------- +# TODO(sprint 2): collapse duplicated SDK-load / _init_tracker scaffolding with +# CuVSLAMVisualOdometry once Inertial mode is removed. Kept separate for sprint 1 +# so the Inertial → Mono-Depth migration is reversible. # Reference altitude used to normalise ORB unit-scale translation in dev/CI. # At this altitude the ORB unit vector is scaled to match expected metric displacement. diff --git a/tests/e2e/test_euroc.py b/tests/e2e/test_euroc.py index d174e86..23f8b69 100644 --- a/tests/e2e/test_euroc.py +++ b/tests/e2e/test_euroc.py @@ -115,19 +115,27 @@ async def test_euroc_mh01_gps_rmse_within_ceiling(euroc_mh01_root: Path): @pytest.mark.needs_dataset @pytest.mark.asyncio async def test_euroc_mh01_mono_depth_within_ceiling(euroc_mh01_root: Path): - """Mono-Depth backend ATE on EuRoC — regression guard for VO migration. + """CuVSLAMMonoDepthVisualOdometry instantiation smoke test + ORB-pipeline regression guard. - Verifies CuVSLAMMonoDepthVisualOdometry._compute_via_orb_scaled produces - metric translations consistent with the baseline ORB pipeline when - depth_hint_m scale equals the calibrated VO_SCALE_M. + Scope of this test: + 1. SMOKE: CuVSLAMMonoDepthVisualOdometry constructs with EuRoC-typical + depth_hint and camera params without raising. + 2. REGRESSION GUARD: runs the existing ORB-based harness (which uses + ORBVisualOdometry() directly — see src/gps_denied/testing/harness.py) + with the calibrated VO_SCALE_M and asserts ATE stays under 0.5 m. - EuRoC indoor != production outdoor nadir. Poor ATE here is not a blocker - for production. Test documents baseline and prevents unexpected regression. + NOT in scope (deliberately): this test does NOT exercise + CuVSLAMMonoDepthVisualOdometry through the pipeline. The E2EHarness + currently hardcodes ORBVisualOdometry(); wiring the Mono-Depth wrapper + through the harness is a sprint 2 task. The scale-math invariant is + covered by test_mono_depth_depth_hint_scales_translation in test_vo.py. + + EuRoC indoor != production outdoor nadir. Baseline ATE 0.2046m documented. """ from gps_denied.core.vo import CuVSLAMMonoDepthVisualOdometry from gps_denied.schemas import CameraParameters - # Sanity: class instantiates and reports metric scale. + # (1) Smoke test — class instantiates with EuRoC-typical params. cam = CameraParameters( focal_length=16.0, sensor_width=23.2, sensor_height=17.4, resolution_width=752, resolution_height=480, @@ -137,8 +145,7 @@ async def test_euroc_mh01_mono_depth_within_ceiling(euroc_mh01_root: Path): ) assert vo._depth_hint_m == EUROC_MH01_MONO_DEPTH_HINT_M - # Full e2e using the calibrated scale — pipeline-equivalent to baseline - # ORB until cuVSLAM SDK lands on Jetson. + # (2) Regression guard — ORB pipeline baseline stays under 0.5m ATE. adapter = EuRoCAdapter(euroc_mh01_root) harness = E2EHarness( adapter, max_frames=EUROC_MH01_MAX_FRAMES, vo_scale_m=EUROC_MH01_VO_SCALE_M, diff --git a/tests/test_vo.py b/tests/test_vo.py index 9bf8665..884cce1 100644 --- a/tests/test_vo.py +++ b/tests/test_vo.py @@ -301,3 +301,43 @@ def test_mono_depth_create_vo_backend_selects_it(): from gps_denied.core.vo import CuVSLAMMonoDepthVisualOdometry, create_vo_backend vo = create_vo_backend(prefer_mono_depth=True, depth_hint_m=600.0) assert isinstance(vo, CuVSLAMMonoDepthVisualOdometry) + + +def test_mono_depth_update_depth_hint_clamps_below_one(): + """update_depth_hint clamps bogus/negative barometer values to minimum 1.0m.""" + from gps_denied.core.vo import CuVSLAMMonoDepthVisualOdometry + vo = CuVSLAMMonoDepthVisualOdometry(depth_hint_m=600.0) + vo.update_depth_hint(-5.0) + assert vo._depth_hint_m == 1.0 + vo.update_depth_hint(0.0) + assert vo._depth_hint_m == 1.0 + vo.update_depth_hint(0.5) + assert vo._depth_hint_m == 1.0 + + +def test_mono_depth_update_depth_hint_affects_subsequent_scale(): + """update_depth_hint changes scale used by next compute_relative_pose call.""" + from unittest.mock import patch + from gps_denied.core.vo import CuVSLAMMonoDepthVisualOdometry, ORBVisualOdometry + from gps_denied.schemas.vo import RelativePose + + cam = CameraParameters( + focal_length=16.0, sensor_width=23.2, sensor_height=17.4, + resolution_width=640, resolution_height=480, + ) + img = np.zeros((480, 640), dtype=np.uint8) + unit_pose = RelativePose( + translation=np.array([1.0, 0.0, 0.0]), + rotation=np.eye(3), + confidence=1.0, inlier_count=100, total_matches=100, + tracking_good=True, scale_ambiguous=True, + ) + + with patch.object(ORBVisualOdometry, "compute_relative_pose", return_value=unit_pose): + vo = CuVSLAMMonoDepthVisualOdometry(depth_hint_m=600.0) + pose_before = vo.compute_relative_pose(img, img, cam) + vo.update_depth_hint(1200.0) + pose_after = vo.compute_relative_pose(img, img, cam) + + assert np.allclose(pose_before.translation, [1.0, 0.0, 0.0]) # 600/600 = 1.0x + assert np.allclose(pose_after.translation, [2.0, 0.0, 0.0]) # 1200/600 = 2.0x