[AZ-337] Archive task spec + batch 47 report + state bump

- _docs/02_tasks/todo/AZ-337_c2_ultra_vpr.md
  -> _docs/02_tasks/done/AZ-337_c2_ultra_vpr.md
- _docs/03_implementation/batch_47_cycle1_report.md (new)
- _docs/_autodev_state.md: last_completed_batch 46 -> 47;
  sub_step.detail "batch 47 complete - selecting batch 48"

AZ-337 transitioned in Jira: In Progress -> In Testing.

Batches 45/46/47 close the C2 production path (Protocol +
FaissBridge + NetVLAD baseline + UltraVPR primary).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-13 22:44:22 +03:00
parent 3c4fd272f1
commit b64f3a1b93
3 changed files with 201 additions and 2 deletions
@@ -0,0 +1,199 @@
# 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.
+2 -2
View File
@@ -8,9 +8,9 @@ status: in_progress
sub_step:
phase: 7
name: batch-loop
detail: "batch 47 — AZ-337 (C2 UltraVPR primary backbone)"
detail: "batch 47 complete — selecting batch 48"
retry_count: 0
cycle: 1
tracker: jira
last_completed_batch: 46
last_completed_batch: 47
last_cumulative_review: batches_43-45