feat(perception): seal M4.8S reference graph replay
This commit is contained in:
@@ -110,6 +110,10 @@ _RollingItem = _Temporal | _StopSignal
|
||||
_ThreatItem = _Temporal | _Rolled | _StopSignal
|
||||
_QueueItem = SourcePacket | _Detected | _Associated | _Temporal | _Rolled | _StopSignal
|
||||
_QueueItemT = TypeVar("_QueueItemT", bound=_QueueItem)
|
||||
DeliveryEvidenceObserver = Callable[
|
||||
[DeliveredFrame, SourcePacket, tuple[ObjectProposal2D, ...], frozenset[str], int],
|
||||
None,
|
||||
]
|
||||
|
||||
|
||||
class ReferencePerceptionGraphV1:
|
||||
@@ -127,6 +131,8 @@ class ReferencePerceptionGraphV1:
|
||||
threat: ThreatProvider,
|
||||
telemetry_identity: PipelineTelemetryIdentity | None = None,
|
||||
telemetry_sink: PipelineTelemetrySink | None = None,
|
||||
delivery_observer: Callable[[DeliveredFrame, int], None] | None = None,
|
||||
delivery_evidence_observer: DeliveryEvidenceObserver | None = None,
|
||||
clock_ns: Callable[[], int] = time.monotonic_ns,
|
||||
) -> None:
|
||||
if config.graph_id != REFERENCE_GRAPH_ID:
|
||||
@@ -145,6 +151,8 @@ class ReferencePerceptionGraphV1:
|
||||
run_mode=GraphRunMode.SOURCE_PACED_LATEST_WINS,
|
||||
telemetry_identity=telemetry_identity,
|
||||
telemetry_sink=telemetry_sink,
|
||||
delivery_observer=delivery_observer,
|
||||
delivery_evidence_observer=delivery_evidence_observer,
|
||||
clock_ns=clock_ns,
|
||||
)
|
||||
|
||||
@@ -162,6 +170,8 @@ class ReferencePerceptionGraphV1:
|
||||
run_mode: GraphRunMode,
|
||||
telemetry_identity: PipelineTelemetryIdentity | None,
|
||||
telemetry_sink: PipelineTelemetrySink | None,
|
||||
delivery_observer: Callable[[DeliveredFrame, int], None] | None,
|
||||
delivery_evidence_observer: DeliveryEvidenceObserver | None,
|
||||
clock_ns: Callable[[], int],
|
||||
) -> None:
|
||||
if (telemetry_identity is None) is not (telemetry_sink is None):
|
||||
@@ -177,6 +187,8 @@ class ReferencePerceptionGraphV1:
|
||||
self.run_mode = run_mode
|
||||
self.telemetry_identity = telemetry_identity
|
||||
self.telemetry_sink = telemetry_sink
|
||||
self.delivery_observer = delivery_observer
|
||||
self.delivery_evidence_observer = delivery_evidence_observer
|
||||
self._clock_ns = clock_ns
|
||||
self._state = GraphState.CREATED
|
||||
self._state_lock = Lock()
|
||||
@@ -510,6 +522,23 @@ class ReferencePerceptionGraphV1:
|
||||
obstacle_map=obstacle_map,
|
||||
threats=threats,
|
||||
)
|
||||
completed_ns = self._now()
|
||||
with self._result_lock:
|
||||
admitted_at_ns = self._admitted_at_ns[item.packet.envelope.sequence]
|
||||
completion_age_ns = item.packet.envelope.source_age_ns + max(
|
||||
0,
|
||||
completed_ns - admitted_at_ns,
|
||||
)
|
||||
if self.delivery_observer is not None:
|
||||
self.delivery_observer(delivery, completion_age_ns)
|
||||
if self.delivery_evidence_observer is not None:
|
||||
self.delivery_evidence_observer(
|
||||
delivery,
|
||||
item.packet,
|
||||
item.proposals,
|
||||
item.associated_proposal_ids,
|
||||
completion_age_ns,
|
||||
)
|
||||
with self._result_lock:
|
||||
self._deliveries.append(delivery)
|
||||
self._terminal(
|
||||
@@ -845,6 +874,8 @@ class ReferencePerceptionGraphV2(ReferencePerceptionGraphV1):
|
||||
run_mode: GraphRunMode,
|
||||
telemetry_identity: PipelineTelemetryIdentity | None = None,
|
||||
telemetry_sink: PipelineTelemetrySink | None = None,
|
||||
delivery_observer: Callable[[DeliveredFrame, int], None] | None = None,
|
||||
delivery_evidence_observer: DeliveryEvidenceObserver | None = None,
|
||||
clock_ns: Callable[[], int] = time.monotonic_ns,
|
||||
) -> None:
|
||||
if config.graph_id != REFERENCE_GRAPH_ID_V2:
|
||||
@@ -865,6 +896,8 @@ class ReferencePerceptionGraphV2(ReferencePerceptionGraphV1):
|
||||
run_mode=run_mode,
|
||||
telemetry_identity=telemetry_identity,
|
||||
telemetry_sink=telemetry_sink,
|
||||
delivery_observer=delivery_observer,
|
||||
delivery_evidence_observer=delivery_evidence_observer,
|
||||
clock_ns=clock_ns,
|
||||
)
|
||||
|
||||
@@ -903,6 +936,7 @@ __all__ = [
|
||||
"REFERENCE_GRAPH_ID_V2",
|
||||
"TERMINAL_OUTCOME_SCHEMA",
|
||||
"DeliveredFrame",
|
||||
"DeliveryEvidenceObserver",
|
||||
"GraphExecutionError",
|
||||
"GraphRunResult",
|
||||
"GraphRunResultV2",
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
"""Class-aware advisory projection over one delivered reference-graph frame."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Final
|
||||
|
||||
from .contracts import FalseAuthority, MotionState, ThreatDecision
|
||||
from .graph_contracts import DeliveredFrame
|
||||
from .object_understanding import AdvisoryResponse
|
||||
|
||||
M48S_ADVISORY_SCHEMA: Final = "missioncore.m48s-semantic-advisory/v0"
|
||||
M48S_ADVISORY_POLICY_ID: Final = "m48s-behavior-relevant-object-advisory/v0"
|
||||
_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$")
|
||||
|
||||
|
||||
class AdvisoryFamily(StrEnum):
|
||||
GENERIC_OBSTACLE = "generic-obstacle"
|
||||
PERSON = "person"
|
||||
ANIMAL = "animal"
|
||||
LIGHT_ROAD_USER = "light-road-user"
|
||||
VEHICLE = "vehicle"
|
||||
|
||||
|
||||
class M48sAdvisoryError(ValueError):
|
||||
"""A semantic advisory escaped its bounded shadow-only policy."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class M48sSemanticAdvisory:
|
||||
component_id: str
|
||||
semantic_hint: str | None
|
||||
family: AdvisoryFamily
|
||||
motion: MotionState
|
||||
threat_decision: ThreatDecision
|
||||
responses: tuple[AdvisoryResponse, ...]
|
||||
reason_codes: tuple[str, ...]
|
||||
policy_id: str = M48S_ADVISORY_POLICY_ID
|
||||
authority: FalseAuthority = FalseAuthority()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if _IDENTIFIER.fullmatch(self.component_id) is None:
|
||||
raise M48sAdvisoryError("advisory component id is invalid")
|
||||
if self.semantic_hint is not None and _IDENTIFIER.fullmatch(self.semantic_hint) is None:
|
||||
raise M48sAdvisoryError("advisory semantic hint is invalid")
|
||||
if self.policy_id != M48S_ADVISORY_POLICY_ID:
|
||||
raise M48sAdvisoryError("advisory policy identity changed")
|
||||
if not self.responses or len(set(self.responses)) != len(self.responses):
|
||||
raise M48sAdvisoryError("advisory responses must be unique and nonempty")
|
||||
if not self.reason_codes or len(set(self.reason_codes)) != len(self.reason_codes):
|
||||
raise M48sAdvisoryError("advisory reasons must be unique and nonempty")
|
||||
if self.threat_decision is ThreatDecision.THREAT and (
|
||||
AdvisoryResponse.STOP not in self.responses
|
||||
):
|
||||
raise M48sAdvisoryError("replay threat must retain a stop advisory")
|
||||
if self.family is AdvisoryFamily.GENERIC_OBSTACLE and (
|
||||
AdvisoryResponse.ROUTE_AROUND not in self.responses
|
||||
and AdvisoryResponse.STOP not in self.responses
|
||||
):
|
||||
raise M48sAdvisoryError("generic obstacles must remain route-around advisories")
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": M48S_ADVISORY_SCHEMA,
|
||||
"policy_id": self.policy_id,
|
||||
"component_id": self.component_id,
|
||||
"semantic_hint": self.semantic_hint,
|
||||
"family": self.family.value,
|
||||
"motion": self.motion.value,
|
||||
"threat_decision": self.threat_decision.value,
|
||||
"responses": [item.value for item in self.responses],
|
||||
"reason_codes": list(self.reason_codes),
|
||||
"authority": self.authority.to_dict(),
|
||||
}
|
||||
|
||||
|
||||
def project_m48s_advisories(
|
||||
delivery: DeliveredFrame,
|
||||
) -> tuple[M48sSemanticAdvisory, ...]:
|
||||
"""Project class-specific caution without changing occupancy or graph threats."""
|
||||
|
||||
threat_by_component = {item.component_id: item.decision for item in delivery.threats}
|
||||
advisories = [
|
||||
_project(
|
||||
component_id=obstacle.component_id,
|
||||
semantic_hint=obstacle.semantic_hint,
|
||||
motion=obstacle.motion,
|
||||
threat_decision=threat_by_component[obstacle.component_id],
|
||||
)
|
||||
for obstacle in (*delivery.obstacle_map.occupied, *delivery.obstacle_map.unknown)
|
||||
]
|
||||
advisories.extend(
|
||||
_project(
|
||||
component_id=proposal.proposal_id,
|
||||
semantic_hint=proposal.semantic_hint,
|
||||
motion=MotionState.UNKNOWN,
|
||||
threat_decision=threat_by_component[proposal.proposal_id],
|
||||
)
|
||||
for proposal in delivery.obstacle_map.camera_uncertainty
|
||||
)
|
||||
return tuple(advisories)
|
||||
|
||||
|
||||
def advisory_policy_matrix() -> dict[AdvisoryFamily, tuple[AdvisoryResponse, ...]]:
|
||||
"""Expose the fixed class-family policy for executable contract checks."""
|
||||
|
||||
return {
|
||||
AdvisoryFamily.GENERIC_OBSTACLE: (AdvisoryResponse.ROUTE_AROUND,),
|
||||
AdvisoryFamily.PERSON: (
|
||||
AdvisoryResponse.YIELD,
|
||||
AdvisoryResponse.REDUCE_SPEED,
|
||||
),
|
||||
AdvisoryFamily.ANIMAL: (
|
||||
AdvisoryResponse.REDUCE_SPEED,
|
||||
AdvisoryResponse.STOP,
|
||||
),
|
||||
AdvisoryFamily.LIGHT_ROAD_USER: (
|
||||
AdvisoryResponse.YIELD,
|
||||
AdvisoryResponse.REDUCE_SPEED,
|
||||
AdvisoryResponse.MONITOR,
|
||||
),
|
||||
AdvisoryFamily.VEHICLE: (
|
||||
AdvisoryResponse.MONITOR,
|
||||
AdvisoryResponse.YIELD,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _project(
|
||||
*,
|
||||
component_id: str,
|
||||
semantic_hint: str | None,
|
||||
motion: MotionState,
|
||||
threat_decision: ThreatDecision,
|
||||
) -> M48sSemanticAdvisory:
|
||||
family = _family(semantic_hint)
|
||||
responses = list(advisory_policy_matrix()[family])
|
||||
reasons = [f"family-{family.value}"]
|
||||
if motion is MotionState.UNKNOWN:
|
||||
reasons.append("unknown-motion-conservative")
|
||||
elif motion is MotionState.MOVING:
|
||||
reasons.append("observed-moving")
|
||||
if AdvisoryResponse.REDUCE_SPEED not in responses:
|
||||
responses.append(AdvisoryResponse.REDUCE_SPEED)
|
||||
else:
|
||||
reasons.append("observed-stationary-not-permanent")
|
||||
if threat_decision is ThreatDecision.THREAT:
|
||||
reasons.append("reference-graph-replay-threat")
|
||||
responses.insert(0, AdvisoryResponse.STOP)
|
||||
elif threat_decision is ThreatDecision.UNKNOWN:
|
||||
reasons.append("reference-graph-threat-unknown")
|
||||
else:
|
||||
reasons.append("reference-graph-threat-clear-at-observation")
|
||||
return M48sSemanticAdvisory(
|
||||
component_id=component_id,
|
||||
semantic_hint=semantic_hint,
|
||||
family=family,
|
||||
motion=motion,
|
||||
threat_decision=threat_decision,
|
||||
responses=tuple(dict.fromkeys(responses)),
|
||||
reason_codes=tuple(reasons),
|
||||
)
|
||||
|
||||
|
||||
def _family(semantic_hint: str | None) -> AdvisoryFamily:
|
||||
if semantic_hint == "person":
|
||||
return AdvisoryFamily.PERSON
|
||||
if semantic_hint in {
|
||||
"bird",
|
||||
"cat",
|
||||
"dog",
|
||||
"horse",
|
||||
"sheep",
|
||||
"cow",
|
||||
"elephant",
|
||||
"bear",
|
||||
"zebra",
|
||||
"giraffe",
|
||||
}:
|
||||
return AdvisoryFamily.ANIMAL
|
||||
if semantic_hint in {"bicycle", "motorcycle", "skateboard"}:
|
||||
return AdvisoryFamily.LIGHT_ROAD_USER
|
||||
if semantic_hint in {"car", "bus", "truck"}:
|
||||
return AdvisoryFamily.VEHICLE
|
||||
return AdvisoryFamily.GENERIC_OBSTACLE
|
||||
|
||||
|
||||
__all__ = [
|
||||
"M48S_ADVISORY_POLICY_ID",
|
||||
"M48S_ADVISORY_SCHEMA",
|
||||
"AdvisoryFamily",
|
||||
"M48sAdvisoryError",
|
||||
"M48sSemanticAdvisory",
|
||||
"advisory_policy_matrix",
|
||||
"project_m48s_advisories",
|
||||
]
|
||||
@@ -0,0 +1,252 @@
|
||||
"""Shadow assembly for RF-DETR inside the source-neutral reference graph."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Callable, Iterator
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from threading import Event
|
||||
|
||||
from .baseline import load_m4_baseline
|
||||
from .detector import RF_DETR_SHADOW_PROVIDER_ID, RfDetrShadowDetectorProvider
|
||||
from .geometry import (
|
||||
Ravnoves00GeometryAssociationProvider,
|
||||
RecordedGeometryStore,
|
||||
load_geometry_profile,
|
||||
)
|
||||
from .graph import DeliveryEvidenceObserver, ReferencePerceptionGraphV2
|
||||
from .graph_contracts import DeliveredFrame, GraphRunMode
|
||||
from .motion import ClassIndependentMotionEstimator
|
||||
from .providers import (
|
||||
ProviderRole,
|
||||
ReferencePerceptionGraphConfigV2,
|
||||
SourcePacket,
|
||||
SourceProvider,
|
||||
)
|
||||
from .recorded_source import (
|
||||
DecodedRecordedSource,
|
||||
PyAvRecordedImageDecoder,
|
||||
RecordedRavnoves00Source,
|
||||
ReplayPacing,
|
||||
)
|
||||
from .reference_graph_runtime import ReferenceGraphRuntimePaths
|
||||
from .rf_detr_object_detector import (
|
||||
RF_DETR_ENGINE_SHA256,
|
||||
RF_DETR_MODEL_ID,
|
||||
RF_DETR_MODEL_VERSION,
|
||||
TritonRfDetrHttpInferenceBackend,
|
||||
)
|
||||
from .rolling_map import RollingLocalObstacleMapProvider, load_rolling_map_profile
|
||||
from .temporal import BoundedSpatialTemporalProvider, load_temporal_motion_profile
|
||||
from .threat import (
|
||||
DualEvidenceReplayThreatProvider,
|
||||
RecordedReplayBodyFrameResolver,
|
||||
load_replay_threat_profile,
|
||||
)
|
||||
from .yolox_object_detector import load_valid_fov_mask
|
||||
|
||||
|
||||
class M48sReferenceGraphRuntimeError(RuntimeError):
|
||||
"""The RF-DETR graph shadow cannot be assembled from its pinned inputs."""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class M48sReferenceGraphRuntime:
|
||||
"""Own one RF-DETR shadow graph and its persistent inference transport."""
|
||||
|
||||
graph: ReferencePerceptionGraphV2
|
||||
inference_backend: TritonRfDetrHttpInferenceBackend
|
||||
|
||||
def close(self) -> None:
|
||||
self.inference_backend.close()
|
||||
|
||||
def __enter__(self) -> M48sReferenceGraphRuntime:
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
self.close()
|
||||
|
||||
|
||||
def build_m48s_reference_graph_runtime(
|
||||
*,
|
||||
paths: ReferenceGraphRuntimePaths,
|
||||
detector_profile: Path,
|
||||
triton_origin: str,
|
||||
run_mode: GraphRunMode,
|
||||
delivery_observer: Callable[[DeliveredFrame, int], None] | None = None,
|
||||
delivery_evidence_observer: DeliveryEvidenceObserver | None = None,
|
||||
maximum_frames: int | None = None,
|
||||
) -> M48sReferenceGraphRuntime:
|
||||
"""Instantiate the complete graph with only its detector pin replaced."""
|
||||
|
||||
config = _load_graph_config(paths.graph_config)
|
||||
pinned_files = {
|
||||
ProviderRole.SOURCE: paths.baseline_profile,
|
||||
ProviderRole.DETECTOR: detector_profile,
|
||||
ProviderRole.GEOMETRY: paths.geometry_profile,
|
||||
ProviderRole.TEMPORAL: paths.temporal_motion_profile,
|
||||
ProviderRole.MOTION: paths.temporal_motion_profile,
|
||||
ProviderRole.ROLLING: paths.rolling_map_profile,
|
||||
ProviderRole.THREAT: paths.threat_profile,
|
||||
}
|
||||
_validate_provider_digests(config, pinned_files)
|
||||
_validate_detector_profile(detector_profile)
|
||||
|
||||
load_m4_baseline(paths.baseline_profile)
|
||||
geometry_profile = load_geometry_profile(paths.geometry_profile)
|
||||
temporal_motion_profile = load_temporal_motion_profile(paths.temporal_motion_profile)
|
||||
rolling_map_profile = load_rolling_map_profile(paths.rolling_map_profile)
|
||||
threat_profile = load_replay_threat_profile(paths.threat_profile)
|
||||
|
||||
if maximum_frames is not None and maximum_frames < 1:
|
||||
raise M48sReferenceGraphRuntimeError("maximum frame count must be positive")
|
||||
source: SourceProvider = DecodedRecordedSource(
|
||||
source=RecordedRavnoves00Source(
|
||||
camera_index_path=paths.camera_index,
|
||||
source_pack_path=paths.source_pack,
|
||||
pacing=(
|
||||
ReplayPacing.ONE_X
|
||||
if run_mode is GraphRunMode.SOURCE_PACED_LATEST_WINS
|
||||
else ReplayPacing.UNCAPPED
|
||||
),
|
||||
),
|
||||
decoder=PyAvRecordedImageDecoder(paths.video),
|
||||
)
|
||||
if maximum_frames is not None:
|
||||
source = _LimitedSource(source, maximum_frames)
|
||||
backend = TritonRfDetrHttpInferenceBackend(triton_origin)
|
||||
try:
|
||||
store = RecordedGeometryStore(
|
||||
source_pack_path=paths.source_pack,
|
||||
local_surface_path=paths.local_surface,
|
||||
profile=geometry_profile,
|
||||
)
|
||||
body_frame_resolver = RecordedReplayBodyFrameResolver(
|
||||
store,
|
||||
profile=threat_profile.body_frame,
|
||||
)
|
||||
graph = ReferencePerceptionGraphV2(
|
||||
config=config,
|
||||
source=source,
|
||||
detector=RfDetrShadowDetectorProvider(
|
||||
mask=load_valid_fov_mask(paths.valid_fov_mask),
|
||||
backend=backend,
|
||||
),
|
||||
geometry=Ravnoves00GeometryAssociationProvider(store=store),
|
||||
temporal=BoundedSpatialTemporalProvider(
|
||||
point_resolver=store,
|
||||
profile=temporal_motion_profile,
|
||||
),
|
||||
motion=ClassIndependentMotionEstimator(profile=temporal_motion_profile),
|
||||
rolling=RollingLocalObstacleMapProvider(
|
||||
pose_resolver=store,
|
||||
profile=rolling_map_profile,
|
||||
),
|
||||
threat=DualEvidenceReplayThreatProvider(
|
||||
body_frame_resolver=body_frame_resolver,
|
||||
profile=threat_profile,
|
||||
),
|
||||
run_mode=run_mode,
|
||||
delivery_observer=delivery_observer,
|
||||
delivery_evidence_observer=delivery_evidence_observer,
|
||||
)
|
||||
except Exception:
|
||||
backend.close()
|
||||
raise
|
||||
return M48sReferenceGraphRuntime(graph=graph, inference_backend=backend)
|
||||
|
||||
|
||||
class _LimitedSource:
|
||||
"""Bound a pilot without changing source or graph provider identity."""
|
||||
|
||||
def __init__(self, source: SourceProvider, maximum_frames: int) -> None:
|
||||
self.source = source
|
||||
self.maximum_frames = maximum_frames
|
||||
self.provider_id = source.provider_id
|
||||
|
||||
def packets(self, stop_event: Event) -> Iterator[SourcePacket]:
|
||||
packets = self.source.packets(stop_event)
|
||||
for _index in range(self.maximum_frames):
|
||||
try:
|
||||
packet = next(packets)
|
||||
except StopIteration:
|
||||
return
|
||||
yield packet
|
||||
|
||||
|
||||
def _load_graph_config(path: Path) -> ReferencePerceptionGraphConfigV2:
|
||||
try:
|
||||
document = json.loads(path.resolve(strict=True).read_text("utf-8"))
|
||||
return ReferencePerceptionGraphConfigV2.from_dict(document)
|
||||
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
||||
raise M48sReferenceGraphRuntimeError("RF-DETR graph config is invalid") from exc
|
||||
|
||||
|
||||
def _validate_provider_digests(
|
||||
config: ReferencePerceptionGraphConfigV2,
|
||||
pinned_files: dict[ProviderRole, Path],
|
||||
) -> None:
|
||||
pins = {pin.role: pin for pin in config.providers}
|
||||
if set(pins) != set(pinned_files):
|
||||
raise M48sReferenceGraphRuntimeError("RF-DETR graph provider pins are incomplete")
|
||||
for role, path in pinned_files.items():
|
||||
if _sha256_file(path) != pins[role].sha256:
|
||||
raise M48sReferenceGraphRuntimeError(
|
||||
f"{role.value} provider profile digest changed"
|
||||
)
|
||||
|
||||
|
||||
def _validate_detector_profile(path: Path) -> None:
|
||||
try:
|
||||
document = json.loads(path.resolve(strict=True).read_text("utf-8"))
|
||||
model = document["model"]
|
||||
status = document["status"]
|
||||
authority = document["authority"]
|
||||
except (OSError, KeyError, TypeError, json.JSONDecodeError) as exc:
|
||||
raise M48sReferenceGraphRuntimeError("RF-DETR profile is incomplete") from exc
|
||||
if (
|
||||
document.get("schema_version")
|
||||
!= "missioncore.rf-detr-risk-shadow-profile/v0"
|
||||
or document.get("provider_id") != RF_DETR_SHADOW_PROVIDER_ID
|
||||
or model.get("model_id") != RF_DETR_MODEL_ID
|
||||
or model.get("model_version") != RF_DETR_MODEL_VERSION
|
||||
or model.get("worker_006_rtx4090_tensorrt_11_engine_sha256")
|
||||
!= RF_DETR_ENGINE_SHA256
|
||||
or status.get("detector_load_gate_passed") is not True
|
||||
or status.get("production_accepted") is not False
|
||||
or any(
|
||||
authority.get(key) is not False
|
||||
for key in (
|
||||
"candidate_accepted",
|
||||
"commands_enabled",
|
||||
"actuation_allowed",
|
||||
"navigation_or_safety_accepted",
|
||||
)
|
||||
)
|
||||
):
|
||||
raise M48sReferenceGraphRuntimeError("RF-DETR shadow profile identity changed")
|
||||
|
||||
|
||||
def _sha256_file(path: Path) -> str:
|
||||
try:
|
||||
resolved = path.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise M48sReferenceGraphRuntimeError("pinned RF-DETR graph input is missing") from exc
|
||||
if resolved.is_symlink() or not resolved.is_file():
|
||||
raise M48sReferenceGraphRuntimeError(
|
||||
"pinned RF-DETR graph input must be a regular file"
|
||||
)
|
||||
digest = hashlib.sha256()
|
||||
with resolved.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"M48sReferenceGraphRuntime",
|
||||
"M48sReferenceGraphRuntimeError",
|
||||
"build_m48s_reference_graph_runtime",
|
||||
]
|
||||
@@ -0,0 +1,426 @@
|
||||
"""Bounded camera, LiDAR, and world-state projection of sealed M4.8S replay evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
from dataclasses import dataclass
|
||||
from itertools import pairwise
|
||||
from pathlib import Path
|
||||
from threading import RLock
|
||||
from typing import Final
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .geometry import RecordedGeometryStore
|
||||
from .geometry_math import project_map_points_kb4
|
||||
from .recorded_source import RECORDED_REPRESENTATION_ID
|
||||
from .spatial_evidence import project_metric_obstacles_to_body, sample_points_in_body_frame
|
||||
from .threat import (
|
||||
DEFAULT_REPLAY_THREAT_PROFILE_PATH,
|
||||
RecordedReplayBodyFrameResolver,
|
||||
load_replay_threat_profile,
|
||||
)
|
||||
from .threat_timeline import (
|
||||
RECORDED_LOCAL_SURFACE_POINT_LIMIT,
|
||||
RECORDED_LOCAL_SURFACE_RADIUS_M,
|
||||
RECORDED_LOCAL_SURFACE_VOXEL_SIZE_M,
|
||||
RECORDED_LOCAL_SURFACE_WINDOW_SECONDS,
|
||||
RECORDED_SPATIAL_CHUNK_SCHEMA,
|
||||
RECORDED_SPATIAL_FRAME_SCHEMA,
|
||||
RECORDED_SPATIAL_MAX_CHUNK_FRAMES,
|
||||
RECORDED_SPATIAL_POINT_LIMIT,
|
||||
RECORDED_SPATIAL_TIMELINE_SCHEMA,
|
||||
)
|
||||
|
||||
FRAME_EVIDENCE_SCHEMA: Final = "missioncore.m48s-reference-graph-frame-evidence/v0"
|
||||
EXPECTED_FRAME_COUNT: Final = 4_489
|
||||
|
||||
|
||||
class M48sReplayTimelineError(RuntimeError):
|
||||
"""The sealed M4.8S evidence cannot produce an exact bounded timeline."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _LedgerIndex:
|
||||
offsets_by_sequence: dict[int, int]
|
||||
|
||||
|
||||
class M48sReplayTimeline:
|
||||
"""Read source-indexed chunks while preserving latest-wins world-state gaps."""
|
||||
|
||||
def __init__(self, *, repository_root: Path, result_root: Path, result_id: str) -> None:
|
||||
self.repository_root = repository_root.resolve(strict=True)
|
||||
self.result_root = result_root.resolve(strict=True)
|
||||
self.result_id = result_id
|
||||
self.frames_path = (
|
||||
self.result_root / "reference-graph-replay-frames.jsonl"
|
||||
).resolve(strict=True)
|
||||
self.worker_path = (
|
||||
self.result_root / "reference-graph-replay-worker-result.json"
|
||||
).resolve(strict=True)
|
||||
if (
|
||||
self.frames_path.parent != self.result_root
|
||||
or self.worker_path.parent != self.result_root
|
||||
or self.frames_path.is_symlink()
|
||||
or self.worker_path.is_symlink()
|
||||
):
|
||||
raise M48sReplayTimelineError("M4.8S replay artifacts are invalid")
|
||||
self.profile = load_replay_threat_profile(
|
||||
self.repository_root / DEFAULT_REPLAY_THREAT_PROFILE_PATH
|
||||
)
|
||||
self.store = RecordedGeometryStore.from_repository(self.repository_root)
|
||||
if self.store.profile.frame_count != EXPECTED_FRAME_COUNT:
|
||||
raise M48sReplayTimelineError("M4.8S source frame count changed")
|
||||
self.body_frames = RecordedReplayBodyFrameResolver(
|
||||
self.store,
|
||||
profile=self.profile.body_frame,
|
||||
)
|
||||
self.source_times_ns = tuple(
|
||||
self.store.temporal_binding_for_index(sequence).source_time_ns
|
||||
for sequence in range(EXPECTED_FRAME_COUNT)
|
||||
)
|
||||
if any(right <= left for left, right in pairwise(self.source_times_ns)):
|
||||
raise M48sReplayTimelineError("M4.8S source clock is not monotonic")
|
||||
worker = _object(json.loads(self.worker_path.read_text("utf-8")), "worker result")
|
||||
self.outcomes = _terminal_outcomes(worker)
|
||||
self.index = _index_ledger(self.frames_path, self.source_times_ns, self.outcomes)
|
||||
self._lock = RLock()
|
||||
|
||||
def metadata(self) -> dict[str, object]:
|
||||
intervals = [
|
||||
(current - previous) / 1_000_000_000
|
||||
for previous, current in pairwise(self.source_times_ns)
|
||||
]
|
||||
nominal_interval = statistics.median(intervals)
|
||||
return {
|
||||
"schema_version": RECORDED_SPATIAL_TIMELINE_SCHEMA,
|
||||
"result_id": self.result_id,
|
||||
"recorded_source": {
|
||||
"session_id": self.profile.session_id,
|
||||
"source_id": self.profile.source_id,
|
||||
"representation_id": RECORDED_REPRESENTATION_ID,
|
||||
"synchronization": "host-arrival-best-effort",
|
||||
},
|
||||
"frame_count": EXPECTED_FRAME_COUNT,
|
||||
"frame_times_ns": list(self.source_times_ns),
|
||||
"timeline_start_seconds": self.source_times_ns[0] / 1_000_000_000,
|
||||
"timeline_end_seconds": self.source_times_ns[-1] / 1_000_000_000,
|
||||
"nominal_frame_interval_seconds": nominal_interval,
|
||||
"nominal_rate_hz": 1 / nominal_interval,
|
||||
"max_chunk_frames": RECORDED_SPATIAL_MAX_CHUNK_FRAMES,
|
||||
"point_sample_limit": RECORDED_SPATIAL_POINT_LIMIT,
|
||||
"maximum_source_points_per_frame": self.store.maximum_current_point_count,
|
||||
"point_delivery": "exact-current-increment",
|
||||
"camera_point_delivery": "factory-kb4-projected-current-increment",
|
||||
"camera_point_sample_limit": RECORDED_SPATIAL_POINT_LIMIT,
|
||||
"world_state_delivery": "source-paced-latest-wins",
|
||||
"world_state_frame_count": len(self.index.offsets_by_sequence),
|
||||
"superseded_frame_count": sum(value == "superseded" for value in self.outcomes.values()),
|
||||
"local_surface_visualization": {
|
||||
"derivation": "bounded-registered-increment-accumulation",
|
||||
"window_seconds": RECORDED_LOCAL_SURFACE_WINDOW_SECONDS,
|
||||
"voxel_size_m": RECORDED_LOCAL_SURFACE_VOXEL_SIZE_M,
|
||||
"radius_m": RECORDED_LOCAL_SURFACE_RADIUS_M,
|
||||
"point_limit": RECORDED_LOCAL_SURFACE_POINT_LIMIT,
|
||||
"authority": "visual-derived",
|
||||
},
|
||||
"image_width": 800,
|
||||
"image_height": 600,
|
||||
"rig": {
|
||||
"length_m": self.profile.rig.body_length_m,
|
||||
"width_m": self.profile.rig.body_width_m,
|
||||
"nominal_sensor_height_m": self.profile.rig.nominal_sensor_height_m,
|
||||
},
|
||||
"corridor": {
|
||||
"forward_length_m": self.profile.corridor.forward_length_m,
|
||||
"rear_margin_m": self.profile.corridor.rear_margin_m,
|
||||
"occupied_voxel_size_m": self.profile.corridor.occupied_voxel_size_m,
|
||||
"half_width_m": (
|
||||
self.profile.rig.body_width_m / 2
|
||||
+ self.profile.corridor.lateral_clearance_m
|
||||
),
|
||||
"prediction_horizon_seconds": self.profile.corridor.prediction_horizon_seconds,
|
||||
},
|
||||
"ground_truth": False,
|
||||
"authority": "replay-simulated",
|
||||
"access": "read-only-bounded-recorded-replay",
|
||||
}
|
||||
|
||||
def chunk(self, *, start_sequence: int, frame_count: int) -> dict[str, object]:
|
||||
if not 0 <= start_sequence < EXPECTED_FRAME_COUNT:
|
||||
raise M48sReplayTimelineError("M4.8S timeline start is invalid")
|
||||
if not 1 <= frame_count <= RECORDED_SPATIAL_MAX_CHUNK_FRAMES:
|
||||
raise M48sReplayTimelineError("M4.8S timeline chunk size is invalid")
|
||||
stop = min(EXPECTED_FRAME_COUNT, start_sequence + frame_count)
|
||||
with self._lock:
|
||||
frames = [self._project_frame(sequence) for sequence in range(start_sequence, stop)]
|
||||
return {
|
||||
"schema_version": RECORDED_SPATIAL_CHUNK_SCHEMA,
|
||||
"result_id": self.result_id,
|
||||
"start_sequence": start_sequence,
|
||||
"frame_count": len(frames),
|
||||
"next_sequence": stop if stop < EXPECTED_FRAME_COUNT else None,
|
||||
"frames": frames,
|
||||
"ground_truth": False,
|
||||
"authority": "replay-simulated",
|
||||
"access": "read-only-bounded-recorded-replay",
|
||||
}
|
||||
|
||||
def _project_frame(self, sequence: int) -> dict[str, object]:
|
||||
terminal_outcome = self.outcomes[sequence]
|
||||
row = self._row(sequence)
|
||||
frame_id = (
|
||||
_text(_object(row["source_envelope"], "source envelope").get("frame_id"), "frame id")
|
||||
if row is not None
|
||||
else f"frame-{sequence:06d}"
|
||||
)
|
||||
binding = self.store.temporal_binding_for_index(sequence)
|
||||
frame = self.store.frame_for_index(sequence)
|
||||
body_frame = self.body_frames.body_frame_for_frame(frame_id)
|
||||
point_cloud: list[list[float]] = []
|
||||
point_source_count = 0
|
||||
projected_points: list[list[float]] = []
|
||||
projected_source_count = 0
|
||||
projected_front_count = 0
|
||||
projected_total = 0
|
||||
if frame is not None:
|
||||
projected = project_map_points_kb4(
|
||||
frame.points_map,
|
||||
position_map_xyz=frame.sensor_position_map,
|
||||
orientation_map_from_lidar_xyzw=frame.sensor_orientation_xyzw,
|
||||
profile=frame.projection,
|
||||
)
|
||||
projected_source_count = projected.source_point_count
|
||||
projected_front_count = projected.camera_front_point_count
|
||||
projected_total = projected.projected_point_count
|
||||
stride = max(1, math.ceil(projected_total / RECORDED_SPATIAL_POINT_LIMIT))
|
||||
indices = np.arange(0, projected_total, stride, dtype=np.int64)[
|
||||
:RECORDED_SPATIAL_POINT_LIMIT
|
||||
]
|
||||
if indices.size:
|
||||
xy = projected.pixels_xy[indices]
|
||||
depth = projected.depths_m[indices, None]
|
||||
projected_points = np.round(np.concatenate((xy, depth), axis=1), 4).tolist()
|
||||
if body_frame is not None:
|
||||
point_cloud, point_source_count = sample_points_in_body_frame(
|
||||
frame.points_map,
|
||||
body_frame,
|
||||
point_limit=RECORDED_SPATIAL_POINT_LIMIT,
|
||||
)
|
||||
|
||||
metric_visuals: list[dict[str, object]] = []
|
||||
camera_proposals: list[dict[str, object]] = []
|
||||
assessments: list[dict[str, object]] = []
|
||||
if row is not None:
|
||||
envelope = _object(row.get("source_envelope"), "source envelope")
|
||||
timestamps = _object(envelope.get("timestamps"), "source timestamps")
|
||||
if (
|
||||
envelope.get("sequence") != sequence
|
||||
or timestamps.get("source_ns") != binding.source_time_ns
|
||||
or terminal_outcome != "delivered"
|
||||
):
|
||||
raise M48sReplayTimelineError("M4.8S delivered frame binding changed")
|
||||
delivery = _object(row.get("delivery"), "delivery")
|
||||
obstacle_map = _object(delivery.get("obstacle_map"), "obstacle map")
|
||||
assessments = _objects(delivery.get("threats"), "threats")
|
||||
assessment_by_component = {
|
||||
_text(item.get("component_id"), "assessment component"): item
|
||||
for item in assessments
|
||||
}
|
||||
metric_rows: list[dict[str, object]] = []
|
||||
for obstacle in (
|
||||
*_objects(obstacle_map.get("occupied"), "occupied obstacles"),
|
||||
*_objects(obstacle_map.get("unknown"), "unknown obstacles"),
|
||||
):
|
||||
component_id = _text(obstacle.get("component_id"), "component id")
|
||||
centroid = obstacle.get("last_centroid_xyz_m")
|
||||
if centroid is None:
|
||||
continue
|
||||
metric_rows.append(
|
||||
{
|
||||
"component_id": component_id,
|
||||
"state": obstacle.get("state"),
|
||||
"motion": obstacle.get("motion"),
|
||||
"centroid_map_xyz_m": centroid,
|
||||
"cells": obstacle.get("cells"),
|
||||
"assessment": assessment_by_component[component_id],
|
||||
}
|
||||
)
|
||||
if body_frame is not None:
|
||||
metric_visuals = project_metric_obstacles_to_body(
|
||||
metric_rows,
|
||||
body_frame,
|
||||
occupied_voxel_size_m=self.profile.corridor.occupied_voxel_size_m,
|
||||
)
|
||||
associated = set(_strings(row.get("associated_proposal_ids"), "associated ids"))
|
||||
for proposal in _objects(row.get("detector_proposals"), "detector proposals"):
|
||||
proposal_id = _text(proposal.get("proposal_id"), "proposal id")
|
||||
region = _object(proposal.get("region"), "proposal region")
|
||||
assessment = assessment_by_component.get(proposal_id)
|
||||
camera_proposals.append(
|
||||
{
|
||||
"proposal_id": proposal_id,
|
||||
"bbox_xyxy": [
|
||||
region.get("x_min"),
|
||||
region.get("y_min"),
|
||||
region.get("x_max"),
|
||||
region.get("y_max"),
|
||||
],
|
||||
"objectness": proposal.get("objectness"),
|
||||
"semantic_hint": proposal.get("semantic_hint"),
|
||||
"occupied_support": proposal_id in associated,
|
||||
"range_m": None,
|
||||
"threat_decision": None if assessment is None else assessment.get("decision"),
|
||||
"threat_reason_codes": []
|
||||
if assessment is None
|
||||
else assessment.get("reason_codes"),
|
||||
}
|
||||
)
|
||||
return {
|
||||
"schema_version": RECORDED_SPATIAL_FRAME_SCHEMA,
|
||||
"sequence": sequence,
|
||||
"frame_id": frame_id,
|
||||
"source_time_ns": binding.source_time_ns,
|
||||
"session_seconds": binding.source_time_ns / 1_000_000_000,
|
||||
"source_available": binding.source_available,
|
||||
"spatial_available": body_frame is not None and frame is not None,
|
||||
"world_state_available": row is not None,
|
||||
"terminal_outcome": terminal_outcome,
|
||||
"body_frame": None
|
||||
if body_frame is None
|
||||
else {
|
||||
"origin_map_xyz_m": list(body_frame.origin_map_xyz_m),
|
||||
"basis_map_from_body": [list(value) for value in body_frame.basis_map_from_body],
|
||||
},
|
||||
"point_cloud_body_xyz_m": point_cloud,
|
||||
"point_cloud_source_count": point_source_count,
|
||||
"point_cloud_sample_count": len(point_cloud),
|
||||
"point_cloud_layer": "current-increment",
|
||||
"camera_projected_points_xyd": projected_points,
|
||||
"camera_projected_source_count": projected_source_count,
|
||||
"camera_projected_front_count": projected_front_count,
|
||||
"camera_projected_point_count": projected_total,
|
||||
"camera_projected_sample_count": len(projected_points),
|
||||
"camera_projection": "factory-kb4-exact",
|
||||
"rolling_map_component_count": sum(
|
||||
item.get("state") == "retained" for item in metric_visuals
|
||||
),
|
||||
"metric_obstacles": metric_visuals,
|
||||
"camera_proposals": camera_proposals,
|
||||
"decision_counts": _decision_counts(assessments),
|
||||
"camera_url": (
|
||||
"/api/v1/laboratory/m48s/fixed-class-detector/"
|
||||
f"{self.result_id}/timeline/frames/{sequence}/camera"
|
||||
),
|
||||
"ground_truth": False,
|
||||
"authority": "replay-simulated",
|
||||
}
|
||||
|
||||
def _row(self, sequence: int) -> dict[str, object] | None:
|
||||
offset = self.index.offsets_by_sequence.get(sequence)
|
||||
if offset is None:
|
||||
return None
|
||||
with self.frames_path.open("rb") as stream:
|
||||
stream.seek(offset)
|
||||
line = stream.readline()
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict) or value.get("schema_version") != FRAME_EVIDENCE_SCHEMA:
|
||||
raise M48sReplayTimelineError("M4.8S frame row is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _index_ledger(
|
||||
path: Path,
|
||||
source_times_ns: tuple[int, ...],
|
||||
outcomes: dict[int, str],
|
||||
) -> _LedgerIndex:
|
||||
offsets: dict[int, int] = {}
|
||||
with path.open("rb") as stream:
|
||||
while True:
|
||||
offset = stream.tell()
|
||||
line = stream.readline()
|
||||
if not line:
|
||||
break
|
||||
row = json.loads(line)
|
||||
if not isinstance(row, dict) or row.get("schema_version") != FRAME_EVIDENCE_SCHEMA:
|
||||
raise M48sReplayTimelineError("M4.8S ledger schema changed")
|
||||
envelope = _object(row.get("source_envelope"), "source envelope")
|
||||
timestamps = _object(envelope.get("timestamps"), "source timestamps")
|
||||
sequence = envelope.get("sequence")
|
||||
if (
|
||||
not isinstance(sequence, int)
|
||||
or isinstance(sequence, bool)
|
||||
or not 0 <= sequence < EXPECTED_FRAME_COUNT
|
||||
or sequence in offsets
|
||||
or outcomes.get(sequence) != "delivered"
|
||||
or timestamps.get("source_ns") != source_times_ns[sequence]
|
||||
):
|
||||
raise M48sReplayTimelineError("M4.8S ledger source binding changed")
|
||||
offsets[sequence] = offset
|
||||
delivered = {sequence for sequence, outcome in outcomes.items() if outcome == "delivered"}
|
||||
if set(offsets) != delivered:
|
||||
raise M48sReplayTimelineError("M4.8S ledger does not match delivered outcomes")
|
||||
return _LedgerIndex(offsets)
|
||||
|
||||
|
||||
def _terminal_outcomes(worker: dict[str, object]) -> dict[int, str]:
|
||||
execution = _object(worker.get("execution"), "execution")
|
||||
loops = execution.get("loops")
|
||||
if not isinstance(loops, list) or len(loops) != 1:
|
||||
raise M48sReplayTimelineError("M4.8S worker loop identity changed")
|
||||
details = _object(loops[0], "worker loop").get("terminal_outcome_details")
|
||||
result: dict[int, str] = {}
|
||||
for item in _objects(details, "terminal outcomes"):
|
||||
sequence = item.get("sequence")
|
||||
outcome = item.get("outcome")
|
||||
if (
|
||||
not isinstance(sequence, int)
|
||||
or isinstance(sequence, bool)
|
||||
or outcome not in {"delivered", "superseded"}
|
||||
or sequence in result
|
||||
):
|
||||
raise M48sReplayTimelineError("M4.8S terminal outcome is invalid")
|
||||
result[sequence] = outcome
|
||||
if sorted(result) != list(range(EXPECTED_FRAME_COUNT)):
|
||||
raise M48sReplayTimelineError("M4.8S terminal outcomes are incomplete")
|
||||
return result
|
||||
|
||||
|
||||
def _decision_counts(assessments: list[dict[str, object]]) -> dict[str, int]:
|
||||
result = {"threat": 0, "not-threat": 0, "unknown": 0}
|
||||
for assessment in assessments:
|
||||
decision = assessment.get("decision")
|
||||
if not isinstance(decision, str) or decision not in result:
|
||||
raise M48sReplayTimelineError("M4.8S threat decision is invalid")
|
||||
result[decision] += 1
|
||||
return result
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, object]:
|
||||
if not isinstance(value, dict):
|
||||
raise M48sReplayTimelineError(f"M4.8S {label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _objects(value: object, label: str) -> list[dict[str, object]]:
|
||||
if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
|
||||
raise M48sReplayTimelineError(f"M4.8S {label} are invalid")
|
||||
return copy.deepcopy(value)
|
||||
|
||||
|
||||
def _strings(value: object, label: str) -> list[str]:
|
||||
if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
|
||||
raise M48sReplayTimelineError(f"M4.8S {label} are invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _text(value: object, label: str) -> str:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise M48sReplayTimelineError(f"M4.8S {label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
__all__ = ["M48sReplayTimeline", "M48sReplayTimelineError"]
|
||||
Reference in New Issue
Block a user