# Batch 51 — Implementation Report (Cycle 1)

**Tasks**: AZ-340 (C2 SelaVPR + EigenPlaces + SALAD Secondary Backbones — Research-only)
**Date**: 2026-05-14
**Cycle**: 1
**Status**: COMPLETE (review verdict: PASS_WITH_WARNINGS, two Low findings)

## What was done

Added three secondary `VprStrategy` implementations for IT-12 comparative-study: `SelaVprStrategy` (D=512, 224×224 input), `EigenPlacesStrategy` (D=2048, 480×480 input) and `SaladStrategy` (D=8448, 322×322 input — DINOv2-Large backbone, heaviest in the C2 family). All run via the C7 TensorRT runtime (or ONNX-Runtime fallback), apply ImageNet mean/std preprocessing + single-stage L2 normalisation, and delegate retrieval to `FaissBridge`. All three are gated OFF for airborne and operator-tooling per ADR-002 — `BUILD_VPR_SELAVPR` / `BUILD_VPR_EIGENPLACES` / `BUILD_VPR_SALAD` ON only for the research binary and replay-cli.

### Files added (7)

| File | Purpose |
|------|---------|
| `src/gps_denied_onboard/components/c2_vpr/sela_vpr.py` | `SelaVprStrategy` class + `create()` factory + `_assert_engine_output_dim` helper |
| `src/gps_denied_onboard/components/c2_vpr/_preprocessor_sela_vpr.py` | `SelaVprBackbonePreprocessor` (centre-crop + 224×224 resize + ImageNet normalise + FP16 NCHW) |
| `src/gps_denied_onboard/components/c2_vpr/eigen_places.py` | `EigenPlacesStrategy` class + `create()` factory + `_assert_engine_output_dim` helper |
| `src/gps_denied_onboard/components/c2_vpr/_preprocessor_eigen_places.py` | `EigenPlacesBackbonePreprocessor` (centre-crop + 480×480 resize + ImageNet normalise + FP16 NCHW) |
| `src/gps_denied_onboard/components/c2_vpr/salad.py` | `SaladStrategy` class + `create()` factory + `_assert_engine_output_dim` helper |
| `src/gps_denied_onboard/components/c2_vpr/_preprocessor_salad.py` | `SaladBackbonePreprocessor` (centre-crop + 322×322 resize + ImageNet normalise + FP16 NCHW) |
| `tests/unit/c2_vpr/test_az340_sela_vpr_eigen_places_salad.py` | 54 parametrised AC tests across all three strategies |

### Files changed
- _None._ The composition-root factory (`runtime_root/vpr_factory.py`) was already wired for `sela_vpr`, `eigen_places`, and `salad` strategy names at AZ-336 land time — `_STRATEGY_TO_BUILD_FLAG` and `_STRATEGY_TO_MODULE` tables already include the rows. The `KNOWN_STRATEGIES` frozenset in `c2_vpr/config.py` already includes all three. The `module-layout.md` `Component: c2_vpr` § Internal list already names `sela_vpr.py`, `eigen_places.py`, and `salad.py` (pre-declared by AZ-336). No CMake change required — `BUILD_VPR_*` gating is environment-variable-based per `_is_build_flag_on` in `vpr_factory.py`.

## AC coverage

All 11 ACs verified per strategy via the parametrised test suite. See `_docs/03_implementation/reviews/batch_51_review.md` § Phase 2 for the AC ↔ test mapping table.

| AC | Status | Notes |
|----|--------|-------|
| AC-1..AC-9 + AC-11 | PASS | Each AC parametrised over all three strategies (54 test cases total) |
| AC-10 | PASS with drift | Implementation raises `StrategyNotAvailableError` (env-flag OFF path) and `ConfigError` (runtime-label mismatch path); the spec literally names `ConfigurationError`. Mirrors the established AZ-337 / AZ-338 / AZ-339 precedent. Logged as Low finding F2. |

## Test results

- `tests/unit/c2_vpr/test_az340_sela_vpr_eigen_places_salad.py` — **54 / 54 PASS**.
- `tests/unit/c2_vpr/test_protocol_conformance.py` — **PASS** (auto-extends across all 7 strategies after AZ-340; the three new ones are picked up by the parametrised `_STRATEGY_MODULES` table without test changes — verified by the full c2_vpr/ run below).
- `tests/unit/c2_vpr/` (full directory: faiss_bridge + net_vlad + ultra_vpr + AZ-339 + AZ-340 + protocol_conformance) — **180 / 180 PASS**.
- `tests/unit/test_az508_iso_timestamps.py` — **18 / 18 PASS** (AZ-526 regression guard confirms no new `_iso_ts_from_clock` duplicates introduced by AZ-340).
- `tests/unit/test_az270_compose_root.py` — **8 / 8 PASS**.
- `ruff check` on all 7 new files — clean (one auto-fixable `RUF022 __all__ not sorted` in `_preprocessor_eigen_places.py` was caught and fixed before commit).

## Architectural decisions

1. **Single parametrised test file `test_az340_sela_vpr_eigen_places_salad.py`** — rather than three near-identical files mirroring `test_ultra_vpr.py` / `test_net_vlad.py`. The three strategies share byte-identical behavioural contracts (same Protocol, same FDR record kinds, same log kinds, same error envelope) and differ only on three values (`DESCRIPTOR_DIM`, `_BACKBONE_LABEL`, preprocessor `input_shape()`). A parametrised approach keeps any future drift visible at the assertion level and reduces the test surface from ~2300 lines (three copies of test_ultra_vpr.py) to ~700 lines. Same precedent as AZ-339.
2. **Preprocessor duplication preserved** (sela_vpr vs eigen_places vs salad vs mega_loc vs mix_vpr vs ultra_vpr vs net_vlad) — per `components/02_c2_vpr/description.md` § 6 and the task spec § Constraints. Each preprocessor owns its own input-shape constants so a future code drop can change a backbone's preprocessing without coupling other strategies' weights-versions.
3. **`_assert_engine_output_dim` duplicated, NOT extracted** — see Spec Drift / Review Finding F1. The cleaner path is the dedicated AZ-527 hygiene PBI (now scoped to consolidate 7 copies, not 4).
4. **`iso_ts_from_clock` imported from the AZ-526 helper from day 1** — none of the three new strategies introduces a local `_iso_ts_from_clock` body. The AZ-526 regression guard test confirms this.
5. **Runtime-label guard placed inside `create()`** (not in `__init__`) — runtime selection is a composition-time concern; once the strategy is constructed it's expected to work. Matches the UltraVPR / NetVLAD / MegaLoc / MixVPR precedent.
6. **SALAD's high embedding dim (8448) is non-negotiable at the strategy layer** — it's the architectural output of the SALAD aggregator over DINOv2-Large patch tokens. PCA-whitening for a smaller SALAD descriptor is an operator-side decision at corpus build time (C10), gated by a future `BUILD_VPR_SALAD_PCA` build flag (out of scope here).

## Spec drift noted (carried into review F2)

AZ-340 § AC-10 literally specifies `ConfigurationError` for the build-flag-OFF case. The existing AZ-336 composition-root factory raises `StrategyNotAvailableError` for this case (per its own contract and test coverage at `test_protocol_conformance.py`). The strategy module's own runtime-label guard raises `ConfigError` for the related "wrong C7 runtime" case. AZ-337 / AZ-338 / AZ-339 followed this same pattern; AZ-340 mirrors them. AC-10 wording should be amended in a future shared spec-pass touching AZ-337..AZ-340; no code change required.

## Cumulative review obligation

This batch closes the K=3 cumulative-review window for batches 49–51 (last cumulative review covered batches 46-48). The cumulative review for batches 49–51 runs immediately after this batch report lands, before batch 52 starts.

## Follow-on PBI

**AZ-527** (Hygiene — consolidate `_assert_engine_output_dim` into a c2-internal helper). 2 points. Now must consolidate **7** copies (was 4 before AZ-339, became 4 again after AZ-339, now 7 after AZ-340). Depends on AZ-340. To be created and prioritised by the cumulative review for batches 49-51 (about to run).
