mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 13:41:14 +00:00
098aabac0c
After every successful current_estimate(), emit one c5.state.smoothed_history FDR record per newly-smoothed past keyframe from IncrementalFixedLagSmoother. AC-4.5 (revised): the smoothed stream goes ONLY to FDR; the C8 outbound forward-time stream is unaffected. Idempotency via _smoothed_fdr_watermark_s (smoother-native float seconds); the same pose key is never emitted twice. Hook is best-effort — internal failures log warnings but do not raise, so a smoother divergence cannot contaminate the forward-time path. Cross-task invariants documented: - AC-3 ESKF no-op — AZ-386 installs an inert hook on the ESKF. - AC-4 No C8 leak — enforced at the C8 boundary by AZ-261. 8 new unit tests against AC-1/2/5/6 + robustness (no-FDR-client, marginals failure). Full suite: 640 passed, 2 skipped. Co-authored-by: Cursor <cursoragent@cursor.com>
7.8 KiB
7.8 KiB
Batch 18 — Cycle 1 Implementation Report
Batch: 18 of N
Tasks landed: AZ-387 (GtsamIsam2StateEstimator — smoothed past-keyframe → FDR side-channel)
Cycle: 1
Date: 2026-05-11
Scope
| Task | Component | Purpose |
|---|---|---|
| AZ-387 | C5 state estimator | Implements the AC-4.5 (revised) onboard-only smoothed-history → FDR path: after every successful current_estimate(), walk the IncrementalFixedLagSmoother active POSE keys and emit one c5.state.smoothed_history FDR record per newly-smoothed past-keyframe. The watermark _smoothed_fdr_watermark_s (a smoother-native float-second timestamp) prevents the same key from being emitted twice. AC-3 (ESKF no-op) is structurally satisfied — AZ-386 owns the ESKF impl; when it lands it installs an inert hook because ESKF doesn't run a smoother. AC-4 (no leak to C8 outbound) remains a cross-task invariant enforced at the C8 boundary by AZ-261; this task only emits to FdrClient and never touches the C8 outbound queue. |
Files added / modified
Modified (prod)
src/gps_denied_onboard/components/c5_state/gtsam_isam2_estimator.py— added the new private hook_emit_smoothed_to_fdr_if_any(emitted_at_ns, source_label, last_anchor_age_ms)(defined aftersmoothed_history); wired one call to it insidecurrent_estimate()immediately after the AZ-388mark_successful_estimatehook; introduced one new state field_smoothed_fdr_watermark_s: float = -inffor the timestamp watermark; new module-level importsdatetime+timezone(for the FDR recordtsfield) andFdrRecordfromgps_denied_onboard.fdr_client.records. The hook is best-effort by design: any internal failure (marginals divergence, per-key SPD failure, FDR queue overflow) logs a WARNING but does NOT raise — the forward-time estimate has already been computed and returned to the caller.
Added (tests)
tests/unit/c5_state/test_az387_smoothed_history_fdr.py— 8 tests across the AC-1/2/5/6 surface (the AZ-387-testable ACs) plus three robustness checks (no-FDR-client → no crash, marginals failure → no raise, AC-3 / AC-4 invariant marker). Uses a realGtsamIsam2StateEstimatorwith a seeded prior factor + iSAM2 update path; the_seed_keys(n)helper plantsnreal POSE keys with strictly-increasing smoother timestamps so the AC-5 watermark behaviour is exercised end-to-end. AC-3 (ESKF) and AC-4 (C8 leak) are documented as cross-task invariants pending AZ-386 / AZ-261 respectively; the test suite carries a marker test so the invariant is not silently lost.
Architectural notes
- Hook lives at the end of
current_estimate— placed AFTER the AZ-388mark_successful_estimateso that a hook failure does NOT cause AC-5.2 fallback to engage. The fallback watcher only cares about the forward-time path; the smoothed-history emission is a forensic side-channel and must not contaminate the live-state lifecycle. - Watermark in smoother-native time, not monotonic_ns — the smoother's
timestamps().at(key)returns the value the caller passed atupdate(timestamps=...), which by convention is the wall-clock decode timestamp converted to seconds. Storing the watermark in the same unit avoids any ns ↔ s drift and makes the comparison strictly correct against the smoother's own ordering.float("-inf")initial value means the first call emits every smoothed key in the window. - Per-key marginals compute is shared with the live path — both the live
current_estimateand the hook callhandle.compute_marginals(), but they are SEPARATE calls. We could cache the live-path marginals to save one GTSAM call percurrent_estimate, but the iSAM2 graph could have advanced between the two calls (no, actually —current_estimateand the hook run on the same thread without interveningupdates, so the graph state is identical). I deliberately did NOT cache the marginals — keeping them separate means future refactors (e.g. moving the hook off the C5 ingest thread) work without a hidden coupling. The cost is one extracompute_marginalspercurrent_estimatep99 — well under the AZ-387 NFR of 5 ms. smoothed=Trueflag is hard-coded in the payload — the C5 contract says smoothed-history records MUST carrysmoothed=True; if a future refactor reuses the hook for a non-smoothed path, the payload field needs to flip. Documented in the impl docstring.- FDR record kind is a single fixed string —
c5.state.smoothed_history, matching the AZ-272 record-schema namespace. The AZ-295 record-kind policy test runs against the FDR sink config; this kind is already on the allow-list (verified by the full-suite passing). - No leak to C8 outbound (AC-4) — single-sink enforcement — the hook calls ONLY
self._fdr_client.enqueue. There is no path from the hook to any other sink. C8's filter (AZ-261) catches any accidental leak at the boundary; this task's contribution to AC-4 is the structural guarantee that the hook itself doesn't introduce a leak. - ESKF no-op (AC-3) — structural by absence —
EskfStateEstimator(AZ-386) will not have a smoother instance to walk, so the equivalent hook there is justdef _emit_smoothed_to_fdr_if_any(self, *_args, **_kwargs) -> None: return. The AZ-386 task gets a one-liner to satisfy AC-3.
Test counts
| Suite | Before (B17) | After (B18) | Delta |
|---|---|---|---|
| Total passing | 632 | 640 | +8 |
| Skipped | 2 | 2 | 0 |
| AZ-387 (new) | 0 | 8 | +8 |
Run command: PYTHONPATH=src pytest tests/ -q → 640 passed, 2 skipped in ~32s.
Lint / type
ruff check src/gps_denied_onboard/components/c5_state/ tests/unit/c5_state/— clean after one auto-fix (unused import).ruff format— 1 file reformatted, 17 unchanged; second pass clean.ReadLintson touched files — 0 errors.
Acceptance evidence
| AC | Test(s) | Status |
|---|---|---|
| AC-1 iSAM2 emits smoothed records to FDR | test_ac1_first_current_estimate_emits_smoothed_records |
PASS |
AC-2 Records have smoothed=True |
test_ac2_smoothed_flag_is_true |
PASS |
| AC-3 ESKF no-op | test_ac3_ac4_cross_task_invariants_documented (marker; AZ-386 owns the impl) |
DEFERRED |
| AC-4 No leak to C8 outbound | test_ac3_ac4_cross_task_invariants_documented (marker; AZ-261 enforces the filter) |
DEFERRED |
| AC-5 Idempotency via watermark | test_ac5_idempotent_no_new_keys_no_second_emission, test_ac5_idempotent_new_keys_only_emitted_once |
PASS |
| AC-6 FDR record shape | test_ac6_fdr_record_has_required_shape |
PASS |
| Robustness — no FDR client → no crash | test_estimator_without_fdr_client_does_not_crash_on_hook |
PASS |
| Robustness — marginals failure → no raise | test_hook_marginals_failure_does_not_raise |
PASS |
Known gaps / followups
- AC-3 ESKF wire-up — owned by AZ-386. The ESKF estimator MUST install a no-op
_emit_smoothed_to_fdr_if_any(or simply skip the hook entirely). The marker test in this batch surfaces the invariant. - AC-4 C8 filter — owned by AZ-261. The C8 inbound subscription / outbound emit path MUST drop any
EstimatorOutputwithsmoothed=True. This task documents the invariant; AZ-261 owns the enforcement. - AZ-272 FDR record schema validation — the FDR record produced by this task uses the v1 envelope with a domain-specific payload. The schema-validation hook in AZ-272 is opt-in per kind; future work may pin a JSON schema for
c5.state.smoothed_historyonce the record shape is verified in flight tests.
Risks accepted
- Extra marginals compute per
current_estimate— measured well under the 5 ms NFR in current fixtures; if a profiling pass later reveals contention we can share the marginals between the live path and the hook. - Hook silent on partial failure — by design (the forward-time path has already returned to the caller). Forensic logs land in the structured
c5.state.smoothed_history_fdr_*_failedlog records.