mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 10:11:12 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user