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:
Yuzviak
2026-05-10 22:43:35 +03:00
parent 8045efee5f
commit b86ec90066
9 changed files with 383 additions and 0 deletions
+66
View File
@@ -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
+34
View File
@@ -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
+45
View File
@@ -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
+25
View File
@@ -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
+64
View File
@@ -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