# C4 — Pose Estimation

## 1. High-Level Overview

**Purpose**: convert `MatchResult` (2D-3D correspondences) into a `PoseEstimate` — WGS84 position + 6×6 covariance + provenance label + `last_satellite_anchor_age_ms` — using OpenCV `solvePnPRansac` (IPPE) wrapped in GTSAM `Marginals` for native 6×6 posterior covariance recovery (D-C4-2 = (b)). Under thermal throttle, auto-degrades to Jacobian-based covariance (D-C4-2 = (a)) per the D-CROSS-LATENCY-1 hybrid.

**Architectural Pattern**: single concrete implementation `OpenCVGtsamPoseEstimator` behind the `PoseEstimator` interface. The pose estimator and the state estimator (C5) **share the GTSAM substrate**; the C4 factor is added directly to C5's iSAM2 graph rather than computed in isolation.

**Upstream dependencies**:
- C3.5 → `MatchResult` (refined or passthrough).
- C5 StateEstimator — supplies the GTSAM iSAM2 handle so C4 can add its factor in-graph (architecture principle: shared substrate per ADR-003).
- Camera calibration artifact — for intrinsics + distortion + body-to-camera extrinsics.
- C7 InferenceRuntime — only indirectly via the LightGlue inliers fed in from C3 / C3.5; C4 itself is OpenCV+GTSAM, not GPU-bound.

**Downstream consumers**:
- C5 StateEstimator (consumes `PoseEstimate`).

## 2. Internal Interfaces

### Interface: `PoseEstimator`

| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `estimate` | `MatchResult, CameraCalibration, ThermalState` | `PoseEstimate` | No | `PnpFailureError`, `CovarianceDegradedWarning` |
| `current_covariance_mode` | `()` | `CovarianceMode` enum {MARGINALS, JACOBIAN} | No | — |

**Input DTOs**:
```
MatchResult:                   see C3 / C3.5
CameraCalibration:             see C5
ThermalState:                  see C7 (telemetry from jetson-stats)
```

**Output DTOs**:
```
PoseEstimate:
  frame_id:                       uuid
  position_wgs84:                 LatLonAlt — degrees, degrees, metres MSL
  orientation_world_T_body:       Quat (w, x, y, z)
  covariance_6x6:                 Matrix6 — position (3x3) + orientation (3x3) sub-matrices
  covariance_mode:                CovarianceMode {MARGINALS, JACOBIAN}
  source_label:                   enum {satellite_anchored, visual_propagated, dead_reckoned}
  last_satellite_anchor_age_ms:   int — bin input for AC-1.3
  emitted_at:                     monotonic_ns
```

## 3. External API Specification

Not applicable.

## 4. Data Access Patterns

Stateless w.r.t. persistent storage; reads camera calibration once at construction.

## 5. Implementation Details

**Algorithmic Complexity**: `solvePnPRansac` is `O(I · trials)` in inlier count and RANSAC trials; `Marginals.marginalCovariance(pose_key)` is `O(K^3)` in keyframe-window size for the steady-state path (D-C5-3 K=10–20). Jacobian-degraded mode is `O(I)`.

**State Management**:
- Stateless w.r.t. flight history (C5 owns history).
- Holds the GTSAM `Marginals` factor handle and the OpenCV `solvePnPRansac` configuration.
- Holds a reference to the shared GTSAM iSAM2 graph owned by C5 — does **not** own it.

**Key Dependencies**:

| Library | Version | Purpose |
|---------|---------|---------|
| OpenCV | ≥ 4.12.0 (CVE-2025-53644 mitigation) | `solvePnPRansac` with `SOLVEPNP_IPPE` flag; D-C4-1 = (b) |
| GTSAM (Python + C++) | per Plan-phase pin | `Marginals.marginalCovariance(pose_key)` for native 6×6 covariance |
| Eigen | matches GTSAM | Lie-algebra math |

**Error Handling Strategy**:
- `PnpFailureError`: RANSAC convergence failure or degenerate match geometry. Emit no `PoseEstimate`; C5 falls back to VIO-only with provenance label `visual_propagated`.
- `CovarianceDegradedWarning`: thermal-throttle telemetry crossed the configurable threshold; auto-switch to Jacobian-based covariance (D-CROSS-LATENCY-1). Emit `PoseEstimate` with `covariance_mode = JACOBIAN`. NOT a fatal condition.
- The thermal-throttle decision is per-frame; once telemetry returns below threshold, switch back to MARGINALS on the next frame.

## 6. Extensions and Helpers

| Helper | Purpose | Used By |
|--------|---------|---------|
| `SE3Utils` | shared with C1, C5 | C1, C4, C5 |
| `WgsConverter` | local-tangent-plane ↔ WGS84 latitude/longitude/altitude | C4, C8 |
| `RansacFilter` | shared RANSAC + reprojection residual | C3, C3.5, C4 |

## 7. Caveats & Edge Cases

**Known limitations**:
- Posterior covariance accuracy depends on the GTSAM substrate being healthy. C5's iSAM2 instability propagates into C4's covariance honesty.
- Jacobian-degraded covariance is a known accuracy trade (~5–10% loss per ADR-006); accepted under thermal throttle, never accepted on the steady-state path.

**Potential race conditions**:
- Concurrent calls to `estimate` would race on the shared GTSAM graph. Single-threaded hot path; composition root binds C4 + C5 to the same thread.

**Performance bottlenecks**:
- `Marginals.marginalCovariance(pose_key)` is the dominant cost in steady state (~30–90 ms). The D-CROSS-LATENCY-1 hybrid trades this for ~5–15 ms Jacobian under thermal throttle.

## 8. Dependency Graph

**Must be implemented after**: C3.5 (input), C5 (shared GTSAM substrate; circular at the design level — both must be co-developed but C5's iSAM2 graph must be constructable without C4 calling into it).

**Can be implemented in parallel with**: C1, C6 — independent paths.

**Blocks**: C5 (graph factor add depends on C4), F3 / F6 / F7.

## 9. Logging Strategy

| Log Level | When | Example |
|-----------|------|---------|
| ERROR | `PnpFailureError` | `C4 PnP failure on frame=12345; mode=marginals; falling back to visual_propagated` |
| WARN | `CovarianceDegradedWarning`; thermal throttle entered | `C4 covariance degraded to JACOBIAN; thermal_throttle=true; clock=mhz=750` |
| INFO | Strategy ready | `C4 ready: estimator=opencv_gtsam, default_covariance=MARGINALS` |
| DEBUG | per-frame inlier count + residual + chosen mode | `C4 frame=12345 inliers=412 residual=0.9px mode=MARGINALS` |

**Log format**: structured JSON.
**Log storage**: stdout / journald / FDR via C13 (ERROR + WARN always; DEBUG only via FDR per-frame estimate row).
