start over again

This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-07 04:08:03 +03:00
parent ee6606a9c2
commit 8382cdae10
351 changed files with 0 additions and 30337 deletions
-1
View File
@@ -1 +0,0 @@
"""Source-root package marker for editable installs."""
-33
View File
@@ -1,33 +0,0 @@
"""Anchor verification component."""
from .interfaces import (
AnchorVerifier,
FeatureMatcher,
GeometryGatedAnchorVerifier,
KeypointRansacMatcher,
)
from .types import (
AnchorFrame,
AnchorVerificationResult,
CandidateTile,
GeometryGateConfig,
MatchEvidence,
MatcherBenchmarkReport,
MatcherBenchmarkResult,
MatcherProfile,
)
__all__ = [
"AnchorFrame",
"AnchorVerificationResult",
"AnchorVerifier",
"CandidateTile",
"FeatureMatcher",
"GeometryGateConfig",
"GeometryGatedAnchorVerifier",
"KeypointRansacMatcher",
"MatchEvidence",
"MatcherBenchmarkReport",
"MatcherBenchmarkResult",
"MatcherProfile",
]
-202
View File
@@ -1,202 +0,0 @@
"""Public anchor verification interfaces."""
from statistics import median
from time import perf_counter
from typing import Protocol
from shared.contracts import AnchorDecision
from .types import (
AnchorFrame,
AnchorVerificationResult,
CandidateTile,
GeometryGateConfig,
MatchEvidence,
MatcherBenchmarkReport,
MatcherBenchmarkResult,
MatcherProfile,
)
class AnchorVerifier(Protocol):
"""Verifies retrieved candidates against camera observations."""
def verify(self, frame: AnchorFrame, evidence: MatchEvidence) -> AnchorVerificationResult:
"""Return an anchor decision for one candidate."""
class FeatureMatcher(Protocol):
"""Computes correspondence evidence from local frame and tile inputs."""
def compute(
self,
frame: AnchorFrame,
tile: CandidateTile,
matcher_profile: MatcherProfile,
) -> MatchEvidence:
"""Return matcher and geometry evidence for one candidate tile."""
class KeypointRansacMatcher:
"""Small CPU matcher for keypoint fixtures and dependency-gated runs."""
def __init__(self, inlier_threshold_px: float = 2.0) -> None:
self._inlier_threshold_px = inlier_threshold_px
def compute(
self,
frame: AnchorFrame,
tile: CandidateTile,
matcher_profile: MatcherProfile,
) -> MatchEvidence:
started = perf_counter()
correspondences = tuple(zip(frame.keypoints, tile.keypoints))
if len(correspondences) < 4:
return MatchEvidence(
candidate=tile.candidate,
matcher_profile=matcher_profile,
inliers=0,
mean_reprojection_error_px=self._inlier_threshold_px + 1.0,
homography=None,
runtime_ms=(perf_counter() - started) * 1000.0,
provenance_trusted=tile.provenance_trusted,
evidence_source="computed_geometry",
)
dx_values = tuple(tile_point[0] - frame_point[0] for frame_point, tile_point in correspondences)
dy_values = tuple(tile_point[1] - frame_point[1] for frame_point, tile_point in correspondences)
dx = median(dx_values)
dy = median(dy_values)
residuals = tuple(
((frame_point[0] + dx - tile_point[0]) ** 2 + (frame_point[1] + dy - tile_point[1]) ** 2)
** 0.5
for frame_point, tile_point in correspondences
)
inlier_residuals = tuple(
residual for residual in residuals if residual <= self._inlier_threshold_px
)
mean_error = (
sum(inlier_residuals) / len(inlier_residuals)
if inlier_residuals
else self._inlier_threshold_px + 1.0
)
homography = (
{
"h00": 1.0,
"h01": 0.0,
"h02": dx,
"h10": 0.0,
"h11": 1.0,
"h12": dy,
"h20": 0.0,
"h21": 0.0,
"h22": 1.0,
}
if inlier_residuals
else None
)
return MatchEvidence(
candidate=tile.candidate,
matcher_profile=matcher_profile,
inliers=len(inlier_residuals),
mean_reprojection_error_px=mean_error,
homography=homography,
runtime_ms=(perf_counter() - started) * 1000.0,
provenance_trusted=tile.provenance_trusted,
evidence_source="computed_geometry",
)
class GeometryGatedAnchorVerifier:
"""Converts matcher evidence into accepted/rejected anchor decisions."""
def __init__(
self,
gates: GeometryGateConfig | None = None,
matcher: FeatureMatcher | None = None,
) -> None:
self._gates = gates or GeometryGateConfig()
self._matcher = matcher or KeypointRansacMatcher()
def verify(self, frame: AnchorFrame, evidence: MatchEvidence) -> AnchorVerificationResult:
accepted, reason = self._classify(frame, evidence)
decision = AnchorDecision(
candidate_id=evidence.candidate.chunk_id,
accepted=accepted,
estimated_pose=self._estimated_pose(evidence) if accepted else None,
inliers=evidence.inliers,
mean_reprojection_error_px=evidence.mean_reprojection_error_px,
rejection_reason=None if accepted else reason,
)
return AnchorVerificationResult(
decision=decision,
matcher_profile=evidence.matcher_profile,
reason=reason,
homography=evidence.homography,
freshness_status=evidence.candidate.freshness_status,
)
def verify_candidate(
self,
frame: AnchorFrame,
tile: CandidateTile,
matcher_profile: MatcherProfile = "sift_orb",
) -> AnchorVerificationResult:
evidence = self._matcher.compute(frame, tile, matcher_profile)
return self.verify(frame, evidence)
def benchmark(
self, frame: AnchorFrame, evidences: tuple[MatchEvidence, ...]
) -> MatcherBenchmarkReport:
results: list[MatcherBenchmarkResult] = []
for evidence in evidences:
verification = self.verify(frame, evidence)
results.append(
MatcherBenchmarkResult(
matcher_profile=evidence.matcher_profile,
runtime_ms=evidence.runtime_ms,
inliers=evidence.inliers,
mean_reprojection_error_px=evidence.mean_reprojection_error_px,
accepted=verification.decision.accepted,
reason=verification.reason,
)
)
return MatcherBenchmarkReport(
results=tuple(results),
)
def benchmark_candidates(
self,
frame: AnchorFrame,
tiles: tuple[CandidateTile, ...],
matcher_profile: MatcherProfile = "sift_orb",
) -> MatcherBenchmarkReport:
return self.benchmark(
frame,
tuple(self._matcher.compute(frame, tile, matcher_profile) for tile in tiles),
)
def _classify(self, frame: AnchorFrame, evidence: MatchEvidence) -> tuple[bool, str]:
if not frame.usable_for_anchor:
return False, "frame_not_usable"
if evidence.candidate.freshness_status != "fresh" or not evidence.provenance_trusted:
return False, "stale_or_untrusted_provenance"
if evidence.homography is None:
return False, "geometry_failure"
if evidence.inliers < self._gates.min_inliers:
return False, "low_inliers"
if evidence.mean_reprojection_error_px > self._gates.max_mean_reprojection_error_px:
return False, "high_mre"
return True, "accepted_geometry"
def _estimated_pose(self, evidence: MatchEvidence) -> dict[str, float]:
footprint = evidence.candidate.footprint
min_lat = footprint.get("min_lat", 0.0)
max_lat = footprint.get("max_lat", min_lat)
min_lon = footprint.get("min_lon", 0.0)
max_lon = footprint.get("max_lon", min_lon)
return {
"latitude_deg": (min_lat + max_lat) / 2.0,
"longitude_deg": (min_lon + max_lon) / 2.0,
"mean_reprojection_error_px": evidence.mean_reprojection_error_px,
}
-3
View File
@@ -1,3 +0,0 @@
# Anchor Verification Matcher Package
Exports local feature matching and geometry verification boundaries owned by `anchor_verification`.
@@ -1,5 +0,0 @@
"""Anchor feature matching package exports."""
from anchor_verification.interfaces import FeatureMatcher, KeypointRansacMatcher
__all__ = ["FeatureMatcher", "KeypointRansacMatcher"]
-65
View File
@@ -1,65 +0,0 @@
"""Public anchor verification models."""
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, NonNegativeFloat, NonNegativeInt
from shared.contracts import AnchorDecision, VprCandidate
class AnchorVerificationModel(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
MatcherProfile = Literal["aliked_lightglue", "disk_lightglue", "sift_orb"]
class AnchorFrame(AnchorVerificationModel):
frame_id: str = Field(min_length=1)
image_ref: str = Field(min_length=1)
usable_for_anchor: bool = True
keypoints: tuple[tuple[float, float], ...] = ()
class CandidateTile(AnchorVerificationModel):
candidate: VprCandidate
image_ref: str = Field(min_length=1)
keypoints: tuple[tuple[float, float], ...] = ()
provenance_trusted: bool = True
class GeometryGateConfig(AnchorVerificationModel):
min_inliers: NonNegativeInt = 20
max_mean_reprojection_error_px: NonNegativeFloat = 3.0
class MatchEvidence(AnchorVerificationModel):
candidate: VprCandidate
matcher_profile: MatcherProfile
inliers: NonNegativeInt
mean_reprojection_error_px: NonNegativeFloat
homography: dict[str, float] | None = None
runtime_ms: NonNegativeFloat
provenance_trusted: bool = True
evidence_source: Literal["computed_geometry", "external_evidence"] = "external_evidence"
class AnchorVerificationResult(AnchorVerificationModel):
decision: AnchorDecision
matcher_profile: MatcherProfile
reason: str = Field(min_length=1)
homography: dict[str, float] | None = None
freshness_status: Literal["fresh", "stale", "rejected"]
class MatcherBenchmarkResult(AnchorVerificationModel):
matcher_profile: MatcherProfile
runtime_ms: NonNegativeFloat
inliers: NonNegativeInt
mean_reprojection_error_px: NonNegativeFloat
accepted: bool
reason: str = Field(min_length=1)
class MatcherBenchmarkReport(AnchorVerificationModel):
results: tuple[MatcherBenchmarkResult, ...] = Field(min_length=1)
-22
View File
@@ -1,22 +0,0 @@
"""Camera ingest and calibration component."""
from .interfaces import CameraFrameIngestor, FrameProvider
from .types import (
CalibrationMetadata,
FrameQualityReport,
IngestedFramePacket,
NavigationFrame,
NormalizationHint,
OcclusionReport,
)
__all__ = [
"CalibrationMetadata",
"CameraFrameIngestor",
"FrameProvider",
"FrameQualityReport",
"IngestedFramePacket",
"NavigationFrame",
"NormalizationHint",
"OcclusionReport",
]
@@ -1,96 +0,0 @@
"""Public camera ingest interfaces."""
from typing import Any, Protocol
from shared.contracts import FramePacket
from .types import (
CalibrationMetadata,
FrameQualityReport,
IngestedFramePacket,
NavigationFrame,
NormalizationHint,
OcclusionReport,
)
class FrameProvider(Protocol):
"""Source of navigation frames for downstream localization components."""
def next_frame(self) -> Any:
"""Return the next frame packet."""
class CameraFrameIngestor:
"""Build metadata-only frame packets for downstream localization components."""
def ingest(
self,
frame: NavigationFrame,
calibration: CalibrationMetadata,
) -> IngestedFramePacket:
quality = self.classify_quality(frame)
occlusion = self.detect_occlusion(frame)
hint = NormalizationHint(
north_up_degrees=frame.north_up_degrees,
should_normalize_downstream=frame.north_up_degrees not in (None, 0.0),
)
contract = FramePacket(
frame_id=frame.frame_id,
timestamp_ns=frame.timestamp_ns,
image_ref=frame.image_ref,
calibration_id=calibration.calibration_id,
occlusion=occlusion.state,
quality=quality.score,
normalization_hint=(
f"north_up_degrees={hint.north_up_degrees}"
if hint.should_normalize_downstream
else None
),
raw_frame_retained=False,
)
return IngestedFramePacket(
contract=contract,
quality_report=quality,
occlusion_report=occlusion,
normalization_hint=hint,
)
def classify_quality(self, frame: NavigationFrame) -> FrameQualityReport:
if not frame.readable:
return FrameQualityReport(score=0.0, state="unusable", reasons=("unreadable",))
score = min(frame.mean_luma, frame.contrast)
reasons: list[str] = []
if frame.mean_luma < 0.05:
reasons.append("blackout")
if frame.contrast < 0.05:
reasons.append("low_contrast")
if reasons:
return FrameQualityReport(score=score, state="unusable", reasons=tuple(reasons))
if score < 0.25:
return FrameQualityReport(score=score, state="degraded", reasons=("low_quality",))
return FrameQualityReport(score=score, state="usable")
def detect_occlusion(self, frame: NavigationFrame) -> OcclusionReport:
if not frame.readable:
return OcclusionReport(
state="unreadable",
usable_for_vio=False,
usable_for_anchor=False,
)
if frame.mean_luma < 0.05 or frame.contrast < 0.05:
return OcclusionReport(
state="total",
usable_for_vio=False,
usable_for_anchor=False,
)
if frame.mean_luma < 0.25 or frame.contrast < 0.25:
return OcclusionReport(
state="partial",
usable_for_vio=True,
usable_for_anchor=False,
)
return OcclusionReport(state="clear", usable_for_vio=True, usable_for_anchor=True)
-70
View File
@@ -1,70 +0,0 @@
"""Public camera ingest models."""
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt, PositiveFloat
from pydantic import model_validator
from shared.contracts import FramePacket
class CameraIngestModel(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
class CalibrationMetadata(CameraIngestModel):
calibration_id: str = Field(min_length=1)
camera_model: str = Field(min_length=1)
image_width_px: int = Field(gt=0)
image_height_px: int = Field(gt=0)
focal_length_px: PositiveFloat
distortion_model: str = Field(min_length=1)
class NavigationFrame(CameraIngestModel):
frame_id: str = Field(min_length=1)
timestamp_ns: NonNegativeInt
image_ref: str = Field(min_length=1)
readable: bool = True
mean_luma: float = Field(ge=0.0, le=1.0)
contrast: float = Field(ge=0.0, le=1.0)
north_up_degrees: float | None = Field(default=None, ge=-180.0, le=180.0)
raw_frame_retained: bool = False
@model_validator(mode="after")
def raw_payload_must_not_be_retained(self) -> "NavigationFrame":
if self.raw_frame_retained:
raise ValueError("camera ingest must retain references only, not raw frames")
return self
class FrameQualityReport(CameraIngestModel):
score: float = Field(ge=0.0, le=1.0)
state: Literal["usable", "degraded", "unusable"]
reasons: tuple[str, ...] = ()
class OcclusionReport(CameraIngestModel):
state: Literal["clear", "partial", "total", "unreadable"]
usable_for_vio: bool
usable_for_anchor: bool
class NormalizationHint(CameraIngestModel):
north_up_degrees: float | None = Field(default=None, ge=-180.0, le=180.0)
should_normalize_downstream: bool = False
class IngestedFramePacket(CameraIngestModel):
contract: FramePacket
quality_report: FrameQualityReport
occlusion_report: OcclusionReport
normalization_hint: NormalizationHint
@property
def usable_for_vio(self) -> bool:
return self.occlusion_report.usable_for_vio and self.quality_report.state != "unusable"
@property
def usable_for_anchor(self) -> bool:
return self.occlusion_report.usable_for_anchor and self.quality_report.state != "unusable"
-22
View File
@@ -1,22 +0,0 @@
"""Flight data recorder and observability component."""
from .interfaces import FlightRecorder, InMemoryFlightRecorder
from .types import (
FdrAppendResult,
FdrExportRequest,
FdrExportResult,
FdrHealth,
FdrPayload,
FdrSegmentSummary,
)
__all__ = [
"FdrAppendResult",
"FdrExportRequest",
"FdrExportResult",
"FdrHealth",
"FdrPayload",
"FdrSegmentSummary",
"FlightRecorder",
"InMemoryFlightRecorder",
]
-121
View File
@@ -1,121 +0,0 @@
"""Public flight recorder interfaces."""
from typing import Any, Protocol
from shared.contracts import FdrEvent
from shared.errors import ErrorEnvelope
from .types import (
FdrAppendResult,
FdrExportRequest,
FdrExportResult,
FdrHealth,
FdrPayload,
FdrSegmentSummary,
)
class FlightRecorder(Protocol):
"""Append-only event recorder for runtime evidence."""
def append_event(self, event: Any) -> None:
"""Persist one FDR event."""
def export(self) -> Any:
"""Export recorded evidence for post-flight analysis."""
class InMemoryFlightRecorder:
"""Bounded append-only recorder for runtime evidence metadata."""
def __init__(self, segment_limit_bytes: int, storage_limit_bytes: int) -> None:
if segment_limit_bytes <= 0:
raise ValueError("segment_limit_bytes must be positive")
if storage_limit_bytes < segment_limit_bytes:
raise ValueError("storage_limit_bytes must be at least one segment")
self._segment_limit_bytes = segment_limit_bytes
self._storage_limit_bytes = storage_limit_bytes
self._segments: list[list[FdrEvent]] = [[]]
self._segment_bytes: list[int] = [0]
self._used_bytes = 0
@property
def health(self) -> FdrHealth:
if self._used_bytes >= self._storage_limit_bytes:
return FdrHealth(
status="critical",
used_bytes=self._used_bytes,
max_bytes=self._storage_limit_bytes,
message="fdr storage limit reached",
)
if self._used_bytes >= int(self._storage_limit_bytes * 0.9):
return FdrHealth(
status="degraded",
used_bytes=self._used_bytes,
max_bytes=self._storage_limit_bytes,
message="fdr storage nearing limit",
)
return FdrHealth(
status="ready",
used_bytes=self._used_bytes,
max_bytes=self._storage_limit_bytes,
message="fdr storage ready",
)
def append_event(self, event: FdrEvent, payload: FdrPayload) -> FdrAppendResult:
if self._used_bytes + payload.size_bytes > self._storage_limit_bytes:
return FdrAppendResult(
appended=False,
error=ErrorEnvelope(
component="fdr_observability",
category="resource",
message="fdr storage limit reached",
severity="critical",
retryable=False,
),
)
rollover = False
if self._segment_bytes[-1] + payload.size_bytes > self._segment_limit_bytes:
self._segments.append([])
self._segment_bytes.append(0)
rollover = True
segment_index = len(self._segments) - 1
stored_event = event.model_copy(update={"payload_ref": payload.ref})
self._segments[segment_index].append(stored_event)
self._segment_bytes[segment_index] += payload.size_bytes
self._used_bytes += payload.size_bytes
return FdrAppendResult(
appended=True,
event=stored_event,
segment_id=self._segment_id(segment_index),
rollover=rollover,
)
def export(self, request: FdrExportRequest) -> FdrExportResult:
segments = tuple(
FdrSegmentSummary(
segment_id=self._segment_id(index),
event_count=len(events),
bytes_used=self._segment_bytes[index],
)
for index, events in enumerate(self._segments)
if events
)
evidence_ref = f"fdr://exports/{request.mission_id}/{request.run_id}/evidence.json"
analytics_ref = (
f"fdr://exports/{request.mission_id}/{request.run_id}/analytics.parquet"
if request.include_analytics
else None
)
return FdrExportResult(
produced=True,
evidence_ref=evidence_ref,
segments=segments,
analytics_ref=analytics_ref,
)
def _segment_id(self, index: int) -> str:
return f"segment-{index + 1:04d}"
-52
View File
@@ -1,52 +0,0 @@
"""Public FDR models."""
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt, PositiveInt
from shared.contracts import FdrEvent
from shared.errors import ErrorEnvelope
class FdrModel(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
class FdrPayload(FdrModel):
ref: str = Field(min_length=1)
size_bytes: PositiveInt
redacted: bool = True
class FdrAppendResult(FdrModel):
appended: bool
event: FdrEvent | None = None
segment_id: str | None = None
rollover: bool = False
error: ErrorEnvelope | None = None
class FdrSegmentSummary(FdrModel):
segment_id: str = Field(min_length=1)
event_count: NonNegativeInt
bytes_used: NonNegativeInt
class FdrHealth(FdrModel):
status: Literal["ready", "degraded", "critical"]
used_bytes: NonNegativeInt
max_bytes: PositiveInt
message: str
class FdrExportRequest(FdrModel):
mission_id: str = Field(min_length=1)
run_id: str = Field(min_length=1)
include_analytics: bool = False
class FdrExportResult(FdrModel):
produced: bool
evidence_ref: str = Field(min_length=1)
segments: tuple[FdrSegmentSummary, ...]
analytics_ref: str | None = None
-24
View File
@@ -1,24 +0,0 @@
"""MAVLink and GCS integration component."""
from .interfaces import InMemoryMavlinkGateway, MavlinkGateway
from .types import (
FlightControllerTelemetry,
GpsEmissionResult,
GpsInputPacket,
OperatorStatusMessage,
StatusEmissionResult,
gps_input_from_estimate,
normalize_telemetry,
)
__all__ = [
"FlightControllerTelemetry",
"GpsEmissionResult",
"GpsInputPacket",
"InMemoryMavlinkGateway",
"MavlinkGateway",
"OperatorStatusMessage",
"StatusEmissionResult",
"gps_input_from_estimate",
"normalize_telemetry",
]
-86
View File
@@ -1,86 +0,0 @@
"""Public MAVLink gateway interfaces."""
from typing import Any, Protocol
from pydantic import ValidationError
from shared.contracts import PositionEstimate, TelemetrySample
from shared.errors import ErrorEnvelope
from .types import (
FlightControllerTelemetry,
GpsEmissionResult,
OperatorStatusMessage,
StatusEmissionResult,
gps_input_from_estimate,
normalize_telemetry,
)
class MavlinkGateway(Protocol):
"""Bridges FC telemetry inputs and localization GPS_INPUT outputs."""
def subscribe_telemetry(self) -> Any:
"""Subscribe to flight-controller telemetry."""
def emit_gps_input(self, estimate: Any) -> None:
"""Emit one localization estimate to the flight controller."""
class InMemoryMavlinkGateway:
"""Deterministic gateway boundary used by runtime adapters and tests."""
def __init__(self, status_rate_limit_ns: int) -> None:
if status_rate_limit_ns < 0:
raise ValueError("status_rate_limit_ns must be non-negative")
self._status_rate_limit_ns = status_rate_limit_ns
self._last_status_timestamp_by_text: dict[str, int] = {}
self.emitted_gps_inputs: list[object] = []
self.emitted_status_messages: list[OperatorStatusMessage] = []
def subscribe_telemetry(
self,
samples: list[FlightControllerTelemetry],
) -> tuple[TelemetrySample, ...]:
return tuple(normalize_telemetry(sample) for sample in samples)
def emit_gps_input(self, estimate: PositionEstimate) -> GpsEmissionResult:
try:
packet = gps_input_from_estimate(estimate)
except ValidationError as error:
return GpsEmissionResult(
emitted=False,
error=ErrorEnvelope(
component="mavlink_gcs_integration",
category="validation",
message="position estimate is unsafe for GPS_INPUT emission",
severity="error",
retryable=False,
cause=str(error),
),
)
self.emitted_gps_inputs.append(packet)
return GpsEmissionResult(emitted=True, packet=packet)
def emit_status(
self,
messages: list[OperatorStatusMessage],
) -> StatusEmissionResult:
emitted: list[OperatorStatusMessage] = []
suppressed: list[OperatorStatusMessage] = []
for message in messages:
last_timestamp = self._last_status_timestamp_by_text.get(message.text)
if (
last_timestamp is not None
and message.timestamp_ns - last_timestamp < self._status_rate_limit_ns
):
suppressed.append(message)
continue
self._last_status_timestamp_by_text[message.text] = message.timestamp_ns
self.emitted_status_messages.append(message)
emitted.append(message)
return StatusEmissionResult(emitted=tuple(emitted), suppressed=tuple(suppressed))
-80
View File
@@ -1,80 +0,0 @@
"""Public MAVLink/GCS models."""
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, NonNegativeFloat, NonNegativeInt
from shared.contracts import PositionEstimate, TelemetrySample
from shared.errors import ErrorEnvelope
class MavlinkModel(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
class FlightControllerTelemetry(MavlinkModel):
timestamp_ns: NonNegativeInt
acceleration_mps2: tuple[float, float, float]
attitude_rad: tuple[float, float, float]
altitude_m: float
airspeed_mps: NonNegativeFloat
gps_health: Literal["healthy", "degraded", "lost", "spoofed"]
class GpsInputPacket(MavlinkModel):
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
fix_type: int = Field(ge=2, le=3)
horizontal_accuracy_m: NonNegativeFloat
source_label: str = Field(min_length=1)
class GpsEmissionResult(MavlinkModel):
emitted: bool
packet: GpsInputPacket | None = None
error: ErrorEnvelope | None = None
class OperatorStatusMessage(MavlinkModel):
timestamp_ns: NonNegativeInt
severity: Literal["info", "warning", "error", "critical"]
text: str = Field(min_length=1)
visible_to_qgc: bool = True
class StatusEmissionResult(MavlinkModel):
emitted: tuple[OperatorStatusMessage, ...]
suppressed: tuple[OperatorStatusMessage, ...] = ()
def normalize_telemetry(sample: FlightControllerTelemetry) -> TelemetrySample:
return TelemetrySample(
timestamp_ns=sample.timestamp_ns,
imu={
"accel_x": sample.acceleration_mps2[0],
"accel_y": sample.acceleration_mps2[1],
"accel_z": sample.acceleration_mps2[2],
},
attitude={
"roll": sample.attitude_rad[0],
"pitch": sample.attitude_rad[1],
"yaw": sample.attitude_rad[2],
},
altitude_m=sample.altitude_m,
airspeed_mps=sample.airspeed_mps,
gps_health=sample.gps_health,
)
def gps_input_from_estimate(estimate: PositionEstimate) -> GpsInputPacket:
return GpsInputPacket(
timestamp_ns=estimate.timestamp_ns,
latitude_deg=estimate.latitude_deg,
longitude_deg=estimate.longitude_deg,
altitude_m=estimate.altitude_m,
fix_type=estimate.fix_type,
horizontal_accuracy_m=estimate.horizontal_accuracy_m,
source_label=estimate.source_label,
)
-18
View File
@@ -1,18 +0,0 @@
"""Safety and anchor wrapper component."""
from .interfaces import LocalizationStateMachine, SafetyAnchorStateMachine
from .types import (
LocalizationSnapshot,
SafetyStateConfig,
TelemetryContext,
TileWriteEligibility,
)
__all__ = [
"LocalizationSnapshot",
"LocalizationStateMachine",
"SafetyAnchorStateMachine",
"SafetyStateConfig",
"TelemetryContext",
"TileWriteEligibility",
]
-151
View File
@@ -1,151 +0,0 @@
"""Public localization state-machine interfaces."""
from typing import Protocol
from shared.contracts import AnchorDecision, PositionEstimate, VioStatePacket
from .types import (
LocalizationSnapshot,
SafetyStateConfig,
TelemetryContext,
TileWriteEligibility,
)
class LocalizationStateMachine(Protocol):
"""Coordinates VIO propagation and anchor promotion decisions."""
def update_vio(
self, vio_state: VioStatePacket, telemetry: TelemetryContext
) -> LocalizationSnapshot:
"""Update the state machine with a VIO state packet."""
def consider_anchor(self, anchor_decision: AnchorDecision) -> LocalizationSnapshot:
"""Evaluate a verified anchor decision."""
class SafetyAnchorStateMachine:
"""Owns authoritative source labels, covariance, and tile eligibility."""
def __init__(self, config: SafetyStateConfig | None = None) -> None:
self._config = config or SafetyStateConfig()
self._snapshot: LocalizationSnapshot | None = None
@property
def snapshot(self) -> LocalizationSnapshot | None:
return self._snapshot
def update_vio(
self,
vio_state: VioStatePacket,
telemetry: TelemetryContext,
) -> LocalizationSnapshot:
covariance_m = self._covariance_from_vio(vio_state)
estimate = PositionEstimate(
timestamp_ns=vio_state.timestamp_ns,
latitude_deg=telemetry.latitude_hint_deg,
longitude_deg=telemetry.longitude_hint_deg,
altitude_m=telemetry.altitude_m,
covariance_semimajor_m=covariance_m,
source_label="vo_extrapolated",
fix_type=3,
horizontal_accuracy_m=covariance_m,
anchor_age_ms=0,
)
self._snapshot = LocalizationSnapshot(
estimate=estimate,
mode="vo_extrapolated",
last_vio_state=vio_state,
)
return self._snapshot
def consider_anchor(self, anchor_decision: AnchorDecision) -> LocalizationSnapshot:
self._require_snapshot()
assert self._snapshot is not None
if not anchor_decision.accepted:
return self._snapshot
pose = anchor_decision.estimated_pose or {}
covariance_m = max(anchor_decision.mean_reprojection_error_px, 0.5)
estimate = PositionEstimate(
timestamp_ns=self._snapshot.estimate.timestamp_ns,
latitude_deg=float(pose.get("latitude_deg", self._snapshot.estimate.latitude_deg)),
longitude_deg=float(pose.get("longitude_deg", self._snapshot.estimate.longitude_deg)),
altitude_m=float(pose.get("altitude_m", self._snapshot.estimate.altitude_m)),
covariance_semimajor_m=covariance_m,
source_label="satellite_anchored",
fix_type=3,
horizontal_accuracy_m=covariance_m,
anchor_age_ms=0,
)
self._snapshot = LocalizationSnapshot(
estimate=estimate,
mode="satellite_anchored",
anchor_evidence=anchor_decision,
last_vio_state=self._snapshot.last_vio_state,
)
return self._snapshot
def propagate_blackout(self, timestamp_ns: int) -> LocalizationSnapshot:
self._require_snapshot()
assert self._snapshot is not None
previous = self._snapshot.estimate
covariance_m = previous.covariance_semimajor_m + self._config.dead_reckoning_growth_m
no_fix = covariance_m >= self._config.no_fix_covariance_threshold_m
source_label = "no_fix" if no_fix else "dead_reckoned"
fix_type = 0 if no_fix else 2
estimate = PositionEstimate(
timestamp_ns=timestamp_ns,
latitude_deg=previous.latitude_deg,
longitude_deg=previous.longitude_deg,
altitude_m=previous.altitude_m,
covariance_semimajor_m=covariance_m,
source_label=source_label,
fix_type=fix_type,
horizontal_accuracy_m=max(covariance_m, 999.0 if no_fix else covariance_m),
anchor_age_ms=previous.anchor_age_ms + 1_000,
)
self._snapshot = LocalizationSnapshot(
estimate=estimate,
mode=source_label,
anchor_evidence=self._snapshot.anchor_evidence,
last_vio_state=self._snapshot.last_vio_state,
)
return self._snapshot
def tile_write_eligibility(self) -> TileWriteEligibility:
self._require_snapshot()
assert self._snapshot is not None
estimate = self._snapshot.estimate
if estimate.source_label not in {"satellite_anchored", "vo_extrapolated"}:
return TileWriteEligibility(
eligible=False,
reason="untrusted_source_label",
estimate=estimate,
)
if estimate.covariance_semimajor_m > self._config.tile_write_covariance_max_m:
return TileWriteEligibility(
eligible=False,
reason="covariance_too_high",
estimate=estimate,
)
return TileWriteEligibility(
eligible=True,
reason="trusted_pose",
estimate=estimate,
)
def _covariance_from_vio(self, vio_state: VioStatePacket) -> float:
if not vio_state.covariance_hint:
return max(
self._config.vio_covariance_floor_m,
self._config.initial_covariance_m / max(vio_state.tracking_quality, 0.1),
)
diagonal = [
row[index] for index, row in enumerate(vio_state.covariance_hint) if index < len(row)
]
return max(self._config.vio_covariance_floor_m, max(diagonal, default=0.0))
def _require_snapshot(self) -> None:
if self._snapshot is None:
raise RuntimeError("safety state requires a VIO update before this operation")
-39
View File
@@ -1,39 +0,0 @@
"""Public safety wrapper models."""
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, NonNegativeFloat, NonNegativeInt
from shared.contracts import AnchorDecision, PositionEstimate, VioStatePacket
class SafetyWrapperModel(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
class TelemetryContext(SafetyWrapperModel):
timestamp_ns: NonNegativeInt
latitude_hint_deg: float = Field(ge=-90.0, le=90.0)
longitude_hint_deg: float = Field(ge=-180.0, le=180.0)
altitude_m: float
class SafetyStateConfig(SafetyWrapperModel):
initial_covariance_m: NonNegativeFloat = 2.0
vio_covariance_floor_m: NonNegativeFloat = 1.0
dead_reckoning_growth_m: NonNegativeFloat = 50.0
no_fix_covariance_threshold_m: NonNegativeFloat = 500.0
tile_write_covariance_max_m: NonNegativeFloat = 3.0
class LocalizationSnapshot(SafetyWrapperModel):
estimate: PositionEstimate
mode: Literal["satellite_anchored", "vo_extrapolated", "dead_reckoned", "no_fix"]
anchor_evidence: AnchorDecision | None = None
last_vio_state: VioStatePacket | None = None
class TileWriteEligibility(SafetyWrapperModel):
eligible: bool
reason: str = Field(min_length=1)
estimate: PositionEstimate
-45
View File
@@ -1,45 +0,0 @@
"""Offline satellite retrieval and synchronization component."""
from .interfaces import (
CpuFaissDescriptorIndex,
DescriptorIndex,
LocalVprRetriever,
SatelliteService,
SatelliteSyncBoundary,
)
from .types import (
DescriptorFidelityReport,
GeneratedTileUploadRecord,
LocalVprIndexPackage,
MissionCacheImportResult,
MissionCachePackage,
RelocalizationRequest,
RuntimePhase,
SatelliteSyncResult,
SatelliteSyncStatus,
UploadOutcome,
VprDescriptorRecord,
VprReadinessReport,
VprRetrievalResult,
)
__all__ = [
"CpuFaissDescriptorIndex",
"DescriptorFidelityReport",
"DescriptorIndex",
"GeneratedTileUploadRecord",
"LocalVprIndexPackage",
"LocalVprRetriever",
"MissionCacheImportResult",
"MissionCachePackage",
"RelocalizationRequest",
"RuntimePhase",
"SatelliteService",
"SatelliteSyncBoundary",
"SatelliteSyncResult",
"SatelliteSyncStatus",
"UploadOutcome",
"VprDescriptorRecord",
"VprReadinessReport",
"VprRetrievalResult",
]
-335
View File
@@ -1,335 +0,0 @@
"""Public satellite service interfaces."""
from math import sqrt
from collections.abc import Callable
from pathlib import Path
from time import perf_counter
from typing import Protocol
from shared.contracts import VprCandidate
from shared.errors import ErrorEnvelope
from tile_manager import GeneratedTileSyncPackage
from .types import (
DescriptorFidelityReport,
GeneratedTileUploadRecord,
LocalVprIndexPackage,
MissionCacheImportResult,
MissionCachePackage,
RelocalizationRequest,
RuntimePhase,
SatelliteSyncResult,
SatelliteSyncStatus,
UploadOutcome,
VprDescriptorRecord,
VprReadinessReport,
VprRetrievalResult,
)
class SatelliteService(Protocol):
"""Retrieves offline VPR candidates from mission cache data."""
def load_index(self, package: LocalVprIndexPackage) -> VprReadinessReport:
"""Load the local descriptor index."""
def retrieve(self, request: RelocalizationRequest) -> VprRetrievalResult:
"""Return candidate anchor records for one frame."""
class DescriptorIndex(Protocol):
"""Search boundary for local descriptor packages."""
@property
def record_count(self) -> int:
"""Return the number of loaded descriptor records."""
def search(
self,
query_descriptor: tuple[float, ...],
top_k: int,
) -> tuple[tuple[float, VprDescriptorRecord], ...]:
"""Return scored descriptor records in descending score order."""
class CpuFaissDescriptorIndex:
"""CPU vector index with a FAISS-compatible search contract."""
def __init__(self, records: tuple[VprDescriptorRecord, ...]) -> None:
self._records = tuple(record for record in records if record.freshness_status != "rejected")
@property
def record_count(self) -> int:
return len(self._records)
def search(
self,
query_descriptor: tuple[float, ...],
top_k: int,
) -> tuple[tuple[float, VprDescriptorRecord], ...]:
scored = sorted(
(
(self._cosine_similarity(query_descriptor, record.descriptor), record)
for record in self._records
),
key=lambda item: item[0],
reverse=True,
)
return tuple(scored[:top_k])
def _cosine_similarity(
self,
query_descriptor: tuple[float, ...],
record_descriptor: tuple[float, ...],
) -> float:
max_length = max(len(query_descriptor), len(record_descriptor))
padded_query = query_descriptor + (0.0,) * (max_length - len(query_descriptor))
padded_record = record_descriptor + (0.0,) * (max_length - len(record_descriptor))
dot_product = sum(
query_value * record_value
for query_value, record_value in zip(padded_query, padded_record)
)
query_norm = sqrt(sum(value * value for value in padded_query)) or 1.0
record_norm = sqrt(sum(value * value for value in padded_record)) or 1.0
return max(0.0, min(1.0, dot_product / (query_norm * record_norm)))
class LocalVprRetriever:
"""Triggered local VPR retrieval over mission-cache descriptor indexes."""
def __init__(self) -> None:
self._package: LocalVprIndexPackage | None = None
self._index: DescriptorIndex | None = None
self._load_error: ErrorEnvelope | None = None
def load_index(self, package: LocalVprIndexPackage) -> VprReadinessReport:
self._package = package
self._index = CpuFaissDescriptorIndex(package.records)
self._load_error = None
return VprReadinessReport(
ready=self._index.record_count > 0,
engine=package.engine,
loaded_records=self._index.record_count,
package_id=package.package_id,
descriptor_model=package.descriptor_model,
error=None
if self._index.record_count > 0
else self._error("local descriptor index has no searchable records", "empty_index"),
)
def load_index_from_path(self, package_path: str | Path) -> VprReadinessReport:
try:
return self.load_index(LocalVprIndexPackage.from_json_file(package_path))
except (FileNotFoundError, OSError, ValueError) as exc:
self._package = None
self._index = None
self._load_error = self._error(
f"local descriptor index package could not be loaded: {exc}",
"index_package_invalid",
)
return VprReadinessReport(
ready=False,
engine="cpu_faiss",
loaded_records=0,
error=self._load_error,
)
def readiness(self) -> VprReadinessReport:
if self._load_error is not None:
return VprReadinessReport(
ready=False,
engine="cpu_faiss",
loaded_records=0,
error=self._load_error,
)
if self._index is None or self._package is None:
return VprReadinessReport(
ready=False,
engine="cpu_faiss",
loaded_records=0,
error=self._error("local descriptor index is not loaded", "index_not_loaded"),
)
return VprReadinessReport(
ready=True,
engine=self._package.engine,
loaded_records=self._index.record_count,
package_id=self._package.package_id,
descriptor_model=self._package.descriptor_model,
)
def retrieve(self, request: RelocalizationRequest) -> VprRetrievalResult:
started = perf_counter()
readiness = self.readiness()
if not readiness.ready:
return VprRetrievalResult(
ready=False,
degraded=True,
retrieval_path="unavailable",
error=readiness.error,
)
if request.query_descriptor is None:
return VprRetrievalResult(
ready=True,
degraded=True,
retrieval_path="unavailable",
error=self._error(
"query descriptor is required for local descriptor index retrieval",
"query_descriptor_missing",
),
)
assert self._index is not None
scored = self._index.search(request.query_descriptor, request.top_k)
candidates = tuple(
VprCandidate(
chunk_id=record.chunk_id,
tile_id=record.tile_id,
score=score,
footprint=record.footprint,
freshness_status=record.freshness_status,
)
for score, record in scored[: request.top_k]
)
latency_ms = (perf_counter() - started) * 1000.0
if not candidates:
return VprRetrievalResult(
ready=True,
degraded=True,
retrieval_path="local_descriptor_index",
latency_ms=latency_ms,
error=self._error(
"local descriptor index produced no valid candidates",
"no_candidates",
),
)
return VprRetrievalResult(
ready=True,
degraded=False,
candidates=candidates,
retrieval_path="local_descriptor_index",
latency_ms=latency_ms,
)
def verify_descriptor_fidelity(
self,
reference_descriptor: tuple[float, ...],
optimized_descriptor: tuple[float, ...],
max_l2_delta: float,
) -> DescriptorFidelityReport:
observed_delta = self._l2_distance(reference_descriptor, optimized_descriptor)
return DescriptorFidelityReport(
accepted=observed_delta <= max_l2_delta,
observed_l2_delta=observed_delta,
max_l2_delta=max_l2_delta,
)
def _l2_distance(
self,
reference_descriptor: tuple[float, ...],
optimized_descriptor: tuple[float, ...],
) -> float:
max_length = max(len(reference_descriptor), len(optimized_descriptor))
padded_reference = reference_descriptor + (0.0,) * (max_length - len(reference_descriptor))
padded_optimized = optimized_descriptor + (0.0,) * (max_length - len(optimized_descriptor))
return sqrt(
sum(
(reference_value - optimized_value) ** 2
for reference_value, optimized_value in zip(padded_reference, padded_optimized)
)
)
def _error(self, message: str, cause: str) -> ErrorEnvelope:
return ErrorEnvelope(
component="satellite_service",
category="runtime",
message=message,
severity="warning",
retryable=False,
cause=cause,
)
class SatelliteSyncBoundary:
"""Owns pre-flight and post-flight package exchange only."""
def __init__(
self,
uploader: Callable[[GeneratedTileSyncPackage], UploadOutcome] | None = None,
) -> None:
self._uploader = uploader or self._default_uploader
self._imports: dict[str, MissionCachePackage] = {}
self._upload_records: list[GeneratedTileUploadRecord] = []
def import_mission_cache(
self,
package: MissionCachePackage,
phase: RuntimePhase = "pre_flight",
) -> MissionCacheImportResult:
if phase != "pre_flight":
return MissionCacheImportResult(
package_id=package.package_id,
mission_id=package.mission_id,
ready_for_tile_validation=False,
error=self._phase_error("mission cache import", phase),
)
self._imports[package.package_id] = package
return MissionCacheImportResult(
package_id=package.package_id,
mission_id=package.mission_id,
ready_for_tile_validation=True,
manifest_entries=package.manifest_entries,
)
def upload_generated_tiles(
self,
package: GeneratedTileSyncPackage,
phase: RuntimePhase = "post_flight",
) -> SatelliteSyncResult:
if phase != "post_flight":
return SatelliteSyncResult(error=self._phase_error("generated tile upload", phase))
if not package.sidecars:
record = GeneratedTileUploadRecord(
package_ref=package.package_ref,
mission_id=package.mission_id,
status="rejected",
reason="empty_generated_tile_package",
retained_for_retry=False,
)
else:
outcome = self._uploader(package)
record = GeneratedTileUploadRecord(
package_ref=package.package_ref,
mission_id=package.mission_id,
status=outcome,
reason=outcome,
retained_for_retry=outcome == "retryable_failure",
)
self._upload_records.append(record)
return SatelliteSyncResult(upload_record=record)
def status(self) -> SatelliteSyncStatus:
return SatelliteSyncStatus(
imported_package_ids=tuple(self._imports),
upload_records=tuple(self._upload_records),
retry_package_refs=tuple(
record.package_ref for record in self._upload_records if record.retained_for_retry
),
)
def _phase_error(self, operation: str, phase: RuntimePhase) -> ErrorEnvelope:
return ErrorEnvelope(
component="satellite_service",
category="security",
message=f"{operation} is not allowed during {phase}",
severity="warning",
retryable=False,
cause="mid_flight_network_blocked" if phase == "in_flight" else "phase_not_allowed",
)
def _default_uploader(self, package: GeneratedTileSyncPackage) -> UploadOutcome:
return "success"
-3
View File
@@ -1,3 +0,0 @@
# Satellite Service Descriptor Index Package
Exports local descriptor index search boundaries owned by `satellite_service`.
-5
View File
@@ -1,5 +0,0 @@
"""Local descriptor index package exports."""
from satellite_service.interfaces import CpuFaissDescriptorIndex, DescriptorIndex
__all__ = ["CpuFaissDescriptorIndex", "DescriptorIndex"]
-105
View File
@@ -1,105 +0,0 @@
"""Public satellite service models."""
import json
from pathlib import Path
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, PositiveInt, ValidationError
from shared.contracts import VprCandidate
from shared.errors import ErrorEnvelope
from tile_manager import TileManifestEntry
class SatelliteServiceModel(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
class MissionCachePackage(SatelliteServiceModel):
package_id: str = Field(min_length=1)
mission_id: str = Field(min_length=1)
manifest_entries: tuple[TileManifestEntry, ...] = Field(min_length=1)
class MissionCacheImportResult(SatelliteServiceModel):
package_id: str = Field(min_length=1)
mission_id: str = Field(min_length=1)
ready_for_tile_validation: bool
manifest_entries: tuple[TileManifestEntry, ...] = ()
error: ErrorEnvelope | None = None
class GeneratedTileUploadRecord(SatelliteServiceModel):
package_ref: str = Field(min_length=1)
mission_id: str = Field(min_length=1)
status: Literal["uploaded", "rejected", "retryable_failure"]
reason: str
retained_for_retry: bool
class SatelliteSyncStatus(SatelliteServiceModel):
imported_package_ids: tuple[str, ...]
upload_records: tuple[GeneratedTileUploadRecord, ...]
retry_package_refs: tuple[str, ...]
class SatelliteSyncResult(SatelliteServiceModel):
upload_record: GeneratedTileUploadRecord | None = None
error: ErrorEnvelope | None = None
class VprDescriptorRecord(SatelliteServiceModel):
chunk_id: str = Field(min_length=1)
tile_id: str = Field(min_length=1)
descriptor: tuple[float, ...] = Field(min_length=1)
footprint: dict[str, float]
freshness_status: Literal["fresh", "stale", "rejected"]
class LocalVprIndexPackage(SatelliteServiceModel):
package_id: str = Field(min_length=1)
engine: Literal["cpu_faiss"] = "cpu_faiss"
descriptor_model: str = Field(default="dinov2_vlad", min_length=1)
records: tuple[VprDescriptorRecord, ...] = Field(min_length=1)
@classmethod
def from_json_file(cls, package_path: str | Path) -> "LocalVprIndexPackage":
payload = json.loads(Path(package_path).read_text(encoding="utf-8"))
return cls.model_validate(payload)
class RelocalizationRequest(SatelliteServiceModel):
frame_id: str = Field(min_length=1)
image_ref: str = Field(min_length=1)
trigger_reason: str = Field(min_length=1)
top_k: PositiveInt = Field(le=50)
query_descriptor: tuple[float, ...] | None = None
class VprReadinessReport(SatelliteServiceModel):
ready: bool
engine: Literal["cpu_faiss"]
loaded_records: int = Field(ge=0)
package_id: str | None = None
descriptor_model: str | None = None
error: ErrorEnvelope | None = None
class VprRetrievalResult(SatelliteServiceModel):
ready: bool
degraded: bool
candidates: tuple[VprCandidate, ...] = ()
retrieval_path: Literal["local_descriptor_index", "unavailable"] = "unavailable"
latency_ms: float | None = Field(default=None, ge=0.0)
error: ErrorEnvelope | None = None
class DescriptorFidelityReport(SatelliteServiceModel):
accepted: bool
observed_l2_delta: float = Field(ge=0.0)
max_l2_delta: float = Field(ge=0.0)
RuntimePhase = Literal["pre_flight", "in_flight", "post_flight"]
UploadOutcome = Literal["success", "retryable_failure", "rejected"]
IndexLoadError = FileNotFoundError | json.JSONDecodeError | ValidationError | OSError
-1
View File
@@ -1 +0,0 @@
"""Shared runtime foundation packages."""
-5
View File
@@ -1,5 +0,0 @@
"""Runtime configuration helper namespace."""
from shared.config.models import RuntimeProfile, readiness_error, validate_runtime_profile
__all__ = ["RuntimeProfile", "readiness_error", "validate_runtime_profile"]
-59
View File
@@ -1,59 +0,0 @@
"""Runtime profile configuration and readiness validation."""
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
from shared.errors import ErrorEnvelope, ResultEnvelope
class RuntimeProfile(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
environment: Literal["development", "ci", "staging", "jetson", "production"]
config_dir: str = Field(min_length=1)
cache_dir: str | None = None
fdr_dir: str | None = None
database_url: str | None = None
mavlink_url: str | None = None
camera_source: str | None = None
signing_key_ref: str | None = None
@model_validator(mode="after")
def production_requires_runtime_paths(self) -> "RuntimeProfile":
if self.environment != "production":
return self
missing = [
name
for name in (
"cache_dir",
"fdr_dir",
"database_url",
"mavlink_url",
"camera_source",
"signing_key_ref",
)
if getattr(self, name) in (None, "")
]
if missing:
raise ValueError(f"production profile missing required settings: {', '.join(missing)}")
return self
def readiness_error(component: str, message: str) -> ErrorEnvelope:
return ErrorEnvelope(
component=component,
category="configuration",
message=message,
severity="critical",
retryable=False,
)
def validate_runtime_profile(component: str, payload: dict[str, object]) -> ResultEnvelope:
try:
RuntimeProfile.model_validate(payload)
except ValidationError as error:
return ResultEnvelope.failure(readiness_error(component, str(error)))
return ResultEnvelope.success()
-28
View File
@@ -1,28 +0,0 @@
"""Shared DTO and interface contract namespace."""
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",
]
-116
View File
@@ -1,116 +0,0 @@
"""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)
-5
View File
@@ -1,5 +0,0 @@
"""Shared error envelope namespace."""
from shared.errors.models import ErrorEnvelope, ResultEnvelope
__all__ = ["ErrorEnvelope", "ResultEnvelope"]
-38
View File
@@ -1,38 +0,0 @@
"""Shared structured error envelopes."""
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
class ErrorEnvelope(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
component: str = Field(min_length=1)
category: Literal[
"configuration",
"dependency",
"validation",
"runtime",
"security",
"resource",
]
message: str = Field(min_length=1)
severity: Literal["info", "warning", "error", "critical"]
retryable: bool
cause: str | None = None
class ResultEnvelope(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
ok: bool
error: ErrorEnvelope | None = None
@classmethod
def success(cls) -> "ResultEnvelope":
return cls(ok=True)
@classmethod
def failure(cls, error: ErrorEnvelope) -> "ResultEnvelope":
return cls(ok=False, error=error)
-21
View File
@@ -1,21 +0,0 @@
"""Geospatial geometry helper namespace."""
from shared.geo_geometry.models import (
CameraFootprint,
LocalNedCoordinate,
Wgs84Coordinate,
distance_m,
local_to_wgs84,
nadir_camera_footprint,
wgs84_to_local,
)
__all__ = [
"CameraFootprint",
"LocalNedCoordinate",
"Wgs84Coordinate",
"distance_m",
"local_to_wgs84",
"nadir_camera_footprint",
"wgs84_to_local",
]
-83
View File
@@ -1,83 +0,0 @@
"""Deterministic geospatial helper models and calculations."""
from math import cos, radians, sqrt
from pydantic import BaseModel, ConfigDict, Field, PositiveFloat
EARTH_RADIUS_M = 6_378_137.0
class GeometryModel(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
class Wgs84Coordinate(GeometryModel):
latitude_deg: float = Field(ge=-90.0, le=90.0)
longitude_deg: float = Field(ge=-180.0, le=180.0)
altitude_m: float = 0.0
class LocalNedCoordinate(GeometryModel):
north_m: float
east_m: float
down_m: float = 0.0
class CameraFootprint(GeometryModel):
center: Wgs84Coordinate
ground_width_m: PositiveFloat
ground_height_m: PositiveFloat
ground_sample_distance_m_per_px: PositiveFloat
def wgs84_to_local(origin: Wgs84Coordinate, point: Wgs84Coordinate) -> LocalNedCoordinate:
lat_delta_rad = radians(point.latitude_deg - origin.latitude_deg)
lon_delta_rad = radians(point.longitude_deg - origin.longitude_deg)
latitude_scale = cos(radians(origin.latitude_deg))
return LocalNedCoordinate(
north_m=lat_delta_rad * EARTH_RADIUS_M,
east_m=lon_delta_rad * EARTH_RADIUS_M * latitude_scale,
down_m=origin.altitude_m - point.altitude_m,
)
def local_to_wgs84(origin: Wgs84Coordinate, local: LocalNedCoordinate) -> Wgs84Coordinate:
latitude_deg = origin.latitude_deg + (local.north_m / EARTH_RADIUS_M) * (
180.0 / 3.141592653589793
)
latitude_scale = cos(radians(origin.latitude_deg))
longitude_deg = origin.longitude_deg + (local.east_m / (EARTH_RADIUS_M * latitude_scale)) * (
180.0 / 3.141592653589793
)
return Wgs84Coordinate(
latitude_deg=latitude_deg,
longitude_deg=longitude_deg,
altitude_m=origin.altitude_m - local.down_m,
)
def distance_m(first: Wgs84Coordinate, second: Wgs84Coordinate) -> float:
local = wgs84_to_local(first, second)
return sqrt(local.north_m**2 + local.east_m**2 + local.down_m**2)
def nadir_camera_footprint(
center: Wgs84Coordinate,
altitude_agl_m: PositiveFloat,
sensor_width_px: int,
sensor_height_px: int,
ground_sample_distance_m_per_px: PositiveFloat,
) -> CameraFootprint:
if sensor_width_px <= 0 or sensor_height_px <= 0:
raise ValueError("sensor dimensions must be positive")
if altitude_agl_m <= 0:
raise ValueError("altitude_agl_m must be positive")
return CameraFootprint(
center=center,
ground_width_m=sensor_width_px * ground_sample_distance_m_per_px,
ground_height_m=sensor_height_px * ground_sample_distance_m_per_px,
ground_sample_distance_m_per_px=ground_sample_distance_m_per_px,
)
-5
View File
@@ -1,5 +0,0 @@
"""Structured telemetry and health metadata namespace."""
from shared.telemetry.models import HealthEvent, MetricsLabels
__all__ = ["HealthEvent", "MetricsLabels"]
-23
View File
@@ -1,23 +0,0 @@
"""FDR-safe health and metrics metadata."""
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt
class HealthEvent(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
component: str = Field(min_length=1)
timestamp_ns: NonNegativeInt
liveness: Literal["alive", "failed"]
readiness: Literal["ready", "not_ready"]
dependency_state: dict[str, str] = Field(default_factory=dict)
class MetricsLabels(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
component: str = Field(min_length=1)
action: str = Field(min_length=1)
status: Literal["ok", "degraded", "failed"]
-15
View File
@@ -1,15 +0,0 @@
"""Clock-domain and timestamp helper namespace."""
from shared.time_sync.models import (
TimeSyncViolation,
TimeWindowResult,
check_monotonic_timestamps,
select_time_window,
)
__all__ = [
"TimeSyncViolation",
"TimeWindowResult",
"check_monotonic_timestamps",
"select_time_window",
]
-77
View File
@@ -1,77 +0,0 @@
"""Timestamp alignment helpers."""
from pydantic import BaseModel, ConfigDict, NonNegativeInt
class TimeSyncModel(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
class TimeSyncViolation(TimeSyncModel):
category: str
message: str
class TimeWindowResult(TimeSyncModel):
frame_timestamp_ns: NonNegativeInt
sample_timestamps_ns: tuple[NonNegativeInt, ...]
max_gap_ns: NonNegativeInt
jitter_ns: NonNegativeInt
violations: tuple[TimeSyncViolation, ...] = ()
@property
def ok(self) -> bool:
return not self.violations
def check_monotonic_timestamps(timestamps_ns: list[int]) -> tuple[TimeSyncViolation, ...]:
violations: list[TimeSyncViolation] = []
for previous, current in zip(timestamps_ns, timestamps_ns[1:]):
if current <= previous:
violations.append(
TimeSyncViolation(
category="timestamp_mismatch",
message="timestamps must be strictly increasing",
)
)
break
return tuple(violations)
def select_time_window(
frame_timestamp_ns: int,
sample_timestamps_ns: list[int],
tolerance_ns: int,
) -> TimeWindowResult:
if tolerance_ns < 0:
raise ValueError("tolerance_ns must be non-negative")
violations = list(check_monotonic_timestamps(sample_timestamps_ns))
selected = tuple(
timestamp
for timestamp in sample_timestamps_ns
if abs(timestamp - frame_timestamp_ns) <= tolerance_ns
)
if not selected:
violations.append(
TimeSyncViolation(
category="gap_exceeded",
message="no telemetry samples fall within the frame tolerance window",
)
)
gaps = [
current - previous
for previous, current in zip(sample_timestamps_ns, sample_timestamps_ns[1:])
if current > previous
]
max_gap_ns = max(gaps, default=0)
jitter_ns = max(gaps, default=0) - min(gaps, default=0) if gaps else 0
return TimeWindowResult(
frame_timestamp_ns=frame_timestamp_ns,
sample_timestamps_ns=selected,
max_gap_ns=max_gap_ns,
jitter_ns=jitter_ns,
violations=tuple(violations),
)
-28
View File
@@ -1,28 +0,0 @@
"""Tile cache and generated tile lifecycle component."""
from .interfaces import LocalTileManager, TileManager
from .types import (
CacheValidationReport,
GeneratedTileCandidate,
GeneratedTileSidecar,
GeneratedTileSyncPackage,
TileGenerationRequest,
TileManifestEntry,
TileMetadataLookup,
TileValidationDecision,
freshness_status,
)
__all__ = [
"CacheValidationReport",
"GeneratedTileCandidate",
"GeneratedTileSidecar",
"GeneratedTileSyncPackage",
"LocalTileManager",
"TileManager",
"TileGenerationRequest",
"TileManifestEntry",
"TileMetadataLookup",
"TileValidationDecision",
"freshness_status",
]
-199
View File
@@ -1,199 +0,0 @@
"""Public tile manager interfaces."""
from datetime import datetime
from typing import Any, Protocol
from shared.contracts import CacheTileRecord
from shared.errors import ErrorEnvelope
from .types import (
CacheValidationReport,
GeneratedTileCandidate,
GeneratedTileSidecar,
GeneratedTileSyncPackage,
TileManifestEntry,
TileGenerationRequest,
TileMetadataLookup,
TileValidationDecision,
freshness_status,
)
class TileManager(Protocol):
"""Validates and serves local cache tile records."""
def validate_cache(self) -> None:
"""Validate local cache metadata and signatures."""
def get_tile_window(self, footprint: Any) -> list[Any]:
"""Return tiles intersecting a requested footprint."""
class LocalTileManager:
"""Validates preloaded local cache metadata and serves trusted tile records."""
def __init__(
self,
trusted_signature_hashes: set[str],
now: datetime,
postgis_available: bool = True,
) -> None:
self._trusted_signature_hashes = trusted_signature_hashes
self._now = now
self._postgis_available = postgis_available
self._trusted_by_tile_id: dict[str, CacheTileRecord] = {}
self._descriptor_by_tile_id: dict[str, str] = {}
self._tile_id_by_chunk_id: dict[str, str] = {}
self._generated_candidates: list[GeneratedTileCandidate] = []
def validate_cache(self, entries: list[TileManifestEntry]) -> CacheValidationReport:
if not self._postgis_available:
decisions = tuple(
TileValidationDecision(
tile_id=entry.tile_id,
accepted=False,
reason="postgis_unavailable",
)
for entry in entries
)
return CacheValidationReport(activated=False, decisions=decisions)
decisions = tuple(self._validate_entry(entry) for entry in entries)
self._trusted_by_tile_id = {
decision.record.tile_id: decision.record
for decision in decisions
if decision.record is not None
}
self._descriptor_by_tile_id = {
entry.tile_id: entry.descriptor_ref
for entry in entries
if entry.tile_id in self._trusted_by_tile_id
}
self._tile_id_by_chunk_id = {
entry.chunk_id: entry.tile_id
for entry in entries
if entry.tile_id in self._trusted_by_tile_id
}
return CacheValidationReport(
activated=bool(self._trusted_by_tile_id)
and all(decision.accepted for decision in decisions),
decisions=decisions,
)
def get_tile_window(self, footprint: Any) -> list[CacheTileRecord]:
if isinstance(footprint, dict) and "chunk_id" in footprint:
tile_id = self._tile_id_by_chunk_id.get(str(footprint["chunk_id"]))
return [self._trusted_by_tile_id[tile_id]] if tile_id is not None else []
return list(self._trusted_by_tile_id.values())
def get_tile_metadata(self, chunk_id: str) -> TileMetadataLookup:
tile_id = self._tile_id_by_chunk_id.get(chunk_id)
if tile_id is None:
return TileMetadataLookup(
found=False,
error=ErrorEnvelope(
component="tile_manager",
category="validation",
message=f"no trusted tile metadata for chunk {chunk_id}",
severity="warning",
retryable=False,
),
)
return TileMetadataLookup(
found=True,
record=self._trusted_by_tile_id[tile_id],
descriptor_ref=self._descriptor_by_tile_id[tile_id],
)
def orthorectify_frame(self, request: TileGenerationRequest) -> GeneratedTileCandidate:
if not request.frame_usable:
return GeneratedTileCandidate(accepted=False, rejection_reason="frame_not_usable")
if request.parent_covariance_m > 5.0:
return GeneratedTileCandidate(accepted=False, rejection_reason="covariance_too_high")
if request.quality_score < 0.25:
return GeneratedTileCandidate(accepted=False, rejection_reason="quality_too_low")
trust_level = "generated" if request.parent_covariance_m <= 3.0 else "candidate"
tile_id = f"generated-{request.mission_id}-{request.frame_id}"
candidate = GeneratedTileCandidate(
accepted=True,
tile_id=tile_id,
cog_ref=f"generated/{request.mission_id}/{tile_id}.cog.tif",
sidecar=GeneratedTileSidecar(
tile_id=tile_id,
parent_frame_id=request.frame_id,
parent_covariance_m=request.parent_covariance_m,
quality_score=request.quality_score,
trust_level=trust_level,
provenance=request.source_provenance,
),
)
self._generated_candidates.append(candidate)
return candidate
def package_sync(self, mission_id: str) -> GeneratedTileSyncPackage:
sidecars = tuple(
candidate.sidecar
for candidate in self._generated_candidates
if candidate.sidecar is not None
)
manifest_delta = tuple(
{
"tile_id": sidecar.tile_id,
"trust_level": sidecar.trust_level,
"parent_covariance_m": sidecar.parent_covariance_m,
"provenance": sidecar.provenance,
}
for sidecar in sidecars
)
return GeneratedTileSyncPackage(
package_ref=f"generated/{mission_id}/sync-package.json",
mission_id=mission_id,
manifest_delta=manifest_delta,
sidecars=sidecars,
)
def _validate_entry(self, entry: TileManifestEntry) -> TileValidationDecision:
if entry.signature_hash not in self._trusted_signature_hashes:
return TileValidationDecision(
tile_id=entry.tile_id,
accepted=False,
reason="signature_not_trusted",
)
if entry.content_hash != entry.expected_content_hash:
return TileValidationDecision(
tile_id=entry.tile_id,
accepted=False,
reason="content_hash_mismatch",
)
if entry.sidecar_hash != entry.expected_sidecar_hash:
return TileValidationDecision(
tile_id=entry.tile_id,
accepted=False,
reason="sidecar_hash_mismatch",
)
freshness = freshness_status(entry.expires_at, self._now)
if freshness == "stale":
return TileValidationDecision(tile_id=entry.tile_id, accepted=False, reason="stale")
record = CacheTileRecord(
tile_id=entry.tile_id,
crs=entry.crs,
meters_per_pixel=entry.meters_per_pixel,
capture_date=entry.capture_date,
signature_hash=entry.signature_hash,
trust_level="trusted",
freshness_status=freshness,
provenance=entry.provenance,
)
return TileValidationDecision(
tile_id=entry.tile_id,
accepted=True,
reason="trusted",
record=record,
)
-97
View File
@@ -1,97 +0,0 @@
"""Public tile manager models."""
from datetime import date, datetime, timezone
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, PositiveFloat
from shared.contracts import CacheTileRecord
from shared.errors import ErrorEnvelope
class TileManagerModel(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
class TileManifestEntry(TileManagerModel):
tile_id: str = Field(min_length=1)
chunk_id: str = Field(min_length=1)
crs: str = Field(min_length=1)
meters_per_pixel: PositiveFloat
capture_date: date
expires_at: datetime
content_hash: str = Field(min_length=1)
expected_content_hash: str = Field(min_length=1)
sidecar_hash: str = Field(min_length=1)
expected_sidecar_hash: str = Field(min_length=1)
signature_hash: str = Field(min_length=1)
provenance: str = Field(min_length=1)
footprint: dict[str, float]
descriptor_ref: str = Field(min_length=1)
class TileValidationDecision(TileManagerModel):
tile_id: str = Field(min_length=1)
accepted: bool
reason: str
record: CacheTileRecord | None = None
class CacheValidationReport(TileManagerModel):
activated: bool
decisions: tuple[TileValidationDecision, ...]
@property
def trusted_records(self) -> tuple[CacheTileRecord, ...]:
return tuple(decision.record for decision in self.decisions if decision.record is not None)
class TileMetadataLookup(TileManagerModel):
found: bool
record: CacheTileRecord | None = None
descriptor_ref: str | None = None
error: ErrorEnvelope | None = None
class TileGenerationRequest(TileManagerModel):
mission_id: str = Field(min_length=1)
frame_id: str = Field(min_length=1)
image_ref: str = Field(min_length=1)
timestamp_ns: int = Field(ge=0)
parent_covariance_m: float = Field(ge=0.0)
frame_usable: bool
quality_score: float = Field(ge=0.0, le=1.0)
footprint: dict[str, float]
source_provenance: str = Field(min_length=1)
class GeneratedTileSidecar(TileManagerModel):
tile_id: str = Field(min_length=1)
parent_frame_id: str = Field(min_length=1)
parent_covariance_m: float = Field(ge=0.0)
quality_score: float = Field(ge=0.0, le=1.0)
trust_level: Literal["generated", "candidate"]
provenance: str = Field(min_length=1)
class GeneratedTileCandidate(TileManagerModel):
accepted: bool
tile_id: str | None = None
cog_ref: str | None = None
sidecar: GeneratedTileSidecar | None = None
rejection_reason: str | None = None
class GeneratedTileSyncPackage(TileManagerModel):
package_ref: str = Field(min_length=1)
mission_id: str = Field(min_length=1)
manifest_delta: tuple[dict[str, object], ...]
sidecars: tuple[GeneratedTileSidecar, ...]
def freshness_status(expires_at: datetime, now: datetime) -> Literal["fresh", "stale"]:
normalized_expiry = expires_at
if normalized_expiry.tzinfo is None:
normalized_expiry = normalized_expiry.replace(tzinfo=timezone.utc)
normalized_now = now if now.tzinfo is not None else now.replace(tzinfo=timezone.utc)
return "fresh" if normalized_expiry >= normalized_now else "stale"
-39
View File
@@ -1,39 +0,0 @@
"""Replaceable VIO adapter component."""
from .interfaces import (
ConfiguredNativeVioBackend,
LocalVioAdapter,
NativeVioBackend,
NativeVioRunner,
NativeVioRunnerFactory,
ReplayVioBackend,
VioAdapter,
VioBackend,
VioBackendError,
create_vio_adapter,
)
from .types import (
VioBackendEstimate,
VioHealthReport,
VioInputPacket,
VioProcessingResult,
VioRuntimeConfig,
)
__all__ = [
"ConfiguredNativeVioBackend",
"LocalVioAdapter",
"NativeVioBackend",
"NativeVioRunner",
"NativeVioRunnerFactory",
"ReplayVioBackend",
"VioAdapter",
"VioBackend",
"VioBackendError",
"VioBackendEstimate",
"VioHealthReport",
"VioInputPacket",
"VioProcessingResult",
"VioRuntimeConfig",
"create_vio_adapter",
]
-328
View File
@@ -1,328 +0,0 @@
"""Public VIO adapter interfaces."""
from collections.abc import Callable
from time import perf_counter
from typing import Any, Protocol, runtime_checkable
from shared.contracts import VioStatePacket
from shared.errors import ErrorEnvelope
from shared.time_sync import select_time_window
from .types import (
VioBackendEstimate,
VioHealthReport,
VioInputPacket,
VioProcessingResult,
VioRuntimeConfig,
)
class VioAdapter(Protocol):
"""Processes frame and telemetry inputs into relative VIO state."""
def initialize(self) -> None:
"""Initialize adapter resources."""
def process(self, packet: VioInputPacket) -> VioProcessingResult:
"""Process one synchronized frame/telemetry pair."""
def health(self) -> VioHealthReport:
"""Return current readiness and degradation state."""
class VioBackend(Protocol):
"""Backend-neutral VIO execution boundary."""
def initialize(self) -> None:
"""Initialize native backend resources."""
def estimate(self, frame: Any, telemetry_window: tuple[Any, ...]) -> VioBackendEstimate:
"""Return one relative VIO estimate."""
@runtime_checkable
class NativeVioRunner(Protocol):
"""Runtime object supplied by the selected VIO engine package."""
def initialize(self) -> None:
"""Prepare engine resources."""
def estimate(
self, frame: Any, telemetry_window: tuple[Any, ...]
) -> VioBackendEstimate | dict[str, Any]:
"""Return an estimate payload for one synchronized replay frame."""
class VioBackendError(RuntimeError):
"""Raised when the configured VIO engine cannot produce an estimate."""
NativeVioRunnerFactory = Callable[[], NativeVioRunner]
class NativeVioBackend:
"""Configurable backend adapter for native VIO engine packages."""
def __init__(self, runner: NativeVioRunner, backend_name: str = "native_vio") -> None:
self._runner = runner
self.backend_name = backend_name
def initialize(self) -> None:
try:
self._runner.initialize()
except Exception as exc:
raise VioBackendError(f"{self.backend_name} initialization failed: {exc}") from exc
def estimate(self, frame: Any, telemetry_window: tuple[Any, ...]) -> VioBackendEstimate:
started = perf_counter()
try:
estimate = self._runner.estimate(frame, telemetry_window)
except Exception as exc:
raise VioBackendError(f"{self.backend_name} estimate failed: {exc}") from exc
try:
if isinstance(estimate, VioBackendEstimate):
if estimate.processing_latency_ms is not None:
return estimate
payload = estimate.model_dump()
else:
payload = dict(estimate)
payload.setdefault("timestamp_ns", frame.timestamp_ns)
payload["processing_latency_ms"] = (
payload.get("processing_latency_ms") or (perf_counter() - started) * 1000.0
)
return VioBackendEstimate.model_validate(payload)
except Exception as exc:
raise VioBackendError(f"{self.backend_name} returned invalid estimate") from exc
class ConfiguredNativeVioBackend:
"""Lazily creates the configured native runner during adapter initialization."""
def __init__(
self,
runner_factory: NativeVioRunnerFactory,
backend_name: str = "basalt",
) -> None:
self._runner_factory = runner_factory
self._backend: NativeVioBackend | None = None
self.backend_name = backend_name
def initialize(self) -> None:
try:
runner = self._runner_factory()
except Exception as exc:
raise VioBackendError(f"{self.backend_name} runner creation failed") from exc
if not isinstance(runner, NativeVioRunner):
raise VioBackendError(f"{self.backend_name} runner does not implement NativeVioRunner")
self._backend = NativeVioBackend(runner, backend_name=self.backend_name)
self._backend.initialize()
def estimate(self, frame: Any, telemetry_window: tuple[Any, ...]) -> VioBackendEstimate:
if self._backend is None:
raise VioBackendError(f"{self.backend_name} runner is not initialized")
return self._backend.estimate(frame, telemetry_window)
class ReplayVioBackend:
"""Small local backend for replay smoke tests when no engine is configured."""
backend_name = "replay_vio"
def initialize(self) -> None:
return None
def estimate(self, frame: Any, telemetry_window: tuple[Any, ...]) -> VioBackendEstimate:
quality = float(getattr(frame, "quality", 1.0))
tracking_quality = max(0.0, min(1.0, quality))
return VioBackendEstimate(
timestamp_ns=frame.timestamp_ns,
relative_pose={
"x_m": tracking_quality,
"y_m": 0.0,
"z_m": 0.0,
"yaw_rad": 0.0,
},
velocity_mps=(tracking_quality, 0.0, 0.0),
tracking_quality=tracking_quality,
bias_estimate={"sample_count": float(len(telemetry_window))},
covariance_hint=[
[1.0 / max(tracking_quality, 0.1), 0.0, 0.0],
[0.0, 1.0 / max(tracking_quality, 0.1), 0.0],
[0.0, 0.0, 1.0 / max(tracking_quality, 0.1)],
],
)
class LocalVioAdapter:
"""Backend-neutral adapter that exposes explicit health and mismatch behavior."""
def __init__(
self,
backend: VioBackend | None = None,
runtime_config: VioRuntimeConfig | None = None,
timestamp_tolerance_ns: int = 5_000_000,
degraded_quality_threshold: float = 0.35,
) -> None:
self._runtime_config = runtime_config or VioRuntimeConfig(mode="replay")
self._backend = backend or _backend_from_runtime_config(self._runtime_config, None)
self._backend_name = getattr(
self._backend, "backend_name", self._backend.__class__.__name__
)
self._timestamp_tolerance_ns = timestamp_tolerance_ns
self._degraded_quality_threshold = degraded_quality_threshold
self._initialized = False
self._health = VioHealthReport(
initialized=False,
state="not_initialized",
tracking_quality=0.0,
backend_name=self._backend_name,
)
def initialize(self) -> None:
try:
self._backend.initialize()
except Exception as exc:
self._initialized = False
self._health = VioHealthReport(
initialized=False,
state="failed",
tracking_quality=0.0,
backend_name=self._backend_name,
error=self._backend_error(str(exc), "backend_initialization_failed", "error"),
)
return None
self._initialized = True
self._health = VioHealthReport(
initialized=True,
state="ready",
tracking_quality=1.0,
backend_name=self._backend_name,
)
def process(self, packet: VioInputPacket) -> VioProcessingResult:
if not self._initialized:
self.initialize()
if self._health.state == "failed":
return VioProcessingResult(health=self._health, error=self._health.error)
telemetry_timestamps = [sample.timestamp_ns for sample in packet.telemetry_samples]
time_window = select_time_window(
packet.frame.timestamp_ns,
telemetry_timestamps,
self._timestamp_tolerance_ns,
)
if not time_window.ok:
error = ErrorEnvelope(
component="vio_adapter",
category="validation",
message="frame and telemetry timestamps are outside the VIO sync window",
severity="warning",
retryable=False,
cause=time_window.violations[0].category,
)
self._health = VioHealthReport(
initialized=True,
state="degraded",
tracking_quality=0.0,
backend_name=self._backend_name,
error=error,
)
return VioProcessingResult(health=self._health, error=error)
telemetry_window = tuple(
sample
for sample in packet.telemetry_samples
if sample.timestamp_ns in set(time_window.sample_timestamps_ns)
)
try:
estimate = self._backend.estimate(packet.frame, telemetry_window)
except Exception as exc:
error = self._backend_error(str(exc), "backend_runtime_failed", "error")
self._health = VioHealthReport(
initialized=True,
state="failed",
tracking_quality=0.0,
backend_name=self._backend_name,
error=error,
)
return VioProcessingResult(health=self._health, error=error)
state_packet = VioStatePacket(
timestamp_ns=estimate.timestamp_ns,
relative_pose=estimate.relative_pose,
velocity_mps=estimate.velocity_mps,
bias_estimate=estimate.bias_estimate,
tracking_quality=estimate.tracking_quality,
covariance_hint=estimate.covariance_hint,
)
health_state = (
"degraded" if estimate.tracking_quality < self._degraded_quality_threshold else "ready"
)
self._health = VioHealthReport(
initialized=True,
state=health_state,
tracking_quality=estimate.tracking_quality,
backend_name=self._backend_name,
)
return VioProcessingResult(
state_packet=state_packet,
health=self._health,
processing_latency_ms=estimate.processing_latency_ms,
)
def health(self) -> VioHealthReport:
return self._health
def _backend_error(self, message: str, cause: str, severity: str) -> ErrorEnvelope:
return ErrorEnvelope(
component="vio_adapter",
category="runtime",
message=message,
severity=severity,
retryable=False,
cause=cause,
)
def create_vio_adapter(
runtime_config: VioRuntimeConfig,
native_runner_factory: NativeVioRunnerFactory | None = None,
timestamp_tolerance_ns: int = 5_000_000,
degraded_quality_threshold: float = 0.35,
) -> LocalVioAdapter:
backend = _backend_from_runtime_config(runtime_config, native_runner_factory)
return LocalVioAdapter(
backend=backend,
runtime_config=runtime_config,
timestamp_tolerance_ns=timestamp_tolerance_ns,
degraded_quality_threshold=degraded_quality_threshold,
)
def _backend_from_runtime_config(
runtime_config: VioRuntimeConfig,
native_runner_factory: NativeVioRunnerFactory | None,
) -> VioBackend:
if runtime_config.effective_mode == "replay":
return ReplayVioBackend()
if native_runner_factory is None:
native_runner_factory = _default_native_runner_factory(runtime_config)
return ConfiguredNativeVioBackend(
native_runner_factory,
backend_name=runtime_config.native_backend_name,
)
def _default_native_runner_factory(runtime_config: VioRuntimeConfig) -> NativeVioRunnerFactory:
def create_runner() -> NativeVioRunner:
from vio_adapter.native.basalt import BasaltNativeRunner
return BasaltNativeRunner(
module_name=runtime_config.native_runner_module,
factory_name=runtime_config.native_runner_factory,
config=runtime_config.native_runner_config,
)
return create_runner
-3
View File
@@ -1,3 +0,0 @@
# VIO Native Backend Package
Exports the configured VIO backend package boundary owned by `vio_adapter`.
-6
View File
@@ -1,6 +0,0 @@
"""Native VIO backend package exports."""
from vio_adapter.interfaces import NativeVioBackend, NativeVioRunner, VioBackendError
from vio_adapter.native.basalt import BasaltNativeRunner
__all__ = ["BasaltNativeRunner", "NativeVioBackend", "NativeVioRunner", "VioBackendError"]
-47
View File
@@ -1,47 +0,0 @@
"""Loader for installed BASALT-compatible VIO runtime packages."""
from collections.abc import Mapping
from importlib import import_module
from typing import Any
from vio_adapter.interfaces import NativeVioRunner, VioBackendError
from vio_adapter.types import VioBackendEstimate
class BasaltNativeRunner:
"""Adapts an installed BASALT binding to the VIO runner protocol."""
def __init__(
self,
module_name: str = "basalt_vio",
factory_name: str = "create_runner",
config: Mapping[str, object] | None = None,
) -> None:
self._module_name = module_name
self._factory_name = factory_name
self._config = dict(config or {})
self._runner: NativeVioRunner | None = None
def initialize(self) -> None:
try:
module = import_module(self._module_name)
factory = getattr(module, self._factory_name)
runner = factory(**self._config)
except Exception as exc:
raise VioBackendError(
f"unable to load BASALT runtime {self._module_name}:{self._factory_name}"
) from exc
if not isinstance(runner, NativeVioRunner):
raise VioBackendError(
f"BASALT runtime {self._module_name}:{self._factory_name} "
"does not implement NativeVioRunner"
)
self._runner = runner
self._runner.initialize()
def estimate(self, frame: Any, telemetry_window: tuple[Any, ...]) -> VioBackendEstimate:
if self._runner is None:
raise VioBackendError("BASALT runtime is not initialized")
return self._runner.estimate(frame, telemetry_window)
-69
View File
@@ -1,69 +0,0 @@
"""Public VIO adapter models."""
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, NonNegativeInt, model_validator
from shared.contracts import FramePacket, TelemetrySample, VioStatePacket
from shared.errors import ErrorEnvelope
class VioAdapterModel(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
class VioInputPacket(VioAdapterModel):
frame: FramePacket
telemetry_samples: tuple[TelemetrySample, ...] = Field(min_length=1)
VioRuntimeEnvironment = Literal["development", "ci", "staging", "jetson", "production"]
VioRuntimeMode = Literal["replay", "native"]
class VioRuntimeConfig(VioAdapterModel):
environment: VioRuntimeEnvironment = "development"
mode: VioRuntimeMode | None = None
native_backend_name: str = Field(default="basalt", min_length=1)
native_runner_module: str = Field(default="basalt_vio", min_length=1)
native_runner_factory: str = Field(default="create_runner", min_length=1)
native_runner_config: dict[str, object] = Field(default_factory=dict)
@model_validator(mode="after")
def production_requires_native_mode(self) -> "VioRuntimeConfig":
if self.environment in {"jetson", "production"} and self.effective_mode != "native":
raise ValueError("jetson and production VIO profiles require native runtime mode")
return self
@property
def effective_mode(self) -> VioRuntimeMode:
if self.mode is not None:
return self.mode
if self.environment in {"jetson", "production"}:
return "native"
return "replay"
class VioHealthReport(VioAdapterModel):
initialized: bool
state: Literal["not_initialized", "ready", "degraded", "failed"]
tracking_quality: float = Field(ge=0.0, le=1.0)
backend_name: str | None = None
error: ErrorEnvelope | None = None
class VioProcessingResult(VioAdapterModel):
state_packet: VioStatePacket | None = None
health: VioHealthReport
processing_latency_ms: float | None = Field(default=None, ge=0.0)
error: ErrorEnvelope | None = None
class VioBackendEstimate(VioAdapterModel):
timestamp_ns: NonNegativeInt
relative_pose: dict[str, float]
velocity_mps: tuple[float, float, float]
tracking_quality: float = Field(ge=0.0, le=1.0)
bias_estimate: dict[str, float] | None = None
covariance_hint: list[list[float]] | None = None
processing_latency_ms: float | None = Field(default=None, ge=0.0)