mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 07:01:14 +00:00
feat(01-01): scaffold hot_types/ package with ARCH-02 dataclasses
- Add @dataclass(slots=True, frozen=True) types for IMUSample, ESKFState, RelativePose, Features, Matches, Motion, AlignmentResult, ChunkAlignmentResult, Sim3Transform, RotationResult, TileCoords, TileBounds, SatelliteAnchor, PositionEstimate - FrameState uses slots=True only (frozen=False) per PATTERNS.md §6.1 — processor.py mutates this object during frame handling - eq=False on every dataclass with np.ndarray fields, matching prior Pydantic incomparability under arbitrary_types_allowed - Barrel __init__.py exposes all public names plus ARCH-02 aliases IMUMeasurement → IMUSample and VOEstimate → RelativePose - Pure addition: no consumer file edited, 216 tests still pass
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
"""Hot-path data types (ARCH-02).
|
||||
|
||||
`@dataclass(slots=True, frozen=True)` types used per-frame on the
|
||||
critical path. Pydantic models stay at boundaries (REST/config/wire);
|
||||
this package replaces the per-frame Pydantic models from `schemas/`.
|
||||
|
||||
Legacy import paths in `gps_denied.schemas.*` continue to work via
|
||||
re-export shims (Plan 01-01 Task 3).
|
||||
|
||||
ARCH-02 canonical-name aliases:
|
||||
- `IMUMeasurement` → `IMUSample`
|
||||
- `VOEstimate` → `RelativePose`
|
||||
"""
|
||||
|
||||
from gps_denied.hot_types.alignment_result import (
|
||||
AlignmentResult,
|
||||
ChunkAlignmentResult,
|
||||
Sim3Transform,
|
||||
)
|
||||
from gps_denied.hot_types.eskf_state import ESKFState
|
||||
from gps_denied.hot_types.frame_state import FrameState
|
||||
from gps_denied.hot_types.imu_sample import IMUSample
|
||||
from gps_denied.hot_types.position_estimate import PositionEstimate
|
||||
from gps_denied.hot_types.rotation_result import RotationResult
|
||||
from gps_denied.hot_types.satellite_anchor import (
|
||||
SatelliteAnchor,
|
||||
TileBounds,
|
||||
TileCoords,
|
||||
)
|
||||
from gps_denied.hot_types.vo_estimate import (
|
||||
Features,
|
||||
Matches,
|
||||
Motion,
|
||||
RelativePose,
|
||||
VOEstimate,
|
||||
)
|
||||
|
||||
# ARCH-02 canonical-name aliases (legacy → new)
|
||||
IMUMeasurement = IMUSample
|
||||
|
||||
__all__ = [
|
||||
# ARCH-02 mandated names
|
||||
"FrameState",
|
||||
"IMUSample",
|
||||
"PositionEstimate",
|
||||
"VOEstimate",
|
||||
"SatelliteAnchor",
|
||||
# VIO outputs
|
||||
"RelativePose",
|
||||
"Features",
|
||||
"Matches",
|
||||
"Motion",
|
||||
# ESKF
|
||||
"ESKFState",
|
||||
# Metric / alignment
|
||||
"AlignmentResult",
|
||||
"ChunkAlignmentResult",
|
||||
"Sim3Transform",
|
||||
# Rotation
|
||||
"RotationResult",
|
||||
# Satellite tile geometry
|
||||
"TileCoords",
|
||||
"TileBounds",
|
||||
# Legacy aliases
|
||||
"IMUMeasurement",
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Metric refinement hot-path dataclasses (ARCH-02).
|
||||
|
||||
AlignmentResult, ChunkAlignmentResult, Sim3Transform — all returned per
|
||||
satellite-match frame.
|
||||
|
||||
`gps_center` composes the still-Pydantic GPSPoint (deferred per
|
||||
PATTERNS.md §6.3); composition is fine.
|
||||
|
||||
eq=False on every dataclass with np.ndarray fields.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
from gps_denied.schemas import GPSPoint
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True, eq=False)
|
||||
class AlignmentResult:
|
||||
"""Result of aligning a UAV image to a single satellite tile."""
|
||||
|
||||
matched: bool
|
||||
homography: np.ndarray # (3, 3)
|
||||
gps_center: GPSPoint
|
||||
confidence: float
|
||||
inlier_count: int
|
||||
total_correspondences: int
|
||||
reprojection_error: float # Mean error in pixels
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True, eq=False)
|
||||
class Sim3Transform:
|
||||
"""Sim(3) transformation: scale, rotation, translation."""
|
||||
|
||||
translation: np.ndarray # (3,)
|
||||
rotation: np.ndarray # (3, 3) rotation matrix
|
||||
scale: float
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True, eq=False)
|
||||
class ChunkAlignmentResult:
|
||||
"""Result of aligning a chunk array of UAV images to a satellite tile."""
|
||||
|
||||
matched: bool
|
||||
chunk_id: str
|
||||
chunk_center_gps: GPSPoint
|
||||
rotation_angle: float
|
||||
confidence: float
|
||||
inlier_count: int
|
||||
transform: Sim3Transform
|
||||
reprojection_error: float
|
||||
@@ -0,0 +1,34 @@
|
||||
"""ESKFState hot-path dataclass (ARCH-02).
|
||||
|
||||
Returned by `ESKF.get_state()` every frame. ConfidenceTier stays in
|
||||
schemas/eskf.py as an enum (boundary), and we import it here for the
|
||||
`confidence` field type.
|
||||
|
||||
eq=False because numpy arrays in `__eq__` would raise; Pydantic was already
|
||||
incomparable for this model.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from gps_denied.schemas.eskf import ConfidenceTier
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True, eq=False)
|
||||
class ESKFState:
|
||||
"""Full ESKF nominal state snapshot."""
|
||||
|
||||
position: np.ndarray # (3,) ENU meters from origin (East, North, Up)
|
||||
velocity: np.ndarray # (3,) ENU m/s
|
||||
quaternion: np.ndarray # (4,) [w, x, y, z] body-to-ENU
|
||||
accel_bias: np.ndarray # (3,) m/s^2
|
||||
gyro_bias: np.ndarray # (3,) rad/s
|
||||
covariance: np.ndarray # (15, 15)
|
||||
timestamp: float # seconds since epoch
|
||||
confidence: ConfidenceTier
|
||||
last_satellite_time: Optional[float] = None
|
||||
last_vo_time: Optional[float] = None
|
||||
@@ -0,0 +1,45 @@
|
||||
"""FrameState — per-frame mutable processing record (ARCH-02).
|
||||
|
||||
PATTERNS.md §6.1 explicitly mandates `slots=True, frozen=False` here:
|
||||
processor.py mutates this object during frame handling. The field list
|
||||
mirrors the current `FrameResult` defined in `core/processor.py` lines
|
||||
~52-62. All fields default-initialize so the dataclass can be constructed
|
||||
with just `frame_id`, matching the existing `FrameResult(frame_id)`
|
||||
constructor signature.
|
||||
|
||||
The actual rename of consumer call-sites from `FrameResult` to
|
||||
`FrameState` happens in Plan 07 (orchestrator rename); Phase 1 only
|
||||
introduces this type.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gps_denied.schemas import GPSPoint
|
||||
|
||||
|
||||
# Forward import: TrackingState lives in core/processor.py today and is
|
||||
# moved here only as a type annotation. We avoid a hard import at module
|
||||
# level so that core/processor.py remains the source of truth in Phase 1.
|
||||
# Plan 07 will pull TrackingState into hot_types proper.
|
||||
TrackingState = "TrackingState" # type: ignore[assignment]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FrameState:
|
||||
"""Intermediate result of processing a single frame (mutable).
|
||||
|
||||
Mirrors stage1's `core.processor.FrameResult` field set; default
|
||||
values let `FrameState(frame_id=N)` reconstruct the existing
|
||||
`FrameResult(frame_id)` semantics.
|
||||
"""
|
||||
|
||||
frame_id: int = 0
|
||||
gps: "Optional[GPSPoint]" = None
|
||||
confidence: float = 0.0
|
||||
tracking_state: str = "normal" # mirrors TrackingState.NORMAL string value
|
||||
vo_success: bool = False
|
||||
alignment_success: bool = False
|
||||
@@ -0,0 +1,25 @@
|
||||
"""IMUSample hot-path dataclass (ARCH-02).
|
||||
|
||||
Renamed from `IMUMeasurement` (Pydantic) per ARCH-02 canonical-name list.
|
||||
The legacy name is preserved as an alias from the schemas/eskf.py shim
|
||||
(`IMUMeasurement = IMUSample`), so existing import sites continue to work.
|
||||
|
||||
eq=False because numpy arrays in `__eq__` would raise; Pydantic was already
|
||||
incomparable for this model (arbitrary_types_allowed), so dataclass behavior
|
||||
matches existing semantics.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True, eq=False)
|
||||
class IMUSample:
|
||||
"""Single IMU reading from flight controller."""
|
||||
|
||||
accel: np.ndarray # (3,) m/s^2 in body frame
|
||||
gyro: np.ndarray # (3,) rad/s in body frame
|
||||
timestamp: float # seconds since epoch
|
||||
@@ -0,0 +1,25 @@
|
||||
"""PositionEstimate hot-path dataclass (ARCH-02).
|
||||
|
||||
NEW in Stage 2 — no stage1 analog. Phase 3 (SAFE-01..03) populates the
|
||||
Optional `source_label` and `anchor_age_ms` fields; Phase 1 only declares
|
||||
them so downstream code can be written against the final type.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class PositionEstimate:
|
||||
"""Unified per-frame position estimate emitted by the pipeline."""
|
||||
|
||||
lat: float
|
||||
lon: float
|
||||
alt: float
|
||||
timestamp: float
|
||||
confidence: float
|
||||
covariance_semimajor_m: float = 0.0
|
||||
source_label: Optional[str] = None # filled in Phase 3 (SAFE)
|
||||
anchor_age_ms: Optional[float] = None # filled in Phase 3 (SAFE)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""RotationResult hot-path dataclass (ARCH-02).
|
||||
|
||||
eq=False because the optional `homography` is a numpy array.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True, eq=False)
|
||||
class RotationResult:
|
||||
"""Result of a rotation sweep alignment."""
|
||||
|
||||
matched: bool
|
||||
initial_angle: float
|
||||
precise_angle: float
|
||||
confidence: float
|
||||
homography: Optional[np.ndarray] = None
|
||||
inlier_count: int = 0
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Satellite-anchor hot-path dataclasses (ARCH-02).
|
||||
|
||||
TileCoords / TileBounds are hot during tile selection (per-frame).
|
||||
SatelliteAnchor is a Phase-1 placeholder — Phase 3 (VERIFY) fills its
|
||||
semantics. Phase 1 only requires it to exist for ARCH-02.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from gps_denied.schemas import GPSPoint
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class TileCoords:
|
||||
"""Web Mercator tile coordinates."""
|
||||
|
||||
x: int
|
||||
y: int
|
||||
zoom: int
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class TileBounds:
|
||||
"""GPS boundaries of a tile."""
|
||||
|
||||
nw: GPSPoint
|
||||
ne: GPSPoint
|
||||
sw: GPSPoint
|
||||
se: GPSPoint
|
||||
center: GPSPoint
|
||||
gsd: float # Ground Sampling Distance (meters/pixel)
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class SatelliteAnchor:
|
||||
"""Placeholder for Phase-3 verified satellite anchor record (ARCH-02).
|
||||
|
||||
Phase 1 declaration only — populated by Phase 3 (VERIFY). Carries the
|
||||
minimum fields required for the ARCH-02 type-surface to exist.
|
||||
"""
|
||||
|
||||
gps_center: GPSPoint
|
||||
timestamp: float
|
||||
matched_inlier_count: int = 0
|
||||
covariance_semimajor_m: float = 0.0
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Visual-odometry hot-path dataclasses (ARCH-02).
|
||||
|
||||
Includes Features, Matches, RelativePose, Motion. `VOEstimate` is a
|
||||
module-level alias of `RelativePose` per ARCH-02 canonical-name list —
|
||||
the existing impl returns RelativePose; VOEstimate is the protocol-level
|
||||
name.
|
||||
|
||||
eq=False on every dataclass that carries np.ndarray fields, matching
|
||||
Pydantic's prior incomparability under arbitrary_types_allowed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True, eq=False)
|
||||
class Features:
|
||||
"""Extracted image features (e.g., from SuperPoint)."""
|
||||
|
||||
keypoints: np.ndarray # (N, 2)
|
||||
descriptors: np.ndarray # (N, 256)
|
||||
scores: np.ndarray # (N,)
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True, eq=False)
|
||||
class Matches:
|
||||
"""Matches between two sets of features (e.g., from LightGlue)."""
|
||||
|
||||
matches: np.ndarray # (M, 2)
|
||||
scores: np.ndarray # (M,)
|
||||
keypoints1: np.ndarray # (M, 2)
|
||||
keypoints2: np.ndarray # (M, 2)
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True, eq=False)
|
||||
class RelativePose:
|
||||
"""Relative pose between two frames."""
|
||||
|
||||
translation: np.ndarray # (3,)
|
||||
rotation: np.ndarray # (3, 3)
|
||||
confidence: float
|
||||
inlier_count: int
|
||||
total_matches: int
|
||||
tracking_good: bool
|
||||
scale_ambiguous: bool = True
|
||||
chunk_id: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True, eq=False)
|
||||
class Motion:
|
||||
"""Motion estimate from OpenCV."""
|
||||
|
||||
translation: np.ndarray # (3,) unit vector
|
||||
rotation: np.ndarray # (3, 3) rotation matrix
|
||||
inliers: np.ndarray # Boolean mask of inliers
|
||||
inlier_count: int
|
||||
|
||||
|
||||
# ARCH-02 canonical name — VOEstimate IS the relative pose returned by VIO.
|
||||
VOEstimate = RelativePose
|
||||
Reference in New Issue
Block a user