# Batch 47 / Cycle 1 — Implementation Report

**Date**: 2026-05-13
**Tasks**: AZ-337 — C2 UltraVPR Primary Backbone (5pt)
**Total complexity**: 5 points
**Result**: PASS_WITH_WARNINGS (per-batch code review)
**Jira tracker state**: AZ-337 transitioned To Do → In Progress → In Testing

## Scope

UltraVPR is the Documentary Lead's PRIMARY backbone per
`components/02_c2_vpr/description.md` § 1 and is the strategy wired
by default when `config.c2_vpr.strategy == "ultra_vpr"`. Runs on the
C7 TensorRT runtime (AZ-298) or ONNX-Runtime fallback (AZ-299) —
explicitly NOT on the PyTorch FP16 runtime (reserved for the
NetVLAD baseline). This runtime isolation means a TRT engine compile
bug can fall back to NetVLAD without simultaneously breaking both
strategies. This batch closes the C2 default production path that
NetVLAD (B46) and FaissBridge (B45) prepared for.

## Files Changed

### Production (new)

- `src/gps_denied_onboard/components/c2_vpr/ultra_vpr.py` —
  `UltraVprStrategy` class implementing the `VprStrategy` Protocol.
  Constructor wires `InferenceRuntimeCut` + `DescriptorIndexCut` +
  `UltraVprBackbonePreprocessor` + `DescriptorNormaliser` +
  `FaissBridge` + `FdrClient` + `Clock`. Module-level
  `create(config, descriptor_index, inference_runtime)` factory
  consumed by `build_vpr_strategy`. Module-level
  `DESCRIPTOR_DIM = 512` constant (NOT a config knob; spec
  § Constraints "preprocessing parameters are hard-coded").
  Engine load + output-shape assertion at `create()` time.
- `src/gps_denied_onboard/components/c2_vpr/_preprocessor_ultra_vpr.py` —
  `UltraVprBackbonePreprocessor` implementing the C2-internal
  `BackbonePreprocessor` Protocol. Decode → centre-crop around the
  camera calibration's principal point (cx, cy from `intrinsics_3x3`,
  fallback to geometric centre + WARN log when calibration is
  absent) → resize (384, 384) → ImageNet mean/std → FP16 NCHW.

### Production (modified)

- None. The composition-root `_register_strategy_architecture`
  helper (added in B46) already no-ops cleanly for strategies that
  do not expose `MODEL_NAME` / `architecture_factory`. UltraVPR
  consumes a pre-compiled `.trt` engine (no PyTorch `nn.Module`),
  so no registration is needed.

### Tests (new)

- `tests/unit/c2_vpr/test_ultra_vpr.py` — 29 tests covering all
  12 ACs (with per-AC variants for AC-2, AC-6, AC-7, AC-8, AC-9,
  AC-11) + preprocessor contract (5 tests, including
  `test_preprocessor_mean_std_correct_on_grey_image`) +
  constructor validation (2) + FDR record emission (1) +
  no-architecture-registration verification (1).

### Docs (new)

- `_docs/03_implementation/reviews/batch_47_review.md` —
  per-batch code review (PASS_WITH_WARNINGS).

## Acceptance Criteria Coverage

All 12 ACs of AZ-337 have at least one covering unit test:

| AC | Description | Status |
|----|-------------|--------|
| AC-1 | Protocol conformance | covered |
| AC-2 | L2-norm == 1.0 ± 1e-3 FP16 (512,) | covered + single-stage L2 enforcement spy |
| AC-3 | Deterministic across 3 calls | covered |
| AC-4 | `retrieve_topk` == k, label="ultra_vpr", sorted | covered |
| AC-5 | `descriptor_dim()` stable returns 512 | covered |
| AC-6 | Engine output shape mismatch → `ConfigError` | covered (shape + missing-key variants) |
| AC-7 | `VprBackboneError` on forward failure + ERROR log + FDR | covered (3 variants) |
| AC-8 | `VprPreprocessError` on corrupt image + ERROR log + FDR | covered (2 variants) |
| AC-9 | Calibration absent → geometric centre + WARN log | covered + offset-changes-crop control test |
| AC-10 | `IndexUnavailableError` propagated unchanged | covered |
| AC-11 | Composition-root wiring + INFO log "c2.vpr.ready" + BUILD-flag gate | covered (TRT accept + ONNX-RT accept + PyTorch reject) |
| AC-12 | Top-1 distance > threshold → WARN log via FaissBridge | covered |

## Test Results

- **Full unit suite**: `1637 passed / 80 environment-skipped / 0 failed`
  in ~66s. Up from 1608 at the close of Batch 46 (+29 new tests).
- **Focused per-component**: `c2_vpr/test_ultra_vpr.py` 29/29 PASS;
  `c2_vpr/test_net_vlad.py` 31/31 PASS (no regression);
  `c2_vpr/test_faiss_bridge.py` 22/22 PASS (no regression).
- **Lint**: `ruff check` clean on every new file.
- **AZ-507 layering lint**: `test_ac6_only_compose_root_imports_concrete_strategies` PASS.

## Architectural Decisions

1. **No composition-root changes**. UltraVPR uses a pre-compiled
   `.trt` engine; there is no PyTorch `nn.Module` to register with
   C7's architecture registry. The strategy module therefore does
   NOT expose `MODEL_NAME` / `architecture_factory`. The
   composition-root `_register_strategy_architecture` helper
   (added in B46 for NetVLAD) no-ops cleanly for this case —
   no code change needed there. Verified by
   `test_create_does_not_register_pytorch_architecture`.

2. **`DESCRIPTOR_DIM = 512` is a module constant, not a config knob**.
   UltraVPR's research code drop ships with a fused embedding head
   that produces D=512. Unlike NetVLAD (whose Linear PCA layer
   makes D a tunable knob), UltraVPR's D is weights-coupled.
   Making it a config knob would let an operator silently break
   AC-2.1b recall floor. AC-5 / AC-6 / AC-7 all assume 512.

3. **Single-stage L2 normalisation, enforced by spy**. UltraVPR is
   single-stage L2 (no intra-cluster step like NetVLAD). The test
   `test_ac2_embedding_is_single_stage_l2_no_intra_cluster_path`
   uses a spy normaliser to assert that
   `intra_cluster_normalise` is NEVER called — a regression
   guard against a future refactor accidentally introducing the
   two-stage path (which would silently degrade recall on the
   Derkachi corpus).

4. **Engine load + output-shape assertion at `create()` time, NOT
   first frame**. Misconfiguration surfaces at startup (5 seconds
   into the binary) rather than 17 minutes into a flight at the
   first VPR query. Matches Constraint § 5 of the task spec.

5. **Principal-point-aware centre-crop**. The preprocessor extracts
   `(cx, cy)` from `intrinsics_3x3` and anchors the square crop
   there, falling back to geometric centre + WARN log when
   calibration is absent. Matches the upstream UltraVPR contract;
   relevant for wide-angle cameras where the principal point is
   not at the image centre.

6. **Pattern parity with NetVLAD where semantics permit**. Method
   shapes for `embed_query` / `retrieve_topk` / `_emit_*` mirror
   `NetVladStrategy` line-for-line; UltraVPR-specific paths
   (single-stage L2, `"embedding"` output key, TRT runtime, no
   architecture registry, principal-point crop) are clearly
   localised. This makes AZ-339 / AZ-340 (the remaining C2
   strategies) drop into the same skeleton with minimal new
   architectural decisions.

7. **AC-12 delegation to `FaissBridge`**. The top-1 distance WARN
   log is owned by `FaissBridge` (B45); UltraVPR inherits this
   path for free via the same one-line `retrieve_topk` delegation
   that NetVLAD uses — one production touchpoint, two strategies.

## Carried-over Findings

- **F1 from cumulative review 43-45 / Batch 46**: `_iso_ts_from_clock`
  duplicated across modules — Batch 47 adds the 7th copy
  (`ultra_vpr.py` line 296). AZ-508 hygiene PBI exists for
  consolidation. Recommend prioritising AZ-508 before AZ-339 /
  AZ-340 add copies #8 and #9.
- **F2 from Batch 46**: spec→implementation drift on C7 API names.
  AZ-337 spec also exhibits the same drift (`runtime.forward()`,
  `runtime.load_engine()` — neither exists in v1.0.0 Protocol).
  Affects upcoming AZ-339 / AZ-340 / AZ-358 / AZ-349. Spec-hygiene
  PBI recommended.

## New Findings (per Batch 47 code review)

- **F3 (Low, Test-Robustness)**: `_extract_principal_point` uses
  `(cx, cy) == (0, 0)` zero-detection to identify "no calibration".
  Safe in production (no real camera has `cx == 0 and cy == 0`)
  but the heuristic exists only because the dataclass field is
  typed `Any` instead of `Optional`. Tighten when calibration
  becomes properly optional.

## Jira Tracker

- AZ-337 transitioned: To Do → In Progress → In Testing.
- Task spec archived: `_docs/02_tasks/todo/AZ-337_c2_ultra_vpr.md` →
  `_docs/02_tasks/done/AZ-337_c2_ultra_vpr.md`.

## Next

Batches 45 / 46 / 47 close the C2 production path: AZ-336 (Protocol),
AZ-341 (FaissBridge), AZ-338 (NetVLAD baseline), AZ-337 (UltraVPR
primary). The Documentary Lead has both baseline + primary backbones
operational; default-strategy wiring works end-to-end at the
composition root.

Per the autodev orchestrator loop: select Batch 48. Top candidates:

- **AZ-508** — ISO-timestamp helper consolidation (2pt). Closes F1
  before AZ-339 / AZ-340 add copies #8 and #9. Strongly recommended.
- **AZ-339** — MegaLoc + MixVPR strategies (5pt). Two strategies in
  one task; same skeleton as UltraVPR but different backbones.
- **AZ-340** — SelaVPR + EigenPlaces + SALAD strategies (5pt).
  Three strategies in one task; closes the C2 alternative-backbone
  buffet.
- **AZ-358** — C4 OpenCV/GTSAM pose estimator (5pt). Opens a new
  component (C4); changes pace from C2-heavy streak.
- **AZ-389** — C5 internal orthorectifier (3pt). Different
  component (C5); unblocks mid-flight tile generation.
- Spec-hygiene PBI to address F2 from B46 / B47 (refresh
  AZ-339 / AZ-340 against the live C7 v1.0.0 Protocol).

The next cumulative review (batches 46-48) will trigger after Batch
48 lands.
