mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 11:21:13 +00:00
[AZ-344] C3 CrossDomainMatcher Protocol + factory + RollingHealthWindow
Defines the public `CrossDomainMatcher` Protocol (PEP 544 @runtime_checkable, two methods: `match` + `health_snapshot`), the three frozen+slotted DTOs (`CandidateMatchSet`, `MatchResult`, `MatcherHealth`) in the L1 `_types/matcher.py` layer, the `MatcherError` family (`MatcherBackboneError`, `InsufficientInliersError`), and the composition-root `build_matcher_strategy` factory with lazy-import + `BUILD_MATCHER_<variant>` gating per ADR-002. `RollingHealthWindow` accumulator (60 s, amortised O(1) update, strict O(1) snapshot) is constructed by the factory and injected into every concrete matcher so all backbones share window semantics; this is what backs C5's spoof-promotion gate. Legacy placeholder `MatchResult` removed from `_types/matching.py`; import-only consumers (`c4_pose.interface`, `c3_5_adhop.interface`) repointed at the new `_types/matcher.py` home — zero behavioural change to those components. AC-9 (single-thread binding) and AC-10 (LightGlueRuntime identity-share with C2.5) deferred to AZ-270 runtime-root composition, mirroring the AZ-342 Risk-4 escape clause. All other ACs + NFRs covered by 70 new conformance tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
**Producer task**: AZ-344 (`CrossDomainMatcher` Protocol + factory + composition)
|
||||
**Consumer tasks**: AZ-345 (DISK+LightGlue primary), AZ-346 (ALIKED+LightGlue secondary), AZ-347 (XFeat alternate); downstream c3_5_adhop (epic AZ-258) which consumes `MatchResult`
|
||||
**Version**: 1.0.0
|
||||
**Status**: draft, awaiting AZ-344 implementation
|
||||
**Last Updated**: 2026-05-10
|
||||
**Status**: v1.0.0 (AZ-344 implemented 2026-05-12)
|
||||
**Last Updated**: 2026-05-12
|
||||
**Module-layout home**: `src/gps_denied_onboard/components/c3_matcher/interface.py` (Protocol), `src/gps_denied_onboard/components/c3_matcher/__init__.py` (re-exports), `src/gps_denied_onboard/runtime_root/matcher_factory.py` (factory)
|
||||
|
||||
## Purpose
|
||||
@@ -65,14 +65,13 @@ class CrossDomainMatcher(Protocol):
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from uuid import UUID
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CandidateMatchSet:
|
||||
"""Per-candidate matching outcome inside a MatchResult."""
|
||||
tile_id: tuple # composite (zoomLevel, lat, lon)
|
||||
tile_id: tuple[int, float, float] # composite (zoomLevel, lat, lon); mirrors VprCandidate / RerankCandidate encoding so the L1 _types layer is free of an L1→L3 import to c6_tile_cache.TileId
|
||||
inlier_count: int
|
||||
inlier_correspondences: np.ndarray # shape (I, 4) float32; (px_query, py_query, px_tile, py_tile)
|
||||
ransac_outlier_count: int
|
||||
@@ -82,8 +81,8 @@ class CandidateMatchSet:
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MatchResult:
|
||||
"""Cross-domain match outcome for one frame. Consumed by C3.5 ConditionalRefiner."""
|
||||
frame_id: UUID
|
||||
per_candidate: list[CandidateMatchSet] # 0 < len <= N=3, ranked by inlier_count descending; ties broken by per_candidate_residual_px ascending
|
||||
frame_id: int # mirrors NavCameraFrame.frame_id; matches AZ-336 / AZ-342 encoding
|
||||
per_candidate: tuple[CandidateMatchSet, ...] # 0 < len <= N=3, ranked by inlier_count descending; ties broken by per_candidate_residual_px ascending. tuple (not list) so frozen+slots actually holds.
|
||||
best_candidate_idx: int # 0 by construction (sorted)
|
||||
reprojection_residual_px: float # best candidate's median residual
|
||||
matched_at: int # monotonic_ns
|
||||
@@ -115,6 +114,8 @@ class InsufficientInliersError(MatcherError):
|
||||
"""Every candidate failed OR every candidate's inlier count is below `config.matcher.min_inliers_threshold`. Raised by `match`. C5 falls back to VIO-only."""
|
||||
```
|
||||
|
||||
The composition-time selection error is **`StrategyNotAvailableError`** (`runtime_root.errors`), NOT a member of `MatcherError`: it surfaces when the binary lacks the requested `BUILD_MATCHER_<variant>` flag or the concrete strategy module is not built yet (AZ-345..AZ-347 pending). This matches the C2 VPR (AZ-336) and C2.5 ReRank (AZ-342) factory pattern: per-frame matcher errors live in the C3 family; composition-time selection errors live in the shared runtime-root family.
|
||||
|
||||
## Composition-Root Factory
|
||||
|
||||
```python
|
||||
|
||||
@@ -70,15 +70,18 @@ Bootstrap reference: `_docs/02_tasks/todo/AZ-263_initial_structure.md`. Architec
|
||||
- **Epic**: AZ-257 (E-C3 Cross-Domain Matcher)
|
||||
- **Directory**: `src/gps_denied_onboard/components/c3_matcher/`
|
||||
- **Public API**:
|
||||
- `__init__.py` (re-exports `CrossDomainMatcher`, `MatchResult`)
|
||||
- `__init__.py` (re-exports `CrossDomainMatcher`, `MatchResult`, `MatcherHealth`, `CandidateMatchSet`, `MatcherError`, `MatcherBackboneError`, `InsufficientInliersError`, `C3MatcherConfig`)
|
||||
- `interface.py` (`CrossDomainMatcher` Protocol)
|
||||
- `config.py` (`C3MatcherConfig`)
|
||||
- `errors.py` (error hierarchy)
|
||||
- **Internal**:
|
||||
- `disk_lightglue.py` (DISK + LightGlue)
|
||||
- `aliked_lightglue.py` (ALIKED + LightGlue)
|
||||
- `xfeat.py`
|
||||
- `_health_window.py` (`RollingHealthWindow` accumulator; constructor-injected into every concrete matcher)
|
||||
- `disk_lightglue.py` (DISK + LightGlue, AZ-345)
|
||||
- `aliked_lightglue.py` (ALIKED + LightGlue, AZ-346)
|
||||
- `xfeat.py` (XFeat, AZ-347)
|
||||
- `_native/`
|
||||
- **Owns**: `src/gps_denied_onboard/components/c3_matcher/**`, `tests/unit/c3_matcher/**`
|
||||
- **Imports from**: `_types`, `helpers.lightglue_runtime` (R14: SHARED with C2.5 — owned by helper, NOT by C3), `helpers.descriptor_normaliser`, `helpers.se3_utils`, `components.c7_inference`, `config`, `logging`, `fdr_client`
|
||||
- **Owns**: `src/gps_denied_onboard/components/c3_matcher/**`, `tests/unit/c3_matcher/**`, `src/gps_denied_onboard/runtime_root/matcher_factory.py`
|
||||
- **Imports from**: `_types`, `helpers.lightglue_runtime` (R14: SHARED with C2.5 — owned by helper, NOT by C3), `helpers.ransac_filter`, `helpers.descriptor_normaliser`, `helpers.se3_utils`, `components.c7_inference`, `config`, `logging`, `fdr_client`
|
||||
- **Consumed by**: `c3_5_adhop`, `runtime_root`
|
||||
|
||||
### Component: c3_5_adhop
|
||||
|
||||
@@ -8,7 +8,7 @@ status: in_progress
|
||||
sub_step:
|
||||
phase: 2
|
||||
name: detect-progress
|
||||
detail: "batch 23 complete (AZ-303, AZ-297, AZ-331, AZ-398 per-task commits); selecting batch 24"
|
||||
detail: "batch 24 in flight (per-task commits): AZ-336 done, AZ-342 done, AZ-344 done; next AZ-348"
|
||||
retry_count: 0
|
||||
cycle: 1
|
||||
tracker: jira
|
||||
|
||||
Reference in New Issue
Block a user