[AZ-776] Open-loop ESKF composition profile via c4_pose.enabled

ADR-012: add c4_pose.enabled (default True) and enforce the
(c4_pose.enabled, c5_state.strategy) 2x2 pairing matrix at compose
time. When enabled=false, compose_root removes c4_pose from the
selection map and build_pre_constructed omits c5_isam2_graph_handle.
Replay protocol Invariant 13 owns the gate. Tier-2 conftest YAML
writes the open-loop profile; un-xfails AC-1/2/5 and both AC-6
variants in Derkachi (AC-3 stays xfailed for AZ-777). 319/319
runtime_root + c4_pose + c5_state tests green.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-21 13:40:01 +03:00
parent 6044a33197
commit 8de2716500
10 changed files with 687 additions and 83 deletions
@@ -213,6 +213,33 @@ Side notes:
- `set_takeoff_origin` (AZ-490 / ADR-010) is invoked identically in replay: the operator's pre-flight C10 Manifest is the source of truth in both modes. The tlog's first GPS fix is the **fallback**, gated through the same Principle #11 bounded-delta check.
- `BUILD_FAISS_INDEX` is ON in the airborne binary (live and replay alike). C2 in replay queries the **real** C6 `FaissDescriptorIndex`, populated by the pre-flight C10 build. This is the architectural change vs. v1.0.0 of this contract.
### Composition profile: open-loop ESKF (AZ-776 / ADR-012)
The replay binary supports a second composition profile alongside the production GTSAM-iSAM2 path: **open-loop ESKF**. It is the Tier-2 smoke baseline for the AZ-265 replay flow and the only profile that can run end-to-end against the Derkachi clip today (the satellite-anchored profile waits on AZ-777's C6 reference tile cache).
The user-facing switch is one YAML field on the C4 block:
```
c4_pose:
enabled: false
c5_state:
strategy: eskf
```
When `c4_pose.enabled = false`:
- `compose_root` removes `c4_pose` from the component selection map before topological ordering; the C4 wrapper never runs, the `OpenCVGtsamPoseEstimator` is never instantiated, and `pose_estimator.estimate(refined)` in the per-frame loop above is a no-op (C5 receives IMU + VIO inputs only, no PnP anchor).
- `build_pre_constructed` omits `c5_isam2_graph_handle` from the `pre_constructed` dict (no consumer requires it). The ESKF estimator is still pre-built and cached in the internal `_c5_prebuilt_estimator` slot exactly as in the gtsam_isam2 path; the C5 wrapper short-circuits onto the prebuilt instance per AZ-625.
- The replay per-frame loop is otherwise unchanged — C1 VIO still runs, C5 still ingests, the JSONL sink still emits per video frame. Position drifts open-loop without satellite re-anchoring; AZ-777 closes that half of the loop by adding C2/C3/C4 against the C6 tile cache.
The 2×2 pairing matrix between `c4_pose.enabled` and `c5_state.strategy` is enforced at compose time (Invariant 13 below). The two valid cells are:
| `c4_pose.enabled` | `c5_state.strategy` | Profile | Status |
|---|---|---|---|
| `true` | `gtsam_isam2` | Steady-state airborne (ADR-003 / ADR-009) | Production |
| `false` | `eskf` | Open-loop ESKF (this section) | Replay Tier-2 smoke baseline |
The two **invalid** cells (`true` + `eskf` and `false` + `gtsam_isam2`) raise `CompositionError` from `compose_root` with explicit error text naming both blocks. The Tier-2 replay fixture (`tests/e2e/replay/conftest.py`) writes the second valid cell. The first cell is the live + airborne default.
## Invariants
1. **Mode-agnostic C1C7, C13**: production components MUST NOT contain `if config.mode == "replay":` branches. Mode-specific behaviour lives in the strategies (FrameSource / FcAdapter / MavlinkTransport / ReplaySink / Clock). Verified by an explicit grep guard in CI (the AZ-404 E2E test owns this assertion).
@@ -227,6 +254,7 @@ Side notes:
10. **Determinism**: same `(video, tlog, config, time_offset_ms, pace=ASAP)` input → same JSONL output within ≤ 1e-6 float drift in position fields (AC-5).
11. **MAVLink signing key required in replay**: the airborne binary refuses to run without `--mavlink-signing-key PATH` in both modes. In replay the operator supplies a dummy file (well-formed key bytes; no real channel to verify against). This preserves Invariant 5 — the encoders' signing code path runs identically in both modes.
12. **Real C6 cache in replay**: the airborne binary in replay mode reads the same pre-built C6 tile cache the operator built via the normal pre-flight C10/C11/C12 flow. There is no replay-specific cache shape. Verified by the AZ-404 E2E fixture, which runs the operator's pre-flight flow before invoking the replay CLI.
13. **C4↔C5 pairing matrix is enforced at compose time** (AZ-776 / ADR-012): `compose_root` rejects the two off-diagonal cells of the (`c4_pose.enabled`, `c5_state.strategy`) matrix with a `CompositionError` naming both blocks. `enabled=False` + `gtsam_isam2` and `enabled=True` + `eskf` are forbidden. The two valid cells are `enabled=True` + `gtsam_isam2` (production steady-state per ADR-003 / ADR-009) and `enabled=False` + `eskf` (open-loop ESKF — replay Tier-2 smoke baseline; satellite anchoring deferred to AZ-777). Verified by `tests/unit/runtime_root/test_az776_open_loop_eskf_composition.py` AC-3a and AC-3b.
## Producer / Consumer Split