# C3.5 — AdHoP-conditional Refinement

## 1. High-Level Overview

**Purpose**: conditionally refine `MatchResult` via OrthoLoC AdHoP (method-agnostic perspective preconditioning) when the initial reprojection residual exceeds a configurable threshold; bypass otherwise. The conditional invocation preserves the AC-4.1 latency budget on the steady-state path while keeping the refinement option for hard frames.

**Architectural Pattern**: Strategy with two concrete implementations: `AdHoPRefiner` (real refinement) and `PassthroughRefiner` (no-op for the non-conditional baseline / smoke tests). Selection at startup by config (ADR-001); both implementations linked into the deployment binary by default (refinement is conditionally invoked at runtime, not gated at build time).

**Cycle-1 operational reality**: the airborne binary wires C3.5 through `_STRATEGY_REGISTRY` + `register_airborne_strategies()` (AZ-591). The `c3_5_adhop` airborne slot's `_C3_5_ADHOP_STRATEGIES` tuple in `runtime_root/airborne_bootstrap.py` registers **only `adhop`** — `PassthroughRefiner` is linked into the binary (no `BUILD_REFINER_*` gate; see `C3_5RefinerConfig.KNOWN_STRATEGIES = {"adhop", "passthrough"}`) and is freely selectable in non-airborne composition (unit tests, IT-12 baseline), but selecting `strategy="passthrough"` via the airborne config currently raises `StrategyNotLinkedError` from the `_STRATEGY_REGISTRY` lookup. Constructor injection flows through the `pre_constructed` dict passed to `compose_root(config, pre_constructed=...)` (AZ-618 umbrella → AZ-621 c3 helpers phase + AZ-623 c7 inference phase). The `c3_5_adhop` slot lists `("c282_ransac_filter", "c7_inference")` in `AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS`; `clock` and `c13_fdr` are optional. Missing required keys raise `AirborneBootstrapError` at composition time, naming the consumer and missing key. The `c282_ransac_filter` and `c3_lightglue_runtime` helper instances are identity-shared with C3 / C4 (R14 / AZ-282).

**Upstream dependencies**:
- C3 → `MatchResult`.
- C7 InferenceRuntime — AdHoP backbone forward pass when invoked.

**Downstream consumers**:
- C4 PoseEstimator (consumes the possibly-refined `MatchResult`).

## 2. Internal Interfaces

### Interface: `ConditionalRefiner`

| Method | Input | Output | Async | Error Types |
|--------|-------|--------|-------|-------------|
| `refine_if_needed` | `NavCameraFrame, MatchResult, residual_threshold_px: float` | `MatchResult` (possibly enriched with refined inliers) | No | `RefinerBackboneError` |
| `was_invoked` | `()` | `bool` — last-call flag for FDR provenance | No | — |

**Input DTOs**:
```
NavCameraFrame:                see C1
MatchResult:                   see C3
```

**Output DTOs**:
```
MatchResult (refined):
  Same shape as C3's MatchResult, with the following enrichments when refinement was invoked:
    per_candidate[i].inlier_correspondences:    refined coordinates from AdHoP perspective preconditioning
    per_candidate[i].per_candidate_residual_px: post-refinement residual
    refinement_label:                            "adhop" | "passthrough"
    refinement_added_latency_ms:                 float — for FDR latency partition
```

## 3. External API Specification

Not applicable.

## 4. Data Access Patterns

| Query | Frequency | Hot Path | Index Needed |
|-------|-----------|----------|--------------|
| Nav-frame pixel access (1 frame per invocation) | conditional, <3 Hz | When invoked | already in memory from C1/C2 path |

No additional caching.

## 5. Implementation Details

**Algorithmic Complexity**: AdHoP perspective preconditioning is `O(F)` in feature count plus one extra backbone forward pass. Steady-state cost is zero (passthrough); worst-case adds ~30–90 ms per invocation on the Jetson.

**State Management**: stateless per-frame.

**Key Dependencies**:

| Library | Version | Purpose |
|---------|---------|---------|
| OrthoLoC AdHoP (research code drop) | upstream HEAD pinned per Plan-phase | Conditional refinement |
| OpenCV (via shared `RansacFilter` / reprojection helpers) | `>=4.11.0.86,<4.12` (cycle-1 relaxed pin; D-CROSS-CVE-1 deferred — see `_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md`) | Reprojection residual computation, perspective transforms, RANSAC re-filtering of AdHoP-preconditioned correspondences |
| TensorRT | matches C7 | AdHoP backbone engine when invoked |

**Error Handling Strategy**:
- `RefinerBackboneError`: AdHoP backbone failure. Fall through to passthrough — emit C3's original `MatchResult` unchanged; downstream pose estimation may fail subsequent quality gates and trigger F6 satellite re-localization.
- The conditional gate is a configuration parameter (`adhop.residual_threshold_px`); no runtime decision logic outside that comparison.

## 6. Extensions and Helpers

| Helper | Purpose | Used By |
|--------|---------|---------|
| `RansacFilter` | shared with C3 | C3, C3.5, C4 |

## 7. Caveats & Edge Cases

**Known limitations**:
- The threshold is a tunable; setting it too low triggers AdHoP every frame and breaks the AC-4.1 budget; too high lets bad matches through to C4. Calibration via NFT-PERF-01 invocation-rate measurement during planning's NFT validation cycle.
- D-CROSS-LATENCY-1 hybrid does not directly change C3.5's behaviour; under thermal throttle, the elevated frame-to-frame latency may push the threshold to be raised at the operator-tooling level pre-flight (C12 concern).

**Potential race conditions**:
- Stateless; concurrent calls are safe in principle but the hot path is single-threaded by design.

**Performance bottlenecks**:
- AdHoP invocation is the variable cost in the F3 budget. NFT-PERF-01 measures the invocation rate; an invocation rate above ~30% suggests the threshold needs revisiting.

**Cycle-1 Tier-2 follow-up dependencies**:
- `PassthroughRefiner` — the module + `register()` hook + `C3_5RefinerConfig.KNOWN_STRATEGIES` entry are all in place, but `c3_5_adhop`'s `_C3_5_ADHOP_STRATEGIES` tuple in `runtime_root/airborne_bootstrap.py` registers only `adhop`. Selecting `strategy="passthrough"` via airborne config currently raises `StrategyNotLinkedError`. Tier-2 follow-up: extend the airborne registration tuple if the IT-12 baseline comparison or a smoke-test deployment needs the passthrough path on a flight binary (today it's available via unit-test composition only).

## 8. Dependency Graph

**Must be implemented after**: C3 (input), C7 (inference runtime).

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

**Blocks**: C4, F3 / F6.

## 9. Logging Strategy

| Log Level | When | Example |
|-----------|------|---------|
| ERROR | `RefinerBackboneError` | `AdHoP backbone failure; frame=12345; falling through to passthrough` |
| WARN | invocation rate over rolling 60 s exceeds budget | `AdHoP invocation rate 38% > target 25%; frame=12345` |
| INFO | Strategy ready | `Refiner ready: strategy=adhop, threshold=2.5px` |
| DEBUG | per-frame invoked/passthrough decision | `Refiner frame=12345 invoked=true added_latency_ms=42` |

**Log format**: structured JSON.
**Log storage**: stdout / journald / FDR via C13 (ERROR + WARN only). DEBUG goes to FDR's per-frame estimate stream as a flag (`refinement_label`).
