# C8 CovarianceProjector — honest 6×6 → 2×2 → equivalent_radius

**Task**: AZ-392_c8_covariance_projector
**Name**: C8 `CovarianceProjector` — honest 6×6 → 2×2 → equivalent_radius helper (D-C8-8 = (b))
**Description**: Implement the `CovarianceProjector` class — a C8-internal helper that projects a 6×6 GTSAM `Marginals` covariance from `EstimatorOutput.covariance_6x6` into the per-FC scalar accuracy field. Steps: 6×6 → 3×3 position sub-matrix (top-left 3×3 block) → 2×2 horizontal sub-matrix (rows/cols 0,1) → `equivalent_radius` per the AC-4.3 formula `sqrt(0.5 * (sigma_xx + sigma_yy + sqrt((sigma_xx - sigma_yy)^2 + 4*sigma_xy^2)))` (largest eigenvalue of 2×2). Two output paths: (i) `to_ardupilot_horiz_accuracy_m(cov_6x6) -> float` returning meters for AP `GPS_INPUT.horiz_accuracy`; (ii) `to_inav_h_pos_accuracy_mm(cov_6x6) -> int` returning millimeters (clamped to int range) for iNav `MSP2_SENSOR_GPS.hPosAccuracy`. Documented as the "honest covariance projection" per IT-10. Constructor injection (no static methods — per coderule SRP, this helper has variant-specific output formatting that belongs on an instance). NaN / non-SPD input → `FcEmitError("non-SPD covariance from C5; refusing emit")`.
**Complexity**: 3 points
**Dependencies**: AZ-390 (error hierarchy includes `FcEmitError`), AZ-263, AZ-269, AZ-266, AZ-272 (FDR for the SPD-violation log)
**Component**: c8_fc_adapter (epic AZ-261 / E-C8)
**Tracker**: AZ-392
**Epic**: AZ-261 (E-C8)

### Document Dependencies

- `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md` — Invariant 4 (honest projection); error path for non-SPD.
- `_docs/02_document/components/10_c8_fc_adapter/description.md` — § 5 covariance projection formula; § 6 helper ownership table.

## Problem

Without this task, both AP and iNav outbound paths must each implement covariance projection independently — risking drift between adapters AND violating Invariant 4 (honest projection). The Frobenius-norm equivalence requirement (within 1% per C8-IT-01) demands a single canonical implementation.

## Outcome

- `src/gps_denied_onboard/components/c8_fc_adapter/_covariance_projector.py` — `CovarianceProjector` class with two methods.
- Re-exported as `c8_fc_adapter.helpers.CovarianceProjector` for internal C8 use only (NOT in Public API per `module-layout.md`).
- Constructor: `CovarianceProjector(fdr_client: FdrClient)` — for SPD-violation logging.
- Unit tests: known-input vs hand-computed expected output; Frobenius-norm equivalence within 1% on 100 synthetic 6×6 SPD matrices; non-SPD raises; NaN raises; clipping at iNav int max documented.

## Scope

### Included
- `CovarianceProjector` class with both per-FC methods.
- 6×6 → 3×3 → 2×2 → eigenvalue formula.
- AP meters output + iNav millimeters output.
- SPD validation + NaN check.
- Unit tests including Frobenius-norm equivalence (the C8-IT-01 acceptance test exercises this from end-to-end; this task's unit tests cover the projector body).

### Excluded
- AP / iNav wire encoding — owned by outbound tasks (they consume this projector).
- The C8-IT-01 end-to-end test — deferred to E-BBT.

## Acceptance Criteria

**AC-1: Hand-computed reference** — for `cov_6x6` with known position-block `[[4, 1, 0], [1, 9, 0], [0, 0, 16]]`, `to_ardupilot_horiz_accuracy_m(...)` returns `sqrt(0.5 * (4 + 9 + sqrt((4-9)^2 + 4)))` ≈ `sqrt(6.5 + sqrt(29)/2)` (assert within 1e-9 numerical tolerance).

**AC-2: Frobenius-norm equivalence** — on 100 synthetic 6×6 SPD matrices, the 2×2 horizontal-block Frobenius norm is within 1% of the 3×3 position-block horizontal-component Frobenius norm. (This is a slightly weaker form of C8-IT-01 — exact equivalence is exercised end-to-end.)

**AC-3: AP units = meters** — `to_ardupilot_horiz_accuracy_m` returns a `float`; documented as meters.

**AC-4: iNav units = millimeters** — `to_inav_h_pos_accuracy_mm` returns an `int`; documented as millimeters; conversion is `m * 1000.0` rounded half-up to int.

**AC-5: iNav int clamping** — covariance with `equivalent_radius > 65.535 m` (uint16 max in mm) → returned value clamped to 65535; WARN log emitted with `kind="c8.cov_projector.inav_clamped"` on every clamp event.

**AC-6: Non-SPD raises** — input with negative eigenvalue (e.g., `[[1, 0, 0], [0, -1, 0], [0, 0, 1]]` in the position block) → `FcEmitError("non-SPD covariance from C5; refusing emit")`.

**AC-7: NaN raises** — input with any NaN entry → `FcEmitError("NaN covariance from C5; refusing emit")`.

**AC-8: SPD-violation FDR log** — every SPD-violation (AC-6) and NaN (AC-7) emits an FDR record `kind="c8.cov_projector.spd_violation"` BEFORE raising.

**AC-9: Per-FC same source** — `to_ardupilot_horiz_accuracy_m(cov) * 1000` equals `to_inav_h_pos_accuracy_mm(cov)` ± 1 (rounding), for any well-conditioned input.

**AC-10: No global state** — two independent `CovarianceProjector` instances do not share state (instance method; not static — per SRP).

## Non-Functional Requirements

- p99 ≤ 100 µs per call (constant-time per § 5).

## Constraints

- Instance method (NOT static) per coderule SRP — variant-specific output formatting on the instance.
- Public API: NOT exposed outside C8 (helper-only).
- numpy is the only allowed dependency for the eigenvalue computation.
- The two methods MUST share the same intermediate 3×3 / 2×2 reduction code path — variant-specific code is the unit conversion at the end (per coderule SRP).

## Risks & Mitigation

- **Risk: numpy eigvalsh vs custom 2×2 closed-form gives different results within numerical tolerance** — *Mitigation*: use the closed-form `sqrt(0.5 * (a + d + sqrt((a-d)^2 + 4*b^2)))` directly; faster + bit-stable.
- **Risk: SPD-violation cascades into a tight error loop in production** — *Mitigation*: the projector raises; the AP / iNav outbound task is responsible for handling `FcEmitError` + dropping the frame + continuing. C5's emit thread does not retry on SPD violation.

## Runtime Completeness

- **Named capability**: honest 6×6 → 2×2 → equivalent_radius projection for both FC variants.
- **Production code**: real numpy-based reduction; real per-FC unit conversion; real SPD + NaN guards; real FDR logging.
- **Unacceptable substitutes**: a constant fixed-radius output ("3.0 m always") — defeats AC-4.3 and Invariant 4.

## Contract

Implements `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md` — Invariant 4.
