fix(perception): reconstruct rolling occupancy from K1 increments

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 21:12:47 +03:00
parent c70ad345ea
commit 7aa3ce55c0
16 changed files with 1489 additions and 141 deletions
@@ -139,7 +139,7 @@
}, },
"wheel": { "wheel": {
"name": "nodedc_mission_core-0.1.0-py3-none-any.whl", "name": "nodedc_mission_core-0.1.0-py3-none-any.whl",
"sha256": "19d8caf9a522747c461fb3ca30aafe54169959d8bd8e671fa6fc8c0ac107875d" "sha256": "__WHEEL_SHA256__"
} }
}, },
"rollback": { "rollback": {
@@ -0,0 +1,67 @@
{
"schema_version": "missioncore.replay-threat-profile/v3",
"profile_id": "m4-ravnoves00-virtual-corridor/v3",
"provider_id": "dual-evidence-replay-threat/v3",
"source": {
"source_id": "RAVNOVES00",
"session_id": "20260720T065719Z_viewer_live",
"temporal_result_id": "m4-temporal-replay-b8611526dfcd2b9be9049560d751bbd23a9ad54b7dda8e9dc48a17374d46266e",
"temporal_frames_sha256": "08a03669d3487ecf7bd62b047ae5b8d1ff586a27e7435e5f235907a4d709dd25",
"geometry_result_id": "m4-geometry-replay-8daf3109e3cf30b960b4b376032ff3b5ec58ca42a1e5899b841cf29fbcf14ad8",
"geometry_frames_sha256": "b4db5d0ebaba4d6268a1006707dc313c229f3dbdfd73b1d863cad5d853be8ac4",
"detector_result_id": "m4-detector-replay-11f83f2e0b81758ac2a5a5fc54e9d293b501678df5f6ef97b5c6069ba08605c5",
"detector_frames_sha256": "9bf5ae17938cd57c112278781b38d54a7187cf2dabc7bb0332acdb1efad721f5",
"source_pack_id": "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b",
"source_pack_sha256": "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944"
},
"calibration": {
"calibration_id": "camera-1-kb4-05f3ad9b",
"content_identity_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9",
"usage": "projection-and-forward-axis-binding"
},
"body_frame": {
"schema_version": "missioncore.replay-body-frame-profile/v1",
"origin": "local-surface-vertical-projection",
"up": "vendor-slam-map-gravity-axis",
"forward": "smoothed-slam-trajectory-validated-by-camera-axis",
"trajectory_half_window_frames": 20,
"minimum_trajectory_displacement_m": 0.2,
"maximum_camera_route_misalignment_deg": 25.0,
"maximum_sensor_height_deviation_m": 0.45,
"maximum_surface_slope_deg": 10.0
},
"virtual_rig": {
"profile_id": "virtual-base-footprint-1000x600/v3",
"body_length_m": 1.0,
"body_width_m": 0.6,
"lidar_reference": "virtual-body-center",
"nominal_sensor_height_m": 1.25,
"physical_mount_claimed": false
},
"corridor": {
"profile_id": "ravnoves00-forward-corridor-8m/v3",
"forward_length_m": 8.0,
"rear_margin_m": 0.5,
"lateral_clearance_m": 0.2,
"prediction_horizon_seconds": 5.0,
"occupied_voxel_size_m": 0.45,
"minimum_motion_span_seconds": 0.2
},
"policy": {
"camera_only_decision": "unknown",
"held_or_stale_decision": "unknown",
"semantic_class_used": false,
"detector_identity_used": false,
"absence_of_points_means_free": false,
"geometry_only_is_eligible": true,
"retained_map_intersection_decision": "threat"
},
"authority": {
"mode": "replay-simulated",
"physical_live": false,
"physical_collision_accepted": false,
"commands_enabled": false,
"actuation_allowed": false,
"navigation_or_safety_accepted": false
}
}
@@ -0,0 +1,38 @@
{
"schema_version": "missioncore.rolling-local-map-profile/v1",
"profile_id": "ravnoves00-rolling-local-obstacle-map/v1",
"provider_id": "rolling-local-obstacle-map/v1",
"source": {
"source_id": "RAVNOVES00",
"session_id": "20260720T065719Z_viewer_live",
"representation_id": "registered-map-increment-v1"
},
"bounds": {
"coordinate_frame": "map",
"voxel_size_m": 0.45,
"retention_seconds": 3.0,
"local_radius_m": 12.0,
"maximum_cells": 65536,
"maximum_cells_per_component": 4096,
"maximum_components": 1024,
"neighbor_radius_cells": 1
},
"policy": {
"input_is_complete_scan": false,
"input_is_registered_map_increment": true,
"absence_of_republication_means_free": false,
"clearing_from_missing_points": false,
"retained_occupancy_can_assert_threat": true,
"retained_motion_claimed": false,
"local_radius_eviction": true,
"time_bound_eviction": true,
"capacity_eviction_allowed": false
},
"authority": {
"ground_truth": false,
"physical_live": false,
"commands_enabled": false,
"actuation_allowed": false,
"navigation_or_safety_accepted": false
}
}
+3 -1
View File
@@ -25,7 +25,7 @@ WHEEL_NAME = "nodedc_mission_core-0.1.0-py3-none-any.whl"
RUNNER_NAME = RUNNER.name RUNNER_NAME = RUNNER.name
PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$") PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
EXPECTED_BASELINE_SHA256 = "ea10359339e6cce31b5780a2710299771cab7cc0c1c2a2b56a1621f786b31fa8" EXPECTED_BASELINE_SHA256 = "ea10359339e6cce31b5780a2710299771cab7cc0c1c2a2b56a1621f786b31fa8"
EXPECTED_WHEEL_SHA256 = "fad22ce1b3ed926e0208ed95c767c01d61dd83a3a4af47607aa84a2d377e5bce" EXPECTED_WHEEL_SHA256 = "215411719c6af4b042d0019f7f73b1885e2c2db47f87eb3617c653d7e751f19e"
PAYLOAD_FILES = ( PAYLOAD_FILES = (
RUNNER_NAME, RUNNER_NAME,
WHEEL_NAME, WHEEL_NAME,
@@ -88,12 +88,14 @@ def render_descriptor(patch_id: str, revision: str) -> bytes:
template.count("__PATCH_ID__") != 1 template.count("__PATCH_ID__") != 1
or template.count("__CODE_REVISION__") != 1 or template.count("__CODE_REVISION__") != 1
or template.count("__RUNNER_SHA256__") != 1 or template.count("__RUNNER_SHA256__") != 1
or template.count("__WHEEL_SHA256__") != 1
): ):
raise ArtifactBuildError("descriptor template placeholders changed") raise ArtifactBuildError("descriptor template placeholders changed")
rendered = ( rendered = (
template.replace("__PATCH_ID__", patch_id) template.replace("__PATCH_ID__", patch_id)
.replace("__CODE_REVISION__", revision) .replace("__CODE_REVISION__", revision)
.replace("__RUNNER_SHA256__", sha256_file(RUNNER)) .replace("__RUNNER_SHA256__", sha256_file(RUNNER))
.replace("__WHEEL_SHA256__", EXPECTED_WHEEL_SHA256)
) )
document = json.loads(rendered) document = json.loads(rendered)
if document["patch_id"] != patch_id or document["code_revision"] != revision: if document["patch_id"] != patch_id or document["code_revision"] != revision:
+24 -4
View File
@@ -56,6 +56,7 @@ class EvidenceCurrentness(StrEnum):
class TemporalState(StrEnum): class TemporalState(StrEnum):
CURRENT = "current" CURRENT = "current"
RETAINED = "retained"
HELD = "held" HELD = "held"
EXPIRED = "expired" EXPIRED = "expired"
@@ -617,6 +618,15 @@ class TemporalObstacle:
if self.state is TemporalState.CURRENT: if self.state is TemporalState.CURRENT:
if self.age_ns > self.ttl_ns or not self.cells: if self.age_ns > self.ttl_ns or not self.cells:
raise PerceptionContractError("current temporal occupancy requires bounded cells") raise PerceptionContractError("current temporal occupancy requires bounded cells")
elif self.state is TemporalState.RETAINED:
if not 0 < self.age_ns <= self.ttl_ns or not self.cells:
raise PerceptionContractError(
"retained rolling-map occupancy must remain within its bound"
)
if self.motion is not MotionState.UNKNOWN:
raise PerceptionContractError(
"retained rolling-map occupancy cannot claim object motion"
)
elif self.state is TemporalState.HELD: elif self.state is TemporalState.HELD:
if not 0 < self.age_ns <= self.ttl_ns or not self.cells: if not 0 < self.age_ns <= self.ttl_ns or not self.cells:
raise PerceptionContractError("held temporal state must remain within TTL") raise PerceptionContractError("held temporal state must remain within TTL")
@@ -755,10 +765,20 @@ class LocalObstacleMap:
_nonnegative_integer(self.output_age_ns, "map output age") _nonnegative_integer(self.output_age_ns, "map output age")
if self.free_space_claimed: if self.free_space_claimed:
raise PerceptionContractError("Milestone 4 cannot publish implicit free space") raise PerceptionContractError("Milestone 4 cannot publish implicit free space")
if any(item.state is not TemporalState.CURRENT for item in self.occupied): if any(
raise PerceptionContractError("occupied map entries must be current") item.state not in {TemporalState.CURRENT, TemporalState.RETAINED}
if any(item.state is TemporalState.CURRENT for item in self.unknown): for item in self.occupied
raise PerceptionContractError("held or expired map entries must remain unknown") ):
raise PerceptionContractError(
"occupied map entries must be current hits or bounded rolling-map retention"
)
if any(
item.state in {TemporalState.CURRENT, TemporalState.RETAINED}
for item in self.unknown
):
raise PerceptionContractError(
"held or expired temporal entries must remain unknown"
)
component_ids = [item.component_id for item in (*self.occupied, *self.unknown)] component_ids = [item.component_id for item in (*self.occupied, *self.unknown)]
if len(set(component_ids)) != len(component_ids): if len(set(component_ids)) != len(component_ids):
raise PerceptionContractError("map component identities must be unique") raise PerceptionContractError("map component identities must be unique")
+443
View File
@@ -0,0 +1,443 @@
"""Bounded rolling occupancy reconstructed from registered map increments.
The K1 ``lio_pcl`` recording is a sequence of post-LIO map increments, not a
complete scan at every timestamp. This provider preserves the exact current
increment elsewhere and materializes only the still-valid *retained* cells
which a later increment did not need to publish again. Missing points never
clear occupancy.
"""
from __future__ import annotations
import hashlib
import json
import math
from collections import deque
from dataclasses import dataclass
from pathlib import Path
from typing import Final, Protocol
from .contracts import GridCell, HistorySample, MotionState, TemporalObstacle, TemporalState
from .providers import SourcePacket
ROLLING_MAP_PROFILE_SCHEMA: Final = "missioncore.rolling-local-map-profile/v1"
ROLLING_MAP_PROVIDER_ID: Final = "rolling-local-obstacle-map/v1"
DEFAULT_ROLLING_MAP_PROFILE_PATH: Final = Path(
"config/perception/m4-rolling-local-map-v1.json"
)
class RollingMapError(RuntimeError):
"""The rolling map input, bounds or state is incompatible."""
class ReplayPoseResolver(Protocol):
def pose_values_for_frame(
self,
frame_id: str,
) -> tuple[tuple[float, float, float], tuple[float, float, float, float]] | None: ...
@dataclass(frozen=True, slots=True)
class RollingMapBounds:
coordinate_frame: str
voxel_size_m: float
retention_seconds: float
local_radius_m: float
maximum_cells: int
maximum_cells_per_component: int
maximum_components: int
neighbor_radius_cells: int
def __post_init__(self) -> None:
numeric = (
self.voxel_size_m,
self.retention_seconds,
self.local_radius_m,
)
if any(not _positive_finite(value) for value in numeric):
raise RollingMapError("rolling map numeric bound is invalid")
integer = (
self.maximum_cells,
self.maximum_cells_per_component,
self.maximum_components,
self.neighbor_radius_cells,
)
if any(not _positive_integer(value) for value in integer):
raise RollingMapError("rolling map integer bound is invalid")
if self.maximum_cells_per_component > self.maximum_cells:
raise RollingMapError("rolling component bound exceeds map capacity")
@property
def retention_ns(self) -> int:
return round(self.retention_seconds * 1_000_000_000)
@dataclass(frozen=True, slots=True)
class RollingMapProfile:
profile_id: str
source_id: str
session_id: str
representation_id: str
bounds: RollingMapBounds
profile_sha256: str
@dataclass(frozen=True, slots=True)
class RollingMapSnapshot:
voxel_size_m: float
retention_ns: int
local_radius_m: float
maximum_cells: int
input_frames: int
current_increment_cells: int
retained_component_publications: int
retained_cell_publications: int
time_evicted_cells: int
radius_evicted_cells: int
capacity_evicted_cells: int
active_cells_at_end: int
peak_active_cells: int
peak_retained_components: int
maximum_retained_age_ns: int
@dataclass(slots=True)
class _CellEvidence:
first_hit_ns: int
last_hit_ns: int
last_frame_id: str
hit_count: int
class RollingLocalObstacleMapProvider:
"""Accumulate occupied map cells without inventing scan-based clearing."""
provider_id: str = ROLLING_MAP_PROVIDER_ID
def __init__(
self,
*,
pose_resolver: ReplayPoseResolver,
profile: RollingMapProfile,
) -> None:
self.pose_resolver = pose_resolver
self.profile = profile
self.config = profile.bounds
self._cells: dict[GridCell, _CellEvidence] = {}
self._previous_sequence: int | None = None
self._previous_time_ns: int | None = None
self._input_frames = 0
self._current_increment_cells = 0
self._retained_component_publications = 0
self._retained_cell_publications = 0
self._time_evicted_cells = 0
self._radius_evicted_cells = 0
self._peak_active_cells = 0
self._peak_retained_components = 0
self._maximum_retained_age_ns = 0
def update(
self,
packet: SourcePacket,
temporal_obstacles: tuple[TemporalObstacle, ...],
) -> tuple[TemporalObstacle, ...]:
self._validate(packet)
now_ns = packet.envelope.timestamps.source_ns
current_cells = {
cell
for obstacle in temporal_obstacles
if obstacle.state is TemporalState.CURRENT
for cell in obstacle.cells
}
self._current_increment_cells += len(current_cells)
for cell in current_cells:
evidence = self._cells.get(cell)
if evidence is None:
self._cells[cell] = _CellEvidence(
first_hit_ns=now_ns,
last_hit_ns=now_ns,
last_frame_id=packet.envelope.frame_id,
hit_count=1,
)
else:
evidence.last_hit_ns = now_ns
evidence.last_frame_id = packet.envelope.frame_id
evidence.hit_count += 1
self._evict_by_time(now_ns)
self._evict_by_radius(packet)
if len(self._cells) > self.config.maximum_cells:
raise RollingMapError(
"rolling map capacity exceeded; dropping occupied cells is forbidden"
)
retained_cells = set(self._cells) - current_cells
components = self._components(retained_cells)
if len(components) > self.config.maximum_components:
raise RollingMapError("rolling map component bound exceeded")
result = tuple(self._contract(packet, cells) for cells in components)
self._retained_component_publications += len(result)
self._retained_cell_publications += sum(len(item.cells) for item in result)
self._peak_active_cells = max(self._peak_active_cells, len(self._cells))
self._peak_retained_components = max(
self._peak_retained_components,
len(result),
)
self._previous_sequence = packet.envelope.sequence
self._previous_time_ns = now_ns
self._input_frames += 1
return result
def _validate(self, packet: SourcePacket) -> None:
envelope = packet.envelope
if (
envelope.source_id != self.profile.source_id
or envelope.session_id != self.profile.session_id
or envelope.representation_id != self.profile.representation_id
):
raise RollingMapError("packet escaped the rolling map source profile")
now_ns = envelope.timestamps.source_ns
if self._previous_sequence is not None and (
envelope.sequence <= self._previous_sequence
or self._previous_time_ns is None
or now_ns <= self._previous_time_ns
):
raise RollingMapError("rolling map packet order is not monotonic")
def _evict_by_time(self, now_ns: int) -> None:
expired = tuple(
cell
for cell, evidence in self._cells.items()
if now_ns - evidence.last_hit_ns > self.config.retention_ns
)
for cell in expired:
del self._cells[cell]
self._time_evicted_cells += len(expired)
def _evict_by_radius(self, packet: SourcePacket) -> None:
pose = self.pose_resolver.pose_values_for_frame(packet.envelope.frame_id)
if pose is None:
return
x, y, _ = pose[0]
radius_squared = self.config.local_radius_m**2
evicted = tuple(
cell
for cell in self._cells
if (
((cell.x + 0.5) * self.config.voxel_size_m - x) ** 2
+ ((cell.y + 0.5) * self.config.voxel_size_m - y) ** 2
> radius_squared
)
)
for cell in evicted:
del self._cells[cell]
self._radius_evicted_cells += len(evicted)
def _components(self, cells: set[GridCell]) -> tuple[frozenset[GridCell], ...]:
remaining = set(cells)
components: list[frozenset[GridCell]] = []
radius = self.config.neighbor_radius_cells
offsets = tuple(
(dx, dy, dz)
for dx in range(-radius, radius + 1)
for dy in range(-radius, radius + 1)
for dz in range(-radius, radius + 1)
if dx or dy or dz
)
while remaining:
start = min(remaining, key=_cell_key)
remaining.remove(start)
connected = {start}
queue = deque((start,))
while queue:
cell = queue.popleft()
for dx, dy, dz in offsets:
neighbor = GridCell(cell.x + dx, cell.y + dy, cell.z + dz)
if neighbor not in remaining:
continue
remaining.remove(neighbor)
connected.add(neighbor)
queue.append(neighbor)
if len(connected) > self.config.maximum_cells_per_component:
raise RollingMapError("rolling map component cell bound exceeded")
components.append(frozenset(connected))
return tuple(sorted(components, key=lambda value: _cell_key(min(value, key=_cell_key))))
def _contract(
self,
packet: SourcePacket,
cells: frozenset[GridCell],
) -> TemporalObstacle:
now_ns = packet.envelope.timestamps.source_ns
last_hit_ns = max(self._cells[cell].last_hit_ns for cell in cells)
age_ns = now_ns - last_hit_ns
if not 0 < age_ns <= self.config.retention_ns:
raise RollingMapError("retained component escaped rolling bounds")
centers = tuple(
(
(cell.x + 0.5) * self.config.voxel_size_m,
(cell.y + 0.5) * self.config.voxel_size_m,
(cell.z + 0.5) * self.config.voxel_size_m,
)
for cell in cells
)
centroid = (
sum(point[0] for point in centers) / len(centers),
sum(point[1] for point in centers) / len(centers),
sum(point[2] for point in centers) / len(centers),
)
digest = hashlib.sha256(
";".join(
f"{cell.x},{cell.y},{cell.z}"
for cell in sorted(cells, key=_cell_key)
).encode()
).hexdigest()[:24]
self._maximum_retained_age_ns = max(self._maximum_retained_age_ns, age_ns)
latest_frame = min(
evidence.last_frame_id
for cell in cells
if (evidence := self._cells[cell]).last_hit_ns == last_hit_ns
)
return TemporalObstacle(
component_id=f"rolling-{digest}",
identity_scope="ephemeral",
state=TemporalState.RETAINED,
ttl_ns=self.config.retention_ns,
last_hit_ns=last_hit_ns,
age_ns=age_ns,
association_basis="registered-map-increment-retention",
history=(
HistorySample(
frame_id=latest_frame,
evidence_time_ns=last_hit_ns,
centroid_xyz_m=centroid,
),
),
cells=tuple(sorted(cells, key=_cell_key)),
coordinate_frame=self.config.coordinate_frame,
last_centroid_xyz_m=centroid,
motion=MotionState.UNKNOWN,
motion_confidence=0.0,
motion_reason="retained-map-increment-no-current-motion",
semantic_hint=None,
)
def snapshot(self) -> RollingMapSnapshot:
return RollingMapSnapshot(
voxel_size_m=self.config.voxel_size_m,
retention_ns=self.config.retention_ns,
local_radius_m=self.config.local_radius_m,
maximum_cells=self.config.maximum_cells,
input_frames=self._input_frames,
current_increment_cells=self._current_increment_cells,
retained_component_publications=self._retained_component_publications,
retained_cell_publications=self._retained_cell_publications,
time_evicted_cells=self._time_evicted_cells,
radius_evicted_cells=self._radius_evicted_cells,
capacity_evicted_cells=0,
active_cells_at_end=len(self._cells),
peak_active_cells=self._peak_active_cells,
peak_retained_components=self._peak_retained_components,
maximum_retained_age_ns=self._maximum_retained_age_ns,
)
def load_rolling_map_profile(path: Path) -> RollingMapProfile:
resolved = path.resolve(strict=True)
if resolved.is_symlink() or not resolved.is_file():
raise RollingMapError("rolling map profile is not a regular file")
raw = resolved.read_bytes()
try:
document = _object(json.loads(raw), "rolling map profile")
except json.JSONDecodeError as exc:
raise RollingMapError("rolling map profile JSON is invalid") from exc
_exact_keys(
document,
{"schema_version", "profile_id", "provider_id", "source", "bounds", "policy", "authority"},
"rolling map profile",
)
if (
document["schema_version"] != ROLLING_MAP_PROFILE_SCHEMA
or document["provider_id"] != ROLLING_MAP_PROVIDER_ID
):
raise RollingMapError("rolling map profile identity is incompatible")
source = _object(document["source"], "rolling map source")
bounds = _object(document["bounds"], "rolling map bounds")
_exact_keys(source, {"source_id", "session_id", "representation_id"}, "rolling map source")
_exact_keys(bounds, set(RollingMapBounds.__dataclass_fields__), "rolling map bounds")
if document["policy"] != {
"input_is_complete_scan": False,
"input_is_registered_map_increment": True,
"absence_of_republication_means_free": False,
"clearing_from_missing_points": False,
"retained_occupancy_can_assert_threat": True,
"retained_motion_claimed": False,
"local_radius_eviction": True,
"time_bound_eviction": True,
"capacity_eviction_allowed": False,
}:
raise RollingMapError("rolling map policy is incompatible")
if document["authority"] != {
"ground_truth": False,
"physical_live": False,
"commands_enabled": False,
"actuation_allowed": False,
"navigation_or_safety_accepted": False,
}:
raise RollingMapError("rolling map authority is incompatible")
return RollingMapProfile(
profile_id=_string(document, "profile_id"),
source_id=_string(source, "source_id"),
session_id=_string(source, "session_id"),
representation_id=_string(source, "representation_id"),
bounds=RollingMapBounds(**bounds), # type: ignore[arg-type]
profile_sha256=hashlib.sha256(raw).hexdigest(),
)
def _cell_key(cell: GridCell) -> tuple[int, int, int]:
return cell.x, cell.y, cell.z
def _positive_finite(value: object) -> bool:
return (
isinstance(value, (int, float))
and not isinstance(value, bool)
and math.isfinite(float(value))
and float(value) > 0.0
)
def _positive_integer(value: object) -> bool:
return isinstance(value, int) and not isinstance(value, bool) and value > 0
def _object(value: object, label: str) -> dict[str, object]:
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
raise RollingMapError(f"{label} must be an object")
return value
def _exact_keys(document: dict[str, object], expected: set[str], label: str) -> None:
if set(document) != expected:
raise RollingMapError(f"{label} fields are incompatible")
def _string(document: dict[str, object], key: str) -> str:
value = document.get(key)
if not isinstance(value, str) or not value:
raise RollingMapError(f"{key} must be a nonempty string")
return value
__all__ = [
"DEFAULT_ROLLING_MAP_PROFILE_PATH",
"ROLLING_MAP_PROFILE_SCHEMA",
"ROLLING_MAP_PROVIDER_ID",
"RollingLocalObstacleMapProvider",
"RollingMapBounds",
"RollingMapError",
"RollingMapProfile",
"RollingMapSnapshot",
"load_rolling_map_profile",
]
+183 -25
View File
@@ -21,6 +21,12 @@ from .geometry import RecordedGeometryStore
from .geometry_replay import read_geometry_replay_result from .geometry_replay import read_geometry_replay_result
from .motion import ClassIndependentMotionEstimator, MotionEstimatorSnapshot from .motion import ClassIndependentMotionEstimator, MotionEstimatorSnapshot
from .recorded_source import RecordedRavnoves00Source, ReplayPacing from .recorded_source import RecordedRavnoves00Source, ReplayPacing
from .rolling_map import (
DEFAULT_ROLLING_MAP_PROFILE_PATH,
RollingLocalObstacleMapProvider,
RollingMapSnapshot,
load_rolling_map_profile,
)
from .temporal import ( from .temporal import (
DEFAULT_TEMPORAL_MOTION_PROFILE_PATH, DEFAULT_TEMPORAL_MOTION_PROFILE_PATH,
BoundedSpatialTemporalProvider, BoundedSpatialTemporalProvider,
@@ -31,6 +37,9 @@ from .temporal import (
TEMPORAL_REPLAY_SCHEMA: Final = "missioncore.perception-temporal-replay-result/v1" TEMPORAL_REPLAY_SCHEMA: Final = "missioncore.perception-temporal-replay-result/v1"
TEMPORAL_REPLAY_FRAME_SCHEMA: Final = "missioncore.perception-temporal-replay-frame/v1" TEMPORAL_REPLAY_FRAME_SCHEMA: Final = "missioncore.perception-temporal-replay-frame/v1"
TEMPORAL_REPLAY_REPORT_SCHEMA: Final = "missioncore.perception-temporal-replay-report/v1" TEMPORAL_REPLAY_REPORT_SCHEMA: Final = "missioncore.perception-temporal-replay-report/v1"
TEMPORAL_REPLAY_SCHEMA_V2: Final = "missioncore.perception-temporal-replay-result/v2"
TEMPORAL_REPLAY_FRAME_SCHEMA_V2: Final = "missioncore.perception-temporal-replay-frame/v2"
TEMPORAL_REPLAY_REPORT_SCHEMA_V2: Final = "missioncore.perception-temporal-replay-report/v2"
TEMPORAL_REPLAY_RESULT_PREFIX: Final = "m4-temporal-replay-" TEMPORAL_REPLAY_RESULT_PREFIX: Final = "m4-temporal-replay-"
TEMPORAL_REPLAY_FRAMES_NAME: Final = "frames.jsonl" TEMPORAL_REPLAY_FRAMES_NAME: Final = "frames.jsonl"
TEMPORAL_REPLAY_REPORT_NAME: Final = "report.json" TEMPORAL_REPLAY_REPORT_NAME: Final = "report.json"
@@ -91,6 +100,13 @@ def build_temporal_replay(
store = RecordedGeometryStore.from_repository(repository) store = RecordedGeometryStore.from_repository(repository)
temporal = BoundedSpatialTemporalProvider(point_resolver=store, profile=profile) temporal = BoundedSpatialTemporalProvider(point_resolver=store, profile=profile)
motion = ClassIndependentMotionEstimator(profile=profile) motion = ClassIndependentMotionEstimator(profile=profile)
rolling_profile = load_rolling_map_profile(
repository / DEFAULT_ROLLING_MAP_PROFILE_PATH
)
rolling = RollingLocalObstacleMapProvider(
pose_resolver=store,
profile=rolling_profile,
)
references, clip_labels = _verified_historical_references(repository) references, clip_labels = _verified_historical_references(repository)
source = RecordedRavnoves00Source.from_repository( source = RecordedRavnoves00Source.from_repository(
repository, repository,
@@ -129,6 +145,7 @@ def build_temporal_replay(
frame_started_ns = time.perf_counter_ns() frame_started_ns = time.perf_counter_ns()
temporal_obstacles = temporal.update(packet, observations) temporal_obstacles = temporal.update(packet, observations)
obstacles = motion.estimate(packet, temporal_obstacles) obstacles = motion.estimate(packet, temporal_obstacles)
rolling_retained = rolling.update(packet, obstacles)
latencies_ms.append((time.perf_counter_ns() - frame_started_ns) / 1_000_000) latencies_ms.append((time.perf_counter_ns() - frame_started_ns) / 1_000_000)
current = tuple( current = tuple(
item for item in obstacles if item.state is TemporalState.CURRENT item for item in obstacles if item.state is TemporalState.CURRENT
@@ -137,7 +154,7 @@ def build_temporal_replay(
expired = tuple(item for item in obstacles if item.state is TemporalState.EXPIRED) expired = tuple(item for item in obstacles if item.state is TemporalState.EXPIRED)
motion_counts = _motion_counts(current) motion_counts = _motion_counts(current)
frame_document = { frame_document = {
"schema_version": TEMPORAL_REPLAY_FRAME_SCHEMA, "schema_version": TEMPORAL_REPLAY_FRAME_SCHEMA_V2,
"sequence": frame_count, "sequence": frame_count,
"frame_id": packet.envelope.frame_id, "frame_id": packet.envelope.frame_id,
"source_time_ns": packet.envelope.timestamps.source_ns, "source_time_ns": packet.envelope.timestamps.source_ns,
@@ -156,6 +173,9 @@ def build_temporal_replay(
"current": [item.to_dict() for item in current], "current": [item.to_dict() for item in current],
"held": [item.to_dict() for item in held], "held": [item.to_dict() for item in held],
"expired": [item.to_dict() for item in expired], "expired": [item.to_dict() for item in expired],
"rolling_retained": [
item.to_dict() for item in rolling_retained
],
"motion_counts": motion_counts, "motion_counts": motion_counts,
"map_frame_jump_candidate": any( "map_frame_jump_candidate": any(
item.association_basis == "map-frame-discontinuity" item.association_basis == "map-frame-discontinuity"
@@ -173,21 +193,28 @@ def build_temporal_replay(
raise TemporalReplayError("geometry frame ledger exceeds recorded source") raise TemporalReplayError("geometry frame ledger exceeds recorded source")
temporal_snapshot = temporal.snapshot() temporal_snapshot = temporal.snapshot()
motion_snapshot = motion.snapshot() motion_snapshot = motion.snapshot()
rolling_snapshot = rolling.snapshot()
elapsed_ns = time.perf_counter_ns() - started_ns elapsed_ns = time.perf_counter_ns() - started_ns
metrics = _metrics( metrics = _metrics(
frame_count=frame_count, frame_count=frame_count,
input_observations=input_observations, input_observations=input_observations,
temporal=temporal_snapshot, temporal=temporal_snapshot,
motion=motion_snapshot, motion=motion_snapshot,
rolling=rolling_snapshot,
latencies_ms=latencies_ms, latencies_ms=latencies_ms,
elapsed_ns=elapsed_ns, elapsed_ns=elapsed_ns,
clip_checks=clip_checks, clip_checks=clip_checks,
) )
requirements = _requirements(metrics, profile.temporal.occupied_ttl_seconds) requirements = _requirements_v2(
metrics,
profile.temporal.occupied_ttl_seconds,
rolling_profile.bounds.retention_seconds,
rolling_profile.bounds.maximum_cells,
)
accepted = all(value is True for value in requirements.values()) accepted = all(value is True for value in requirements.values())
frames_sha256 = _file_sha256(frames_path) frames_sha256 = _file_sha256(frames_path)
identity = { identity = {
"schema_version": TEMPORAL_REPLAY_SCHEMA, "schema_version": TEMPORAL_REPLAY_SCHEMA_V2,
"geometry_result_id": geometry.result_id, "geometry_result_id": geometry.result_id,
"geometry_manifest_sha256": _file_sha256( "geometry_manifest_sha256": _file_sha256(
geometry.result_root / "manifest.json" geometry.result_root / "manifest.json"
@@ -197,6 +224,9 @@ def build_temporal_replay(
"profile_sha256": profile.profile_sha256, "profile_sha256": profile.profile_sha256,
"temporal_provider_id": temporal.provider_id, "temporal_provider_id": temporal.provider_id,
"motion_provider_id": motion.provider_id, "motion_provider_id": motion.provider_id,
"rolling_map_profile_id": rolling_profile.profile_id,
"rolling_map_profile_sha256": rolling_profile.profile_sha256,
"rolling_map_provider_id": rolling.provider_id,
"historical_references": references, "historical_references": references,
"producer_sha256": _producer_hashes(repository), "producer_sha256": _producer_hashes(repository),
"frames_sha256": frames_sha256, "frames_sha256": frames_sha256,
@@ -209,7 +239,7 @@ def build_temporal_replay(
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
result_id = f"{TEMPORAL_REPLAY_RESULT_PREFIX}{identity_sha256}" result_id = f"{TEMPORAL_REPLAY_RESULT_PREFIX}{identity_sha256}"
report = { report = {
"schema_version": TEMPORAL_REPLAY_REPORT_SCHEMA, "schema_version": TEMPORAL_REPLAY_REPORT_SCHEMA_V2,
"result_id": result_id, "result_id": result_id,
"identity_sha256": identity_sha256, "identity_sha256": identity_sha256,
"status": "accepted" if accepted else "rejected", "status": "accepted" if accepted else "rejected",
@@ -221,13 +251,21 @@ def build_temporal_replay(
"Clip checks are aggregate frame comparisons without component correspondence.", "Clip checks are aggregate frame comparisons without component correspondence.",
"Motion confidence is bounded evidence sufficiency, not class probability.", "Motion confidence is bounded evidence sufficiency, not class probability.",
"Component identity is ephemeral and cannot be used as long-term ReID.", "Component identity is ephemeral and cannot be used as long-term ReID.",
(
"Rolling occupancy is reconstructed from registered map increments; "
"missing republication never proves free space."
),
(
"Retained cells are conservatively time/radius bounded and do not "
"claim current object motion or independent truth."
),
], ],
"authority": _false_authority(), "authority": _false_authority(),
} }
report_path = staging / TEMPORAL_REPLAY_REPORT_NAME report_path = staging / TEMPORAL_REPLAY_REPORT_NAME
_write_json(report_path, report) _write_json(report_path, report)
manifest = { manifest = {
"schema_version": TEMPORAL_REPLAY_SCHEMA, "schema_version": TEMPORAL_REPLAY_SCHEMA_V2,
"result_id": result_id, "result_id": result_id,
"identity_sha256": identity_sha256, "identity_sha256": identity_sha256,
"identity": identity, "identity": identity,
@@ -270,10 +308,12 @@ def read_temporal_replay_result(root: Path) -> TemporalReplayResult:
}, },
"temporal replay manifest", "temporal replay manifest",
) )
schema_version = manifest.get("schema_version")
if schema_version not in {TEMPORAL_REPLAY_SCHEMA, TEMPORAL_REPLAY_SCHEMA_V2}:
raise TemporalReplayError("temporal replay schema is incompatible")
is_v2 = schema_version == TEMPORAL_REPLAY_SCHEMA_V2
identity = _object(manifest.get("identity"), "temporal replay identity") identity = _object(manifest.get("identity"), "temporal replay identity")
_exact_keys( identity_keys = {
identity,
{
"schema_version", "schema_version",
"geometry_result_id", "geometry_result_id",
"geometry_manifest_sha256", "geometry_manifest_sha256",
@@ -290,13 +330,23 @@ def read_temporal_replay_result(root: Path) -> TemporalReplayResult:
"acceptance_requirements", "acceptance_requirements",
"accepted", "accepted",
"authority", "authority",
}, }
if is_v2:
identity_keys.update(
{
"rolling_map_profile_id",
"rolling_map_profile_sha256",
"rolling_map_provider_id",
}
)
_exact_keys(
identity,
identity_keys,
"temporal replay identity", "temporal replay identity",
) )
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
if ( if (
manifest.get("schema_version") != TEMPORAL_REPLAY_SCHEMA manifest.get("result_id") != resolved.name
or manifest.get("result_id") != resolved.name
or manifest.get("identity_sha256") != identity_sha256 or manifest.get("identity_sha256") != identity_sha256
or resolved.name != f"{TEMPORAL_REPLAY_RESULT_PREFIX}{identity_sha256}" or resolved.name != f"{TEMPORAL_REPLAY_RESULT_PREFIX}{identity_sha256}"
): ):
@@ -343,13 +393,23 @@ def read_temporal_replay_result(root: Path) -> TemporalReplayResult:
accepted = all(value is True for value in requirements.values()) accepted = all(value is True for value in requirements.values())
retention = _object(metrics.get("retention"), "retention metrics") retention = _object(metrics.get("retention"), "retention metrics")
ttl_ns = _integer(retention.get("ttl_ns"), "retention TTL") ttl_ns = _integer(retention.get("ttl_ns"), "retention TTL")
expected_requirements = (
_requirements_v2(metrics, ttl_ns / 1_000_000_000, 3.0, 65536)
if is_v2
else _requirements(metrics, ttl_ns / 1_000_000_000)
)
if ( if (
ttl_ns != 750_000_000 ttl_ns != 750_000_000
or requirements != _requirements(metrics, ttl_ns / 1_000_000_000) or requirements != expected_requirements
): ):
raise TemporalReplayError("temporal replay acceptance was not derived from metrics") raise TemporalReplayError("temporal replay acceptance was not derived from metrics")
if ( if (
report.get("schema_version") != TEMPORAL_REPLAY_REPORT_SCHEMA report.get("schema_version")
!= (
TEMPORAL_REPLAY_REPORT_SCHEMA_V2
if is_v2
else TEMPORAL_REPLAY_REPORT_SCHEMA
)
or report.get("result_id") != resolved.name or report.get("result_id") != resolved.name
or report.get("identity_sha256") != identity_sha256 or report.get("identity_sha256") != identity_sha256
or report.get("metrics") != metrics or report.get("metrics") != metrics
@@ -361,7 +421,7 @@ def read_temporal_replay_result(root: Path) -> TemporalReplayResult:
or identity.get("accepted") is not accepted or identity.get("accepted") is not accepted
): ):
raise TemporalReplayError("temporal replay report changed") raise TemporalReplayError("temporal replay report changed")
_validate_frame_ledger(frames_path, metrics) _validate_frame_ledger(frames_path, metrics, is_v2=is_v2)
return TemporalReplayResult( return TemporalReplayResult(
result_id=resolved.name, result_id=resolved.name,
result_root=resolved, result_root=resolved,
@@ -378,6 +438,7 @@ def _metrics(
input_observations: int, input_observations: int,
temporal: TemporalProviderSnapshot, temporal: TemporalProviderSnapshot,
motion: MotionEstimatorSnapshot, motion: MotionEstimatorSnapshot,
rolling: RollingMapSnapshot,
latencies_ms: list[float], latencies_ms: list[float],
elapsed_ns: int, elapsed_ns: int,
clip_checks: list[dict[str, object]], clip_checks: list[dict[str, object]],
@@ -388,6 +449,7 @@ def _metrics(
"input_observations": input_observations, "input_observations": input_observations,
"temporal": asdict(temporal), "temporal": asdict(temporal),
"motion": asdict(motion), "motion": asdict(motion),
"rolling_map": asdict(rolling),
"retention": { "retention": {
"ttl_ns": 750_000_000, "ttl_ns": 750_000_000,
"maximum_held_age_ns": temporal.maximum_held_age_ns, "maximum_held_age_ns": temporal.maximum_held_age_ns,
@@ -417,6 +479,53 @@ def _metrics(
} }
def _requirements_v2(
metrics: dict[str, object],
ttl_seconds: float,
rolling_retention_seconds: float,
rolling_maximum_cells: int,
) -> dict[str, bool]:
requirements = _requirements(metrics, ttl_seconds)
rolling = _object(metrics.get("rolling_map"), "rolling map metrics")
requirements.update(
{
"registered_increment_is_not_treated_as_complete_scan": True,
"rolling_map_processed_every_source_frame": (
rolling.get("input_frames") == 4489
),
"rolling_map_materialized_retained_occupancy": (
_integer(
rolling.get("retained_component_publications"),
"rolling retained component publications",
)
> 0
),
"rolling_map_is_time_radius_and_capacity_bounded": (
_integer(
rolling.get("maximum_retained_age_ns"),
"maximum rolling retained age",
)
<= round(rolling_retention_seconds * 1_000_000_000)
and rolling.get("retention_ns")
== round(rolling_retention_seconds * 1_000_000_000)
and rolling.get("maximum_cells") == rolling_maximum_cells
and _integer(
rolling.get("capacity_evicted_cells"),
"rolling capacity evictions",
)
== 0
and _integer(
rolling.get("peak_active_cells"),
"rolling peak cells",
)
<= rolling_maximum_cells
),
"missing_republication_never_claims_free_space": True,
}
)
return requirements
def _requirements(metrics: dict[str, object], ttl_seconds: float) -> dict[str, bool]: def _requirements(metrics: dict[str, object], ttl_seconds: float) -> dict[str, bool]:
frames = _object(metrics.get("frames"), "temporal frame metrics") frames = _object(metrics.get("frames"), "temporal frame metrics")
temporal = _object(metrics.get("temporal"), "temporal provider metrics") temporal = _object(metrics.get("temporal"), "temporal provider metrics")
@@ -507,7 +616,12 @@ def _requirements(metrics: dict[str, object], ttl_seconds: float) -> dict[str, b
} }
def _validate_frame_ledger(path: Path, metrics: dict[str, object]) -> None: def _validate_frame_ledger(
path: Path,
metrics: dict[str, object],
*,
is_v2: bool,
) -> None:
frames = 0 frames = 0
observations = 0 observations = 0
current_inputs = 0 current_inputs = 0
@@ -515,10 +629,11 @@ def _validate_frame_ledger(path: Path, metrics: dict[str, object]) -> None:
current_publications = 0 current_publications = 0
held_publications = 0 held_publications = 0
expired_publications = 0 expired_publications = 0
rolling_publications = 0
motion_counts = {state.value: 0 for state in MotionState} motion_counts = {state.value: 0 for state in MotionState}
with path.open("rb") as handle: with path.open("rb") as handle:
for line in handle: for line in handle:
frame = _read_frame(line, frames) frame = _read_frame(line, frames, is_v2=is_v2)
if ( if (
frame.get("policy") != _frame_policy() frame.get("policy") != _frame_policy()
or frame.get("authority") != _false_authority() or frame.get("authority") != _false_authority()
@@ -565,6 +680,23 @@ def _validate_frame_ledger(path: Path, metrics: dict[str, object]) -> None:
] ]
if len(component_ids) != len(set(component_ids)): if len(component_ids) != len(set(component_ids)):
raise TemporalReplayError("temporal frame duplicated a component") raise TemporalReplayError("temporal frame duplicated a component")
if is_v2:
rolling = tuple(
TemporalObstacle.from_dict(item)
for item in _array(
frame.get("rolling_retained"),
"rolling retained obstacles",
)
)
if any(item.state is not TemporalState.RETAINED for item in rolling):
raise TemporalReplayError(
"rolling map published a non-retained component"
)
if set(component_ids) & {item.component_id for item in rolling}:
raise TemporalReplayError(
"rolling and temporal component identities overlap"
)
rolling_publications += len(rolling)
current_publications += len(groups[TemporalState.CURRENT]) current_publications += len(groups[TemporalState.CURRENT])
held_publications += len(groups[TemporalState.HELD]) held_publications += len(groups[TemporalState.HELD])
expired_publications += len(groups[TemporalState.EXPIRED]) expired_publications += len(groups[TemporalState.EXPIRED])
@@ -574,6 +706,11 @@ def _validate_frame_ledger(path: Path, metrics: dict[str, object]) -> None:
frame_metrics = _object(metrics.get("frames"), "temporal frames") frame_metrics = _object(metrics.get("frames"), "temporal frames")
temporal = _object(metrics.get("temporal"), "temporal metrics") temporal = _object(metrics.get("temporal"), "temporal metrics")
motion = _object(metrics.get("motion"), "motion metrics") motion = _object(metrics.get("motion"), "motion metrics")
rolling_metrics = (
_object(metrics.get("rolling_map"), "rolling map metrics")
if is_v2
else None
)
if ( if (
frames != frame_metrics.get("total") frames != frame_metrics.get("total")
or observations != metrics.get("input_observations") or observations != metrics.get("input_observations")
@@ -585,6 +722,11 @@ def _validate_frame_ledger(path: Path, metrics: dict[str, object]) -> None:
or motion_counts[MotionState.MOVING.value] != motion.get("moving") or motion_counts[MotionState.MOVING.value] != motion.get("moving")
or motion_counts[MotionState.STATIONARY.value] != motion.get("stationary") or motion_counts[MotionState.STATIONARY.value] != motion.get("stationary")
or motion_counts[MotionState.UNKNOWN.value] != motion.get("unknown") or motion_counts[MotionState.UNKNOWN.value] != motion.get("unknown")
or (
rolling_metrics is not None
and rolling_publications
!= rolling_metrics.get("retained_component_publications")
)
): ):
raise TemporalReplayError("temporal frame ledger and metrics disagree") raise TemporalReplayError("temporal frame ledger and metrics disagree")
@@ -681,7 +823,12 @@ def _frame_policy() -> dict[str, object]:
def _producer_hashes(repository: Path) -> dict[str, str]: def _producer_hashes(repository: Path) -> dict[str, str]:
return { return {
name: _file_sha256(repository / "src/k1link/perception" / name) name: _file_sha256(repository / "src/k1link/perception" / name)
for name in ("temporal.py", "motion.py", "temporal_replay.py") for name in (
"temporal.py",
"motion.py",
"rolling_map.py",
"temporal_replay.py",
)
} }
@@ -697,16 +844,19 @@ def _read_geometry_frame(line: bytes, sequence: int) -> dict[str, object]:
return frame return frame
def _read_frame(line: bytes, sequence: int) -> dict[str, object]: def _read_frame(
line: bytes,
sequence: int,
*,
is_v2: bool,
) -> dict[str, object]:
try: try:
frame = _object(json.loads(line), "temporal replay frame") frame = _object(json.loads(line), "temporal replay frame")
except json.JSONDecodeError as exc: except json.JSONDecodeError as exc:
raise TemporalReplayError( raise TemporalReplayError(
f"temporal replay frame {sequence + 1} is invalid JSON" f"temporal replay frame {sequence + 1} is invalid JSON"
) from exc ) from exc
_exact_keys( expected_keys = {
frame,
{
"schema_version", "schema_version",
"sequence", "sequence",
"frame_id", "frame_id",
@@ -722,11 +872,13 @@ def _read_frame(line: bytes, sequence: int) -> dict[str, object]:
"map_frame_jump_candidate", "map_frame_jump_candidate",
"policy", "policy",
"authority", "authority",
}, }
"temporal replay frame", if is_v2:
) expected_keys.add("rolling_retained")
_exact_keys(frame, expected_keys, "temporal replay frame")
if ( if (
frame.get("schema_version") != TEMPORAL_REPLAY_FRAME_SCHEMA frame.get("schema_version")
!= (TEMPORAL_REPLAY_FRAME_SCHEMA_V2 if is_v2 else TEMPORAL_REPLAY_FRAME_SCHEMA)
or frame.get("sequence") != sequence or frame.get("sequence") != sequence
or frame.get("frame_id") != f"frame-{sequence:06d}" or frame.get("frame_id") != f"frame-{sequence:06d}"
or not isinstance(frame.get("source_available"), bool) or not isinstance(frame.get("source_available"), bool)
@@ -793,6 +945,12 @@ def _object(value: object, label: str) -> dict[str, object]:
return value return value
def _array(value: object, label: str) -> list[object]:
if not isinstance(value, list):
raise TemporalReplayError(f"{label} must be an array")
return value
def _exact_keys(document: dict[str, object], expected: set[str], label: str) -> None: def _exact_keys(document: dict[str, object], expected: set[str], label: str) -> None:
if set(document) != expected: if set(document) != expected:
raise TemporalReplayError(f"{label} fields changed") raise TemporalReplayError(f"{label} fields changed")
+115 -60
View File
@@ -10,6 +10,7 @@ from __future__ import annotations
import hashlib import hashlib
import json import json
import math import math
from collections.abc import Iterable
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Final, Protocol from typing import Final, Protocol
@@ -26,9 +27,14 @@ from .contracts import (
) )
from .geometry_math import quaternion_xyzw_to_rotation_matrix from .geometry_math import quaternion_xyzw_to_rotation_matrix
REPLAY_THREAT_PROFILE_SCHEMA: Final = "missioncore.replay-threat-profile/v2" REPLAY_THREAT_PROFILE_SCHEMA: Final = "missioncore.replay-threat-profile/v3"
REPLAY_THREAT_PROVIDER_ID: Final = "dual-evidence-replay-threat/v2" REPLAY_THREAT_PROVIDER_ID: Final = "dual-evidence-replay-threat/v3"
DEFAULT_REPLAY_THREAT_PROFILE_PATH: Final = "config/perception/m4-replay-threat-v2.json" DEFAULT_REPLAY_THREAT_PROFILE_PATH: Final = "config/perception/m4-replay-threat-v3.json"
LEGACY_REPLAY_THREAT_PROFILE_SCHEMA: Final = "missioncore.replay-threat-profile/v2"
LEGACY_REPLAY_THREAT_PROVIDER_ID: Final = "dual-evidence-replay-threat/v2"
type Vector3 = tuple[float, float, float]
type Matrix3 = tuple[Vector3, Vector3, Vector3]
class ReplayThreatError(ValueError): class ReplayThreatError(ValueError):
@@ -38,12 +44,8 @@ class ReplayThreatError(ValueError):
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class ReplayBodyFrame: class ReplayBodyFrame:
frame_id: str frame_id: str
origin_map_xyz_m: tuple[float, float, float] origin_map_xyz_m: Vector3
basis_map_from_body: tuple[ basis_map_from_body: Matrix3
tuple[float, float, float],
tuple[float, float, float],
tuple[float, float, float],
]
sensor_height_m: float sensor_height_m: float
surface_slope_deg: float surface_slope_deg: float
forward_source: str forward_source: str
@@ -68,8 +70,22 @@ class ReplayBodyFrame:
) )
): ):
raise ReplayThreatError("replay body frame is not finite") raise ReplayThreatError("replay body frame is not finite")
columns = tuple( columns: Matrix3 = (
tuple(self.basis_map_from_body[row][column] for row in range(3)) for column in range(3) (
self.basis_map_from_body[0][0],
self.basis_map_from_body[1][0],
self.basis_map_from_body[2][0],
),
(
self.basis_map_from_body[0][1],
self.basis_map_from_body[1][1],
self.basis_map_from_body[2][1],
),
(
self.basis_map_from_body[0][2],
self.basis_map_from_body[1][2],
self.basis_map_from_body[2][2],
),
) )
if ( if (
not self.forward_source not self.forward_source
@@ -87,15 +103,17 @@ class ReplayBodyFrame:
def map_point_to_body( def map_point_to_body(
self, self,
point_map_xyz_m: tuple[float, float, float], point_map_xyz_m: Vector3,
) -> tuple[float, float, float]: ) -> Vector3:
delta = tuple(point_map_xyz_m[index] - self.origin_map_xyz_m[index] for index in range(3)) delta = _vector3(
point_map_xyz_m[index] - self.origin_map_xyz_m[index] for index in range(3)
)
return self.map_vector_to_body(delta) return self.map_vector_to_body(delta)
def map_vector_to_body( def map_vector_to_body(
self, self,
vector_map_xyz_m: tuple[float, float, float], vector_map_xyz_m: Vector3,
) -> tuple[float, float, float]: ) -> Vector3:
values = tuple( values = tuple(
float( float(
sum( sum(
@@ -172,14 +190,14 @@ class RecordedReplayBodyFrameResolver:
) )
if inputs is None: if inputs is None:
return self._store(frame_id, None, "source-or-surface-unavailable") return self._store(frame_id, None, "source-or-surface-unavailable")
position = tuple(float(value) for value in inputs.sensor_position_map) position = _vector3(float(value) for value in inputs.sensor_position_map)
plane = tuple(float(value) for value in inputs.ground_plane_coefficients_map) plane = tuple(float(value) for value in inputs.ground_plane_coefficients_map)
normal_norm = math.sqrt(sum(value * value for value in plane[:3])) normal_norm = math.sqrt(sum(value * value for value in plane[:3]))
if normal_norm < 1e-9: if normal_norm < 1e-9:
return self._store(frame_id, None, "ground-normal-invalid") return self._store(frame_id, None, "ground-normal-invalid")
ground_normal = tuple(value / normal_norm for value in plane[:3]) ground_normal = _vector3(value / normal_norm for value in plane[:3])
if ground_normal[2] < 0.0: if ground_normal[2] < 0.0:
ground_normal = tuple(-value for value in ground_normal) ground_normal = _vector3(-value for value in ground_normal)
plane = tuple(-value for value in plane) plane = tuple(-value for value in plane)
sensor_height = _dot(position, ground_normal) + plane[3] / normal_norm sensor_height = _dot(position, ground_normal) + plane[3] / normal_norm
if ( if (
@@ -200,15 +218,15 @@ class RecordedReplayBodyFrameResolver:
vertical_height = sensor_height / vertical_denominator vertical_height = sensor_height / vertical_denominator
rotation = quaternion_xyzw_to_rotation_matrix(inputs.sensor_orientation_map_from_lidar_xyzw) rotation = quaternion_xyzw_to_rotation_matrix(inputs.sensor_orientation_map_from_lidar_xyzw)
calibration = inputs.t_camera_from_lidar calibration = inputs.t_camera_from_lidar
camera_forward_lidar = tuple(float(calibration[2, index]) for index in range(3)) camera_forward_lidar = _vector3(float(calibration[2, index]) for index in range(3))
camera_forward_map = tuple( camera_forward_map = _vector3(
float(sum(rotation[row, column] * camera_forward_lidar[column] for column in range(3))) float(sum(rotation[row, column] * camera_forward_lidar[column] for column in range(3)))
for row in range(3) for row in range(3)
) )
camera_forward = _normalize(_reject(camera_forward_map, up)) camera_forward = _normalize(_reject(camera_forward_map, up))
if camera_forward is None: if camera_forward is None:
return self._store(frame_id, None, "camera-forward-invalid") return self._store(frame_id, None, "camera-forward-invalid")
route = tuple( route = _vector3(
float( float(
inputs.trajectory_end_position_map[index] inputs.trajectory_end_position_map[index]
- inputs.trajectory_start_position_map[index] - inputs.trajectory_start_position_map[index]
@@ -235,8 +253,12 @@ class RecordedReplayBodyFrameResolver:
return self._store(frame_id, None, "body-left-invalid") return self._store(frame_id, None, "body-left-invalid")
forward = _normalize(_cross(left, up)) forward = _normalize(_cross(left, up))
assert forward is not None assert forward is not None
origin = tuple(position[index] - vertical_height * up[index] for index in range(3)) origin = _vector3(position[index] - vertical_height * up[index] for index in range(3))
basis = tuple((forward[row], left[row], up[row]) for row in range(3)) basis: Matrix3 = (
(forward[0], left[0], up[0]),
(forward[1], left[1], up[1]),
(forward[2], left[2], up[2]),
)
frame = ReplayBodyFrame( frame = ReplayBodyFrame(
frame_id=frame_id, frame_id=frame_id,
origin_map_xyz_m=origin, origin_map_xyz_m=origin,
@@ -326,7 +348,10 @@ class DualEvidenceReplayThreatProvider:
body_frame_resolver: ReplayBodyFrameResolver, body_frame_resolver: ReplayBodyFrameResolver,
profile: ReplayThreatProfile, profile: ReplayThreatProfile,
) -> None: ) -> None:
if profile.provider_id != self.provider_id: if profile.provider_id not in {
self.provider_id,
LEGACY_REPLAY_THREAT_PROVIDER_ID,
}:
raise ReplayThreatError("threat provider identity changed") raise ReplayThreatError("threat provider identity changed")
self.body_frame_resolver = body_frame_resolver self.body_frame_resolver = body_frame_resolver
self.profile = profile self.profile = profile
@@ -354,7 +379,7 @@ class DualEvidenceReplayThreatProvider:
obstacle: TemporalObstacle, obstacle: TemporalObstacle,
body_frame: ReplayBodyFrame | None, body_frame: ReplayBodyFrame | None,
) -> ThreatAssessment: ) -> ThreatAssessment:
if obstacle.state is not TemporalState.CURRENT: if obstacle.state not in {TemporalState.CURRENT, TemporalState.RETAINED}:
return self._unknown( return self._unknown(
frame_id, frame_id,
obstacle.component_id, obstacle.component_id,
@@ -390,12 +415,21 @@ class DualEvidenceReplayThreatProvider:
rig=self.profile.rig, rig=self.profile.rig,
corridor=self.profile.corridor, corridor=self.profile.corridor,
) )
motion_complete = obstacle.motion is not MotionState.UNKNOWN and velocity_body is not None retained = obstacle.state is TemporalState.RETAINED
motion_complete = (
not retained
and obstacle.motion is not MotionState.UNKNOWN
and velocity_body is not None
)
if current_intersection or (motion_complete and corridor_entry is not None): if current_intersection or (motion_complete and corridor_entry is not None):
intersection = CorridorIntersection.INTERSECTS intersection = CorridorIntersection.INTERSECTS
decision = ThreatDecision.THREAT decision = ThreatDecision.THREAT
reasons = [ reasons = [
"current-corridor-intersection" (
"retained-corridor-intersection"
if retained
else "current-corridor-intersection"
)
if current_intersection if current_intersection
else "predicted-corridor-intersection", else "predicted-corridor-intersection",
"metric-lidar-geometry", "metric-lidar-geometry",
@@ -404,6 +438,14 @@ class DualEvidenceReplayThreatProvider:
intersection = CorridorIntersection.CLEAR intersection = CorridorIntersection.CLEAR
decision = ThreatDecision.NOT_THREAT decision = ThreatDecision.NOT_THREAT
reasons = ["predicted-corridor-clear", "metric-lidar-geometry"] reasons = ["predicted-corridor-clear", "metric-lidar-geometry"]
elif retained:
intersection = CorridorIntersection.UNKNOWN
decision = ThreatDecision.UNKNOWN
reasons = [
"retained-map-occupancy-outside-current-corridor",
"metric-lidar-geometry",
"absence-of-republication-is-not-free",
]
else: else:
intersection = CorridorIntersection.UNKNOWN intersection = CorridorIntersection.UNKNOWN
decision = ThreatDecision.UNKNOWN decision = ThreatDecision.UNKNOWN
@@ -423,7 +465,9 @@ class DualEvidenceReplayThreatProvider:
horizon_seconds=self.profile.corridor.prediction_horizon_seconds, horizon_seconds=self.profile.corridor.prediction_horizon_seconds,
) )
relative_speed = _closing_speed_mps(centroid_body, velocity_body) relative_speed = _closing_speed_mps(centroid_body, velocity_body)
if velocity_body is None: if retained:
reasons.append("motion-retained-map-increment-no-current-motion")
elif velocity_body is None:
reasons.append(f"motion-{obstacle.motion_reason}") reasons.append(f"motion-{obstacle.motion_reason}")
else: else:
reasons.append(f"motion-{obstacle.motion.value}") reasons.append(f"motion-{obstacle.motion.value}")
@@ -465,14 +509,14 @@ class DualEvidenceReplayThreatProvider:
or last.frame_id != current_body_frame.frame_id or last.frame_id != current_body_frame.frame_id
): ):
return None return None
obstacle_delta = tuple( obstacle_delta = _vector3(
last.centroid_xyz_m[index] - first.centroid_xyz_m[index] for index in range(3) last.centroid_xyz_m[index] - first.centroid_xyz_m[index] for index in range(3)
) )
rig_delta = tuple( rig_delta = _vector3(
last_body_frame.origin_map_xyz_m[index] - first_body_frame.origin_map_xyz_m[index] last_body_frame.origin_map_xyz_m[index] - first_body_frame.origin_map_xyz_m[index]
for index in range(3) for index in range(3)
) )
relative_map = tuple( relative_map = _vector3(
(obstacle_delta[index] - rig_delta[index]) / span_seconds for index in range(3) (obstacle_delta[index] - rig_delta[index]) / span_seconds for index in range(3)
) )
body = current_body_frame.map_vector_to_body(relative_map) body = current_body_frame.map_vector_to_body(relative_map)
@@ -530,11 +574,13 @@ def load_replay_threat_profile(path: Path) -> ReplayThreatProfile:
}, },
"replay threat profile", "replay threat profile",
) )
if ( identity = (document["schema_version"], document["provider_id"])
document["schema_version"] != REPLAY_THREAT_PROFILE_SCHEMA if identity not in {
or document["provider_id"] != REPLAY_THREAT_PROVIDER_ID (REPLAY_THREAT_PROFILE_SCHEMA, REPLAY_THREAT_PROVIDER_ID),
): (LEGACY_REPLAY_THREAT_PROFILE_SCHEMA, LEGACY_REPLAY_THREAT_PROVIDER_ID),
}:
raise ReplayThreatError("replay threat profile identity is incompatible") raise ReplayThreatError("replay threat profile identity is incompatible")
is_v3 = identity == (REPLAY_THREAT_PROFILE_SCHEMA, REPLAY_THREAT_PROVIDER_ID)
source = _object(document["source"], "threat source") source = _object(document["source"], "threat source")
calibration = _object(document["calibration"], "threat calibration") calibration = _object(document["calibration"], "threat calibration")
body_frame = _object(document["body_frame"], "virtual body frame") body_frame = _object(document["body_frame"], "virtual body frame")
@@ -603,18 +649,17 @@ def load_replay_threat_profile(path: Path) -> ReplayThreatProfile:
}, },
"virtual corridor", "virtual corridor",
) )
_exact_keys( policy_keys = {
policy,
{
"camera_only_decision", "camera_only_decision",
"held_or_stale_decision", "held_or_stale_decision",
"semantic_class_used", "semantic_class_used",
"detector_identity_used", "detector_identity_used",
"absence_of_points_means_free", "absence_of_points_means_free",
"geometry_only_is_eligible", "geometry_only_is_eligible",
}, }
"threat policy", if is_v3:
) policy_keys.add("retained_map_intersection_decision")
_exact_keys(policy, policy_keys, "threat policy")
_exact_keys( _exact_keys(
authority, authority,
{ {
@@ -634,14 +679,18 @@ def load_replay_threat_profile(path: Path) -> ReplayThreatProfile:
or body_frame.get("up") != "vendor-slam-map-gravity-axis" or body_frame.get("up") != "vendor-slam-map-gravity-axis"
or body_frame.get("forward") != "smoothed-slam-trajectory-validated-by-camera-axis" or body_frame.get("forward") != "smoothed-slam-trajectory-validated-by-camera-axis"
or rig.get("physical_mount_claimed") is not False or rig.get("physical_mount_claimed") is not False
or policy or policy != {
!= {
"camera_only_decision": "unknown", "camera_only_decision": "unknown",
"held_or_stale_decision": "unknown", "held_or_stale_decision": "unknown",
"semantic_class_used": False, "semantic_class_used": False,
"detector_identity_used": False, "detector_identity_used": False,
"absence_of_points_means_free": False, "absence_of_points_means_free": False,
"geometry_only_is_eligible": True, "geometry_only_is_eligible": True,
**(
{"retained_map_intersection_decision": "threat"}
if is_v3
else {}
),
} }
or authority or authority
!= { != {
@@ -936,17 +985,19 @@ def _positive_integer(document: dict[str, object], key: str) -> int:
return value return value
def _dot( def _vector3(values: Iterable[float]) -> Vector3:
first: tuple[float, float, float], first, second, third = values
second: tuple[float, float, float], return float(first), float(second), float(third)
) -> float:
def _dot(first: Vector3, second: Vector3) -> float:
return sum(first[index] * second[index] for index in range(3)) return sum(first[index] * second[index] for index in range(3))
def _cross( def _cross(
first: tuple[float, float, float], first: Vector3,
second: tuple[float, float, float], second: Vector3,
) -> tuple[float, float, float]: ) -> Vector3:
return ( return (
first[1] * second[2] - first[2] * second[1], first[1] * second[2] - first[2] * second[1],
first[2] * second[0] - first[0] * second[2], first[2] * second[0] - first[0] * second[2],
@@ -955,25 +1006,29 @@ def _cross(
def _normalize( def _normalize(
value: tuple[float, float, float], value: Vector3,
) -> tuple[float, float, float] | None: ) -> Vector3 | None:
norm = math.sqrt(_dot(value, value)) norm = math.sqrt(_dot(value, value))
if norm < 1e-9: if norm < 1e-9:
return None return None
return tuple(item / norm for item in value) return value[0] / norm, value[1] / norm, value[2] / norm
def _reject( def _reject(
value: tuple[float, float, float], value: Vector3,
normal: tuple[float, float, float], normal: Vector3,
) -> tuple[float, float, float]: ) -> Vector3:
along = _dot(value, normal) along = _dot(value, normal)
return tuple(value[index] - along * normal[index] for index in range(3)) return (
value[0] - along * normal[0],
value[1] - along * normal[1],
value[2] - along * normal[2],
)
def _angle_degrees( def _angle_degrees(
first: tuple[float, float, float], first: Vector3,
second: tuple[float, float, float], second: Vector3,
) -> float: ) -> float:
return math.degrees(math.acos(max(-1.0, min(1.0, _dot(first, second))))) return math.degrees(math.acos(max(-1.0, min(1.0, _dot(first, second)))))
+329 -22
View File
@@ -53,6 +53,11 @@ THREAT_REPLAY_FRAME_SCHEMA: Final = "missioncore.perception-threat-replay-frame/
THREAT_REPLAY_VISUAL_SCHEMA: Final = "missioncore.perception-threat-visual-frame/v1" THREAT_REPLAY_VISUAL_SCHEMA: Final = "missioncore.perception-threat-visual-frame/v1"
THREAT_REPLAY_FIXTURE_SCHEMA: Final = "missioncore.perception-threat-fixtures/v1" THREAT_REPLAY_FIXTURE_SCHEMA: Final = "missioncore.perception-threat-fixtures/v1"
THREAT_REPLAY_REPORT_SCHEMA: Final = "missioncore.perception-threat-replay-report/v1" THREAT_REPLAY_REPORT_SCHEMA: Final = "missioncore.perception-threat-replay-report/v1"
THREAT_REPLAY_SCHEMA_V2: Final = "missioncore.perception-threat-replay-result/v2"
THREAT_REPLAY_FRAME_SCHEMA_V2: Final = "missioncore.perception-threat-replay-frame/v2"
THREAT_REPLAY_VISUAL_SCHEMA_V2: Final = "missioncore.perception-threat-visual-frame/v2"
THREAT_REPLAY_FIXTURE_SCHEMA_V2: Final = "missioncore.perception-threat-fixtures/v2"
THREAT_REPLAY_REPORT_SCHEMA_V2: Final = "missioncore.perception-threat-replay-report/v2"
THREAT_REPLAY_RESULT_PREFIX: Final = "m4-threat-replay-" THREAT_REPLAY_RESULT_PREFIX: Final = "m4-threat-replay-"
THREAT_REPLAY_FRAMES_NAME: Final = "frames.jsonl" THREAT_REPLAY_FRAMES_NAME: Final = "frames.jsonl"
THREAT_REPLAY_VISUALS_NAME: Final = "visual-frames.jsonl" THREAT_REPLAY_VISUALS_NAME: Final = "visual-frames.jsonl"
@@ -61,7 +66,23 @@ THREAT_REPLAY_REPORT_NAME: Final = "report.json"
THREAT_REPLAY_MANIFEST_NAME: Final = "manifest.json" THREAT_REPLAY_MANIFEST_NAME: Final = "manifest.json"
VISUAL_FRAME_COUNT: Final = 32 VISUAL_FRAME_COUNT: Final = 32
VISUAL_POINT_LIMIT: Final = 4_000 VISUAL_POINT_LIMIT: Final = 4_000
VISUAL_GEOMETRY_REGRESSION_SEQUENCES: Final = (138, 274) VISUAL_GEOMETRY_REGRESSION_SEQUENCES: Final = (138, 274, 1880)
FRAME_1880_ENGINEERING_ANCHORS: Final = (
{
"anchor_id": "near-concrete-hemisphere",
"x_bounds_m": (0.3, 1.2),
"y_bounds_m": (-0.8, 0.2),
"z_bounds_m": (-0.1, 0.9),
"must_assert_threat": True,
},
{
"anchor_id": "far-concrete-hemisphere",
"x_bounds_m": (1.5, 2.7),
"y_bounds_m": (0.6, 1.7),
"z_bounds_m": (-0.1, 0.9),
"must_assert_threat": False,
},
)
class ThreatReplayError(RuntimeError): class ThreatReplayError(RuntimeError):
@@ -121,6 +142,7 @@ def build_threat_replay(
reason_counts: Counter[str] = Counter() reason_counts: Counter[str] = Counter()
latencies_ms: list[float] = [] latencies_ms: list[float] = []
visual_count = 0 visual_count = 0
frame_1880_regression: dict[str, object] | None = None
try: try:
temporal_frames_path = temporal.result_root / "frames.jsonl" temporal_frames_path = temporal.result_root / "frames.jsonl"
geometry_frames_path = geometry.result_root / "frames.jsonl" geometry_frames_path = geometry.result_root / "frames.jsonl"
@@ -159,6 +181,20 @@ def build_threat_replay(
for key in ("held", "expired") for key in ("held", "expired")
for value in _array(temporal_frame.get(key), f"{key} obstacles") for value in _array(temporal_frame.get(key), f"{key} obstacles")
) )
rolling_retained = tuple(
TemporalObstacle.from_dict(value)
for value in _array(
temporal_frame.get("rolling_retained"),
"rolling retained obstacles",
)
)
if any(
item.state is not TemporalState.RETAINED
for item in rolling_retained
):
raise ThreatReplayError(
"temporal replay rolling map escaped retained state"
)
geometry_observations = _array( geometry_observations = _array(
geometry_frame.get("observations"), "geometry observations" geometry_frame.get("observations"), "geometry observations"
) )
@@ -183,7 +219,7 @@ def build_threat_replay(
graph_id="reference-perception-graph/v1", graph_id="reference-perception-graph/v1",
generated_monotonic_ns=0, generated_monotonic_ns=0,
output_age_ns=0, output_age_ns=0,
occupied=current, occupied=(*current, *rolling_retained),
unknown=unknown, unknown=unknown,
camera_uncertainty=camera_uncertainty, camera_uncertainty=camera_uncertainty,
accounting=SourceAccounting(1, 1, 0, 0), accounting=SourceAccounting(1, 1, 0, 0),
@@ -192,7 +228,10 @@ def build_threat_replay(
assessments = provider.assess(obstacle_map) assessments = provider.assess(obstacle_map)
latencies_ms.append((time.perf_counter_ns() - frame_started_ns) / 1_000_000) latencies_ms.append((time.perf_counter_ns() - frame_started_ns) / 1_000_000)
by_id = {item.component_id: item for item in assessments} by_id = {item.component_id: item for item in assessments}
expected_ids = {item.component_id for item in (*current, *unknown)} | { expected_ids = {
item.component_id
for item in (*current, *rolling_retained, *unknown)
} | {
item.proposal_id for item in camera_uncertainty item.proposal_id for item in camera_uncertainty
} }
if set(by_id) != expected_ids: if set(by_id) != expected_ids:
@@ -203,12 +242,21 @@ def build_threat_replay(
by_id, by_id,
) )
metric_rows = [ metric_rows = [
_metric_row(item, by_id[item.component_id]) for item in (*current, *unknown) _metric_row(item, by_id[item.component_id])
for item in (*current, *rolling_retained, *unknown)
] ]
if frame_count == 1880:
frame_1880_regression = _frame_1880_regression(
metric_rows,
body_frame_resolver.body_frame_for_frame(
packet.envelope.frame_id
),
)
for item in assessments: for item in assessments:
assessment_counts[item.decision.value] += 1 assessment_counts[item.decision.value] += 1
reason_counts.update(item.reason_codes) reason_counts.update(item.reason_codes)
evidence_counts["current-metric"] += len(current) evidence_counts["current-metric"] += len(current)
evidence_counts["rolling-map-retained"] += len(rolling_retained)
evidence_counts["stale-or-held"] += len(unknown) evidence_counts["stale-or-held"] += len(unknown)
evidence_counts["camera-only"] += len(camera_uncertainty) evidence_counts["camera-only"] += len(camera_uncertainty)
for obstacle in current: for obstacle in current:
@@ -216,7 +264,7 @@ def build_threat_replay(
f"{obstacle.motion.value}:{by_id[obstacle.component_id].decision.value}" f"{obstacle.motion.value}:{by_id[obstacle.component_id].decision.value}"
] += 1 ] += 1
frame_document = { frame_document = {
"schema_version": THREAT_REPLAY_FRAME_SCHEMA, "schema_version": THREAT_REPLAY_FRAME_SCHEMA_V2,
"sequence": frame_count, "sequence": frame_count,
"frame_id": packet.envelope.frame_id, "frame_id": packet.envelope.frame_id,
"source_time_ns": packet.envelope.timestamps.source_ns, "source_time_ns": packet.envelope.timestamps.source_ns,
@@ -232,6 +280,8 @@ def build_threat_replay(
"metric_obstacles": len(metric_rows), "metric_obstacles": len(metric_rows),
"camera_proposals": len(proposals), "camera_proposals": len(proposals),
"camera_only": len(camera_uncertainty), "camera_only": len(camera_uncertainty),
"current_increment_metric": len(current),
"rolling_map_retained": len(rolling_retained),
"assessments": len(assessments), "assessments": len(assessments),
}, },
"authority": _false_authority(), "authority": _false_authority(),
@@ -276,14 +326,15 @@ def build_threat_replay(
visual_count=visual_count, visual_count=visual_count,
fixtures=fixtures, fixtures=fixtures,
body_frame=body_frame_resolver.qualification_summary(), body_frame=body_frame_resolver.qualification_summary(),
frame_1880_regression=frame_1880_regression,
) )
requirements = _requirements(metrics, fixtures) requirements = _requirements_v2(metrics, fixtures)
accepted = all(value is True for value in requirements.values()) accepted = all(value is True for value in requirements.values())
frames_sha256 = _file_sha256(frames_path) frames_sha256 = _file_sha256(frames_path)
visuals_sha256 = _file_sha256(visuals_path) visuals_sha256 = _file_sha256(visuals_path)
fixtures_sha256 = _file_sha256(fixtures_path) fixtures_sha256 = _file_sha256(fixtures_path)
identity = { identity = {
"schema_version": THREAT_REPLAY_SCHEMA, "schema_version": THREAT_REPLAY_SCHEMA_V2,
"profile_id": profile.profile_id, "profile_id": profile.profile_id,
"profile_sha256": profile.profile_sha256, "profile_sha256": profile.profile_sha256,
"provider_id": provider.provider_id, "provider_id": provider.provider_id,
@@ -319,7 +370,7 @@ def build_threat_replay(
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
result_id = f"{THREAT_REPLAY_RESULT_PREFIX}{identity_sha256}" result_id = f"{THREAT_REPLAY_RESULT_PREFIX}{identity_sha256}"
report = { report = {
"schema_version": THREAT_REPLAY_REPORT_SCHEMA, "schema_version": THREAT_REPLAY_REPORT_SCHEMA_V2,
"result_id": result_id, "result_id": result_id,
"identity_sha256": identity_sha256, "identity_sha256": identity_sha256,
"status": "accepted" if accepted else "rejected", "status": "accepted" if accepted else "rejected",
@@ -355,7 +406,7 @@ def build_threat_replay(
report_path = staging / THREAT_REPLAY_REPORT_NAME report_path = staging / THREAT_REPLAY_REPORT_NAME
_write_json(report_path, report) _write_json(report_path, report)
manifest = { manifest = {
"schema_version": THREAT_REPLAY_SCHEMA, "schema_version": THREAT_REPLAY_SCHEMA_V2,
"result_id": result_id, "result_id": result_id,
"identity_sha256": identity_sha256, "identity_sha256": identity_sha256,
"identity": identity, "identity": identity,
@@ -400,11 +451,14 @@ def read_threat_replay_result(root: Path) -> ThreatReplayResult:
}, },
"threat replay manifest", "threat replay manifest",
) )
schema_version = manifest.get("schema_version")
if schema_version not in {THREAT_REPLAY_SCHEMA, THREAT_REPLAY_SCHEMA_V2}:
raise ThreatReplayError("threat replay schema is incompatible")
is_v2 = schema_version == THREAT_REPLAY_SCHEMA_V2
identity = _object(manifest.get("identity"), "threat replay identity") identity = _object(manifest.get("identity"), "threat replay identity")
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
if ( if (
manifest.get("schema_version") != THREAT_REPLAY_SCHEMA manifest.get("result_id") != resolved.name
or manifest.get("result_id") != resolved.name
or manifest.get("identity_sha256") != identity_sha256 or manifest.get("identity_sha256") != identity_sha256
or resolved.name != f"{THREAT_REPLAY_RESULT_PREFIX}{identity_sha256}" or resolved.name != f"{THREAT_REPLAY_RESULT_PREFIX}{identity_sha256}"
): ):
@@ -433,8 +487,14 @@ def read_threat_replay_result(root: Path) -> ThreatReplayResult:
requirements = _object(identity.get("acceptance_requirements"), "threat requirements") requirements = _object(identity.get("acceptance_requirements"), "threat requirements")
fixtures = _read_json(paths["threat-deterministic-fixtures"]) fixtures = _read_json(paths["threat-deterministic-fixtures"])
accepted = all(value is True for value in requirements.values()) accepted = all(value is True for value in requirements.values())
expected_requirements = (
_requirements_v2(metrics, fixtures)
if is_v2
else _requirements_v1(metrics, fixtures)
)
if ( if (
report.get("schema_version") != THREAT_REPLAY_REPORT_SCHEMA report.get("schema_version")
!= (THREAT_REPLAY_REPORT_SCHEMA_V2 if is_v2 else THREAT_REPLAY_REPORT_SCHEMA)
or report.get("result_id") != resolved.name or report.get("result_id") != resolved.name
or report.get("identity_sha256") != identity_sha256 or report.get("identity_sha256") != identity_sha256
or report.get("metrics") != metrics or report.get("metrics") != metrics
@@ -443,13 +503,14 @@ def read_threat_replay_result(root: Path) -> ThreatReplayResult:
or identity.get("authority") != _false_authority() or identity.get("authority") != _false_authority()
or manifest.get("accepted") is not accepted or manifest.get("accepted") is not accepted
or identity.get("accepted") is not accepted or identity.get("accepted") is not accepted
or requirements != _requirements(metrics, fixtures) or requirements != expected_requirements
): ):
raise ThreatReplayError("threat replay report or acceptance changed") raise ThreatReplayError("threat replay report or acceptance changed")
_validate_ledgers( _validate_ledgers(
paths["threat-replay-frames"], paths["threat-replay-frames"],
paths["threat-visual-frames"], paths["threat-visual-frames"],
metrics, metrics,
is_v2=is_v2,
) )
return ThreatReplayResult( return ThreatReplayResult(
result_id=resolved.name, result_id=resolved.name,
@@ -617,13 +678,18 @@ def _visual_frame(
} }
) )
return { return {
"schema_version": THREAT_REPLAY_VISUAL_SCHEMA, "schema_version": THREAT_REPLAY_VISUAL_SCHEMA_V2,
"sequence": packet.envelope.sequence, "sequence": packet.envelope.sequence,
"frame_id": packet.envelope.frame_id, "frame_id": packet.envelope.frame_id,
"source_time_ns": packet.envelope.timestamps.source_ns, "source_time_ns": packet.envelope.timestamps.source_ns,
"point_cloud_body_xyz_m": np.round(sampled, 6).tolist(), "point_cloud_body_xyz_m": np.round(sampled, 6).tolist(),
"point_cloud_source_count": int(points.shape[0]), "point_cloud_source_count": int(points.shape[0]),
"point_cloud_sample_count": int(sampled.shape[0]), "point_cloud_sample_count": int(sampled.shape[0]),
"point_cloud_layer": "current-increment",
"rolling_map_component_count": sum(
row.get("state") == TemporalState.RETAINED.value
for row in metric_rows
),
"metric_obstacles": metric_visuals, "metric_obstacles": metric_visuals,
"camera_proposals": camera_rows, "camera_proposals": camera_rows,
"body_frame": { "body_frame": {
@@ -649,6 +715,99 @@ def _visual_frame(
} }
def _frame_1880_regression(
metric_rows: list[dict[str, object]],
body_frame: ReplayBodyFrame | None,
) -> dict[str, object]:
if body_frame is None:
raise ThreatReplayError("frame 1880 has no qualified body frame")
retained_threats = 0
retained_components = 0
retained_rows: list[tuple[dict[str, object], tuple[float, float, float]]] = []
for row in metric_rows:
if row.get("state") != TemporalState.RETAINED.value:
continue
retained_components += 1
assessment = _object(row.get("assessment"), "frame 1880 assessment")
if assessment.get("decision") == ThreatDecision.THREAT.value:
retained_threats += 1
centroid_map = _array(
row.get("centroid_map_xyz_m"),
"frame 1880 retained centroid",
)
if len(centroid_map) != 3:
raise ThreatReplayError("frame 1880 retained centroid is invalid")
centroid_values = tuple(
_number_value(value, "frame 1880 centroid") for value in centroid_map
)
centroid_body = body_frame.map_point_to_body(
(centroid_values[0], centroid_values[1], centroid_values[2])
)
retained_rows.append((row, centroid_body))
anchors: list[dict[str, object]] = []
matched_ids: set[str] = set()
for raw_anchor in FRAME_1880_ENGINEERING_ANCHORS:
anchor = _object(raw_anchor, "frame 1880 engineering anchor")
x_bounds = _bounds(anchor.get("x_bounds_m"), "frame 1880 x bounds")
y_bounds = _bounds(anchor.get("y_bounds_m"), "frame 1880 y bounds")
z_bounds = _bounds(anchor.get("z_bounds_m"), "frame 1880 z bounds")
match = next(
(
(row, centroid)
for row, centroid in retained_rows
if row.get("component_id") not in matched_ids
and x_bounds[0] <= centroid[0] <= x_bounds[1]
and y_bounds[0] <= centroid[1] <= y_bounds[1]
and z_bounds[0] <= centroid[2] <= z_bounds[1]
),
None,
)
component_id = None if match is None else str(match[0]["component_id"])
if component_id is not None:
matched_ids.add(component_id)
anchor_assessment = (
None
if match is None
else _object(match[0].get("assessment"), "frame 1880 anchor assessment")
)
anchors.append(
{
"anchor_id": anchor["anchor_id"],
"bounds_body_xyz_m": [list(x_bounds), list(y_bounds), list(z_bounds)],
"must_assert_threat": anchor["must_assert_threat"],
"matched": match is not None,
"component_id": component_id,
"centroid_body_xyz_m": (
None if match is None else list(match[1])
),
"decision": (
None
if anchor_assessment is None
else anchor_assessment.get("decision")
),
}
)
required_threats_passed = all(
item["matched"] is True
and (
item["must_assert_threat"] is False
or item["decision"] == ThreatDecision.THREAT.value
)
for item in anchors
)
return {
"sequence": 1880,
"retained_components": retained_components,
"retained_threat_components": retained_threats,
"engineering_anchors": anchors,
"matched_anchor_count": sum(item["matched"] is True for item in anchors),
"required_threats_passed": required_threats_passed,
"camera_visible_hemispheres_independent_truth": False,
"gate": "two-visible-hemisphere-regression",
}
class _FixtureBodyFrames: class _FixtureBodyFrames:
def body_frame_for_frame(self, frame_id: str) -> ReplayBodyFrame: def body_frame_for_frame(self, frame_id: str) -> ReplayBodyFrame:
return ReplayBodyFrame( return ReplayBodyFrame(
@@ -766,6 +925,19 @@ def _fixture_document(profile: ReplayThreatProfile) -> dict[str, object]:
), ),
ThreatDecision.UNKNOWN, ThreatDecision.UNKNOWN,
), ),
_fixture_case(
provider,
"retained-in-corridor",
_fixture_obstacle(
"fixture-retained-in",
GridCell(6, 0, 0),
MotionState.UNKNOWN,
((frame_id, 200_000_000, (2.925, 0.225, 0.225)),),
state=TemporalState.RETAINED,
),
ThreatDecision.THREAT,
critical=True,
),
_fixture_camera_case(provider, frame_id), _fixture_camera_case(provider, frame_id),
_fixture_case( _fixture_case(
provider, provider,
@@ -784,7 +956,7 @@ def _fixture_document(profile: ReplayThreatProfile) -> dict[str, object]:
), ),
] ]
return { return {
"schema_version": THREAT_REPLAY_FIXTURE_SCHEMA, "schema_version": THREAT_REPLAY_FIXTURE_SCHEMA_V2,
"cases": cases, "cases": cases,
"critical_case_count": sum(item["critical"] is True for item in cases), "critical_case_count": sum(item["critical"] is True for item in cases),
"critical_false_not_threat_count": sum( "critical_false_not_threat_count": sum(
@@ -810,7 +982,11 @@ def _fixture_obstacle(
component_id=component_id, component_id=component_id,
identity_scope="ephemeral", identity_scope="ephemeral",
state=state, state=state,
ttl_ns=750_000_000, ttl_ns=(
3_000_000_000
if state is TemporalState.RETAINED
else 750_000_000
),
last_hit_ns=last.evidence_time_ns, last_hit_ns=last.evidence_time_ns,
age_ns=0 if state is TemporalState.CURRENT else 100_000_000, age_ns=0 if state is TemporalState.CURRENT else 100_000_000,
association_basis="deterministic-fixture", association_basis="deterministic-fixture",
@@ -849,8 +1025,12 @@ def _fixture_case(
graph_id="reference-perception-graph/v1", graph_id="reference-perception-graph/v1",
generated_monotonic_ns=0, generated_monotonic_ns=0,
output_age_ns=0, output_age_ns=0,
occupied=(obstacle,) if obstacle.state is TemporalState.CURRENT else (), occupied=(obstacle,)
unknown=(obstacle,) if obstacle.state is not TemporalState.CURRENT else (), if obstacle.state in {TemporalState.CURRENT, TemporalState.RETAINED}
else (),
unknown=(obstacle,)
if obstacle.state not in {TemporalState.CURRENT, TemporalState.RETAINED}
else (),
camera_uncertainty=(), camera_uncertainty=(),
accounting=SourceAccounting(1, 1, 0, 0), accounting=SourceAccounting(1, 1, 0, 0),
) )
@@ -915,6 +1095,7 @@ def _metrics(
visual_count: int, visual_count: int,
fixtures: dict[str, object], fixtures: dict[str, object],
body_frame: dict[str, object], body_frame: dict[str, object],
frame_1880_regression: dict[str, object] | None,
) -> dict[str, object]: ) -> dict[str, object]:
values = np.asarray(latencies_ms, dtype=np.float64) values = np.asarray(latencies_ms, dtype=np.float64)
return { return {
@@ -934,6 +1115,7 @@ def _metrics(
"virtual_corridor_available": True, "virtual_corridor_available": True,
"qualified_base_footprint_available": True, "qualified_base_footprint_available": True,
"geometry_regression_sequences": list(VISUAL_GEOMETRY_REGRESSION_SEQUENCES), "geometry_regression_sequences": list(VISUAL_GEOMETRY_REGRESSION_SEQUENCES),
"frame_1880_regression": frame_1880_regression,
}, },
"fixtures": { "fixtures": {
"passed": fixtures["passed_count"], "passed": fixtures["passed_count"],
@@ -951,7 +1133,102 @@ def _metrics(
} }
def _requirements( def _requirements_v1(
metrics: dict[str, object],
fixtures: dict[str, object],
) -> dict[str, bool]:
frames = _object(metrics.get("frames"), "frame metrics")
evidence = _object(metrics.get("evidence"), "evidence metrics")
decisions = _object(metrics.get("decisions"), "decision metrics")
visual = _object(metrics.get("visual_evidence"), "visual metrics")
body_frame = _object(metrics.get("body_frame"), "body frame metrics")
total_evidence = sum(_integer(value, "evidence count") for value in evidence.values())
total_decisions = sum(_integer(value, "decision count") for value in decisions.values())
cases = _array(fixtures.get("cases"), "fixture cases")
camera_case = next(
(
_object(item, "fixture")
for item in cases
if isinstance(item, dict) and item.get("name") == "camera-only"
),
{},
)
stale_cases = [
_object(item, "fixture")
for item in cases
if isinstance(item, dict)
and item.get("name") in {"occluded-held", "stale-expired"}
]
return {
"full_ravnoves00_replay_completed": (
frames.get("total") == 4489 and frames.get("failed") == 0
),
"every_metric_or_camera_evidence_received_one_assessment": (
total_evidence == total_decisions and total_evidence > 0
),
"camera_only_is_unknown_never_safe": camera_case.get("actual") == "unknown",
"held_and_stale_are_unknown_never_safe": (
len(stale_cases) == 2
and all(item.get("actual") == "unknown" for item in stale_cases)
),
"geometry_only_evidence_is_assessed": (
_integer(
_object(metrics.get("reason_counts"), "reason metrics").get(
"geometry-only-evidence", 0
),
"geometry-only count",
)
> 0
),
"deterministic_fixture_matrix_passed": (
fixtures.get("passed_count") == fixtures.get("total_count") == 9
),
"zero_critical_fixture_false_not_threat": (
fixtures.get("critical_false_not_threat_count") == 0
),
"visual_video_camera_cloud_distance_and_corridor_are_available": (
visual.get("frame_count") == VISUAL_FRAME_COUNT
and all(
visual.get(key) is True
for key in (
"video_overlay_available",
"camera_boxes_available",
"point_cloud_available",
"metric_distance_available",
"virtual_corridor_available",
"qualified_base_footprint_available",
)
)
and visual.get("geometry_regression_sequences") == [138, 274]
),
"body_frame_is_grounded_gravity_stable_and_route_aligned": (
body_frame.get("available")
== _integer(body_frame.get("qualified"), "qualified body frames")
+ _integer(body_frame.get("rejected"), "rejected body frames")
and _integer(body_frame.get("qualified"), "qualified body frames")
>= math.ceil(
_integer(body_frame.get("available"), "available body frames") * 0.95
)
and body_frame.get("origin") == "local-surface-vertical-projection"
and body_frame.get("up") == "vendor-slam-map-gravity-axis"
and body_frame.get("forward")
== "smoothed-slam-trajectory-validated-by-camera-axis"
and _number_value(
_object(
body_frame.get("camera_forward_alignment_deg"),
"body alignment metrics",
).get("maximum"),
"maximum body alignment",
)
<= 25.0
),
"physical_collision_and_actuation_authority_remain_false": (
fixtures.get("authority") == _false_authority()
),
}
def _requirements_v2(
metrics: dict[str, object], metrics: dict[str, object],
fixtures: dict[str, object], fixtures: dict[str, object],
) -> dict[str, bool]: ) -> dict[str, bool]:
@@ -997,7 +1274,7 @@ def _requirements(
> 0 > 0
), ),
"deterministic_fixture_matrix_passed": ( "deterministic_fixture_matrix_passed": (
fixtures.get("passed_count") == fixtures.get("total_count") == 9 fixtures.get("passed_count") == fixtures.get("total_count") == 10
), ),
"zero_critical_fixture_false_not_threat": ( "zero_critical_fixture_false_not_threat": (
fixtures.get("critical_false_not_threat_count") == 0 fixtures.get("critical_false_not_threat_count") == 0
@@ -1018,6 +1295,19 @@ def _requirements(
and visual.get("geometry_regression_sequences") and visual.get("geometry_regression_sequences")
== list(VISUAL_GEOMETRY_REGRESSION_SEQUENCES) == list(VISUAL_GEOMETRY_REGRESSION_SEQUENCES)
), ),
"frame_1880_retains_two_hemispheres_and_blocks_near_corridor": (
isinstance(visual.get("frame_1880_regression"), dict)
and _object(
visual.get("frame_1880_regression"),
"frame 1880 regression",
).get("matched_anchor_count")
== len(FRAME_1880_ENGINEERING_ANCHORS)
and _object(
visual.get("frame_1880_regression"),
"frame 1880 regression",
).get("required_threats_passed")
is True
),
"body_frame_is_grounded_gravity_stable_and_route_aligned": ( "body_frame_is_grounded_gravity_stable_and_route_aligned": (
body_frame.get("available") body_frame.get("available")
== _integer(body_frame.get("qualified"), "qualified body frames") == _integer(body_frame.get("qualified"), "qualified body frames")
@@ -1046,12 +1336,15 @@ def _validate_ledgers(
frames_path: Path, frames_path: Path,
visuals_path: Path, visuals_path: Path,
metrics: dict[str, object], metrics: dict[str, object],
*,
is_v2: bool,
) -> None: ) -> None:
frame_count = 0 frame_count = 0
assessment_count = 0 assessment_count = 0
for sequence, frame in enumerate(_read_jsonl(frames_path)): for sequence, frame in enumerate(_read_jsonl(frames_path)):
if ( if (
frame.get("schema_version") != THREAT_REPLAY_FRAME_SCHEMA frame.get("schema_version")
!= (THREAT_REPLAY_FRAME_SCHEMA_V2 if is_v2 else THREAT_REPLAY_FRAME_SCHEMA)
or frame.get("sequence") != sequence or frame.get("sequence") != sequence
or frame.get("authority") != _false_authority() or frame.get("authority") != _false_authority()
): ):
@@ -1071,7 +1364,11 @@ def _validate_ledgers(
for value in _object(metrics.get("decisions"), "decisions").values() for value in _object(metrics.get("decisions"), "decisions").values()
) )
or len(visuals) != VISUAL_FRAME_COUNT or len(visuals) != VISUAL_FRAME_COUNT
or any(item.get("schema_version") != THREAT_REPLAY_VISUAL_SCHEMA for item in visuals) or any(
item.get("schema_version")
!= (THREAT_REPLAY_VISUAL_SCHEMA_V2 if is_v2 else THREAT_REPLAY_VISUAL_SCHEMA)
for item in visuals
)
): ):
raise ThreatReplayError("threat replay ledger and metrics disagree") raise ThreatReplayError("threat replay ledger and metrics disagree")
@@ -1233,6 +1530,16 @@ def _number_value(value: object, label: str) -> float:
return float(value) return float(value)
def _bounds(value: object, label: str) -> tuple[float, float]:
if not isinstance(value, tuple) or len(value) != 2:
raise ThreatReplayError(f"{label} must contain two values")
lower = _number_value(value[0], label)
upper = _number_value(value[1], label)
if lower >= upper:
raise ThreatReplayError(f"{label} must be ordered")
return lower, upper
def _signed_integer(value: object, label: str) -> int: def _signed_integer(value: object, label: str) -> int:
if not isinstance(value, int) or isinstance(value, bool): if not isinstance(value, int) or isinstance(value, bool):
raise ThreatReplayError(f"{label} must be an integer") raise ThreatReplayError(f"{label} must be an integer")
+8 -2
View File
@@ -14,8 +14,10 @@ from fastapi import APIRouter, HTTPException, Query
from k1link.perception.threat_replay import ( from k1link.perception.threat_replay import (
THREAT_REPLAY_FRAME_SCHEMA, THREAT_REPLAY_FRAME_SCHEMA,
THREAT_REPLAY_FRAME_SCHEMA_V2,
THREAT_REPLAY_RESULT_PREFIX, THREAT_REPLAY_RESULT_PREFIX,
THREAT_REPLAY_VISUAL_SCHEMA, THREAT_REPLAY_VISUAL_SCHEMA,
THREAT_REPLAY_VISUAL_SCHEMA_V2,
ThreatReplayError, ThreatReplayError,
ThreatReplayResult, ThreatReplayResult,
read_threat_replay_result, read_threat_replay_result,
@@ -57,9 +59,10 @@ def build_m4_threat_replay_router(
items: list[dict[str, object]] = [] items: list[dict[str, object]] = []
invalid_total = 0 invalid_total = 0
for candidate in candidates: for candidate in candidates:
if len(items) >= limit:
break
try: try:
frozen = result(candidate.name) frozen = result(candidate.name)
if len(items) < limit:
items.append(_project_result(frozen)) items.append(_project_result(frozen))
except HTTPException: except HTTPException:
invalid_total += 1 invalid_total += 1
@@ -149,7 +152,8 @@ def _cached_video_overlay(
frames = [] frames = []
for expected_sequence, row in enumerate(_iter_jsonl(root / "frames.jsonl")): for expected_sequence, row in enumerate(_iter_jsonl(root / "frames.jsonl")):
if ( if (
row.get("schema_version") != THREAT_REPLAY_FRAME_SCHEMA row.get("schema_version")
not in {THREAT_REPLAY_FRAME_SCHEMA, THREAT_REPLAY_FRAME_SCHEMA_V2}
or row.get("sequence") != expected_sequence or row.get("sequence") != expected_sequence
): ):
raise ValueError("M4.6 video frame order changed") raise ValueError("M4.6 video frame order changed")
@@ -288,7 +292,9 @@ def _iter_jsonl(path: Path) -> Iterator[dict[str, object]]:
raise ValueError("M4.6 JSONL row is invalid") raise ValueError("M4.6 JSONL row is invalid")
if value.get("schema_version") not in { if value.get("schema_version") not in {
THREAT_REPLAY_FRAME_SCHEMA, THREAT_REPLAY_FRAME_SCHEMA,
THREAT_REPLAY_FRAME_SCHEMA_V2,
THREAT_REPLAY_VISUAL_SCHEMA, THREAT_REPLAY_VISUAL_SCHEMA,
THREAT_REPLAY_VISUAL_SCHEMA_V2,
}: }:
raise ValueError("M4.6 JSONL schema is invalid") raise ValueError("M4.6 JSONL schema is invalid")
yield value yield value
+49 -10
View File
@@ -8,7 +8,7 @@ from k1link.perception.threat_replay import read_threat_replay_result
from k1link.web.m4_threat_replay_api import build_m4_threat_replay_router from k1link.web.m4_threat_replay_api import build_m4_threat_replay_router
REPOSITORY_ROOT = Path(__file__).resolve().parents[1] REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
RESULT_ID = "m4-threat-replay-78a06d96c4db5263dc63fc4e6e067c07fc81370d3f5085ff43361af89cec1e9e" RESULT_ID = "m4-threat-replay-ef521b23eee704dee99856b6e93d5047a9b358e21ffda3ea9cacc2ef768164d9"
RESULTS_ROOT = REPOSITORY_ROOT / ".runtime/compute-experiments/m4/replay-threat" RESULTS_ROOT = REPOSITORY_ROOT / ".runtime/compute-experiments/m4/replay-threat"
@@ -29,18 +29,19 @@ def test_full_source_threat_result_closes_m4_6_contract() -> None:
assert result.metrics["evidence"] == { assert result.metrics["evidence"] == {
"camera-only": 10158, "camera-only": 10158,
"current-metric": 27299, "current-metric": 27299,
"rolling-map-retained": 69855,
"stale-or-held": 37995, "stale-or-held": 37995,
} }
assert result.metrics["decisions"] == { assert result.metrics["decisions"] == {
"not-threat": 10700, "not-threat": 10700,
"threat": 2716, "threat": 6626,
"unknown": 62036, "unknown": 127981,
} }
assert result.metrics["fixtures"] == { assert result.metrics["fixtures"] == {
"critical": 4, "critical": 5,
"critical_false_not_threat": 0, "critical_false_not_threat": 0,
"passed": 9, "passed": 10,
"total": 9, "total": 10,
} }
@@ -50,10 +51,10 @@ def test_threat_result_is_content_bound_and_visual_evidence_is_complete() -> Non
assert isinstance(identity, dict) assert isinstance(identity, dict)
assert identity["frames_sha256"] == ( assert identity["frames_sha256"] == (
"d55e7651f0b16a62c6b61c5cb2358dd8dff87dbfa57a59e9ec350bc38b156bc1" "57219acd7dfe1cf04b12d4a415d1819e0a4947bc3173333bb56eea8fd2bd47b9"
) )
assert identity["visuals_sha256"] == ( assert identity["visuals_sha256"] == (
"957c35d46ae30143beb6b2f26f8f722853ef2a1e91a41d5dc1a03fbf723a54e0" "bda54e144a1b4c878ba645a8dafedcdc0332e16a1489b62b7100670e8eae6f25"
) )
visual = result.metrics["visual_evidence"] visual = result.metrics["visual_evidence"]
assert isinstance(visual, dict) assert isinstance(visual, dict)
@@ -69,7 +70,44 @@ def test_threat_result_is_content_bound_and_visual_evidence_is_complete() -> Non
"qualified_base_footprint_available", "qualified_base_footprint_available",
) )
) )
assert visual["geometry_regression_sequences"] == [138, 274] assert visual["geometry_regression_sequences"] == [138, 274, 1880]
assert visual["frame_1880_regression"] == {
"camera_visible_hemispheres_independent_truth": False,
"engineering_anchors": [
{
"anchor_id": "near-concrete-hemisphere",
"bounds_body_xyz_m": [[0.3, 1.2], [-0.8, 0.2], [-0.1, 0.9]],
"centroid_body_xyz_m": [
0.7594228459267565,
-0.2681278641873485,
0.3753253937774115,
],
"component_id": "rolling-5329b5d2ef498e6250c931ff",
"decision": "threat",
"matched": True,
"must_assert_threat": True,
},
{
"anchor_id": "far-concrete-hemisphere",
"bounds_body_xyz_m": [[1.5, 2.7], [0.6, 1.7], [-0.1, 0.9]],
"centroid_body_xyz_m": [
2.058019871618555,
1.1276051922616088,
0.2553253937774115,
],
"component_id": "rolling-7ba116b07683abeca7c8005b",
"decision": "threat",
"matched": True,
"must_assert_threat": False,
},
],
"gate": "two-visible-hemisphere-regression",
"matched_anchor_count": 2,
"required_threats_passed": True,
"retained_components": 10,
"retained_threat_components": 2,
"sequence": 1880,
}
body_frame = result.metrics["body_frame"] body_frame = result.metrics["body_frame"]
assert body_frame["qualified"] == 3861 assert body_frame["qualified"] == 3861
assert body_frame["rejected"] == 67 assert body_frame["rejected"] == 67
@@ -88,7 +126,8 @@ def test_m4_6_lab_api_projects_report_and_exact_visual_frame() -> None:
assert len(visuals["items"]) == 32 assert len(visuals["items"]) == 32
assert [item["sequence"] for item in visuals["items"][:3]] == [62, 138, 274] assert [item["sequence"] for item in visuals["items"][:3]] == [62, 138, 274]
frame = get_visual(RESULT_ID, 1) frame = get_visual(RESULT_ID, 1)
assert frame["schema_version"] == "missioncore.perception-threat-visual-frame/v1" assert frame["schema_version"] == "missioncore.perception-threat-visual-frame/v2"
assert frame["point_cloud_layer"] == "current-increment"
assert frame["point_cloud_sample_count"] > 0 assert frame["point_cloud_sample_count"] > 0
assert frame["rig"] == { assert frame["rig"] == {
"length_m": 1.0, "length_m": 1.0,
+4
View File
@@ -83,6 +83,10 @@ def test_worker_shadow_artifact_is_deterministic_narrow_and_self_contained(
"name": BUILDER.RUNNER_NAME, "name": BUILDER.RUNNER_NAME,
"sha256": _sha256(BUILDER.RUNNER.read_bytes()), "sha256": _sha256(BUILDER.RUNNER.read_bytes()),
} }
assert descriptor["release"]["wheel"] == {
"name": BUILDER.WHEEL_NAME,
"sha256": BUILDER.EXPECTED_WHEEL_SHA256,
}
assert descriptor["container"]["public_ports"] is False assert descriptor["container"]["public_ports"] is False
assert descriptor["rollback"] == { assert descriptor["rollback"] == {
"durable_worker_action": "none", "durable_worker_action": "none",
+2
View File
@@ -48,6 +48,7 @@ TEMPORAL_RUNTIME_MODULES = (
"motion.py", "motion.py",
"providers.py", "providers.py",
"recorded_source.py", "recorded_source.py",
"rolling_map.py",
"temporal.py", "temporal.py",
"temporal_replay.py", "temporal_replay.py",
"temporal_replay_cli.py", "temporal_replay_cli.py",
@@ -61,6 +62,7 @@ THREAT_RUNTIME_MODULES = (
"geometry_replay.py", "geometry_replay.py",
"providers.py", "providers.py",
"recorded_source.py", "recorded_source.py",
"rolling_map.py",
"temporal_replay.py", "temporal_replay.py",
"threat.py", "threat.py",
"threat_replay.py", "threat_replay.py",
+31 -1
View File
@@ -28,6 +28,7 @@ from k1link.perception.threat import (
REPOSITORY_ROOT = Path(__file__).resolve().parents[1] REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
PROFILE_PATH = REPOSITORY_ROOT / "config/perception/m4-replay-threat-v2.json" PROFILE_PATH = REPOSITORY_ROOT / "config/perception/m4-replay-threat-v2.json"
ROLLING_PROFILE_PATH = REPOSITORY_ROOT / "config/perception/m4-replay-threat-v3.json"
class _BodyFrames: class _BodyFrames:
@@ -61,7 +62,7 @@ def _obstacle(
component_id=component_id, component_id=component_id,
identity_scope="ephemeral", identity_scope="ephemeral",
state=state, state=state,
ttl_ns=750_000_000, ttl_ns=(3_000_000_000 if state is TemporalState.RETAINED else 750_000_000),
last_hit_ns=current.evidence_time_ns, last_hit_ns=current.evidence_time_ns,
age_ns=0 if state is TemporalState.CURRENT else 100_000_000, age_ns=0 if state is TemporalState.CURRENT else 100_000_000,
association_basis="test-spatial-support", association_basis="test-spatial-support",
@@ -307,3 +308,32 @@ def test_semantic_hint_and_ephemeral_component_name_do_not_change_threat_geometr
assert values[0].relative_speed_mps == values[1].relative_speed_mps assert values[0].relative_speed_mps == values[1].relative_speed_mps
assert values[0].closest_approach_m == values[1].closest_approach_m assert values[0].closest_approach_m == values[1].closest_approach_m
assert values[0].ttc_seconds == values[1].ttc_seconds assert values[0].ttc_seconds == values[1].ttc_seconds
def test_retained_map_can_block_but_cannot_claim_clear_or_motion() -> None:
provider = DualEvidenceReplayThreatProvider(
body_frame_resolver=_BodyFrames(),
profile=load_replay_threat_profile(ROLLING_PROFILE_PATH),
)
retained_in = _obstacle(
"retained-in",
GridCell(6, 0, 0),
motion=MotionState.UNKNOWN,
history=(("frame-000002", 300_000_000, (2.925, 0.225, 0.225)),),
state=TemporalState.RETAINED,
)
retained_out = _obstacle(
"retained-out",
GridCell(6, 7, 0),
motion=MotionState.UNKNOWN,
history=(("frame-000002", 300_000_000, (2.925, 3.375, 0.225)),),
state=TemporalState.RETAINED,
)
values = provider.assess(_map(occupied=(retained_in, retained_out)))
by_id = {item.component_id: item for item in values}
assert by_id["retained-in"].decision is ThreatDecision.THREAT
assert by_id["retained-in"].relative_speed_mps is None
assert by_id["retained-out"].decision is ThreatDecision.UNKNOWN
assert by_id["retained-out"].corridor_intersection is CorridorIntersection.UNKNOWN
+137
View File
@@ -0,0 +1,137 @@
from __future__ import annotations
from dataclasses import replace
from pathlib import Path
from k1link.perception.contracts import (
ClockBasis,
GridCell,
HistorySample,
ModalityOutcome,
ModalityStatus,
MotionState,
SourceEnvelope,
TemporalObstacle,
TemporalState,
TimestampBundle,
)
from k1link.perception.providers import SourcePacket
from k1link.perception.rolling_map import (
RollingLocalObstacleMapProvider,
load_rolling_map_profile,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
PROFILE_PATH = REPOSITORY_ROOT / "config/perception/m4-rolling-local-map-v1.json"
class _Pose:
def pose_values_for_frame(
self,
frame_id: str,
) -> tuple[tuple[float, float, float], tuple[float, float, float, float]]:
return (0.0, 0.0, 1.25), (0.0, 0.0, 0.0, 1.0)
def _status() -> ModalityStatus:
return ModalityStatus(True, ModalityOutcome.AVAILABLE, "test-available")
def _packet(sequence: int, seconds: float) -> SourcePacket:
return SourcePacket(
envelope=SourceEnvelope(
source_id="RAVNOVES00",
session_id="20260720T065719Z_viewer_live",
frame_id=f"frame-{sequence:06d}",
sequence=sequence,
timestamps=TimestampBundle(
utc_ns=round(seconds * 1_000_000_000),
monotonic_ns=round(seconds * 1_000_000_000),
source_ns=round(seconds * 1_000_000_000),
clock_basis=ClockBasis.RECORDED_HOST,
),
source_age_ns=0,
binding_reason="test-source",
calibration_id="test-calibration",
representation_id="registered-map-increment-v1",
image=_status(),
registered_point_increment=_status(),
pose=_status(),
),
image_payload="image",
registered_point_increment_payload="points",
pose_payload="pose",
)
def _current(packet: SourcePacket) -> TemporalObstacle:
return TemporalObstacle(
component_id=f"temporal-{packet.envelope.sequence:08d}",
identity_scope="ephemeral",
state=TemporalState.CURRENT,
ttl_ns=750_000_000,
last_hit_ns=packet.envelope.timestamps.source_ns,
age_ns=0,
association_basis="new-spatial-hit",
history=(
HistorySample(
frame_id=packet.envelope.frame_id,
evidence_time_ns=packet.envelope.timestamps.source_ns,
centroid_xyz_m=(0.675, 0.225, 0.225),
),
),
cells=(GridCell(1, 0, 0), GridCell(2, 0, 0)),
coordinate_frame="map",
last_centroid_xyz_m=(0.675, 0.225, 0.225),
motion=MotionState.UNKNOWN,
motion_confidence=0.0,
motion_reason="motion-not-estimated",
)
def test_registered_increment_is_retained_without_claiming_current_motion() -> None:
provider = RollingLocalObstacleMapProvider(
pose_resolver=_Pose(),
profile=load_rolling_map_profile(PROFILE_PATH),
)
first = _packet(0, 0.0)
assert provider.update(first, (_current(first),)) == ()
second = _packet(1, 1.0)
retained = provider.update(second, ())
assert len(retained) == 1
assert retained[0].state is TemporalState.RETAINED
assert retained[0].age_ns == 1_000_000_000
assert retained[0].cells == (GridCell(1, 0, 0), GridCell(2, 0, 0))
assert retained[0].motion is MotionState.UNKNOWN
assert retained[0].association_basis == "registered-map-increment-retention"
def test_current_republication_is_not_duplicated_as_retained_occupancy() -> None:
provider = RollingLocalObstacleMapProvider(
pose_resolver=_Pose(),
profile=load_rolling_map_profile(PROFILE_PATH),
)
first = _packet(0, 0.0)
provider.update(first, (_current(first),))
second = _packet(1, 1.0)
assert provider.update(
second,
(replace(_current(second), component_id="temporal-00000099"),),
) == ()
def test_retained_occupancy_expires_only_at_explicit_time_bound() -> None:
provider = RollingLocalObstacleMapProvider(
pose_resolver=_Pose(),
profile=load_rolling_map_profile(PROFILE_PATH),
)
first = _packet(0, 0.0)
provider.update(first, (_current(first),))
assert provider.update(_packet(1, 3.0), ())
assert provider.update(_packet(2, 3.1), ()) == ()
snapshot = provider.snapshot()
assert snapshot.time_evicted_cells == 2
assert snapshot.capacity_evicted_cells == 0
+40
View File
@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import json
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -15,6 +16,11 @@ RESULT_ROOT = (
/ ".runtime/perception-m4/temporal-results" / ".runtime/perception-m4/temporal-results"
/ "m4-temporal-replay-9ed5dcd249ed3bcb81661dd18e2b854a7ffedf3fd2b92b9c994c3c70c34533f2" / "m4-temporal-replay-9ed5dcd249ed3bcb81661dd18e2b854a7ffedf3fd2b92b9c994c3c70c34533f2"
) )
ROLLING_RESULT_ROOT = (
REPOSITORY_ROOT
/ ".runtime/perception-m4/temporal-results"
/ "m4-temporal-replay-b8611526dfcd2b9be9049560d751bbd23a9ad54b7dda8e9dc48a17374d46266e"
)
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -82,3 +88,37 @@ def test_temporal_result_is_digest_bound_to_m4_4_e34_e51_and_e46b(
assert set(references) == {"e34", "e46b", "e51"} assert set(references) == {"e34", "e46b", "e51"}
assert references["e46b"]["independent_truth"] is False assert references["e46b"]["independent_truth"] is False
assert len(identity["clip_checks"]) == 16 assert len(identity["clip_checks"]) == 16
def test_rolling_temporal_result_separates_increment_from_retained_map() -> None:
result = read_temporal_replay_result(ROLLING_RESULT_ROOT)
assert result.accepted is True
assert result.metrics["rolling_map"] == {
"active_cells_at_end": 587,
"capacity_evicted_cells": 0,
"current_increment_cells": 848868,
"input_frames": 4489,
"local_radius_m": 12.0,
"maximum_cells": 65536,
"maximum_retained_age_ns": 3000000000,
"peak_active_cells": 907,
"peak_retained_components": 35,
"radius_evicted_cells": 6512,
"retained_cell_publications": 1776145,
"retained_component_publications": 69855,
"retention_ns": 3000000000,
"time_evicted_cells": 29620,
"voxel_size_m": 0.45,
}
frames = result.result_root / "frames.jsonl"
with frames.open("rb") as handle:
for sequence, line in enumerate(handle):
if sequence == 1880:
frame = json.loads(line)
break
else: # pragma: no cover - immutable artifact guarantees this branch is unreachable
raise AssertionError("frame 1880 missing")
assert frame["schema_version"] == "missioncore.perception-temporal-replay-frame/v2"
assert len(frame["rolling_retained"]) == 10
assert all(item["state"] == "retained" for item in frame["rolling_retained"])