# C5 GtsamIsam2StateEstimator — Marginals + current_estimate / smoothed_history / health_snapshot **Task**: AZ-384_c5_marginals_outputs **Name**: C5 `GtsamIsam2StateEstimator` — Marginals + output methods **Description**: Implement `current_estimate()`, `smoothed_history(n)`, and `health_snapshot()` on `GtsamIsam2StateEstimator`. `current_estimate()`: get the current pose key from the most-recent frame; recover the 6×6 covariance via `_isam2_handle.compute_marginals().marginalCovariance(pose_key)`; convert local-tangent-plane pose to WGS84 via `WgsConverter`; assemble `EstimatorOutput(smoothed=False, source_label = , last_satellite_anchor_age_ms = handle.last_anchor_age_ms())`. `smoothed_history(n)`: return up to `min(n, K)` smoothed past keyframes from `IncrementalFixedLagSmoother.calculateEstimate()` projection; each entry has `smoothed=True`. `health_snapshot()`: report `IsamState` (INIT/TRACKING/DEGRADED/LOST) based on convergence quality; `keyframe_count = len(_smoother.timestamps())`; `cov_norm_growing_for_s` (rolling counter incremented when frame-to-frame cov norm rises monotonically per AC-NEW-8); `spoof_promotion_blocked` (queries the source-label state machine — owned by AZ-385; this task introduces a stub that returns False until AZ-385 lands). SPD-invariant defensive check on every emitted covariance. **Complexity**: 3 points **Dependencies**: AZ-383 (graph populated with factors), AZ-382 / AZ-381 (handle + Protocol), AZ-279 (`WgsConverter`), AZ-277 (`SE3Utils`), AZ-263, AZ-269, AZ-266, AZ-272 **Component**: c5_state (epic AZ-260 / E-C5) **Tracker**: AZ-384 **Epic**: AZ-260 (E-C5) ### Document Dependencies - `_docs/02_document/contracts/c5_state/state_estimator_protocol.md` — Invariants 4 (current_estimate fresh), 6/7 (smoothed history bounded + flagged), 10 (SPD). - `_docs/02_document/components/07_c5_state/description.md` — § 2 outputs; § 7 Marginals dominant cost. ## Problem Without this task, the system has no way to read the posterior pose; downstream C8 cannot emit FC corrections; FDR has no smoothed history. ## Outcome - `current_estimate()` body: get current pose + Marginals 6×6; WGS84-convert via helper; assemble `EstimatorOutput`. - `smoothed_history(n)` body: iterate smoother's active keyframes; build `EstimatorOutput(smoothed=True)` for each. - `health_snapshot()` body: `IsamState` derivation + `keyframe_count` + `cov_norm_growing_for_s` rolling counter + `spoof_promotion_blocked` (from injected source-label state machine; default `False` until AZ-79). - `_cov_norm_window` private rolling-window counter (60 s lazy-pruned) for AC-NEW-8 monotonicity check. - SPD-invariant defensive check before every `EstimatorOutput` emission; on failure raise `EstimatorFatalError`. - Constructor extension: optional `source_label_state_machine` arg (default `None`; AZ-79 wires it up). ## Scope ### Included - All three method bodies. - `_cov_norm_window` rolling-window counter. - SPD defensive check. - Source-label state machine injection point (Optional, default None). - Unit tests: synthetic graph with known pose → `current_estimate()` returns expected pose+covariance; `smoothed_history(20)` bounded by K=15; SPD invariant; `IsamState` derivation; `cov_norm_growing_for_s` monotonicity counter accuracy. ### Excluded - Source-label state machine impl — owned by AZ-79. - Spoof gate logic body — owned by AZ-79. - AC-5.2 fallback — owned by AZ-81. - ESKF baseline — owned by AZ-80. ## Acceptance Criteria **AC-1: `current_estimate` returns fresh `EstimatorOutput`** — every call returns a new instance with `smoothed=False`. **AC-2: SPD covariance** — `np.linalg.cholesky(out.covariance_6x6)` succeeds for every emitted output; non-SPD raises `EstimatorFatalError`. **AC-3: WGS84 conversion** — uses shared `WgsConverter`; output matches helper test vectors. **AC-4: `smoothed_history(n)` bounded by K** — `len(smoothed_history(100)) <= K=15`; each has `smoothed=True`. **AC-5: `current_estimate` has `smoothed=False`** — distinguishes from history. **AC-6: `health_snapshot.isam2_state` matches convergence quality** — INIT before first factor; TRACKING after; DEGRADED on inflated cov; LOST on `EstimatorFatalError`. **AC-7: `keyframe_count` accuracy** — matches `IncrementalFixedLagSmoother.timestamps().size()`. **AC-8: `cov_norm_growing_for_s`** — increments while consecutive frames show monotone-rising cov norm; resets to 0 on a non-rising frame. **AC-9: `spoof_promotion_blocked` via injected state machine** — queries `source_label_state_machine.is_spoof_promotion_blocked()`; default `False` if no state machine wired. **AC-10: `last_satellite_anchor_age_ms` pass-through** — every `EstimatorOutput.last_satellite_anchor_age_ms == handle.last_anchor_age_ms()`. ## Non-Functional Requirements - `current_estimate` p95 ≤ 60 ms (Marginals dominant). - `smoothed_history(K)` p99 ≤ 20 ms. - `health_snapshot` p99 ≤ 5 µs (O(1) accumulator reads). ## Constraints - Single-writer thread. - SPD defensive check is mandatory. - `WgsConverter` use is mandatory (no inline math). ## Risks & Mitigation - **Risk: `IncrementalFixedLagSmoother.calculateEstimate()` returns full Values not just keyframe poses** — filter by key; verify against pinned GTSAM API. - **Risk: SPD invariant fails under iSAM2 numerical instability** — defensive raise (AC-2) maps to `EstimatorFatalError`; AC-5.2 fallback then triggers. ## Runtime Completeness - **Named capability**: posterior pose + covariance recovery + smoothed history. - **Production code**: real Marginals, real WGS84 conversion, real rolling-window counter, real SPD defensive check. - **Unacceptable substitutes**: synthetic Marginals; inline WGS84 math.