diff --git a/src/gps_denied/hot_types/__init__.py b/src/gps_denied/hot_types/__init__.py new file mode 100644 index 0000000..1c87d47 --- /dev/null +++ b/src/gps_denied/hot_types/__init__.py @@ -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", +] diff --git a/src/gps_denied/hot_types/alignment_result.py b/src/gps_denied/hot_types/alignment_result.py new file mode 100644 index 0000000..cfb6478 --- /dev/null +++ b/src/gps_denied/hot_types/alignment_result.py @@ -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 diff --git a/src/gps_denied/hot_types/eskf_state.py b/src/gps_denied/hot_types/eskf_state.py new file mode 100644 index 0000000..1551250 --- /dev/null +++ b/src/gps_denied/hot_types/eskf_state.py @@ -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 diff --git a/src/gps_denied/hot_types/frame_state.py b/src/gps_denied/hot_types/frame_state.py new file mode 100644 index 0000000..6fca5da --- /dev/null +++ b/src/gps_denied/hot_types/frame_state.py @@ -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 diff --git a/src/gps_denied/hot_types/imu_sample.py b/src/gps_denied/hot_types/imu_sample.py new file mode 100644 index 0000000..31cba5a --- /dev/null +++ b/src/gps_denied/hot_types/imu_sample.py @@ -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 diff --git a/src/gps_denied/hot_types/position_estimate.py b/src/gps_denied/hot_types/position_estimate.py new file mode 100644 index 0000000..bfb7e36 --- /dev/null +++ b/src/gps_denied/hot_types/position_estimate.py @@ -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) diff --git a/src/gps_denied/hot_types/rotation_result.py b/src/gps_denied/hot_types/rotation_result.py new file mode 100644 index 0000000..77aed45 --- /dev/null +++ b/src/gps_denied/hot_types/rotation_result.py @@ -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 diff --git a/src/gps_denied/hot_types/satellite_anchor.py b/src/gps_denied/hot_types/satellite_anchor.py new file mode 100644 index 0000000..0effa90 --- /dev/null +++ b/src/gps_denied/hot_types/satellite_anchor.py @@ -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 diff --git a/src/gps_denied/hot_types/vo_estimate.py b/src/gps_denied/hot_types/vo_estimate.py new file mode 100644 index 0000000..b4656ba --- /dev/null +++ b/src/gps_denied/hot_types/vo_estimate.py @@ -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