Files
gps-denied-onboard/docs/superpowers/specs/2026-04-18-oss-stack-tech-audit-design.md
T
Yuzviak dfac8d32b4 docs(tech-audit): expand design doc with reconciliation, risk budget, aero-vloc plan, SITL decomposition
Adds: solution.md reconciliation (cuVSLAM Inertial→Mono-Depth gap),
migration steps through e2e harness, risk budget decision tree,
aero-vloc benchmark action plan with pass/fail criteria,
and SITL GPS_INPUT test decomposition with MAVProxy reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:57:49 +03:00

18 KiB
Raw Blame History

Tech Audit — Open-Source Stack для GPS-Denied Navigation

Дата: 2026-04-18
Горизонт: 23 тижні (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 км маршруту: 15 м. Прийнятно для 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 Не потрібен на 510 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) 1012 мс TRT FP16 на Jetson, offline
Local matching XFeat (satellite↔UAV frame) ~50100 мс 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 510 Hz.

FGO дає перевагу (~15 vs 34 см) лише при non-Gaussian noise з outliers. Коли прийде час — GTSAM 4.2 stable (не 4.3a1 alpha). miniSAM стейл з 2019. g2o Python experimental.

Рішення Обґрунтування
pymavlink залишаємо MAVSDK-Python не підтримує GPS_INPUT (PX4-first)
Reference: MAVProxy/modules/mavproxy_GPSInput.py Точне кодування GPS_INPUT #232: lat/lon ×1e7, alt мм, vel см/с
Injection rate: 510 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)

# Зафіксувати 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
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, немає реальних тайлів — ок)

Деталі в §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 — benchmark framework для aerial visual localization. Запускається на UAV↔satellite парах.

Навіщо

До того як фіксувати дизайн Faiss index (tile size, descriptor dim, retrieval strategy) — треба знати реальні числа для нашого типу зображень: nadir, fixed-wing altitude, Ukrainian terrain.

Що запустити

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

# Запустити 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):

# Верифікувати кодування полів за 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):

# Верифікувати що GPS_INPUT виходить на 510 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, розширити):

# 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):

# Запустити справжній 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 — негайно

# 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, або питати постачальника
  • 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?