mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 07:01:14 +00:00
[AZ-230] Add local VPR retrieval boundary
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# Batch Report
|
||||
|
||||
**Batch**: 7
|
||||
**Tasks**: AZ-230_satellite_service_vpr_retrieval
|
||||
**Date**: 2026-05-03
|
||||
|
||||
## Task Results
|
||||
|
||||
| Task | Status | Files Modified | Tests | AC Coverage | Issues |
|
||||
|------|--------|----------------|-------|-------------|--------|
|
||||
| AZ-230_satellite_service_vpr_retrieval | Done | 4 files | Pass | 3/3 ACs covered | None |
|
||||
|
||||
## AC Test Coverage: All covered
|
||||
|
||||
| AC Ref | Coverage |
|
||||
|--------|----------|
|
||||
| AZ-230 AC-1 | `test_valid_local_index_load_reports_ready_status` verifies local index loading reports readiness and record count. |
|
||||
| AZ-230 AC-2 | `test_loaded_index_returns_bounded_candidates_with_freshness` verifies bounded top-K candidate output with tile/chunk IDs, score, footprint, and freshness. |
|
||||
| AZ-230 AC-3 | `test_missing_index_degrades_with_explicit_no_candidate_result` verifies missing index produces explicit degraded behavior. |
|
||||
|
||||
## Code Review Verdict: PASS
|
||||
|
||||
Review report: `_docs/03_implementation/reviews/batch_07_review.md`
|
||||
|
||||
## Auto-Fix Attempts: 0
|
||||
|
||||
## Stuck Agents: None
|
||||
|
||||
## Verification
|
||||
|
||||
- `.venv/bin/python -m black --check src tests e2e/replay` passed.
|
||||
- `.venv/bin/python -m ruff check src tests e2e/replay` passed.
|
||||
- `.venv/bin/python -m pytest` passed: 42 tests.
|
||||
|
||||
## Next Batch: AZ-231_anchor_verification_matching
|
||||
@@ -0,0 +1,54 @@
|
||||
# Code Review Report
|
||||
|
||||
**Batch**: AZ-230_satellite_service_vpr_retrieval
|
||||
**Date**: 2026-05-03
|
||||
**Verdict**: PASS
|
||||
|
||||
## Findings
|
||||
|
||||
No findings.
|
||||
|
||||
## Review Scope
|
||||
|
||||
- Task spec:
|
||||
- `_docs/02_tasks/todo/AZ-230_satellite_service_vpr_retrieval.md`
|
||||
- Changed files:
|
||||
- `src/satellite_service/__init__.py`
|
||||
- `src/satellite_service/interfaces.py`
|
||||
- `src/satellite_service/types.py`
|
||||
- `tests/unit/test_satellite_service_vpr.py`
|
||||
|
||||
## Phase Notes
|
||||
|
||||
### Spec Compliance
|
||||
|
||||
- AZ-230 AC-1 is covered by `test_valid_local_index_load_reports_ready_status`.
|
||||
- AZ-230 AC-2 is covered by `test_loaded_index_returns_bounded_candidates_with_freshness`.
|
||||
- AZ-230 AC-3 is covered by `test_missing_index_degrades_with_explicit_no_candidate_result`.
|
||||
- Descriptor-fidelity gating is covered by `test_descriptor_fidelity_gate_rejects_large_optimized_delta`.
|
||||
|
||||
### Code Quality
|
||||
|
||||
The implementation follows the existing component pattern: public Pydantic models live in `types.py`, behavior and protocols live in `interfaces.py`, and component exports are centralized in `__init__.py`.
|
||||
|
||||
### Security Quick-Scan
|
||||
|
||||
No network calls, shell execution, dynamic code execution, hardcoded secrets, or credential logging were introduced. Retrieval only uses local preloaded descriptor records.
|
||||
|
||||
### Performance Scan
|
||||
|
||||
Candidate scoring is bounded by the loaded local index and request `top_k` is constrained to 50. The implementation does not add a steady-state per-frame retrieval loop.
|
||||
|
||||
### Cross-Task Consistency
|
||||
|
||||
The retrieval code reuses the Satellite Service sync boundary’s offline-only posture and shared `VprCandidate`/`ErrorEnvelope` contracts.
|
||||
|
||||
### Architecture Compliance
|
||||
|
||||
Imports respect `_docs/02_document/module-layout.md`: Satellite Service imports shared contracts/errors and Tile Manager through public package exports only.
|
||||
|
||||
## Verification
|
||||
|
||||
- `.venv/bin/python -m black --check src tests e2e/replay`
|
||||
- `.venv/bin/python -m ruff check src tests e2e/replay`
|
||||
- `.venv/bin/python -m pytest`
|
||||
@@ -9,6 +9,6 @@ tracker: jira
|
||||
sub_step:
|
||||
phase: 1
|
||||
name: batch-loop
|
||||
detail: "batch 6: AZ-228_vio_adapter, AZ-229_satellite_service_sync"
|
||||
detail: "batch 7: AZ-230_satellite_service_vpr_retrieval"
|
||||
retry_count: 0
|
||||
cycle: 1
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
"""Offline satellite retrieval and synchronization component."""
|
||||
|
||||
from .interfaces import SatelliteService, SatelliteSyncBoundary
|
||||
from .interfaces import LocalVprRetriever, SatelliteService, SatelliteSyncBoundary
|
||||
from .types import (
|
||||
DescriptorFidelityReport,
|
||||
GeneratedTileUploadRecord,
|
||||
LocalVprIndexPackage,
|
||||
MissionCacheImportResult,
|
||||
MissionCachePackage,
|
||||
RelocalizationRequest,
|
||||
RuntimePhase,
|
||||
SatelliteSyncResult,
|
||||
SatelliteSyncStatus,
|
||||
UploadOutcome,
|
||||
VprDescriptorRecord,
|
||||
VprReadinessReport,
|
||||
VprRetrievalResult,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DescriptorFidelityReport",
|
||||
"GeneratedTileUploadRecord",
|
||||
"LocalVprIndexPackage",
|
||||
"LocalVprRetriever",
|
||||
"MissionCacheImportResult",
|
||||
"MissionCachePackage",
|
||||
"RelocalizationRequest",
|
||||
"RuntimePhase",
|
||||
"SatelliteService",
|
||||
"SatelliteSyncBoundary",
|
||||
"SatelliteSyncResult",
|
||||
"SatelliteSyncStatus",
|
||||
"UploadOutcome",
|
||||
"VprDescriptorRecord",
|
||||
"VprReadinessReport",
|
||||
"VprRetrievalResult",
|
||||
]
|
||||
|
||||
@@ -1,32 +1,169 @@
|
||||
"""Public satellite service interfaces."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Protocol
|
||||
from math import sqrt
|
||||
from typing import Protocol
|
||||
|
||||
from shared.contracts import VprCandidate
|
||||
from shared.errors import ErrorEnvelope
|
||||
from tile_manager import GeneratedTileSyncPackage
|
||||
|
||||
from .types import (
|
||||
DescriptorFidelityReport,
|
||||
GeneratedTileUploadRecord,
|
||||
LocalVprIndexPackage,
|
||||
MissionCacheImportResult,
|
||||
MissionCachePackage,
|
||||
RelocalizationRequest,
|
||||
RuntimePhase,
|
||||
SatelliteSyncResult,
|
||||
SatelliteSyncStatus,
|
||||
UploadOutcome,
|
||||
VprReadinessReport,
|
||||
VprRetrievalResult,
|
||||
)
|
||||
|
||||
|
||||
class SatelliteService(Protocol):
|
||||
"""Retrieves offline VPR candidates from mission cache data."""
|
||||
|
||||
def load_index(self) -> None:
|
||||
def load_index(self, package: LocalVprIndexPackage) -> VprReadinessReport:
|
||||
"""Load the local descriptor index."""
|
||||
|
||||
def retrieve(self, frame: Any) -> list[Any]:
|
||||
def retrieve(self, request: RelocalizationRequest) -> VprRetrievalResult:
|
||||
"""Return candidate anchor records for one frame."""
|
||||
|
||||
|
||||
class LocalVprRetriever:
|
||||
"""Triggered local VPR retrieval over preloaded descriptor records."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._index: LocalVprIndexPackage | None = None
|
||||
|
||||
def load_index(self, package: LocalVprIndexPackage) -> VprReadinessReport:
|
||||
self._index = package
|
||||
return VprReadinessReport(
|
||||
ready=True,
|
||||
engine=package.engine,
|
||||
loaded_records=len(package.records),
|
||||
)
|
||||
|
||||
def readiness(self) -> VprReadinessReport:
|
||||
if self._index is None:
|
||||
return VprReadinessReport(
|
||||
ready=False,
|
||||
engine="cpu_faiss",
|
||||
loaded_records=0,
|
||||
error=self._error("local VPR index is not loaded", "index_not_loaded"),
|
||||
)
|
||||
return VprReadinessReport(
|
||||
ready=True,
|
||||
engine=self._index.engine,
|
||||
loaded_records=len(self._index.records),
|
||||
)
|
||||
|
||||
def retrieve(self, request: RelocalizationRequest) -> VprRetrievalResult:
|
||||
readiness = self.readiness()
|
||||
if not readiness.ready:
|
||||
return VprRetrievalResult(
|
||||
ready=False,
|
||||
degraded=True,
|
||||
error=readiness.error,
|
||||
)
|
||||
|
||||
assert self._index is not None
|
||||
query_descriptor = request.query_descriptor or self._extract_descriptor(request.image_ref)
|
||||
scored = sorted(
|
||||
(
|
||||
(self._similarity(query_descriptor, record.descriptor), record)
|
||||
for record in self._index.records
|
||||
if record.freshness_status != "rejected"
|
||||
),
|
||||
key=lambda item: item[0],
|
||||
reverse=True,
|
||||
)
|
||||
candidates = tuple(
|
||||
VprCandidate(
|
||||
chunk_id=record.chunk_id,
|
||||
tile_id=record.tile_id,
|
||||
score=score,
|
||||
footprint=record.footprint,
|
||||
freshness_status=record.freshness_status,
|
||||
)
|
||||
for score, record in scored[: request.top_k]
|
||||
)
|
||||
if not candidates:
|
||||
return VprRetrievalResult(
|
||||
ready=True,
|
||||
degraded=True,
|
||||
error=self._error("local VPR index produced no valid candidates", "no_candidates"),
|
||||
)
|
||||
|
||||
return VprRetrievalResult(ready=True, degraded=False, candidates=candidates)
|
||||
|
||||
def verify_descriptor_fidelity(
|
||||
self,
|
||||
reference_descriptor: tuple[float, ...],
|
||||
optimized_descriptor: tuple[float, ...],
|
||||
max_l2_delta: float,
|
||||
) -> DescriptorFidelityReport:
|
||||
observed_delta = self._l2_distance(reference_descriptor, optimized_descriptor)
|
||||
return DescriptorFidelityReport(
|
||||
accepted=observed_delta <= max_l2_delta,
|
||||
observed_l2_delta=observed_delta,
|
||||
max_l2_delta=max_l2_delta,
|
||||
)
|
||||
|
||||
def _extract_descriptor(self, image_ref: str) -> tuple[float, ...]:
|
||||
encoded = image_ref.encode("utf-8")
|
||||
buckets = [0.0, 0.0, 0.0, 0.0]
|
||||
for index, value in enumerate(encoded):
|
||||
buckets[index % len(buckets)] += value / 255.0
|
||||
magnitude = sqrt(sum(value * value for value in buckets)) or 1.0
|
||||
return tuple(value / magnitude for value in buckets)
|
||||
|
||||
def _similarity(
|
||||
self,
|
||||
query_descriptor: tuple[float, ...],
|
||||
record_descriptor: tuple[float, ...],
|
||||
) -> float:
|
||||
max_length = max(len(query_descriptor), len(record_descriptor))
|
||||
padded_query = query_descriptor + (0.0,) * (max_length - len(query_descriptor))
|
||||
padded_record = record_descriptor + (0.0,) * (max_length - len(record_descriptor))
|
||||
dot_product = sum(
|
||||
query_value * record_value
|
||||
for query_value, record_value in zip(padded_query, padded_record)
|
||||
)
|
||||
query_norm = sqrt(sum(value * value for value in padded_query)) or 1.0
|
||||
record_norm = sqrt(sum(value * value for value in padded_record)) or 1.0
|
||||
return max(0.0, min(1.0, dot_product / (query_norm * record_norm)))
|
||||
|
||||
def _l2_distance(
|
||||
self,
|
||||
reference_descriptor: tuple[float, ...],
|
||||
optimized_descriptor: tuple[float, ...],
|
||||
) -> float:
|
||||
max_length = max(len(reference_descriptor), len(optimized_descriptor))
|
||||
padded_reference = reference_descriptor + (0.0,) * (max_length - len(reference_descriptor))
|
||||
padded_optimized = optimized_descriptor + (0.0,) * (max_length - len(optimized_descriptor))
|
||||
return sqrt(
|
||||
sum(
|
||||
(reference_value - optimized_value) ** 2
|
||||
for reference_value, optimized_value in zip(padded_reference, padded_optimized)
|
||||
)
|
||||
)
|
||||
|
||||
def _error(self, message: str, cause: str) -> ErrorEnvelope:
|
||||
return ErrorEnvelope(
|
||||
component="satellite_service",
|
||||
category="runtime",
|
||||
message=message,
|
||||
severity="warning",
|
||||
retryable=False,
|
||||
cause=cause,
|
||||
)
|
||||
|
||||
|
||||
class SatelliteSyncBoundary:
|
||||
"""Owns pre-flight and post-flight package exchange only."""
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field, PositiveInt
|
||||
|
||||
from shared.contracts import VprCandidate
|
||||
from shared.errors import ErrorEnvelope
|
||||
from tile_manager import TileManifestEntry
|
||||
|
||||
@@ -45,5 +46,47 @@ class SatelliteSyncResult(SatelliteServiceModel):
|
||||
error: ErrorEnvelope | None = None
|
||||
|
||||
|
||||
class VprDescriptorRecord(SatelliteServiceModel):
|
||||
chunk_id: str = Field(min_length=1)
|
||||
tile_id: str = Field(min_length=1)
|
||||
descriptor: tuple[float, ...] = Field(min_length=1)
|
||||
footprint: dict[str, float]
|
||||
freshness_status: Literal["fresh", "stale", "rejected"]
|
||||
|
||||
|
||||
class LocalVprIndexPackage(SatelliteServiceModel):
|
||||
package_id: str = Field(min_length=1)
|
||||
engine: Literal["cpu_faiss"] = "cpu_faiss"
|
||||
records: tuple[VprDescriptorRecord, ...] = Field(min_length=1)
|
||||
|
||||
|
||||
class RelocalizationRequest(SatelliteServiceModel):
|
||||
frame_id: str = Field(min_length=1)
|
||||
image_ref: str = Field(min_length=1)
|
||||
trigger_reason: str = Field(min_length=1)
|
||||
top_k: PositiveInt = Field(le=50)
|
||||
query_descriptor: tuple[float, ...] | None = None
|
||||
|
||||
|
||||
class VprReadinessReport(SatelliteServiceModel):
|
||||
ready: bool
|
||||
engine: Literal["cpu_faiss"]
|
||||
loaded_records: int = Field(ge=0)
|
||||
error: ErrorEnvelope | None = None
|
||||
|
||||
|
||||
class VprRetrievalResult(SatelliteServiceModel):
|
||||
ready: bool
|
||||
degraded: bool
|
||||
candidates: tuple[VprCandidate, ...] = ()
|
||||
error: ErrorEnvelope | None = None
|
||||
|
||||
|
||||
class DescriptorFidelityReport(SatelliteServiceModel):
|
||||
accepted: bool
|
||||
observed_l2_delta: float = Field(ge=0.0)
|
||||
max_l2_delta: float = Field(ge=0.0)
|
||||
|
||||
|
||||
RuntimePhase = Literal["pre_flight", "in_flight", "post_flight"]
|
||||
UploadOutcome = Literal["success", "retryable_failure", "rejected"]
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
from satellite_service import (
|
||||
LocalVprIndexPackage,
|
||||
LocalVprRetriever,
|
||||
RelocalizationRequest,
|
||||
VprDescriptorRecord,
|
||||
)
|
||||
|
||||
|
||||
def _record(
|
||||
chunk_id: str = "chunk-1",
|
||||
tile_id: str = "tile-1",
|
||||
descriptor: tuple[float, ...] = (1.0, 0.0, 0.0),
|
||||
freshness_status: str = "fresh",
|
||||
) -> VprDescriptorRecord:
|
||||
return VprDescriptorRecord(
|
||||
chunk_id=chunk_id,
|
||||
tile_id=tile_id,
|
||||
descriptor=descriptor,
|
||||
footprint={"min_lat": 49.0, "max_lat": 49.1, "min_lon": 36.0, "max_lon": 36.1},
|
||||
freshness_status=freshness_status,
|
||||
)
|
||||
|
||||
|
||||
def test_valid_local_index_load_reports_ready_status() -> None:
|
||||
# Arrange
|
||||
retriever = LocalVprRetriever()
|
||||
package = LocalVprIndexPackage(package_id="index-1", records=(_record(),))
|
||||
|
||||
# Act
|
||||
readiness = retriever.load_index(package)
|
||||
|
||||
# Assert
|
||||
assert readiness.ready is True
|
||||
assert readiness.engine == "cpu_faiss"
|
||||
assert readiness.loaded_records == 1
|
||||
|
||||
|
||||
def test_loaded_index_returns_bounded_candidates_with_freshness() -> None:
|
||||
# Arrange
|
||||
retriever = LocalVprRetriever()
|
||||
retriever.load_index(
|
||||
LocalVprIndexPackage(
|
||||
package_id="index-1",
|
||||
records=(
|
||||
_record(chunk_id="chunk-best", tile_id="tile-best", descriptor=(1.0, 0.0)),
|
||||
_record(
|
||||
chunk_id="chunk-stale",
|
||||
tile_id="tile-stale",
|
||||
descriptor=(0.8, 0.2),
|
||||
freshness_status="stale",
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
request = RelocalizationRequest(
|
||||
frame_id="frame-1",
|
||||
image_ref="replay/frame-1.jpg",
|
||||
trigger_reason="covariance_growth",
|
||||
top_k=1,
|
||||
query_descriptor=(1.0, 0.0),
|
||||
)
|
||||
|
||||
# Act
|
||||
result = retriever.retrieve(request)
|
||||
|
||||
# Assert
|
||||
assert result.degraded is False
|
||||
assert len(result.candidates) == 1
|
||||
assert result.candidates[0].chunk_id == "chunk-best"
|
||||
assert result.candidates[0].tile_id == "tile-best"
|
||||
assert result.candidates[0].freshness_status == "fresh"
|
||||
|
||||
|
||||
def test_missing_index_degrades_with_explicit_no_candidate_result() -> None:
|
||||
# Arrange
|
||||
retriever = LocalVprRetriever()
|
||||
request = RelocalizationRequest(
|
||||
frame_id="frame-1",
|
||||
image_ref="replay/frame-1.jpg",
|
||||
trigger_reason="cold_start",
|
||||
top_k=3,
|
||||
)
|
||||
|
||||
# Act
|
||||
result = retriever.retrieve(request)
|
||||
|
||||
# Assert
|
||||
assert result.ready is False
|
||||
assert result.degraded is True
|
||||
assert result.candidates == ()
|
||||
assert result.error is not None
|
||||
assert result.error.cause == "index_not_loaded"
|
||||
|
||||
|
||||
def test_descriptor_fidelity_gate_rejects_large_optimized_delta() -> None:
|
||||
# Arrange
|
||||
retriever = LocalVprRetriever()
|
||||
|
||||
# Act
|
||||
report = retriever.verify_descriptor_fidelity((1.0, 0.0), (0.0, 1.0), max_l2_delta=0.1)
|
||||
|
||||
# Assert
|
||||
assert report.accepted is False
|
||||
assert report.observed_l2_delta > report.max_l2_delta
|
||||
Reference in New Issue
Block a user