mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 01:01:12 +00:00
[AZ-220] Add shared runtime contract models
Implement the shared DTO contract surface with validation so runtime components consume one public model set instead of duplicating shapes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,3 +1,28 @@
|
||||
"""Shared DTO and interface contract namespace."""
|
||||
|
||||
CONTRACT_VERSION = "0.1.0"
|
||||
from shared.contracts.models import (
|
||||
AnchorDecision,
|
||||
CacheTileRecord,
|
||||
FdrEvent,
|
||||
FramePacket,
|
||||
PositionEstimate,
|
||||
RuntimeContractModel,
|
||||
TelemetrySample,
|
||||
VioStatePacket,
|
||||
VprCandidate,
|
||||
)
|
||||
|
||||
CONTRACT_VERSION = "1.0.0"
|
||||
|
||||
__all__ = [
|
||||
"AnchorDecision",
|
||||
"CONTRACT_VERSION",
|
||||
"CacheTileRecord",
|
||||
"FdrEvent",
|
||||
"FramePacket",
|
||||
"PositionEstimate",
|
||||
"RuntimeContractModel",
|
||||
"TelemetrySample",
|
||||
"VioStatePacket",
|
||||
"VprCandidate",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Shared runtime DTO contracts.
|
||||
|
||||
These models intentionally carry only cross-component shape and validation rules.
|
||||
Component algorithms and storage choices stay in their owning packages.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, NonNegativeFloat, NonNegativeInt, PositiveFloat
|
||||
from pydantic import model_validator
|
||||
|
||||
|
||||
class RuntimeContractModel(BaseModel):
|
||||
"""Base settings for public runtime contracts."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
|
||||
|
||||
class FramePacket(RuntimeContractModel):
|
||||
frame_id: str = Field(min_length=1)
|
||||
timestamp_ns: NonNegativeInt
|
||||
image_ref: str = Field(min_length=1)
|
||||
calibration_id: str = Field(min_length=1)
|
||||
occlusion: Literal["clear", "partial", "total", "unreadable"]
|
||||
quality: float = Field(ge=0.0, le=1.0)
|
||||
normalization_hint: str | None = None
|
||||
raw_frame_retained: bool = False
|
||||
|
||||
@model_validator(mode="after")
|
||||
def raw_frames_must_not_be_retained(self) -> "FramePacket":
|
||||
if self.raw_frame_retained:
|
||||
raise ValueError("raw frame payloads must be referenced, not retained")
|
||||
return self
|
||||
|
||||
|
||||
class TelemetrySample(RuntimeContractModel):
|
||||
timestamp_ns: NonNegativeInt
|
||||
imu: dict[str, float]
|
||||
attitude: dict[str, float]
|
||||
altitude_m: float
|
||||
airspeed_mps: NonNegativeFloat
|
||||
gps_health: Literal["healthy", "degraded", "lost", "spoofed"]
|
||||
|
||||
|
||||
class VioStatePacket(RuntimeContractModel):
|
||||
timestamp_ns: NonNegativeInt
|
||||
relative_pose: dict[str, float]
|
||||
velocity_mps: tuple[float, float, float]
|
||||
bias_estimate: dict[str, float] | None = None
|
||||
tracking_quality: float = Field(ge=0.0, le=1.0)
|
||||
covariance_hint: list[list[float]] | None = None
|
||||
|
||||
|
||||
class PositionEstimate(RuntimeContractModel):
|
||||
timestamp_ns: NonNegativeInt
|
||||
latitude_deg: float = Field(ge=-90.0, le=90.0)
|
||||
longitude_deg: float = Field(ge=-180.0, le=180.0)
|
||||
altitude_m: float
|
||||
covariance_semimajor_m: NonNegativeFloat
|
||||
source_label: Literal["satellite_anchored", "vo_extrapolated", "dead_reckoned", "no_fix"]
|
||||
fix_type: int = Field(ge=0, le=3)
|
||||
horizontal_accuracy_m: NonNegativeFloat
|
||||
anchor_age_ms: NonNegativeInt
|
||||
|
||||
@model_validator(mode="after")
|
||||
def accuracy_must_not_under_report_covariance(self) -> "PositionEstimate":
|
||||
if self.horizontal_accuracy_m < self.covariance_semimajor_m:
|
||||
raise ValueError("horizontal_accuracy_m must not under-report covariance_semimajor_m")
|
||||
return self
|
||||
|
||||
|
||||
class VprCandidate(RuntimeContractModel):
|
||||
chunk_id: str = Field(min_length=1)
|
||||
tile_id: str = Field(min_length=1)
|
||||
score: float = Field(ge=0.0, le=1.0)
|
||||
footprint: dict[str, float]
|
||||
freshness_status: Literal["fresh", "stale", "rejected"]
|
||||
|
||||
|
||||
class AnchorDecision(RuntimeContractModel):
|
||||
candidate_id: str = Field(min_length=1)
|
||||
accepted: bool
|
||||
estimated_pose: dict[str, float] | None = None
|
||||
inliers: NonNegativeInt
|
||||
mean_reprojection_error_px: NonNegativeFloat
|
||||
rejection_reason: str | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def accepted_anchors_require_pose(self) -> "AnchorDecision":
|
||||
if self.accepted and self.estimated_pose is None:
|
||||
raise ValueError("accepted anchor decisions require estimated_pose")
|
||||
if self.accepted and self.rejection_reason is not None:
|
||||
raise ValueError("accepted anchor decisions must not include rejection_reason")
|
||||
return self
|
||||
|
||||
|
||||
class CacheTileRecord(RuntimeContractModel):
|
||||
tile_id: str = Field(min_length=1)
|
||||
crs: str = Field(min_length=1)
|
||||
meters_per_pixel: PositiveFloat
|
||||
capture_date: date
|
||||
signature_hash: str = Field(min_length=1)
|
||||
trust_level: Literal["trusted", "generated", "quarantined", "rejected"]
|
||||
freshness_status: Literal["fresh", "stale", "rejected"]
|
||||
provenance: str = Field(min_length=1)
|
||||
|
||||
|
||||
class FdrEvent(RuntimeContractModel):
|
||||
event_type: str = Field(min_length=1)
|
||||
timestamp_ns: NonNegativeInt
|
||||
component: str = Field(min_length=1)
|
||||
severity: Literal["debug", "info", "warning", "error", "critical"]
|
||||
payload_ref: str = Field(min_length=1)
|
||||
mission_id: str = Field(min_length=1)
|
||||
run_id: str = Field(min_length=1)
|
||||
Reference in New Issue
Block a user