ADR 0002: hexagonal/ports-and-adapters architecture — components/ layout, protocol.py per component, composition root, core/ for concentrated math. ADR 0003: @dataclass(slots=True, frozen=True) on hot path; Pydantic retained only at REST/config/DB boundaries. Pose/GPSPoint migration deferred to Phase 2. ADR 0004: Stage 2 as independent iteration — own phases 1-6, own requirements, stage1 code treated as MVP starting capital. PROJECT.md: Stage 2 Key Decisions updated from Pending → Accepted with Phase 1 implementation notes, deferred work list, and final architecture summary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
13 KiB
Requirements: GPS-Denied Onboard Navigation System — Stage 2
Defined: 2026-05-10
Stage: 2 (independent iteration)
Branch: stage2 (HEAD = stage1; v1.0 archived)
Core Value: The flight controller must receive valid MAVLink GPS_INPUT at 5-10Hz with position accuracy ≤50m for 80% of frames — without this, the UAV cannot navigate in GPS-denied airspace.
Stage 2 Requirements
Stage 2 is a self-contained iteration. Phases are numbered 1–6 within this stage. Stage 1 work (its 36 v1 requirements + 7 phases) is archived in .planning/archive/v1.0/ as starting capital, not as active backlog.
The stage 1 codebase (ESKF + cuVSLAM + GPR + MAVLink + pipeline + 195 passing tests) is treated as MVP — refactoring is allowed and expected. Concept-level ideas from the parallel try02 branch are re-implemented (not merged).
ARCH — Hexagonal architecture & composition
- ARCH-01: Codebase reorganized to
src/gps_denied/components/{vio, satellite_matcher, gpr, anchor_verifier, safety_state, flight_recorder, mavlink_io, coordinate_transforms}/, each containingprotocol.py+ concrete implementations + (where applicable)native/for backend bridges - ARCH-02: Hot-path data types (
FrameState,IMUSample,PositionEstimate,VOEstimate,SatelliteAnchor) implemented as@dataclass(slots=True, frozen=True)insrc/gps_denied/hot_types/; Pydantic retained only for REST/config/DB boundary schemas - ARCH-03: Explicit DI composition root
src/gps_denied/pipeline/composition.pyexposesbuild_pipeline(env: Literal["jetson", "x86_dev", "ci", "sitl"]) -> Pipelinethat wires environment-specific implementations - ARCH-04:
core/retained for concentrated math (ESKF, factor graph, RANSAC, coordinate transforms) — these stay as pure-function single files, NOT split intointerfaces.py + types.py + impl.py - ARCH-05: All component Protocols defined with
typing.Protocol; concrete adapters implement them;Pipelineconstructor takes Protocol-typed dependencies (no concrete imports inside pipeline orchestration) - ARCH-06: Per-environment YAML configuration in
config/{jetson,x86_dev,ci,sitl}.yaml, loaded viapydantic-settingsinto a typedRuntimeConfigmodel passed tobuild_pipeline - ARCH-07: All 195 stage1 tests + 8 SITL skipped continue to pass after refactor; no regression in accuracy benchmarks
AC — Formal acceptance criteria document
- AC-01:
_docs/00_problem/acceptance_criteria.mdrewritten with formal AC-1.x…AC-NEW-x list adapted fromtry02and validated against this project's actual constraints - AC-02: Each AC entry includes (a) numeric thresholds, (b) validation method, (c) at least one test ID linking to
tests/ - AC-03: Position accuracy AC (50m@80%, 20m@50%, anchor age tracking, drift bounds) bound to
tests/integration/accuracy/andtests/e2e/ - AC-04: Failure-mode AC (visual blackout, spoofing promotion, dead reckoning, ≥3 disconnected segments) bound to
tests/blackbox/failure_modes/ - AC-05: Real-time performance AC (<400ms p95 e2e, <8GB RAM, ≥5Hz GPS_INPUT output) bound to a benchmark harness producing CI-tracked metrics
- AC-06: Traceability matrix
.planning/AC-TRACEABILITY.mdgenerated linking every AC ID → test ID(s) → implementing component(s)
SAFE — Safety anchor state machine
- SAFE-01:
components/safety_state/SafetyAnchorStateMachineowns authoritativesource_label ∈ {satellite_anchored, vo_extrapolated, dead_reckoned}for every emittedPositionEstimate - SAFE-02: Covariance growth is monotonic in non-anchored modes; resets only on accepted satellite anchor
- SAFE-03:
anchor_age_msrecorded on every estimate; transitions tovo_extrapolatedafter configurable max-age threshold - SAFE-04: State machine receives anchor decisions from
AnchorVerifier, never raw VPR top-K — bad candidates cannot poison the state - SAFE-05: Tile write eligibility flag exposed (
can_persist_tile: bool) — false indead_reckonedmode to prevent corrupt tile cache writes - SAFE-06: Unit tests cover all 9 state transitions; property-based test asserts covariance never decreases without an accepted anchor
VERIFY — Geometry-gated anchor verification
- VERIFY-01:
components/anchor_verifier/GeometryGatedAnchorVerifieraccepts/rejects satellite candidate matches based on configurable gates: min inliers, max mean reprojection error (px), max homography condition number - VERIFY-02: Rejection reason string emitted on every reject (
"too_few_inliers","mre_above_threshold","degenerate_homography","freshness_expired") - VERIFY-03: Freshness check integrates with sector classification (active-conflict <6mo, stable-rear <12mo) — expired tiles produce
freshness_expiredreject - VERIFY-04: Verifier benchmark mode evaluates multiple matcher profiles on the same frame for offline comparison
- VERIFY-05: Unit tests cover each gate independently; integration test with real Azaion frame verifies end-to-end accept/reject
FDR — Flight data recorder
- FDR-01:
components/flight_recorder/FlightRecorderProtocol withappend_event(event)andexport() -> FdrExportResult - FDR-02:
InMemoryFlightRecorderimpl with bounded segments and configurable segment+storage byte limits - FDR-03:
DiskFlightRecorderimpl writing append-only JSONL segments underdata/fdr/{flight_id}/segment-NNNN.jsonl - FDR-04: Health states
ok / degraded (≥90% storage) / critical (limit reached)exposed viahealthproperty - FDR-05: Pipeline emits FDR events at every state transition, anchor decision, MAVLink send, and pipeline error
- FDR-06: AC-NEW-3 forensic-thumbnail rate (≤0.1Hz on tile-generation failures) wired through FDR with size budget enforcement
VPR — Conditional + multi-scale visual place recognition
- VPR-01: VPR retrieval triggered conditionally — DINOv2 forward runs only on re-loc triggers (cold start, sharp turn AC-3.2, σ_xy > 50m, VO failure ≥2 frames, disconnected segment AC-3.3); steady-state uses geometric prior (IMU+VO predicted position) ranking by distance
- VPR-02: VPR chunks decoupled from storage tiles — chunks sized to ground footprint (600-800m at deployment altitude band) with 40-50% overlap; any frame footprint falls fully inside ≥1 chunk
- VPR-03: Multi-scale FAISS index — fine-scale (z=20-derived) + coarse-scale (z=17 or z=18) descriptor sets; coarse used in active-conflict sectors for change-robust retrieval
- VPR-04: Dynamic top-K — K=5 in stable sectors with σ_xy ≤ 20m, K=20 in active-conflict, K=50 on expanding-window fallback
- VPR-05: Chunking and indexing integrated into existing
chunk_manager.py/gpr.pywithout breaking stage1 GPR API contracts
MAVOUT — MAVLink output: source labels, dual-channel scaffold
- MAVOUT-01: Every emitted
GPS_INPUTincludessource_label,anchor_age_ms,covariance_semimajor_mpropagated fromPositionEstimate(mapped intohoriz_accuracyand a custom STATUSTEXT for label/age) - MAVOUT-02:
ODOMETRYemitter scaffolded behind feature flag (config.mavlink.odometry_enabled); flag is false in stage 2; integration test asserts ODOMETRY is intentionally absent on the wire - MAVOUT-03: Spoofing-promotion latency monitor — listens to
GPS_RAW_INT/EKF_STATUS_REPORT/SYS_STATUS; promotes own estimate to FC primary within <3s when real-GPS health rolling avg < threshold; emitsSTATUSTEXTon every promotion/demotion - MAVOUT-04: Visual blackout handling — pipeline switches to
dead_reckonedwithin ≤1 processed frame OR ≤400ms when camera produces no usable signal; emitsVISUAL_BLACKOUT_IMU_ONLYSTATUSTEXT @ 1-2Hz
FIXTURE — Real-flight integration fixture (Azaion 10.05.2026)
- FIXTURE-01:
tests/integration/azaion_flight/integration test suite consumingData/Azaion/10.05.2026/(tlog + cropped EO video + MAVLink CSV) - FIXTURE-02: Preprocessing script
scripts/prep_azaion_fixture.pyproducing — (a) HUD-stripped EO frames at 0.7 fps, (b) IMU/GPS/ATTITUDE CSV from tlog, (c) timestamp-aligned manifest - FIXTURE-03: MAVLink replay test — feed tlog through
MAVLinkBridgeparser, assert allGLOBAL_POSITION_INT/RAW_IMU/ATTITUDEmessages decoded without error - FIXTURE-04: ESKF real-IMU smoke test — replay IMU samples through
ESKFCore.predict, assert no NaN/Inf, bounded covariance growth - FIXTURE-05: VO smoke test on cropped EO frames using ORB-SLAM3 backend — assert ≥30% frame registration success
- FIXTURE-06: GPS-denial simulation — mask
GPS_RAW_INTfor t∈[180s, 280s], replay rest of stream, assert pipeline switches tovo_extrapolatedand back tosatellite_anchoredcorrectly - FIXTURE-07: Azaion fixture documented in
_docs/00_problem/fixtures.mdwith ground-truth references and known limitations (low altitude, multirotor dynamics, HUD overlay)
TEST — Test taxonomy & infrastructure
- TEST-01:
tests/reorganized totests/{unit,integration,blackbox,sitl,e2e}/; existing tests redistributed by category - TEST-02:
pyproject.tomltest markers updated —pytest -m unit/-m integration/ etc.; CI runs unit+integration on every push, blackbox on PR, sitl+e2e nightly - TEST-03: AC traceability auto-generated — pytest plugin tags each test with
@pytest.mark.ac("AC-1.1");scripts/gen_ac_traceability.pyproduces the matrix in.planning/AC-TRACEABILITY.md
OBS — Observability & tooling
- OBS-01: Structured JSON logging via
structlogwithcorrelation_id(frame_id) propagated through pipeline; Pydantic logging schemas at boundaries - OBS-02: CLI tool
gps_denied(typer-based) with subcommands —replay --tlog ... --video ...,benchmark --scenario ...,bench-ac AC-1.1for AC-driven benchmark runs - OBS-03: Per-environment Docker images split —
Dockerfile.x86_devfor CI/dev,Dockerfile.jetson(multi-stage with TRT engine prebuild step) for hardware
Stage 3 candidates (parking lot)
- Mid-flight tile generation + write-back to Azaion Satellite Service (AC-8.4)
- On-device hardware validation on Jetson Orin Nano Super
- Dual-channel MAVLink ODOMETRY enabled (depends on ArduPilot fixes for EKF3 source switching)
- AC-NEW-1 cold-boot time-to-first-fix bench (<30s, 50× cold reboot)
- BASALT VIO backend evaluation (only if cuVSLAM hits a blocker)
Out of Scope (Stage 2)
- Migration to PostgreSQL (SQLite remains embedded default; Postgres optional for ground station only)
- Folder-per-component layout for
core/math files (ESKF/factor graph stay concentrated) - Real microservices with separate processes / IPC
- Pydantic on per-frame hot path (dataclasses replace it)
- Mobile/web ground station UI
- Multi-UAV coordination
Traceability
Populated by roadmapper on 2026-05-10. Test IDs will be filled in by /gsd:plan-phase and /gsd:implement as each phase produces concrete tests.
| REQ | Phase | Tests |
|---|---|---|
| ARCH-01 | Phase 1 | pending plan-phase |
| ARCH-02 | Phase 1 | pending plan-phase |
| ARCH-03 | Phase 1 | pending plan-phase |
| ARCH-04 | Phase 1 | pending plan-phase |
| ARCH-05 | Phase 1 | pending plan-phase |
| ARCH-06 | Phase 1 | pending plan-phase |
| ARCH-07 | Phase 1 | pending plan-phase |
| AC-01 | Phase 2 | pending plan-phase |
| AC-02 | Phase 2 | pending plan-phase |
| AC-03 | Phase 2 | pending plan-phase |
| AC-04 | Phase 2 | pending plan-phase |
| AC-05 | Phase 2 | pending plan-phase |
| AC-06 | Phase 2 | pending plan-phase |
| TEST-01 | Phase 2 | pending plan-phase |
| TEST-02 | Phase 2 | pending plan-phase |
| TEST-03 | Phase 2 | pending plan-phase |
| OBS-01 | Phase 2 | pending plan-phase |
| SAFE-01 | Phase 3 | pending plan-phase |
| SAFE-02 | Phase 3 | pending plan-phase |
| SAFE-03 | Phase 3 | pending plan-phase |
| SAFE-04 | Phase 3 | pending plan-phase |
| SAFE-05 | Phase 3 | pending plan-phase |
| SAFE-06 | Phase 3 | pending plan-phase |
| VERIFY-01 | Phase 3 | pending plan-phase |
| VERIFY-02 | Phase 3 | pending plan-phase |
| VERIFY-03 | Phase 3 | pending plan-phase |
| VERIFY-04 | Phase 3 | pending plan-phase |
| VERIFY-05 | Phase 3 | pending plan-phase |
| VPR-01 | Phase 4 | pending plan-phase |
| VPR-02 | Phase 4 | pending plan-phase |
| VPR-03 | Phase 4 | pending plan-phase |
| VPR-04 | Phase 4 | pending plan-phase |
| VPR-05 | Phase 4 | pending plan-phase |
| FDR-01 | Phase 4 | pending plan-phase |
| FDR-02 | Phase 4 | pending plan-phase |
| FDR-03 | Phase 4 | pending plan-phase |
| FDR-04 | Phase 4 | pending plan-phase |
| FDR-05 | Phase 4 | pending plan-phase |
| FDR-06 | Phase 4 | pending plan-phase |
| MAVOUT-01 | Phase 5 | pending plan-phase |
| MAVOUT-02 | Phase 5 | pending plan-phase |
| MAVOUT-03 | Phase 5 | pending plan-phase |
| MAVOUT-04 | Phase 5 | pending plan-phase |
| FIXTURE-01 | Phase 6 | pending plan-phase |
| FIXTURE-02 | Phase 6 | pending plan-phase |
| FIXTURE-03 | Phase 6 | pending plan-phase |
| FIXTURE-04 | Phase 6 | pending plan-phase |
| FIXTURE-05 | Phase 6 | pending plan-phase |
| FIXTURE-06 | Phase 6 | pending plan-phase |
| FIXTURE-07 | Phase 6 | pending plan-phase |
| OBS-02 | Phase 6 | pending plan-phase |
| OBS-03 | Phase 6 | pending plan-phase |
Coverage: 52/52 requirements mapped. No orphans, no duplicates.