mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-23 06:11:12 +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