mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 20:01: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)."""
|
"""Image Rotation Manager (Component F06)."""
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
import math
|
import math
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -77,8 +78,13 @@ class ImageRotationManager:
|
|||||||
|
|
||||||
if result.matched:
|
if result.matched:
|
||||||
precise_angle = self.calculate_precise_angle(result.homography, float(angle))
|
precise_angle = self.calculate_precise_angle(result.homography, float(angle))
|
||||||
result.precise_angle = precise_angle
|
# RotationResult is now a frozen dataclass (ARCH-02 / Plan 01-01);
|
||||||
result.initial_angle = float(angle)
|
# 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)
|
self.update_heading(flight_id, frame_id, precise_angle, timestamp)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -38,7 +38,15 @@ class Matches:
|
|||||||
|
|
||||||
@dataclass(slots=True, frozen=True, eq=False)
|
@dataclass(slots=True, frozen=True, eq=False)
|
||||||
class RelativePose:
|
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,)
|
translation: np.ndarray # (3,)
|
||||||
rotation: np.ndarray # (3, 3)
|
rotation: np.ndarray # (3, 3)
|
||||||
@@ -48,6 +56,7 @@ class RelativePose:
|
|||||||
tracking_good: bool
|
tracking_good: bool
|
||||||
scale_ambiguous: bool = True
|
scale_ambiguous: bool = True
|
||||||
chunk_id: Optional[str] = None
|
chunk_id: Optional[str] = None
|
||||||
|
covariance: Optional[np.ndarray] = None # (6, 6) SE(3) covariance — optional
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True, eq=False)
|
@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 enum import Enum
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
@@ -15,15 +22,6 @@ class ConfidenceTier(str, Enum):
|
|||||||
FAILED = "FAILED" # 3+ consecutive total failures
|
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):
|
class ESKFConfig(BaseModel):
|
||||||
"""ESKF tuning parameters."""
|
"""ESKF tuning parameters."""
|
||||||
|
|
||||||
@@ -55,17 +53,22 @@ class ESKFConfig(BaseModel):
|
|||||||
mahalanobis_threshold: float = 16.27 # chi2(3, 0.99999) ≈ 5-sigma gate
|
mahalanobis_threshold: float = 16.27 # chi2(3, 0.99999) ≈ 5-sigma gate
|
||||||
|
|
||||||
|
|
||||||
class ESKFState(BaseModel):
|
# Hot-path types — re-exported from gps_denied.hot_types (Plan 01-01).
|
||||||
"""Full ESKF nominal state snapshot."""
|
# Tests and existing consumers continue to import from this path; the
|
||||||
model_config = {"arbitrary_types_allowed": True}
|
# 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)
|
# Legacy alias preserved until Phase 2 test taxonomy reshuffle.
|
||||||
velocity: np.ndarray # (3,) ENU m/s
|
IMUMeasurement = IMUSample
|
||||||
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
|
__all__ = [
|
||||||
covariance: np.ndarray # (15, 15)
|
"ConfidenceTier",
|
||||||
timestamp: float # seconds since epoch
|
"ESKFConfig",
|
||||||
confidence: ConfidenceTier
|
"ESKFState",
|
||||||
last_satellite_time: Optional[float] = None
|
"IMUMeasurement",
|
||||||
last_vo_time: Optional[float] = None
|
"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 pydantic import BaseModel
|
||||||
|
|
||||||
from gps_denied.schemas import GPSPoint
|
from gps_denied.hot_types.alignment_result import ( # noqa: F401
|
||||||
|
AlignmentResult,
|
||||||
|
ChunkAlignmentResult,
|
||||||
class AlignmentResult(BaseModel):
|
Sim3Transform,
|
||||||
"""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
|
|
||||||
|
|
||||||
|
|
||||||
class LiteSAMConfig(BaseModel):
|
class LiteSAMConfig(BaseModel):
|
||||||
@@ -51,3 +22,11 @@ class LiteSAMConfig(BaseModel):
|
|||||||
max_reprojection_error: float = 2.0 # pixels
|
max_reprojection_error: float = 2.0 # pixels
|
||||||
multi_scale_levels: int = 3
|
multi_scale_levels: int = 3
|
||||||
chunk_min_inliers: int = 30
|
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 datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from gps_denied.hot_types.rotation_result import RotationResult # noqa: F401
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class HeadingHistory(BaseModel):
|
class HeadingHistory(BaseModel):
|
||||||
@@ -36,3 +28,10 @@ class RotationConfig(BaseModel):
|
|||||||
sharp_turn_threshold: float = 45.0
|
sharp_turn_threshold: float = 45.0
|
||||||
confidence_threshold: float = 0.7
|
confidence_threshold: float = 0.7
|
||||||
history_size: int = 10
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
class TileCoords(BaseModel):
|
"SatelliteAnchor",
|
||||||
"""Web Mercator tile coordinates."""
|
"TileBounds",
|
||||||
x: int
|
"TileCoords",
|
||||||
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)
|
|
||||||
|
|||||||
@@ -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 gps_denied.hot_types.vo_estimate import ( # noqa: F401
|
||||||
from pydantic import BaseModel
|
Features,
|
||||||
|
Matches,
|
||||||
|
Motion,
|
||||||
|
RelativePose,
|
||||||
|
VOEstimate,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
class Features(BaseModel):
|
"Features",
|
||||||
"""Extracted image features (e.g., from SuperPoint)."""
|
"Matches",
|
||||||
model_config = {"arbitrary_types_allowed": True}
|
"Motion",
|
||||||
|
"RelativePose",
|
||||||
keypoints: np.ndarray # (N, 2)
|
"VOEstimate",
|
||||||
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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user