mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 21:11:13 +00:00
Decompose Step 6 snapshot: 140 task specs + contract docs
Closes out greenfield Step 6 (Decompose) for all 14 components (C1-C13 + cross-cutting helpers/replay). Covers tasks AZ-266..AZ-446 plus the _dependencies_table.md and component contract documents. State file updated to greenfield Step 7 (Implement), not_started. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
# Contract: VioStrategy Protocol
|
||||
|
||||
**Component**: c1_vio
|
||||
**Producer task**: AZ-331 — `_docs/02_tasks/todo/AZ-331_c1_vio_strategy_protocol.md`
|
||||
**Consumer tasks**:
|
||||
- AZ-332 (OKVIS2 implementation — implements)
|
||||
- AZ-333 (VINS-Mono implementation — implements)
|
||||
- AZ-334 (KLT/RANSAC implementation — implements)
|
||||
- AZ-335 (warm-start + F8 reboot recovery wiring — invokes `reset_to_warm_start`)
|
||||
- E-C5 state estimator tasks under AZ-260 (consume `VioOutput`)
|
||||
- E-C13 FDR writer tasks under AZ-248 (consume `VioHealth`)
|
||||
- `runtime_root` composition under AZ-270 (selects strategy by config)
|
||||
**Version**: 1.0.0
|
||||
**Status**: draft
|
||||
**Last Updated**: 2026-05-10
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the typed boundary between the on-Jetson visual / visual-inertial odometry runtime and every downstream consumer (C5 state estimator, C13 FDR, runtime_root composition). The Protocol is the single point of contact that lets ADR-001 select between three concrete strategies (OKVIS2 production-default, VINS-Mono research-only, KLT/RANSAC mandatory simple-baseline) at startup without consumers caring which is wired. Per-frame DTOs (`VioOutput`, `VioHealth`) are frozen here so C5 fusion and C13 FDR records do not drift across implementations.
|
||||
|
||||
## Shape
|
||||
|
||||
### Protocol surface
|
||||
|
||||
The Protocol is `typing.Protocol` (PEP 544 structural typing) with `runtime_checkable=True`.
|
||||
|
||||
| Method | Signature | Throws / Errors | Blocking? |
|
||||
|--------|-----------|-----------------|-----------|
|
||||
| `process_frame` | `(frame: NavCameraFrame, imu: ImuWindow, calibration: CameraCalibration) -> VioOutput` | `VioInitializingError`, `VioDegradedError`, `VioFatalError` | sync (camera-ingest hot path; bound by C1-PT-01 latency budget) |
|
||||
| `reset_to_warm_start` | `(hint: WarmStartPose) -> None` | `VioFatalError` (only on irrecoverable backend init failure) | sync |
|
||||
| `health_snapshot` | `() -> VioHealth` | — | sync |
|
||||
| `current_strategy_label` | `() -> Literal["okvis2", "vins_mono", "klt_ransac"]` | — | sync |
|
||||
|
||||
### DTOs
|
||||
|
||||
`NavCameraFrame`, `ImuSample`, `ImuWindow`, `ImuBias`, `CameraCalibration` are owned by `gps_denied_onboard._types.nav` (AZ-263). This contract owns `WarmStartPose`, `VioOutput`, `VioHealth`, `FeatureQuality`, and the `VioState` enum, all `@dataclass(frozen=True)` (or `enum.Enum`). `VioOutput` and `VioHealth` are placed in `_types/nav.py` for cross-component access; the `VioStrategy` Protocol itself lives in `components/c1_vio/interface.py`.
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Protocol, Literal, runtime_checkable
|
||||
from gps_denied_onboard._types.nav import (
|
||||
NavCameraFrame, ImuWindow, ImuBias, CameraCalibration,
|
||||
)
|
||||
from gps_denied_onboard._types.geom import SE3, Vector3, Matrix6
|
||||
|
||||
|
||||
class VioState(str, Enum):
|
||||
INIT = "init"
|
||||
TRACKING = "tracking"
|
||||
DEGRADED = "degraded"
|
||||
LOST = "lost"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WarmStartPose:
|
||||
body_T_world: SE3
|
||||
velocity_b: Vector3
|
||||
bias: ImuBias
|
||||
captured_at_ns: int # monotonic_ns when the hint was produced
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FeatureQuality:
|
||||
tracked: int
|
||||
new: int
|
||||
lost: int
|
||||
mean_parallax: float
|
||||
mre_px: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VioOutput:
|
||||
frame_id: str # echoes NavCameraFrame.frame_id
|
||||
relative_pose_T: SE3
|
||||
pose_covariance_6x6: Matrix6
|
||||
imu_bias: ImuBias
|
||||
feature_quality: FeatureQuality
|
||||
emitted_at_ns: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VioHealth:
|
||||
state: VioState
|
||||
consecutive_lost: int
|
||||
bias_norm: float
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class VioStrategy(Protocol):
|
||||
def process_frame(
|
||||
self,
|
||||
frame: NavCameraFrame,
|
||||
imu: ImuWindow,
|
||||
calibration: CameraCalibration,
|
||||
) -> VioOutput: ...
|
||||
|
||||
def reset_to_warm_start(self, hint: WarmStartPose) -> None: ...
|
||||
|
||||
def health_snapshot(self) -> VioHealth: ...
|
||||
|
||||
def current_strategy_label(self) -> Literal["okvis2", "vins_mono", "klt_ransac"]: ...
|
||||
```
|
||||
|
||||
### Error hierarchy
|
||||
|
||||
All under `gps_denied_onboard.components.c1_vio.errors`:
|
||||
|
||||
```
|
||||
VioError (base; subclasses Exception)
|
||||
├── VioInitializingError (state == INIT; no VioOutput emitted; C5 falls back to FC IMU prior)
|
||||
├── VioDegradedError (state == DEGRADED; output IS still emitted with inflated covariance — see Invariants)
|
||||
└── VioFatalError (state == LOST after configurable consecutive frames; AC-5.2 fallback path)
|
||||
```
|
||||
|
||||
`VioDegradedError` is documented but is **not raised** during normal `process_frame` returns when degraded — degraded operation returns a `VioOutput` with inflated covariance and `VioHealth.state = DEGRADED`. The error type exists for the rare case where degradation transitions to fatality and consumer wrappers want to catch the family.
|
||||
|
||||
### Composition-root selection
|
||||
|
||||
```python
|
||||
def build_vio_strategy(config: Config, *, fdr_client: FdrClient) -> VioStrategy: ...
|
||||
```
|
||||
|
||||
Lives at `src/gps_denied_onboard/runtime_root/vio_factory.py`. Selects the strategy by `config.vio.strategy` (`okvis2 | vins_mono | klt_ransac`) and respects compile-time `BUILD_*` gating (`BUILD_OKVIS2`, `BUILD_VINS_MONO`, `BUILD_KLT_RANSAC`). Requesting a strategy whose `BUILD_*` flag is OFF raises `StrategyNotAvailableError` at composition time (NOT at first frame). Lazy-imports the concrete strategy module so a Tier-0 workstation build without OKVIS2 native libs still composes successfully when only KLT/RANSAC is requested.
|
||||
|
||||
## Invariants
|
||||
|
||||
- **6×6 SPD covariance always returned**: `pose_covariance_6x6` is symmetric and positive-definite for every `VioOutput`. Implementations MUST NOT return a "tightened" covariance (smaller Frobenius norm) during a degradation event; honest covariance is the safety floor for AC-NEW-4 and AC-NEW-7. A test (covariance-monotonicity contract test, deferred to Step 9 / E-BBT) asserts this across all three strategies.
|
||||
- **`frame_id` echo**: `VioOutput.frame_id` equals the input `NavCameraFrame.frame_id`. C5 relies on this for time-aligned factor insertion.
|
||||
- **Single-threaded by contract**: each `VioStrategy` instance is bound to one writer thread (the camera ingest thread). Concurrent calls to `process_frame` on the same instance are undefined behaviour. The composition root binds one instance per ingest thread.
|
||||
- **`reset_to_warm_start` is destructive**: clears the strategy's keyframe window, IMU integration state, and feature track buffer; subsequent `process_frame` calls re-initialise from the hint. Calling `reset_to_warm_start` mid-flight is allowed (F8 reboot recovery) but must not be issued concurrently with a `process_frame` call on the same instance.
|
||||
- **`current_strategy_label()` is constant per instance**: returns the same string for the lifetime of the instance and matches `config.vio.strategy` exactly. The label is FDR-stamped on every `VioHealth` event for AC-NEW-3 audit.
|
||||
- **No ambient state**: implementations MUST NOT read environment variables, wall clock, or filesystem inside `process_frame`; calibration arrives via constructor + per-call argument; logging uses the injected logger only.
|
||||
- **Error envelope is closed**: `process_frame` raises only members of `VioError` (the family). Lower-level exceptions from OpenCV / OKVIS2 / VINS-Mono / GTSAM MUST be caught and rewrapped.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- IMU preintegration mathematics — owned by AZ-276 / `helpers.imu_preintegrator`. Strategies feed `ImuWindow` to the helper; they do NOT implement preintegration internally.
|
||||
- Bias estimation policy — each strategy decides when to update its bias; the contract does not prescribe a schedule.
|
||||
- WarmStartPose persistence (write to disk after takeoff, read after F8 reboot) — owned by the warm-start + F8 reboot recovery wiring task in this same epic. The contract here only defines the in-memory DTO and the `reset_to_warm_start` method.
|
||||
- C5 fusion semantics — owned by E-C5; this contract only delivers `VioOutput`.
|
||||
- Multi-camera strategies — out of scope this cycle (single nav-camera per ADR / RESTRICT-UAV-3).
|
||||
|
||||
## Versioning Rules
|
||||
|
||||
- **Breaking changes** (method renamed/removed, parameter type changed, return type changed, invariant relaxed) require a new major version + a deprecation pass through every consumer task in the header.
|
||||
- **Non-breaking additions** (new optional method, new diagnostic accessor that does not mutate state, new `VioState` enum variant added at the end) require a minor version bump.
|
||||
- The `VioState` enum is treated as a closed set for switch-style consumer code (C5 fusion); adding a new variant is a minor bump but consumers MUST handle the new state defensively (default branch → treat as LOST).
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Case | Input | Expected | Notes |
|
||||
|------|-------|----------|-------|
|
||||
| protocol-conformance | three concrete strategy classes | `isinstance(impl, VioStrategy)` returns True for each | Catches drift between impl and Protocol surface |
|
||||
| frozen-dto-mutation | a constructed `VioOutput` instance and an attempt to set `.relative_pose_T` | `dataclasses.FrozenInstanceError` raised | Confirms DTOs are immutable |
|
||||
| error-family-catchable | each of `VioInitializingError`, `VioDegradedError`, `VioFatalError` raised | `except VioError` catches all three; `except ValueError` does NOT | Confirms error envelope |
|
||||
| factory-build-flag-respected | `config.vio.strategy = "vins_mono"` and `BUILD_VINS_MONO=OFF` | `StrategyNotAvailableError` raised at composition; `sys.modules` has no `vins_mono` entry | Confirms lazy-import gating |
|
||||
| current-strategy-label-exact-match | each strategy constructed via factory with matching config | `current_strategy_label()` returns the literal config value | AC-NEW-3 audit gate |
|
||||
| frame-id-echoed | a `NavCameraFrame` with a known UUID fed into `process_frame` | the returned `VioOutput.frame_id` equals the input UUID | C5 alignment invariant |
|
||||
| covariance-spd | inspect 100 emitted `VioOutput.pose_covariance_6x6` matrices | every matrix is symmetric and positive-definite (eigenvalues > 0) | AC-1.4 floor |
|
||||
|
||||
## Change Log
|
||||
|
||||
| Version | Date | Change | Author |
|
||||
|---------|------|--------|--------|
|
||||
| 1.0.0 | 2026-05-10 | Initial contract derived from `_docs/02_document/components/01_c1_vio/description.md` § 2 + AZ-254 epic child issue #1 | autodev decompose Step 2 |
|
||||
Reference in New Issue
Block a user