refactor(01-01): convert hot-path schemas/*.py to hot_types re-export shims

- schemas/eskf.py: keep ConfidenceTier + ESKFConfig; re-export IMUSample
  and ESKFState from hot_types (define ConfidenceTier BEFORE the
  hot_types imports to avoid circular import — eskf_state.py imports
  ConfidenceTier from this module). Legacy alias IMUMeasurement = IMUSample.
- schemas/vo.py: re-export Features, Matches, RelativePose, Motion,
  VOEstimate from hot_types.vo_estimate.
- schemas/satellite.py: re-export TileCoords, TileBounds, SatelliteAnchor.
- schemas/metric.py: keep LiteSAMConfig; re-export AlignmentResult,
  ChunkAlignmentResult, Sim3Transform.
- schemas/rotation.py: keep HeadingHistory + RotationConfig; re-export
  RotationResult.

Auto-fixes (Rules 1 + 3) needed to keep the 216-test floor green:
- core/rotation.py: refactor try_rotation_steps to use
  dataclasses.replace instead of attribute assignment on RotationResult
  (Rule 1 — frozen dataclass forbids mutation; Pydantic silently allowed
  it). PATTERNS.md §6.1 already flagged Pose mutation but missed this site.
- hot_types/vo_estimate.py: add Optional `covariance: np.ndarray` field
  to RelativePose (Rule 3 — five test sites construct RelativePose with
  `covariance=np.eye(6)`; Pydantic v2 silently accepted the extra kwarg
  via default `extra="ignore"`. Declaring the field preserves the
  construction contract under the dataclass migration without editing
  tests).

Verification: pytest tests/ -q --ignore=tests/e2e → 216 passed, 8 skipped
(matches baseline). Accuracy bench (23 tests) passes.
This commit is contained in:
Yuzviak
2026-05-10 22:47:56 +03:00
parent b86ec90066
commit f67c5f3cd0
7 changed files with 110 additions and 147 deletions
+8 -2
View File
@@ -1,5 +1,6 @@
"""Image Rotation Manager (Component F06)."""
import dataclasses
import math
from abc import ABC, abstractmethod
from datetime import datetime
@@ -77,8 +78,13 @@ class ImageRotationManager:
if result.matched:
precise_angle = self.calculate_precise_angle(result.homography, float(angle))
result.precise_angle = precise_angle
result.initial_angle = float(angle)
# RotationResult is now a frozen dataclass (ARCH-02 / Plan 01-01);
# use `dataclasses.replace` instead of attribute assignment.
result = dataclasses.replace(
result,
precise_angle=precise_angle,
initial_angle=float(angle),
)
self.update_heading(flight_id, frame_id, precise_angle, timestamp)
return result
+10 -1
View File
@@ -38,7 +38,15 @@ class Matches:
@dataclass(slots=True, frozen=True, eq=False)
class RelativePose:
"""Relative pose between two frames."""
"""Relative pose between two frames.
Note: `covariance` is included as an optional 6×6 SE(3) uncertainty
matrix. The legacy Pydantic model did not declare this field but
silently accepted `covariance=...` kwargs (Pydantic v2 default
`extra="ignore"` behavior). Several stage1 tests rely on that
construction signature; declaring the field here preserves the
contract under the dataclass migration without editing tests.
"""
translation: np.ndarray # (3,)
rotation: np.ndarray # (3, 3)
@@ -48,6 +56,7 @@ class RelativePose:
tracking_good: bool
scale_ambiguous: bool = True
chunk_id: Optional[str] = None
covariance: Optional[np.ndarray] = None # (6, 6) SE(3) covariance — optional
@dataclass(slots=True, frozen=True, eq=False)
+28 -25
View File
@@ -1,9 +1,16 @@
"""Error-State Kalman Filter schemas."""
"""Error-State Kalman Filter schemas.
Phase 1 shim — hot-path types `IMUSample` (legacy: `IMUMeasurement`) and
`ESKFState` live in `gps_denied.hot_types`. `ConfidenceTier` (enum) and
`ESKFConfig` (Pydantic config) stay here as boundary types.
`ConfidenceTier` is defined BEFORE the hot_types re-imports because
`hot_types.eskf_state` imports `ConfidenceTier` from this module — load
order matters to avoid a circular import.
"""
from enum import Enum
from typing import Optional
import numpy as np
from pydantic import BaseModel
@@ -15,15 +22,6 @@ class ConfidenceTier(str, Enum):
FAILED = "FAILED" # 3+ consecutive total failures
class IMUMeasurement(BaseModel):
"""Single IMU reading from flight controller."""
model_config = {"arbitrary_types_allowed": True}
accel: np.ndarray # (3,) m/s^2 in body frame
gyro: np.ndarray # (3,) rad/s in body frame
timestamp: float # seconds since epoch
class ESKFConfig(BaseModel):
"""ESKF tuning parameters."""
@@ -55,17 +53,22 @@ class ESKFConfig(BaseModel):
mahalanobis_threshold: float = 16.27 # chi2(3, 0.99999) ≈ 5-sigma gate
class ESKFState(BaseModel):
"""Full ESKF nominal state snapshot."""
model_config = {"arbitrary_types_allowed": True}
# Hot-path types — re-exported from gps_denied.hot_types (Plan 01-01).
# Tests and existing consumers continue to import from this path; the
# underlying type changed from a Pydantic BaseModel to a frozen dataclass.
# These imports MUST come AFTER `ConfidenceTier` is defined above —
# `hot_types.eskf_state` imports `ConfidenceTier` from this module.
from gps_denied.hot_types.eskf_state import ESKFState # noqa: E402, F401
from gps_denied.hot_types.imu_sample import IMUSample # noqa: E402, F401
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
# Legacy alias preserved until Phase 2 test taxonomy reshuffle.
IMUMeasurement = IMUSample
__all__ = [
"ConfidenceTier",
"ESKFConfig",
"ESKFState",
"IMUMeasurement",
"IMUSample",
]
+18 -39
View File
@@ -1,46 +1,17 @@
"""Metric Refinement schemas (Component F09)."""
"""Metric Refinement schemas (Component F09).
Phase 1 shim — hot-path types `AlignmentResult`, `ChunkAlignmentResult`,
`Sim3Transform` live in `gps_denied.hot_types.alignment_result`.
`LiteSAMConfig` (config) stays here as a Pydantic boundary type.
"""
import numpy as np
from pydantic import BaseModel
from gps_denied.schemas import GPSPoint
class AlignmentResult(BaseModel):
"""Result of aligning a UAV image to a single satellite tile."""
model_config = {"arbitrary_types_allowed": True}
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
class Sim3Transform(BaseModel):
"""Sim(3) transformation: scale, rotation, translation."""
model_config = {"arbitrary_types_allowed": True}
translation: np.ndarray # (3,)
rotation: np.ndarray # (3, 3) rotation matrix
scale: float
class ChunkAlignmentResult(BaseModel):
"""Result of aligning a chunk array of UAV images to a satellite tile."""
model_config = {"arbitrary_types_allowed": True}
matched: bool
chunk_id: str
chunk_center_gps: GPSPoint
rotation_angle: float
confidence: float
inlier_count: int
transform: Sim3Transform
reprojection_error: float
from gps_denied.hot_types.alignment_result import ( # noqa: F401
AlignmentResult,
ChunkAlignmentResult,
Sim3Transform,
)
class LiteSAMConfig(BaseModel):
@@ -51,3 +22,11 @@ class LiteSAMConfig(BaseModel):
max_reprojection_error: float = 2.0 # pixels
multi_scale_levels: int = 3
chunk_min_inliers: int = 30
__all__ = [
"AlignmentResult",
"ChunkAlignmentResult",
"LiteSAMConfig",
"Sim3Transform",
]
+14 -15
View File
@@ -1,24 +1,16 @@
"""Rotation schemas (Component F06)."""
"""Rotation schemas (Component F06).
Phase 1 shim — hot-path `RotationResult` lives in
`gps_denied.hot_types.rotation_result`. `HeadingHistory` (mutable
bookkeeping) and `RotationConfig` (config) stay here as Pydantic.
"""
from datetime import datetime
from typing import Optional
import numpy as np
from pydantic import BaseModel
class RotationResult(BaseModel):
"""Result of a rotation sweep alignment."""
matched: bool
initial_angle: float
precise_angle: float
confidence: float
# We will exclude np.ndarray from BaseModel to avoid validation issues,
# but store it as an attribute if needed or use arbitrary_types_allowed.
model_config = {"arbitrary_types_allowed": True}
homography: Optional[np.ndarray] = None
inlier_count: int = 0
from gps_denied.hot_types.rotation_result import RotationResult # noqa: F401
class HeadingHistory(BaseModel):
@@ -36,3 +28,10 @@ class RotationConfig(BaseModel):
sharp_turn_threshold: float = 45.0
confidence_threshold: float = 0.7
history_size: int = 10
__all__ = [
"HeadingHistory",
"RotationConfig",
"RotationResult",
]
+14 -19
View File
@@ -1,22 +1,17 @@
"""Satellite domain schemas."""
"""Satellite domain schemas.
from pydantic import BaseModel
Phase 1 shim — `TileCoords`, `TileBounds`, and the Phase-3 placeholder
`SatelliteAnchor` live in `gps_denied.hot_types.satellite_anchor`.
"""
from gps_denied.schemas import GPSPoint
from gps_denied.hot_types.satellite_anchor import ( # noqa: F401
SatelliteAnchor,
TileBounds,
TileCoords,
)
class TileCoords(BaseModel):
"""Web Mercator tile coordinates."""
x: int
y: int
zoom: int
class TileBounds(BaseModel):
"""GPS boundaries of a tile."""
nw: GPSPoint
ne: GPSPoint
sw: GPSPoint
se: GPSPoint
center: GPSPoint
gsd: float # Ground Sampling Distance (meters/pixel)
__all__ = [
"SatelliteAnchor",
"TileBounds",
"TileCoords",
]
+18 -46
View File
@@ -1,49 +1,21 @@
"""Sequential Visual Odometry schemas (Component F07)."""
"""Sequential Visual Odometry schemas (Component F07).
from typing import Optional
Phase 1 shim — `Features`, `Matches`, `RelativePose`, `Motion` and the
ARCH-02 alias `VOEstimate` live in `gps_denied.hot_types.vo_estimate`.
"""
import numpy as np
from pydantic import BaseModel
from gps_denied.hot_types.vo_estimate import ( # noqa: F401
Features,
Matches,
Motion,
RelativePose,
VOEstimate,
)
class Features(BaseModel):
"""Extracted image features (e.g., from SuperPoint)."""
model_config = {"arbitrary_types_allowed": True}
keypoints: np.ndarray # (N, 2)
descriptors: np.ndarray # (N, 256)
scores: np.ndarray # (N,)
class Matches(BaseModel):
"""Matches between two sets of features (e.g., from LightGlue)."""
model_config = {"arbitrary_types_allowed": True}
matches: np.ndarray # (M, 2)
scores: np.ndarray # (M,)
keypoints1: np.ndarray # (M, 2)
keypoints2: np.ndarray # (M, 2)
class RelativePose(BaseModel):
"""Relative pose between two frames."""
model_config = {"arbitrary_types_allowed": True}
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
class Motion(BaseModel):
"""Motion estimate from OpenCV."""
model_config = {"arbitrary_types_allowed": True}
translation: np.ndarray # (3,) unit vector
rotation: np.ndarray # (3, 3) rotation matrix
inliers: np.ndarray # Boolean mask of inliers
inlier_count: int
__all__ = [
"Features",
"Matches",
"Motion",
"RelativePose",
"VOEstimate",
]