# Tech Audit — Open-Source Stack для GPS-Denied Navigation **Дата:** 2026-04-18 **Горизонт:** 2–3 тижні (sprint 1) **Контекст:** Fixed-wing БПЛА, Jetson Orin Nano Super, nadir камера ADTI 20L V1, ArduPilot FC **Regression guard:** 196 passed / 8 skipped (non-e2e). EuRoC ESKF ATE baseline ~0.205 м (100 кадрів). --- ## 0. Reconciliation: три джерела Цей документ синтезує три джерела і де вони суперечать одне одному — фіксує явно. | Джерело | Ключове твердження | Статус | |---|---|---| | `solution.md` | cuVSLAM **Inertial mode** з IMU @200 Hz | ⚠️ Архітектурна помилка — Inertial потребує stereo | | `stage2_ideas/cross_view_place_recognition_stack.md` | SP+LG+FAISS для satellite matching | ✅ Backlog сам каже "порівняй з AnyLoc першим" | | Ресерч (2026-04-18) | cuVSLAM Mono-Depth + barometer; DINOv2-VLAD як baseline GPR | ✅ Консистентний з stage2 backlog | **Що НЕ робимо зараз:** переписувати `solution.md`. Це окрема задача з `next_steps.md` ("Аудит відповідності солюшну"). Цей план — migration поверх існуючого ORB baseline, не greenfield. **Що змінюємо:** фіксуємо cuVSLAM Mono-Depth як sprint 1 production VO (замість Inertial який фізично неможливий без stereo). GPR: AnyLoc як sprint 1 baseline, SP+LG — stage 2 evaluation (консистентно з backlog). --- ## 1. Архітектурний розрив ### cuVSLAM Inertial mode потребує stereo — у нас mono `solution.md` планує cuVSLAM Inertial mode. Але: - cuVSLAM Visual-Inertial = **stereo камера обов'язкова**. Mono-Inertial режиму не існує у v15. - cuVSLAM Mono дає лише rotation, без metric scale → неможливо побудувати GPS_INPUT. - Camera пілота (вперед) + nadir камера (вниз) ≠ stereo (stereo = дві камери в одному напрямку, ∆x = 10–20 см). **Рішення для sprint 1:** cuVSLAM **Mono-Depth** — барометрична висота подається як synthetic depth, відновлює metric scale. Scale для nadir над рівною місцевістю детермінований: `scale = altitude / focal_length`. Барометр → ESKF → scale. VO потребує лише точний 2D planar tracking. ### Чому drift на маршруті не критичний ``` VO drift між кадрами (~0.3–0.5 м на сотні метрів) ↓ ESKF + IMU (короткострокова стабілізація, ~мс) ↓ Place Recognition ← satellite tiles (глобальна корекція кожні ~500 м) ↓ GTSAM loop closure (stage 2) ``` GPR скидає накопичений drift → загальна похибка на 10 км маршруту: **1–5 м**. Прийнятно для GPS-denied. **Ризик GPR:** хмари, однорідна місцевість (поле, ліс), застарілі тайли → drift накопичується до наступного match. Це відоме обмеження, не сюрприз. --- ## 2. Рішення по шарах стеку ### 2.1 Visual Odometry | Компонент | Dev/CI | Production (Jetson) | Рішення | |---|---|---|---| | **VO backend** | ORBVisualOdometry (OpenCV) | cuVSLAM **Mono-Depth** | Фіксуємо Mono-Depth замість Inertial | | **SuperPoint+LightGlue** | — | — | ❌ Відхилено для VO: 15–33× повільніше cuVSLAM, немає IMU fusion | | **XFeat** | — | Satellite tile matching | Не VO fallback — окремий трек (§2.3) | **Відхилені альтернативи для sprint 1:** - **ORB-SLAM3 Mono-Inertial** (~0.08 м EuRoC) — потребує IMU ≥100 Hz по MAVLink. ArduPilot за замовчуванням шле 50 Hz. Можливо після підняття `SR*_RAW_SENS`, але не пріоритет. - **cuVSLAM Stereo-Inertial** (0.054 м) — потребує hardware (друга камера). Довгострокова ціль. ### 2.2 ESKF | Рішення | Обґрунтування | |---|---| | Залишаємо numpy 15-state ESKF | Достатній для 5–10 Hz VO на повільному fixed-wing | | **Пінити `numpy==1.26.4`** | NumPy 2.0 ламає GTSAM Python bindings silently (issue #2264) | | `manifpy` — тільки якщо знайдемо quaternion баги | pip-installable, не додаємо превентивно | | IMU preintegration | Не потрібен на 5–10 Hz — step-by-step propagation еквівалентний | ### 2.3 Place Recognition **Поточний стан:** MockInferenceEngine (dev), Faiss numpy fallback. Реального GPR ще немає. **Sprint 1 baseline:** AnyLoc-VLAD-DINOv2 (offline-capable). | Компонент | Рішення | Обґрунтування | |---|---|---| | **Global descriptor** | DINOv2-VLAD (AnyLoc) | 10–12 мс TRT FP16 на Jetson, offline | | **Local matching** | XFeat (satellite↔UAV frame) | ~50–100 мс TRT, cross-view gap краще ніж ORB | | **Index** | Faiss GPU | Залишаємо | | **INT8 quantization** | ❌ Не робити | Broken для ViT на Jetson (NVIDIA/TRT#4348, dinov2#489) | | **NetVLAD** | ❌ Deprecated | DINOv2-VLAD +2.4% R@1 на MSLS 2024 | | **Satellite tiles** | MapTiler offline MBTiles | Zoom 18 (0.6 м/px), Україна, JPEG offline | **Stage 2 evaluation (не зараз):** SuperPoint+LightGlue+FAISS (з `stage2_ideas/`) — backlog сам каже порівняти з AnyLoc першим. Ця оцінка відбудеться після того як AnyLoc baseline зафіксований і виміряний. **Довгострокова ціль:** EigenPlaces (ICCV 2023) — кращий ONNX export, viewpoint-robust. ### 2.4 Factor Graph (GTSAM) **Sprint 1: пропускаємо.** ESKF достатній для Gaussian GPS_INPUT 5–10 Hz. FGO дає перевагу (~15 vs 34 см) лише при non-Gaussian noise з outliers. Коли прийде час — GTSAM 4.2 stable (не 4.3a1 alpha). miniSAM стейл з 2019. g2o Python experimental. ### 2.5 MAVLink / ArduPilot | Рішення | Обґрунтування | |---|---| | **pymavlink** залишаємо | MAVSDK-Python не підтримує GPS_INPUT (PX4-first) | | Reference: `MAVProxy/modules/mavproxy_GPSInput.py` | Точне кодування GPS_INPUT #232: lat/lon ×1e7, alt мм, vel см/с | | Injection rate: 5–10 Hz | Не 20 Hz — timing jitter задокументований при вищих rate | | Yaw extension field | Не використовувати — ArduPilot 4.x ігнорує | --- ## 3. Міграційний план через e2e-харнес **Принцип:** кожна зміна VO backend проходить через E2EHarness до merge. Поточний regression guard — 196 passed / 8 skipped. EuRoC ESKF RMSE ceiling = 0.5 м (2× від baseline ~0.205 м). ### Крок 0 — стабілізація baseline (тиждень 1, день 1–2) ```bash # Зафіксувати numpy pip install numpy==1.26.4 # Записати в pyproject.toml: numpy>=1.24,<2.0 # Верифікувати що baseline не зламався pytest tests/ -x --ignore=tests/e2e -q # має бути 196 passed pytest tests/e2e/test_euroc.py -v --dataset-path ./datasets/euroc # ATE ~0.205 м ``` **Gate:** якщо ATE після numpy pin відхиляється > 5% від 0.205 м → зупинитись і дебажити до merge. ### Крок 1 — cuVSLAM Mono-Depth adapter (тиждень 1, день 3–5) 1. Додати `CuVSLAMMonoDepthVisualOdometry` backend у `src/gps_denied/core/vo.py` 2. Приймає барометричну висоту як `depth_hint_m` параметр 3. Mock реалізація для dev/CI (повертає scale-corrected RelativePose) 4. Запустити e2e на EuRoC: порівняти ATE з ORB baseline ```bash pytest tests/e2e/test_euroc.py -v -k "cuvslam_mono_depth" # Записати результат в docs/ ``` **Gate:** ATE cuVSLAM Mono-Depth має бути ≤ 0.5 м (поточний ceiling). Якщо гірше → див. Risk Budget (§4). ### Крок 2 — AnyLoc GPR integration (тиждень 2) 1. Реалізувати `AnyLocGPR` клас що замінює MockInferenceEngine 2. Offline DINOv2-VLAD descriptor, Faiss index на test tile set 3. e2e на VPAIR sample (є satellite-like tiles): перевірити GPR hit rate 4. EuRoC: GPS-estimate ATE залишиться xfail (indoor, немає реальних тайлів — ок) ### Крок 3 — SITL MAVLink integration (тиждень 3) Деталі в §6. ### Крок 4 — aero-vloc benchmark (тиждень 2–3, паралельно) Деталі в §5. --- ## 4. Risk Budget: якщо cuVSLAM Mono-Depth гірше ніж ORB **Сценарій:** cuVSLAM Mono-Depth на EuRoC дає ATE > 0.205 м (поточний ORB baseline). Це **очікувано і не є блокером** — EuRoC indoor ≠ наш production сценарій (outdoor nadir, відома висота). Але потрібне рішення. ### Дерево рішень ``` cuVSLAM Mono-Depth ATE на EuRoC │ ├─ ≤ 0.205 м (краще або рівно ORB) │ → ✅ Merge, продовжуємо │ ├─ 0.205–0.5 м (гірше ніж ORB але в межах ceiling) │ → ⚠️ Прийнятно для sprint 1 — EuRoC не є target dataset │ → Записати в docs, відкрити тікет для наступного кроку │ → Продовжуємо, але плануємо IMU rate upgrade │ └─ > 0.5 м (перевищує ceiling) → Три варіанти: │ ├─ A) Тюнінг depth_hint scaling (барометр calibration) │ Тривалість: 1 день │ Спробуй першим — часто проблема в неправильному scale factor │ ├─ B) Підняти IMU rate → 200 Hz (SR*_RAW_SENS) │ + перейти на ORB-SLAM3 Mono-Inertial (~0.08 м EuRoC) │ Тривалість: 3–5 днів (C++ build + Python binding) │ Потрібно: підтвердити FC має H743, перевірити UART bandwidth │ └─ C) Залишити ORB як production VO тимчасово + зосередитись на GPR (AnyLoc) як основному correction механізмі Тривалість: 0 (нічого не міняємо у VO) Прийнятно якщо GPR hits кожні ≤ 500 м ``` **Важливо:** EuRoC MH_01 — indoor MAV з forward camera. cuVSLAM Mono-Depth оптимізований для outdoor nadir. Поганий ATE на EuRoC ≠ поганий ATE на реальному польоті. Льотні тести є остаточним арбітром. --- ## 5. aero-vloc benchmark — що робити з результатами [aero-vloc](https://github.com/prime-slam/aero-vloc) — benchmark framework для aerial visual localization. Запускається на UAV↔satellite парах. ### Навіщо До того як фіксувати дизайн Faiss index (tile size, descriptor dim, retrieval strategy) — треба знати реальні числа для нашого типу зображень: nadir, fixed-wing altitude, Ukrainian terrain. ### Що запустити ```bash git clone https://github.com/prime-slam/aero-vloc cd aero-vloc # 1. Підготувати пари: UAV кадри з наших SRT/відео + відповідні MapTiler тайли # 2. Запустити benchmark з кількома дескрипторами: python benchmark.py --query ./our_nadir_frames/ \ --database ./satellite_tiles/ \ --descriptors netvlad dinov2_vlad eigenplaces \ --top-k 5 # 3. Записати R@1, R@5, медіанна помилка локалізації в метрах ``` ### Що зробити з результатами | Результат | Дія | |---|---| | DINOv2-VLAD R@1 ≥ 60% | ✅ Підтверджує AnyLoc як sprint 1 baseline | | DINOv2-VLAD R@1 < 40% | ⚠️ Перевірити domain gap — можливо треба fine-tuning або EigenPlaces | | EigenPlaces > DINOv2-VLAD на ≥10% R@1 | Прискорити перехід на EigenPlaces (з stage 2 → sprint 2) | | Медіанна помилка > 50 м | Проблема з tile resolution або GSD mismatch → перевірити zoom level | | Медіанна помилка ≤ 20 м | ✅ Відповідає `solution.md` цілі `<20 м absolute anchor` | **Результати зберегти в:** `_docs/00_research/oss_stack_options/aero_vloc_results.md` **Використати для:** фіналізації Faiss index design (tile size, overlap, descriptor dim) перед реалізацією AnyLocGPR. --- ## 6. SITL Integration Test Plan SITL (Software-In-The-Loop) = ArduPilot запущений як симулятор, приймає GPS_INPUT від нашої системи через MAVLink без реального hardware. ### Налаштування SITL ```bash # Запустити ArduPilot SITL (потрібен окремий binary або Docker) sim_vehicle.py -v ArduPlane --console --map # Параметри ArduPilot для GPS_INPUT: # GPS1_TYPE = 14 (MAVLink) # GPS_RATE_MS = 200 (5 Hz мінімум) # EK3_SRC1_POSXY = 1, EK3_SRC1_VELXY = 1 ``` ### Декомпозиція тестів Тест 1 — **Field encoding** (unit, без SITL): ```python # Верифікувати кодування полів за MAVProxy reference: # MAVProxy/modules/mavproxy_GPSInput.py def test_gps_input_field_encoding(): msg = build_gps_input(lat=48.123, lon=37.456, alt_m=600.0, vn=10.0, ve=5.0, vd=0.0, h_acc=15.0, v_acc=8.0, fix_type=3) assert msg.lat == int(48.123 * 1e7) # lat ×1e7 assert msg.lon == int(37.456 * 1e7) # lon ×1e7 assert msg.alt == int(600.0 * 1000) # alt мм assert msg.vn == int(10.0 * 100) # vel см/с assert msg.satellites_visible == 10 # synthetic, prevent failsafe assert msg.fix_type == 3 ``` Тест 2 — **Rate delivery** (з реальним pymavlink, mock SITL endpoint): ```python # Верифікувати що GPS_INPUT виходить на 5–10 Hz без jitter > ±20 мс def test_gps_input_rate_5hz(): timestamps = collect_gps_input_timestamps(duration_s=10) intervals = np.diff(timestamps) assert np.mean(intervals) == pytest.approx(0.2, abs=0.02) # 5 Hz ±10% assert np.max(intervals) < 0.25 # жоден interval не > 250 мс ``` Тест 3 — **Confidence tier transitions** (вже є в `test_sitl_integration.py`, розширити): ```python # HIGH → MEDIUM → LOW → FAILED transitions # Верифікувати fix_type і horiz_accuracy змінюються коректно # Вже: test_reloc_request_after_3_failures_with_sitl # Додати: test_fix_type_degrades_without_satellite_match ``` Тест 4 — **ArduPilot EKF acceptance** (повний SITL): ```python # Запустити справжній SITL, подати GPS_INPUT, перевірити що EKF приймає # Метрика: GLOBAL_POSITION_INT від SITL відповідає нашому GPS_INPUT з похибкою < 5 м # Це верифікує що ArduPilot не відкидає наші повідомлення (наприклад через fix_type=0) ``` ### Reference implementation Взяти за основу `MAVProxy/modules/mavproxy_GPSInput.py`: - Точне кодування всіх 15+ полів GPS_INPUT (#232) - Обробка GPS time (Unix → GPS epoch, leap seconds) - hdop/vdop synthetic values **Зауваження щодо FC процесора:** H743 приймає GPS_INPUT over UART. F405 — мовчки ігнорує. Перевірити до льотних тестів. --- ## 7. Критичні перевірки перед кодом ### 7.1 numpy pin — негайно ```toml # pyproject.toml dependencies = [ "numpy>=1.24,<2.0", # NumPy 2.0 ламає GTSAM bindings (issue #2264) ... ] ``` ### 7.2 Flight Controller processor - **H743** → GPS_INPUT over serial ✅ - **F405** → мовчки ігнорує GPS_INPUT ❌ - Перевірити: Mission Planner → Help → About, або питати постачальника ### 7.3 IMU rate по MAVLink - Default ArduPilot: **50 Hz** - Для ORB-SLAM3 Mono-Inertial (майбутнє): **≥100 Hz** - Змінити: `SR2_RAW_SENS = 10` (×10 → 100 Hz) або `= 20` (200 Hz) - Sprint 1 (cuVSLAM Mono-Depth): не критично, але виміряти поточне значення --- ## 8. Аналоги системи | Система | VO | Scale source | Fusion | Satellite anchor | |---|---|---|---|---| | **Наша (sprint 1)** | cuVSLAM Mono-Depth | Барометр | ESKF | DINOv2-VLAD+Faiss | | **Наша (stage 2)** | cuVSLAM Stereo-Inertial | IMU | ESKF+GTSAM | SP+LG+Faiss | | **VINS-Fusion** | Mono-Inertial | IMU | Factor graph | Немає | | **OpenVINS** | MSCKF Mono | IMU | EKF | Немає | | **ArduPilot EKF3** | — | GPS | EKF | GPS (не satellite matching) | Унікальність нашої архітектури: **VIO + global satellite anchor** без GPS. Не просто odometry — прив'язка до карти в реальному часі. --- ## 9. Відкриті питання 1. Якість cuVSLAM Mono-Depth на feature-poor nadir terrain (рівне поле, ліс) — тільки льотні тести відповідять 2. aero-vloc покаже реальний R@1 для DINOv2-VLAD на нашому типі зображень 3. FC processor: H743 чи F405? — блокер для SITL тестів 4. IMU rate: скільки зараз по MAVLink? — вплине на roadmap VO upgrade 5. MapTiler MBTiles для України: ліцензія дозволяє offline onboard deployment?