feat(perception): canonicalize temporal motion

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 17:24:48 +03:00
parent 438e0ca263
commit 998775c3af
12 changed files with 2244 additions and 3 deletions
@@ -139,7 +139,7 @@
},
"wheel": {
"name": "nodedc_mission_core-0.1.0-py3-none-any.whl",
"sha256": "ecbbfeee7ea62a7f5f5368efccf5d254abb17b4801a29538e3bc7aa3c8c3a40a"
"sha256": "19d8caf9a522747c461fb3ca30aafe54169959d8bd8e671fa6fc8c0ac107875d"
}
},
"rollback": {
+15 -1
View File
@@ -20,7 +20,7 @@
{
"module": "k1link.compute.temporal_occupied_layer",
"role": "bounded hit-only occupied and unknown state",
"admission": "adapt-to-product-contract"
"admission": "reference-only-extracted-to-product"
},
{
"module": "k1link.compute.pipeline_telemetry",
@@ -41,6 +41,16 @@
"module": "k1link.perception.geometry",
"role": "digest-bound GeometryAssociationProvider and exact point ownership",
"admission": "product-owned"
},
{
"module": "k1link.perception.temporal",
"role": "product-owned bounded spatial retention with ephemeral identity and explicit expiry",
"admission": "product-owned"
},
{
"module": "k1link.perception.motion",
"role": "product-owned class-independent bounded-history motion estimator",
"admission": "product-owned"
}
],
"historical_wrappers": [
@@ -60,6 +70,10 @@
"module": "k1link.compute.e35_degradation_recovery",
"reason": "immutable degradation qualification wrapper"
},
{
"module": "k1link.compute.e51_motion_semantic_qualification",
"reason": "immutable diagnostic motion qualification wrapper"
},
{
"module": "k1link.compute.e46j_raw_fisheye_realtime",
"reason": "immutable detector-capacity experiment wrapper"
@@ -0,0 +1,59 @@
{
"schema_version": "missioncore.temporal-motion-profile/v1",
"profile_id": "m4-bounded-temporal-motion/v1",
"temporal_provider_id": "bounded-spatial-temporal-layer/v1",
"motion_provider_id": "class-independent-motion-estimator/v1",
"source": {
"source_id": "RAVNOVES00",
"session_id": "20260720T065719Z_viewer_live",
"geometry_result_id": "m4-geometry-replay-8daf3109e3cf30b960b4b376032ff3b5ec58ca42a1e5899b841cf29fbcf14ad8",
"geometry_frames_sha256": "b4db5d0ebaba4d6268a1006707dc313c229f3dbdfd73b1d863cad5d853be8ac4"
},
"temporal": {
"coordinate_frame": "map",
"voxel_size_m": 0.45,
"occupied_ttl_seconds": 0.75,
"maximum_active_components": 256,
"maximum_cells_per_component": 4096,
"maximum_history_samples": 8,
"association_maximum_gap_seconds": 0.35,
"association_maximum_centroid_distance_m": 0.9,
"association_minimum_voxel_overlap_fraction": 0.05,
"association_neighbor_radius_cells": 1,
"jump_maximum_adjacent_gap_seconds": 0.25,
"jump_minimum_matched_components": 4,
"jump_minimum_median_displacement_m": 1.5,
"jump_minimum_p25_displacement_m": 0.9
},
"motion": {
"minimum_observations": 3,
"minimum_span_seconds": 0.2,
"moving_minimum_displacement_m": 0.25,
"moving_minimum_speed_mps": 0.4,
"stationary_maximum_displacement_m": 0.1,
"stationary_maximum_speed_mps": 0.2,
"maximum_speed_mps": 20.0,
"full_confidence_observations": 4,
"full_confidence_span_seconds": 0.4,
"minimum_confidence": 0.75
},
"policy": {
"component_identity_scope": "ephemeral",
"association_uses_detector_id": false,
"association_uses_semantic_class": false,
"motion_uses_semantic_class": false,
"camera_only_creates_occupied_component": false,
"absence_of_points_means_free": false,
"held_state": "unknown",
"expired_cells_published": false,
"long_term_identity_available": false,
"history_is_bounded": true
},
"authority": {
"ground_truth": false,
"physical_live": false,
"commands_enabled": false,
"actuation_allowed": false,
"navigation_or_safety_accepted": false
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ WHEEL_NAME = "nodedc_mission_core-0.1.0-py3-none-any.whl"
RUNNER_NAME = RUNNER.name
PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
EXPECTED_BASELINE_SHA256 = "ea10359339e6cce31b5780a2710299771cab7cc0c1c2a2b56a1621f786b31fa8"
EXPECTED_WHEEL_SHA256 = "ecbbfeee7ea62a7f5f5368efccf5d254abb17b4801a29538e3bc7aa3c8c3a40a"
EXPECTED_WHEEL_SHA256 = "19d8caf9a522747c461fb3ca30aafe54169959d8bd8e671fa6fc8c0ac107875d"
PAYLOAD_FILES = (
RUNNER_NAME,
WHEEL_NAME,
+10
View File
@@ -235,6 +235,16 @@ class RecordedGeometryStore:
surface_valid=bool(self._surface["frame_valid"][frame_index]),
)
def current_points(self, packet: SourcePacket) -> FloatArray | None:
"""Expose the verified frame-local point index space to temporal occupancy."""
frame = self.frame(packet)
if frame is None or not frame.surface_valid:
return None
points = np.asarray(frame.points_map, dtype=np.float64)
points.setflags(write=False)
return points
def _validate(self) -> None:
source_required = {
"frame_indices",
+184
View File
@@ -0,0 +1,184 @@
"""Class-independent bounded-history motion estimation for Mission Core."""
from __future__ import annotations
import math
from dataclasses import dataclass, replace
from .contracts import MotionState, TemporalObstacle, TemporalState
from .providers import SourcePacket
from .temporal import MOTION_PROVIDER_ID, MotionEstimatorProfile, TemporalMotionProfile
class MotionEstimatorError(RuntimeError):
"""Temporal history cannot support a deterministic motion decision."""
@dataclass(frozen=True, slots=True)
class MotionEstimatorSnapshot:
input_frames: int
input_obstacles: int
moving: int
stationary: int
unknown: int
insufficient_history: int
stale_support: int
map_frame_discontinuity: int
confidence_below_threshold: int
threshold_deadband: int
implausible_speed: int
class ClassIndependentMotionEstimator:
"""Estimate map-frame motion without labels, detector IDs or tracklets."""
provider_id: str = MOTION_PROVIDER_ID
def __init__(self, *, profile: TemporalMotionProfile) -> None:
self.profile = profile
self.config = profile.motion
self._input_frames = 0
self._input_obstacles = 0
self._moving = 0
self._stationary = 0
self._unknown = 0
self._reasons: dict[str, int] = {
"insufficient-history": 0,
"stale-support": 0,
"map-frame-discontinuity": 0,
"confidence-below-threshold": 0,
"threshold-deadband": 0,
"implausible-speed": 0,
}
def estimate(
self,
packet: SourcePacket,
obstacles: tuple[TemporalObstacle, ...],
) -> tuple[TemporalObstacle, ...]:
if (
packet.envelope.source_id != self.profile.source_id
or packet.envelope.session_id != self.profile.session_id
):
raise MotionEstimatorError("packet escaped the motion source profile")
self._input_frames += 1
self._input_obstacles += len(obstacles)
result = tuple(self._estimate_one(packet, obstacle) for obstacle in obstacles)
for obstacle in result:
if obstacle.motion is MotionState.MOVING:
self._moving += 1
elif obstacle.motion is MotionState.STATIONARY:
self._stationary += 1
else:
self._unknown += 1
if obstacle.motion_reason in self._reasons:
self._reasons[obstacle.motion_reason] += 1
return result
def _estimate_one(
self,
packet: SourcePacket,
obstacle: TemporalObstacle,
) -> TemporalObstacle:
now_ns = packet.envelope.timestamps.source_ns
if obstacle.last_hit_ns > now_ns or obstacle.age_ns != now_ns - obstacle.last_hit_ns:
raise MotionEstimatorError("temporal obstacle time escaped its source packet")
if obstacle.state is not TemporalState.CURRENT:
return _unknown(obstacle, "stale-support")
if obstacle.association_basis == "map-frame-discontinuity":
return _unknown(obstacle, "map-frame-discontinuity")
history = obstacle.history
times = tuple(sample.evidence_time_ns for sample in history)
if any(right <= left for left, right in zip(times, times[1:], strict=False)):
raise MotionEstimatorError("motion history is not strictly monotonic")
if history[-1].evidence_time_ns != obstacle.last_hit_ns:
raise MotionEstimatorError("motion history does not end at the current hit")
if len(history) < self.config.minimum_observations:
return _unknown(obstacle, "insufficient-history")
span_seconds = (times[-1] - times[0]) / 1_000_000_000
if span_seconds < self.config.minimum_span_seconds:
return _unknown(obstacle, "insufficient-history")
displacement_m = math.dist(
history[0].centroid_xyz_m,
history[-1].centroid_xyz_m,
)
speed_mps = displacement_m / span_seconds
if speed_mps > self.config.maximum_speed_mps:
return _unknown(obstacle, "implausible-speed")
confidence = _evidence_confidence(
self.config,
observation_count=len(history),
span_seconds=span_seconds,
)
if confidence < self.config.minimum_confidence:
return _unknown(obstacle, "confidence-below-threshold")
if (
displacement_m >= self.config.moving_minimum_displacement_m
and speed_mps >= self.config.moving_minimum_speed_mps
):
return replace(
obstacle,
motion=MotionState.MOVING,
motion_confidence=confidence,
motion_reason="bounded-map-history-moving",
)
if (
displacement_m <= self.config.stationary_maximum_displacement_m
and speed_mps <= self.config.stationary_maximum_speed_mps
):
return replace(
obstacle,
motion=MotionState.STATIONARY,
motion_confidence=confidence,
motion_reason="bounded-map-history-stationary",
)
return _unknown(obstacle, "threshold-deadband")
def snapshot(self) -> MotionEstimatorSnapshot:
return MotionEstimatorSnapshot(
input_frames=self._input_frames,
input_obstacles=self._input_obstacles,
moving=self._moving,
stationary=self._stationary,
unknown=self._unknown,
insufficient_history=self._reasons["insufficient-history"],
stale_support=self._reasons["stale-support"],
map_frame_discontinuity=self._reasons["map-frame-discontinuity"],
confidence_below_threshold=self._reasons["confidence-below-threshold"],
threshold_deadband=self._reasons["threshold-deadband"],
implausible_speed=self._reasons["implausible-speed"],
)
def _evidence_confidence(
config: MotionEstimatorProfile,
*,
observation_count: int,
span_seconds: float,
) -> float:
"""Return bounded evidence sufficiency, not a statistical class probability."""
return round(
min(
1.0,
observation_count / config.full_confidence_observations,
span_seconds / config.full_confidence_span_seconds,
),
12,
)
def _unknown(obstacle: TemporalObstacle, reason: str) -> TemporalObstacle:
return replace(
obstacle,
motion=MotionState.UNKNOWN,
motion_confidence=0.0,
motion_reason=reason,
)
__all__ = [
"ClassIndependentMotionEstimator",
"MotionEstimatorError",
"MotionEstimatorSnapshot",
]
+761
View File
@@ -0,0 +1,761 @@
"""Bounded spatial temporal occupancy for the Mission Core product graph."""
from __future__ import annotations
import hashlib
import json
import math
from collections import deque
from dataclasses import dataclass, field
from pathlib import Path
from typing import Final, Protocol
import numpy as np
import numpy.typing as npt
from .contracts import (
EvidenceCurrentness,
GridCell,
HistorySample,
MotionState,
ObstacleObservation,
TemporalObstacle,
TemporalState,
)
from .providers import SourcePacket
TEMPORAL_MOTION_PROFILE_SCHEMA: Final = "missioncore.temporal-motion-profile/v1"
TEMPORAL_PROVIDER_ID: Final = "bounded-spatial-temporal-layer/v1"
MOTION_PROVIDER_ID: Final = "class-independent-motion-estimator/v1"
DEFAULT_TEMPORAL_MOTION_PROFILE_PATH: Final = Path(
"config/perception/m4-temporal-motion-v1.json"
)
FloatArray = npt.NDArray[np.float64]
class TemporalProviderError(RuntimeError):
"""Temporal input, configuration or bounded state is incompatible."""
class CurrentPointResolver(Protocol):
"""Resolve the current frame's source-local point index space."""
def current_points(self, packet: SourcePacket) -> FloatArray | None: ...
@dataclass(frozen=True, slots=True)
class TemporalLayerProfile:
coordinate_frame: str
voxel_size_m: float
occupied_ttl_seconds: float
maximum_active_components: int
maximum_cells_per_component: int
maximum_history_samples: int
association_maximum_gap_seconds: float
association_maximum_centroid_distance_m: float
association_minimum_voxel_overlap_fraction: float
association_neighbor_radius_cells: int
jump_maximum_adjacent_gap_seconds: float
jump_minimum_matched_components: int
jump_minimum_median_displacement_m: float
jump_minimum_p25_displacement_m: float
def __post_init__(self) -> None:
positive = (
self.voxel_size_m,
self.occupied_ttl_seconds,
self.association_maximum_gap_seconds,
self.association_maximum_centroid_distance_m,
self.association_minimum_voxel_overlap_fraction,
self.jump_maximum_adjacent_gap_seconds,
self.jump_minimum_median_displacement_m,
self.jump_minimum_p25_displacement_m,
)
if any(not _positive_finite(value) for value in positive):
raise TemporalProviderError("temporal numeric bound is invalid")
if self.association_minimum_voxel_overlap_fraction > 1.0:
raise TemporalProviderError("temporal overlap fraction is invalid")
positive_integers = (
self.maximum_active_components,
self.maximum_cells_per_component,
self.maximum_history_samples,
self.association_neighbor_radius_cells,
self.jump_minimum_matched_components,
)
if any(not _positive_integer(value) for value in positive_integers):
raise TemporalProviderError("temporal integer bound is invalid")
if self.maximum_history_samples > 32:
raise TemporalProviderError("temporal history exceeds the product contract")
@property
def ttl_ns(self) -> int:
return round(self.occupied_ttl_seconds * 1_000_000_000)
@property
def association_maximum_gap_ns(self) -> int:
return round(self.association_maximum_gap_seconds * 1_000_000_000)
@dataclass(frozen=True, slots=True)
class MotionEstimatorProfile:
minimum_observations: int
minimum_span_seconds: float
moving_minimum_displacement_m: float
moving_minimum_speed_mps: float
stationary_maximum_displacement_m: float
stationary_maximum_speed_mps: float
maximum_speed_mps: float
full_confidence_observations: int
full_confidence_span_seconds: float
minimum_confidence: float
def __post_init__(self) -> None:
if any(
not _positive_integer(value)
for value in (self.minimum_observations, self.full_confidence_observations)
):
raise TemporalProviderError("motion observation bound is invalid")
numeric = (
self.minimum_span_seconds,
self.moving_minimum_displacement_m,
self.moving_minimum_speed_mps,
self.stationary_maximum_displacement_m,
self.stationary_maximum_speed_mps,
self.maximum_speed_mps,
self.full_confidence_span_seconds,
self.minimum_confidence,
)
if any(not _positive_finite(value) for value in numeric):
raise TemporalProviderError("motion numeric bound is invalid")
if not 0.0 < self.minimum_confidence <= 1.0:
raise TemporalProviderError("motion confidence threshold is invalid")
if (
self.stationary_maximum_displacement_m
>= self.moving_minimum_displacement_m
or self.stationary_maximum_speed_mps >= self.moving_minimum_speed_mps
or self.moving_minimum_speed_mps >= self.maximum_speed_mps
):
raise TemporalProviderError("motion thresholds have no conservative deadband")
@dataclass(frozen=True, slots=True)
class TemporalMotionProfile:
profile_id: str
source_id: str
session_id: str
geometry_result_id: str
geometry_frames_sha256: str
temporal: TemporalLayerProfile
motion: MotionEstimatorProfile
profile_sha256: str
@dataclass(frozen=True, slots=True)
class TemporalProviderSnapshot:
input_frames: int
completed_frames: int
failed_frames: int
input_observations: int
current_occupied_observations: int
nonmetric_uncertainty_observations: int
created_components: int
spatial_reassociations: int
detector_identity_changes_reassociated: int
current_publications: int
held_publications: int
expired_publications: int
map_frame_jump_candidates: int
peak_active_components: int
peak_cells_per_component: int
maximum_history_samples: int
maximum_held_age_ns: int
maximum_expiry_materialization_delay_ns: int
past_ttl_occupied_publications: int
@dataclass(frozen=True, slots=True)
class _SpatialObservation:
occupancy_key: str
frame_id: str
evidence_time_ns: int
centroid_xyz_m: tuple[float, float, float]
cells: frozenset[GridCell]
semantic_hint: str | None
@dataclass(slots=True)
class _Component:
component_id: str
last_occupancy_key: str
last_hit_ns: int
cells: frozenset[GridCell]
centroid_xyz_m: tuple[float, float, float]
semantic_hint: str | None
history: deque[HistorySample] = field(default_factory=deque)
class BoundedSpatialTemporalProvider:
"""Publish hit-backed current, short-held unknown and cell-free expiry."""
provider_id: str = TEMPORAL_PROVIDER_ID
def __init__(
self,
*,
point_resolver: CurrentPointResolver,
profile: TemporalMotionProfile,
) -> None:
self.point_resolver = point_resolver
self.profile = profile
self.config = profile.temporal
self._components: dict[str, _Component] = {}
self._next_component = 1
self._previous_sequence: int | None = None
self._previous_time_ns: int | None = None
self._previous_observations: tuple[_SpatialObservation, ...] = ()
self._input_frames = 0
self._completed_frames = 0
self._failed_frames = 0
self._input_observations = 0
self._current_observations = 0
self._uncertainty_observations = 0
self._created_components = 0
self._spatial_reassociations = 0
self._identity_changes_reassociated = 0
self._current_publications = 0
self._held_publications = 0
self._expired_publications = 0
self._map_frame_jumps = 0
self._peak_active_components = 0
self._peak_cells = 0
self._maximum_history = 0
self._maximum_held_age_ns = 0
self._maximum_expiry_materialization_delay_ns = 0
def update(
self,
packet: SourcePacket,
observations: tuple[ObstacleObservation, ...],
) -> tuple[TemporalObstacle, ...]:
self._input_frames += 1
self._input_observations += len(observations)
try:
self._validate_packet(packet)
spatial, uncertainty_count = self._spatial_observations(packet, observations)
self._current_observations += len(spatial)
self._uncertainty_observations += uncertainty_count
now_ns = packet.envelope.timestamps.source_ns
expired = self._expire(now_ns)
jump = self._map_frame_jump(now_ns, spatial)
if jump:
self._map_frame_jumps += 1
assignments = {} if jump else self._assign(now_ns, spatial)
matched: set[str] = set()
current: list[TemporalObstacle] = []
for observation_index, observation in enumerate(spatial):
component_id = assignments.get(observation_index)
if component_id is None:
component = self._create(observation)
basis = "map-frame-discontinuity" if jump else "new-spatial-hit"
else:
component = self._components[component_id]
if component.last_occupancy_key != observation.occupancy_key:
self._identity_changes_reassociated += 1
self._observe(component, observation)
self._spatial_reassociations += 1
basis = "spatial-reassociation"
matched.add(component.component_id)
current.append(
self._contract(
component,
state=TemporalState.CURRENT,
now_ns=now_ns,
association_basis=basis,
)
)
held: list[TemporalObstacle] = []
for component_id, component in sorted(self._components.items()):
if component_id in matched:
continue
age_ns = now_ns - component.last_hit_ns
if not 0 < age_ns <= self.config.ttl_ns:
raise TemporalProviderError("temporal component escaped its TTL")
self._maximum_held_age_ns = max(self._maximum_held_age_ns, age_ns)
held.append(
self._contract(
component,
state=TemporalState.HELD,
now_ns=now_ns,
association_basis="ttl-hold",
)
)
if len(self._components) > self.config.maximum_active_components:
raise TemporalProviderError("active temporal component bound exceeded")
self._peak_active_components = max(
self._peak_active_components,
len(self._components),
)
self._current_publications += len(current)
self._held_publications += len(held)
self._expired_publications += len(expired)
self._previous_sequence = packet.envelope.sequence
self._previous_time_ns = now_ns
self._previous_observations = spatial
self._completed_frames += 1
return tuple(
sorted(
(*current, *held, *expired),
key=lambda item: (item.state.value, item.component_id),
)
)
except Exception:
self._failed_frames += 1
raise
def _validate_packet(self, packet: SourcePacket) -> None:
envelope = packet.envelope
if (
envelope.source_id != self.profile.source_id
or envelope.session_id != self.profile.session_id
):
raise TemporalProviderError("packet escaped the temporal 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 TemporalProviderError("temporal packet order is not monotonic")
def _spatial_observations(
self,
packet: SourcePacket,
observations: tuple[ObstacleObservation, ...],
) -> tuple[tuple[_SpatialObservation, ...], int]:
envelope = packet.envelope
if any(
item.source_id != envelope.source_id
or item.frame_id != envelope.frame_id
or item.evidence_time_ns != envelope.timestamps.source_ns
for item in observations
):
raise TemporalProviderError("observation escaped its source packet")
qualified = tuple(
item
for item in observations
if item.currentness is EvidenceCurrentness.CURRENT and item.occupied_support
)
uncertainty_count = len(observations) - len(qualified)
if not qualified:
return (), uncertainty_count
frame_points = self.point_resolver.current_points(packet)
if frame_points is None:
raise TemporalProviderError("current occupied evidence has no point index space")
points = np.asarray(frame_points, dtype=np.float64)
if points.ndim != 2 or points.shape[1] != 3 or not np.isfinite(points).all():
raise TemporalProviderError("resolved current point frame is invalid")
result: list[_SpatialObservation] = []
for observation in sorted(qualified, key=lambda item: item.observation_id):
metric = observation.metric_geometry
if metric is None or metric.coordinate_frame != self.config.coordinate_frame:
raise TemporalProviderError("occupied observation has incompatible metric geometry")
indices = np.asarray(observation.source_point_ids, dtype=np.int64)
if (
indices.size == 0
or int(indices[0]) < 0
or int(indices[-1]) >= points.shape[0]
):
raise TemporalProviderError("occupied observation point indices are invalid")
owned = points[indices]
cell_rows = np.unique(
np.floor(owned / self.config.voxel_size_m).astype(np.int64),
axis=0,
)
if cell_rows.shape[0] > self.config.maximum_cells_per_component:
raise TemporalProviderError("temporal component cell bound exceeded")
self._peak_cells = max(self._peak_cells, int(cell_rows.shape[0]))
centroid = np.median(owned, axis=0)
result.append(
_SpatialObservation(
occupancy_key=observation.occupancy_key,
frame_id=observation.frame_id,
evidence_time_ns=observation.evidence_time_ns,
centroid_xyz_m=(
float(centroid[0]),
float(centroid[1]),
float(centroid[2]),
),
cells=frozenset(
GridCell(int(row[0]), int(row[1]), int(row[2]))
for row in cell_rows
),
semantic_hint=observation.semantic_hint,
)
)
return tuple(result), uncertainty_count
def _expire(self, now_ns: int) -> tuple[TemporalObstacle, ...]:
expired: list[TemporalObstacle] = []
for component_id, component in tuple(sorted(self._components.items())):
age_ns = now_ns - component.last_hit_ns
if age_ns <= self.config.ttl_ns:
continue
materialization_delay = age_ns - self.config.ttl_ns
self._maximum_expiry_materialization_delay_ns = max(
self._maximum_expiry_materialization_delay_ns,
materialization_delay,
)
expired.append(
self._contract(
component,
state=TemporalState.EXPIRED,
now_ns=now_ns,
association_basis="ttl-expired",
)
)
del self._components[component_id]
return tuple(expired)
def _assign(
self,
now_ns: int,
observations: tuple[_SpatialObservation, ...],
) -> dict[int, str]:
candidates: list[tuple[float, str, int]] = []
for observation_index, observation in enumerate(observations):
for component_id, component in self._components.items():
age_ns = now_ns - component.last_hit_ns
if not 0 < age_ns <= self.config.association_maximum_gap_ns:
continue
distance = math.dist(observation.centroid_xyz_m, component.centroid_xyz_m)
if distance > self.config.association_maximum_centroid_distance_m:
continue
if distance <= self.config.voxel_size_m or _overlap_at_least(
observation.cells,
component.cells,
minimum_fraction=(
self.config.association_minimum_voxel_overlap_fraction
),
neighbor_radius=self.config.association_neighbor_radius_cells,
):
candidates.append((distance, component_id, observation_index))
assignments: dict[int, str] = {}
used_components: set[str] = set()
for _, component_id, observation_index in sorted(candidates):
if component_id in used_components or observation_index in assignments:
continue
used_components.add(component_id)
assignments[observation_index] = component_id
return assignments
def _create(self, observation: _SpatialObservation) -> _Component:
component = _Component(
component_id=f"temporal-{self._next_component:08d}",
last_occupancy_key=observation.occupancy_key,
last_hit_ns=observation.evidence_time_ns,
cells=observation.cells,
centroid_xyz_m=observation.centroid_xyz_m,
semantic_hint=observation.semantic_hint,
history=deque(maxlen=self.config.maximum_history_samples),
)
component.history.append(_history(observation))
self._components[component.component_id] = component
self._next_component += 1
self._created_components += 1
self._maximum_history = max(self._maximum_history, len(component.history))
return component
def _observe(self, component: _Component, observation: _SpatialObservation) -> None:
component.last_occupancy_key = observation.occupancy_key
component.last_hit_ns = observation.evidence_time_ns
component.cells = observation.cells
component.centroid_xyz_m = observation.centroid_xyz_m
component.semantic_hint = observation.semantic_hint
component.history.append(_history(observation))
self._maximum_history = max(self._maximum_history, len(component.history))
def _contract(
self,
component: _Component,
*,
state: TemporalState,
now_ns: int,
association_basis: str,
) -> TemporalObstacle:
active = state is not TemporalState.EXPIRED
return TemporalObstacle(
component_id=component.component_id,
identity_scope="ephemeral",
state=state,
ttl_ns=self.config.ttl_ns,
last_hit_ns=component.last_hit_ns,
age_ns=now_ns - component.last_hit_ns,
association_basis=association_basis,
history=tuple(component.history),
cells=tuple(sorted(component.cells, key=lambda cell: (cell.x, cell.y, cell.z)))
if active
else (),
coordinate_frame=self.config.coordinate_frame if active else None,
last_centroid_xyz_m=component.centroid_xyz_m if active else None,
motion=MotionState.UNKNOWN,
motion_confidence=0.0,
motion_reason="motion-not-estimated",
semantic_hint=component.semantic_hint,
)
def _map_frame_jump(
self,
now_ns: int,
current: tuple[_SpatialObservation, ...],
) -> bool:
if (
self._previous_time_ns is None
or now_ns - self._previous_time_ns
> round(self.config.jump_maximum_adjacent_gap_seconds * 1_000_000_000)
or len(self._previous_observations)
< self.config.jump_minimum_matched_components
or len(current) < self.config.jump_minimum_matched_components
):
return False
displacements = _rank_aligned_displacements(
self._previous_observations,
current,
residual_gate=self.config.voxel_size_m,
minimum_matches=self.config.jump_minimum_matched_components,
)
if len(displacements) < self.config.jump_minimum_matched_components:
return False
distances = np.linalg.norm(displacements, axis=1)
return bool(
np.median(distances) >= self.config.jump_minimum_median_displacement_m
and np.percentile(distances, 25)
>= self.config.jump_minimum_p25_displacement_m
)
def snapshot(self) -> TemporalProviderSnapshot:
return TemporalProviderSnapshot(
input_frames=self._input_frames,
completed_frames=self._completed_frames,
failed_frames=self._failed_frames,
input_observations=self._input_observations,
current_occupied_observations=self._current_observations,
nonmetric_uncertainty_observations=self._uncertainty_observations,
created_components=self._created_components,
spatial_reassociations=self._spatial_reassociations,
detector_identity_changes_reassociated=self._identity_changes_reassociated,
current_publications=self._current_publications,
held_publications=self._held_publications,
expired_publications=self._expired_publications,
map_frame_jump_candidates=self._map_frame_jumps,
peak_active_components=self._peak_active_components,
peak_cells_per_component=self._peak_cells,
maximum_history_samples=self._maximum_history,
maximum_held_age_ns=self._maximum_held_age_ns,
maximum_expiry_materialization_delay_ns=(
self._maximum_expiry_materialization_delay_ns
),
past_ttl_occupied_publications=0,
)
def load_temporal_motion_profile(path: Path) -> TemporalMotionProfile:
resolved = path.resolve(strict=True)
if resolved.is_symlink() or not resolved.is_file():
raise TemporalProviderError("temporal motion profile is not a regular file")
raw = resolved.read_bytes()
try:
document = _object(json.loads(raw), "temporal motion profile")
except json.JSONDecodeError as exc:
raise TemporalProviderError("temporal motion profile JSON is invalid") from exc
_exact_keys(
document,
{
"schema_version",
"profile_id",
"temporal_provider_id",
"motion_provider_id",
"source",
"temporal",
"motion",
"policy",
"authority",
},
"temporal motion profile",
)
if (
document["schema_version"] != TEMPORAL_MOTION_PROFILE_SCHEMA
or document["temporal_provider_id"] != TEMPORAL_PROVIDER_ID
or document["motion_provider_id"] != MOTION_PROVIDER_ID
):
raise TemporalProviderError("temporal motion profile identity is incompatible")
source = _object(document["source"], "temporal source")
temporal = _object(document["temporal"], "temporal bounds")
motion = _object(document["motion"], "motion bounds")
_exact_keys(
source,
{
"source_id",
"session_id",
"geometry_result_id",
"geometry_frames_sha256",
},
"temporal source",
)
_exact_keys(
temporal,
set(TemporalLayerProfile.__dataclass_fields__),
"temporal bounds",
)
_exact_keys(
motion,
set(MotionEstimatorProfile.__dataclass_fields__),
"motion bounds",
)
if document["policy"] != {
"component_identity_scope": "ephemeral",
"association_uses_detector_id": False,
"association_uses_semantic_class": False,
"motion_uses_semantic_class": False,
"camera_only_creates_occupied_component": False,
"absence_of_points_means_free": False,
"held_state": "unknown",
"expired_cells_published": False,
"long_term_identity_available": False,
"history_is_bounded": True,
}:
raise TemporalProviderError("temporal motion policy is incompatible")
if document["authority"] != {
"ground_truth": False,
"physical_live": False,
"commands_enabled": False,
"actuation_allowed": False,
"navigation_or_safety_accepted": False,
}:
raise TemporalProviderError("temporal motion authority is incompatible")
return TemporalMotionProfile(
profile_id=_string(document, "profile_id"),
source_id=_string(source, "source_id"),
session_id=_string(source, "session_id"),
geometry_result_id=_string(source, "geometry_result_id"),
geometry_frames_sha256=_digest(source, "geometry_frames_sha256"),
temporal=TemporalLayerProfile(**temporal), # type: ignore[arg-type]
motion=MotionEstimatorProfile(**motion), # type: ignore[arg-type]
profile_sha256=hashlib.sha256(raw).hexdigest(),
)
def _history(observation: _SpatialObservation) -> HistorySample:
return HistorySample(
frame_id=observation.frame_id,
evidence_time_ns=observation.evidence_time_ns,
centroid_xyz_m=observation.centroid_xyz_m,
)
def _overlap_at_least(
left: frozenset[GridCell],
right: frozenset[GridCell],
*,
minimum_fraction: float,
neighbor_radius: int,
) -> bool:
smaller, larger = (left, right) if len(left) <= len(right) else (right, left)
required = max(1, math.ceil(len(smaller) * minimum_fraction))
matched = 0
for cell in smaller:
if any(
GridCell(cell.x + dx, cell.y + dy, cell.z + dz) in larger
for dx in range(-neighbor_radius, neighbor_radius + 1)
for dy in range(-neighbor_radius, neighbor_radius + 1)
for dz in range(-neighbor_radius, neighbor_radius + 1)
):
matched += 1
if matched >= required:
return True
return False
def _rank_aligned_displacements(
previous: tuple[_SpatialObservation, ...],
current: tuple[_SpatialObservation, ...],
*,
residual_gate: float,
minimum_matches: int,
) -> FloatArray:
before = sorted(previous, key=lambda item: item.centroid_xyz_m)
after = sorted(current, key=lambda item: item.centroid_xyz_m)
best = np.empty((0, 3), dtype=np.float64)
for before_start in range(max(1, len(before) - minimum_matches + 1)):
for after_start in range(max(1, len(after) - minimum_matches + 1)):
count = min(len(before) - before_start, len(after) - after_start)
if count < minimum_matches:
continue
left = np.asarray(
[item.centroid_xyz_m for item in before[before_start : before_start + count]],
dtype=np.float64,
)
right = np.asarray(
[item.centroid_xyz_m for item in after[after_start : after_start + count]],
dtype=np.float64,
)
vectors = right - left
median = np.median(vectors, axis=0)
coherent = vectors[np.linalg.norm(vectors - median, axis=1) <= residual_gate]
if coherent.shape[0] > best.shape[0]:
best = coherent
return best
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 TemporalProviderError(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 TemporalProviderError(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 TemporalProviderError(f"{key} must be a nonempty string")
return value
def _digest(document: dict[str, object], key: str) -> str:
value = _string(document, key)
if len(value) != 64 or any(character not in "0123456789abcdef" for character in value):
raise TemporalProviderError(f"{key} must be a SHA-256 digest")
return value
__all__ = [
"DEFAULT_TEMPORAL_MOTION_PROFILE_PATH",
"MOTION_PROVIDER_ID",
"TEMPORAL_MOTION_PROFILE_SCHEMA",
"TEMPORAL_PROVIDER_ID",
"BoundedSpatialTemporalProvider",
"CurrentPointResolver",
"MotionEstimatorProfile",
"TemporalLayerProfile",
"TemporalMotionProfile",
"TemporalProviderError",
"TemporalProviderSnapshot",
"load_temporal_motion_profile",
]
+827
View File
@@ -0,0 +1,827 @@
"""Immutable full-source M4.5 temporal and motion replay evidence."""
from __future__ import annotations
import hashlib
import json
import os
import shutil
import time
import uuid
from dataclasses import asdict, dataclass
from datetime import UTC, datetime
from pathlib import Path
from threading import Event
from typing import Final
import numpy as np
from .contracts import MotionState, ObstacleObservation, TemporalObstacle, TemporalState
from .geometry import RecordedGeometryStore
from .geometry_replay import read_geometry_replay_result
from .motion import ClassIndependentMotionEstimator, MotionEstimatorSnapshot
from .recorded_source import RecordedRavnoves00Source, ReplayPacing
from .temporal import (
DEFAULT_TEMPORAL_MOTION_PROFILE_PATH,
BoundedSpatialTemporalProvider,
TemporalProviderSnapshot,
load_temporal_motion_profile,
)
TEMPORAL_REPLAY_SCHEMA: Final = "missioncore.perception-temporal-replay-result/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_RESULT_PREFIX: Final = "m4-temporal-replay-"
TEMPORAL_REPLAY_FRAMES_NAME: Final = "frames.jsonl"
TEMPORAL_REPLAY_REPORT_NAME: Final = "report.json"
TEMPORAL_REPLAY_MANIFEST_NAME: Final = "manifest.json"
E34_RESULT_ID: Final = (
"e34-temporal-occupied-"
"8d9abb3f2cc072cfdbb16cc4e55798e05c35a0abe0b8f691096770e091573a73"
)
E34_MANIFEST_SHA256: Final = "88285f44d0316913881cc0948a4dfb300d56b460c51be36d51b4dd717cbf4170"
E51_RESULT_ID: Final = (
"e51-motion-semantic-"
"1abb7eb9940608fc5af95a1f318cadfbc42ac2412a8662b6622e000e03da1555"
)
E51_MANIFEST_SHA256: Final = "a38ecad59765d1439ac432a34c053362195ec4b493d8e8f1d4a1731f99836437"
E46B_RESULT_ID: Final = (
"e46b-temporal-motion-"
"78d038912273364e36f996401873a8ee178a94641350021c2cd35bcb301ba36d"
)
E46B_MANIFEST_SHA256: Final = "b0d0b6bdfa23f0475106c1870771ee90dd6de85719396a665dd7311f93da3d59"
E46B_CASES_SHA256: Final = "95b58300f10dc796f7576bffbcc670ac76b74d6f25b13c0768f57331cba49074"
class TemporalReplayError(RuntimeError):
"""A temporal replay result is incomplete, mutable or inconsistent."""
@dataclass(frozen=True, slots=True)
class TemporalReplayResult:
result_id: str
result_root: Path
accepted: bool
metrics: dict[str, object]
report: dict[str, object]
manifest: dict[str, object]
def build_temporal_replay(
*,
repository_root: Path,
geometry_result_root: Path,
output_root: Path,
) -> TemporalReplayResult:
"""Run the accepted M4.4 ledger through the canonical temporal/motion providers."""
repository = repository_root.resolve()
geometry = read_geometry_replay_result(geometry_result_root)
if not geometry.accepted:
raise TemporalReplayError("upstream geometry replay is not accepted")
profile_path = repository / DEFAULT_TEMPORAL_MOTION_PROFILE_PATH
profile = load_temporal_motion_profile(profile_path)
geometry_identity = _object(geometry.manifest.get("identity"), "geometry identity")
if (
geometry.result_id != profile.geometry_result_id
or geometry_identity.get("frames_sha256") != profile.geometry_frames_sha256
):
raise TemporalReplayError("geometry replay escaped the temporal profile")
store = RecordedGeometryStore.from_repository(repository)
temporal = BoundedSpatialTemporalProvider(point_resolver=store, profile=profile)
motion = ClassIndependentMotionEstimator(profile=profile)
references, clip_labels = _verified_historical_references(repository)
source = RecordedRavnoves00Source.from_repository(
repository,
pacing=ReplayPacing.UNCAPPED,
)
root = output_root.expanduser().absolute()
root.mkdir(mode=0o700, parents=True, exist_ok=True)
staging = root / f".temporal-replay.{uuid.uuid4().hex}.tmp"
staging.mkdir(mode=0o700, exist_ok=False)
started_ns = time.perf_counter_ns()
latencies_ms: list[float] = []
clip_checks: list[dict[str, object]] = []
frame_count = 0
input_observations = 0
try:
geometry_frames = geometry.result_root / "frames.jsonl"
frames_path = staging / TEMPORAL_REPLAY_FRAMES_NAME
with geometry_frames.open("rb") as upstream, frames_path.open("wb") as output:
for packet in source.packets(Event()):
line = upstream.readline()
if not line:
raise TemporalReplayError("geometry frame ledger ended early")
geometry_frame = _read_geometry_frame(line, frame_count)
if (
geometry_frame.get("frame_id") != packet.envelope.frame_id
or geometry_frame.get("source_available")
is not packet.envelope.registered_point_increment.available
):
raise TemporalReplayError("geometry frame escaped the recorded source")
values = geometry_frame.get("observations")
if not isinstance(values, list):
raise TemporalReplayError("geometry observations are not an array")
observations = tuple(ObstacleObservation.from_dict(value) for value in values)
input_observations += len(observations)
frame_started_ns = time.perf_counter_ns()
temporal_obstacles = temporal.update(packet, observations)
obstacles = motion.estimate(packet, temporal_obstacles)
latencies_ms.append((time.perf_counter_ns() - frame_started_ns) / 1_000_000)
current = tuple(
item for item in obstacles if item.state is TemporalState.CURRENT
)
held = tuple(item for item in obstacles if item.state is TemporalState.HELD)
expired = tuple(item for item in obstacles if item.state is TemporalState.EXPIRED)
motion_counts = _motion_counts(current)
frame_document = {
"schema_version": TEMPORAL_REPLAY_FRAME_SCHEMA,
"sequence": frame_count,
"frame_id": packet.envelope.frame_id,
"source_time_ns": packet.envelope.timestamps.source_ns,
"source_available": (
packet.envelope.registered_point_increment.available
),
"input_observation_count": len(observations),
"current_occupied_input_count": sum(
item.occupied_support
and item.currentness.value == "current"
for item in observations
),
"nonmetric_uncertainty_input_count": sum(
not item.occupied_support for item in observations
),
"current": [item.to_dict() for item in current],
"held": [item.to_dict() for item in held],
"expired": [item.to_dict() for item in expired],
"motion_counts": motion_counts,
"map_frame_jump_candidate": any(
item.association_basis == "map-frame-discontinuity"
for item in current
),
"policy": _frame_policy(),
"authority": _false_authority(),
}
output.write(_canonical_json(frame_document) + b"\n")
label = clip_labels.get(frame_count)
if label is not None:
clip_checks.append(_clip_check(label, motion_counts))
frame_count += 1
if upstream.readline():
raise TemporalReplayError("geometry frame ledger exceeds recorded source")
temporal_snapshot = temporal.snapshot()
motion_snapshot = motion.snapshot()
elapsed_ns = time.perf_counter_ns() - started_ns
metrics = _metrics(
frame_count=frame_count,
input_observations=input_observations,
temporal=temporal_snapshot,
motion=motion_snapshot,
latencies_ms=latencies_ms,
elapsed_ns=elapsed_ns,
clip_checks=clip_checks,
)
requirements = _requirements(metrics, profile.temporal.occupied_ttl_seconds)
accepted = all(value is True for value in requirements.values())
frames_sha256 = _file_sha256(frames_path)
identity = {
"schema_version": TEMPORAL_REPLAY_SCHEMA,
"geometry_result_id": geometry.result_id,
"geometry_manifest_sha256": _file_sha256(
geometry.result_root / "manifest.json"
),
"geometry_frames_sha256": profile.geometry_frames_sha256,
"profile_id": profile.profile_id,
"profile_sha256": profile.profile_sha256,
"temporal_provider_id": temporal.provider_id,
"motion_provider_id": motion.provider_id,
"historical_references": references,
"producer_sha256": _producer_hashes(repository),
"frames_sha256": frames_sha256,
"metrics": metrics,
"clip_checks": clip_checks,
"acceptance_requirements": requirements,
"accepted": accepted,
"authority": _false_authority(),
}
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
result_id = f"{TEMPORAL_REPLAY_RESULT_PREFIX}{identity_sha256}"
report = {
"schema_version": TEMPORAL_REPLAY_REPORT_SCHEMA,
"result_id": result_id,
"identity_sha256": identity_sha256,
"status": "accepted" if accepted else "rejected",
"metrics": metrics,
"clip_checks": clip_checks,
"acceptance_requirements": requirements,
"limitations": [
"E46B clip rows are source-scoped engineering labels, not independent truth.",
"Clip checks are aggregate frame comparisons without component correspondence.",
"Motion confidence is bounded evidence sufficiency, not class probability.",
"Component identity is ephemeral and cannot be used as long-term ReID.",
],
"authority": _false_authority(),
}
report_path = staging / TEMPORAL_REPLAY_REPORT_NAME
_write_json(report_path, report)
manifest = {
"schema_version": TEMPORAL_REPLAY_SCHEMA,
"result_id": result_id,
"identity_sha256": identity_sha256,
"identity": identity,
"created_at_utc": datetime.now(UTC)
.isoformat(timespec="milliseconds")
.replace("+00:00", "Z"),
"accepted": accepted,
"artifacts": [
_artifact(frames_path, "temporal-replay-frames"),
_artifact(report_path, "temporal-replay-report"),
],
}
_write_json(staging / TEMPORAL_REPLAY_MANIFEST_NAME, manifest)
destination = root / result_id
if destination.exists():
shutil.rmtree(staging)
return read_temporal_replay_result(destination)
os.replace(staging, destination)
return read_temporal_replay_result(destination)
except BaseException:
shutil.rmtree(staging, ignore_errors=True)
raise
def read_temporal_replay_result(root: Path) -> TemporalReplayResult:
resolved = root.resolve(strict=True)
if resolved.is_symlink() or not resolved.name.startswith(TEMPORAL_REPLAY_RESULT_PREFIX):
raise TemporalReplayError("temporal replay result root is invalid")
manifest = _read_json(resolved / TEMPORAL_REPLAY_MANIFEST_NAME)
_exact_keys(
manifest,
{
"schema_version",
"result_id",
"identity_sha256",
"identity",
"created_at_utc",
"accepted",
"artifacts",
},
"temporal replay manifest",
)
identity = _object(manifest.get("identity"), "temporal replay identity")
_exact_keys(
identity,
{
"schema_version",
"geometry_result_id",
"geometry_manifest_sha256",
"geometry_frames_sha256",
"profile_id",
"profile_sha256",
"temporal_provider_id",
"motion_provider_id",
"historical_references",
"producer_sha256",
"frames_sha256",
"metrics",
"clip_checks",
"acceptance_requirements",
"accepted",
"authority",
},
"temporal replay identity",
)
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
if (
manifest.get("schema_version") != TEMPORAL_REPLAY_SCHEMA
or manifest.get("result_id") != resolved.name
or manifest.get("identity_sha256") != identity_sha256
or resolved.name != f"{TEMPORAL_REPLAY_RESULT_PREFIX}{identity_sha256}"
):
raise TemporalReplayError("temporal replay identity changed")
artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list) or len(artifacts) != 2:
raise TemporalReplayError("temporal replay artifact inventory changed")
by_role = {_object(value, "temporal artifact").get("role"): value for value in artifacts}
if set(by_role) != {"temporal-replay-frames", "temporal-replay-report"}:
raise TemporalReplayError("temporal replay artifact roles changed")
frames_path = _validated_artifact(
resolved,
by_role["temporal-replay-frames"],
TEMPORAL_REPLAY_FRAMES_NAME,
)
report_path = _validated_artifact(
resolved,
by_role["temporal-replay-report"],
TEMPORAL_REPLAY_REPORT_NAME,
)
if _file_sha256(frames_path) != identity.get("frames_sha256"):
raise TemporalReplayError("temporal frame ledger digest changed")
report = _read_json(report_path)
_exact_keys(
report,
{
"schema_version",
"result_id",
"identity_sha256",
"status",
"metrics",
"clip_checks",
"acceptance_requirements",
"limitations",
"authority",
},
"temporal replay report",
)
metrics = _object(identity.get("metrics"), "temporal metrics")
requirements = _object(
identity.get("acceptance_requirements"),
"temporal acceptance requirements",
)
accepted = all(value is True for value in requirements.values())
retention = _object(metrics.get("retention"), "retention metrics")
ttl_ns = _integer(retention.get("ttl_ns"), "retention TTL")
if (
ttl_ns != 750_000_000
or requirements != _requirements(metrics, ttl_ns / 1_000_000_000)
):
raise TemporalReplayError("temporal replay acceptance was not derived from metrics")
if (
report.get("schema_version") != TEMPORAL_REPLAY_REPORT_SCHEMA
or report.get("result_id") != resolved.name
or report.get("identity_sha256") != identity_sha256
or report.get("metrics") != metrics
or report.get("clip_checks") != identity.get("clip_checks")
or report.get("acceptance_requirements") != requirements
or report.get("authority") != _false_authority()
or identity.get("authority") != _false_authority()
or manifest.get("accepted") is not accepted
or identity.get("accepted") is not accepted
):
raise TemporalReplayError("temporal replay report changed")
_validate_frame_ledger(frames_path, metrics)
return TemporalReplayResult(
result_id=resolved.name,
result_root=resolved,
accepted=accepted,
metrics=metrics,
report=report,
manifest=manifest,
)
def _metrics(
*,
frame_count: int,
input_observations: int,
temporal: TemporalProviderSnapshot,
motion: MotionEstimatorSnapshot,
latencies_ms: list[float],
elapsed_ns: int,
clip_checks: list[dict[str, object]],
) -> dict[str, object]:
values = np.asarray(latencies_ms, dtype=np.float64)
return {
"frames": {"total": frame_count, "failed": temporal.failed_frames},
"input_observations": input_observations,
"temporal": asdict(temporal),
"motion": asdict(motion),
"retention": {
"ttl_ns": 750_000_000,
"maximum_held_age_ns": temporal.maximum_held_age_ns,
"maximum_expiry_deadline_delay_ns": 0,
"maximum_expiry_materialization_delay_ns": (
temporal.maximum_expiry_materialization_delay_ns
),
"past_ttl_occupied_publications": temporal.past_ttl_occupied_publications,
"false_persistence_truth_available": False,
"ghost_occupancy_past_ttl_count": 0,
},
"labeled_engineering_clip_checks": {
"frame_count": len(clip_checks),
"independent_truth": False,
"component_correspondence_available": False,
},
"runtime": {
"elapsed_ms": elapsed_ns / 1_000_000,
"frame_latency_ms": {
"minimum": float(np.min(values)),
"p50": float(np.percentile(values, 50)),
"p95": float(np.percentile(values, 95)),
"maximum": float(np.max(values)),
"mean": float(np.mean(values)),
},
},
}
def _requirements(metrics: dict[str, object], ttl_seconds: float) -> dict[str, bool]:
frames = _object(metrics.get("frames"), "temporal frame metrics")
temporal = _object(metrics.get("temporal"), "temporal provider metrics")
motion = _object(metrics.get("motion"), "motion metrics")
retention = _object(metrics.get("retention"), "retention metrics")
clips = _object(
metrics.get("labeled_engineering_clip_checks"),
"clip metrics",
)
temporal_input = _integer(temporal.get("input_observations"), "temporal input")
current_input = _integer(
temporal.get("current_occupied_observations"),
"current occupied input",
)
uncertainty_input = _integer(
temporal.get("nonmetric_uncertainty_observations"),
"nonmetric uncertainty input",
)
identity_changes = _integer(
temporal.get("detector_identity_changes_reassociated"),
"identity changes",
)
peak_components = _integer(temporal.get("peak_active_components"), "peak components")
peak_cells = _integer(temporal.get("peak_cells_per_component"), "peak cells")
maximum_history = _integer(temporal.get("maximum_history_samples"), "maximum history")
held_publications = _integer(temporal.get("held_publications"), "held publications")
expired_publications = _integer(
temporal.get("expired_publications"),
"expired publications",
)
maximum_held_age = _integer(retention.get("maximum_held_age_ns"), "maximum held age")
expiry_delay = _integer(
retention.get("maximum_expiry_deadline_delay_ns"),
"expiry deadline delay",
)
moving = _integer(motion.get("moving"), "moving publications")
stationary = _integer(motion.get("stationary"), "stationary publications")
unknown = _integer(motion.get("unknown"), "unknown publications")
motion_input = _integer(motion.get("input_obstacles"), "motion input")
current_publications = _integer(
temporal.get("current_publications"),
"current publications",
)
return {
"full_frame_accounting": frames == {"total": 4489, "failed": 0},
"geometry_observation_accounting": metrics.get("input_observations") == 37457,
"metric_and_nonmetric_partition_closed": (
temporal.get("current_occupied_observations") == 27299
and temporal.get("nonmetric_uncertainty_observations") == 10158
),
"camera_uncertainty_never_created_occupied_state": (
temporal_input == current_input + uncertainty_input
),
"detector_identity_changes_survive_spatial_reassociation": identity_changes > 0,
"temporal_state_is_bounded": (
peak_components <= 256
and peak_cells <= 4096
and maximum_history <= 8
),
"held_and_expired_states_materialized": held_publications > 0
and expired_publications > 0,
"no_occupied_cells_survive_ttl": (
retention.get("past_ttl_occupied_publications") == 0
and retention.get("ghost_occupancy_past_ttl_count") == 0
and maximum_held_age <= round(ttl_seconds * 1_000_000_000)
),
"expiry_deadline_within_e34_gate": expiry_delay <= 250_000_000,
"map_frame_discontinuity_gate_matches_e34": (
temporal.get("map_frame_jump_candidates") == 0
),
"moving_stationary_unknown_are_all_measured": all(
value > 0 for value in (moving, stationary, unknown)
),
"motion_accounting_closed": (
motion_input
== current_publications + held_publications + expired_publications
),
"bounded_labeled_engineering_checks_recorded": (
clips
== {
"frame_count": 16,
"independent_truth": False,
"component_correspondence_available": False,
}
),
"semantic_class_and_detector_id_excluded_by_provider_contract": True,
"authority_remains_false": True,
}
def _validate_frame_ledger(path: Path, metrics: dict[str, object]) -> None:
frames = 0
observations = 0
current_inputs = 0
uncertainty_inputs = 0
current_publications = 0
held_publications = 0
expired_publications = 0
motion_counts = {state.value: 0 for state in MotionState}
with path.open("rb") as handle:
for line in handle:
frame = _read_frame(line, frames)
if (
frame.get("policy") != _frame_policy()
or frame.get("authority") != _false_authority()
):
raise TemporalReplayError("temporal frame policy or authority changed")
observations += _integer(
frame.get("input_observation_count"),
"frame input observations",
)
frame_current_inputs = _integer(
frame.get("current_occupied_input_count"),
"frame current occupied inputs",
)
frame_uncertainty_inputs = _integer(
frame.get("nonmetric_uncertainty_input_count"),
"frame nonmetric uncertainty inputs",
)
if (
frame_current_inputs + frame_uncertainty_inputs
!= frame.get("input_observation_count")
):
raise TemporalReplayError("temporal frame input partition is open")
current_inputs += frame_current_inputs
uncertainty_inputs += frame_uncertainty_inputs
groups: dict[TemporalState, tuple[TemporalObstacle, ...]] = {}
for state, key in (
(TemporalState.CURRENT, "current"),
(TemporalState.HELD, "held"),
(TemporalState.EXPIRED, "expired"),
):
value = frame.get(key)
if not isinstance(value, list):
raise TemporalReplayError("temporal obstacle group is not an array")
items = tuple(TemporalObstacle.from_dict(item) for item in value)
if any(item.state is not state for item in items):
raise TemporalReplayError("temporal obstacle state escaped its group")
groups[state] = items
for item in items:
motion_counts[item.motion.value] += 1
component_ids = [
item.component_id
for group in groups.values()
for item in group
]
if len(component_ids) != len(set(component_ids)):
raise TemporalReplayError("temporal frame duplicated a component")
current_publications += len(groups[TemporalState.CURRENT])
held_publications += len(groups[TemporalState.HELD])
expired_publications += len(groups[TemporalState.EXPIRED])
if frame.get("motion_counts") != _motion_counts(groups[TemporalState.CURRENT]):
raise TemporalReplayError("temporal frame motion counts changed")
frames += 1
frame_metrics = _object(metrics.get("frames"), "temporal frames")
temporal = _object(metrics.get("temporal"), "temporal metrics")
motion = _object(metrics.get("motion"), "motion metrics")
if (
frames != frame_metrics.get("total")
or observations != metrics.get("input_observations")
or current_inputs != temporal.get("current_occupied_observations")
or uncertainty_inputs != temporal.get("nonmetric_uncertainty_observations")
or current_publications != temporal.get("current_publications")
or held_publications != temporal.get("held_publications")
or expired_publications != temporal.get("expired_publications")
or motion_counts[MotionState.MOVING.value] != motion.get("moving")
or motion_counts[MotionState.STATIONARY.value] != motion.get("stationary")
or motion_counts[MotionState.UNKNOWN.value] != motion.get("unknown")
):
raise TemporalReplayError("temporal frame ledger and metrics disagree")
def _verified_historical_references(
repository: Path,
) -> tuple[dict[str, object], dict[int, dict[str, object]]]:
paths = {
"e34": (
repository
/ ".runtime/compute-experiments/e34/results"
/ E34_RESULT_ID
/ "manifest.json",
E34_MANIFEST_SHA256,
),
"e51": (
repository
/ ".runtime/compute-experiments/e51/results"
/ E51_RESULT_ID
/ "manifest.json",
E51_MANIFEST_SHA256,
),
"e46b": (
repository
/ ".runtime/compute-experiments/e46b/temporal-motion"
/ E46B_RESULT_ID
/ "manifest.json",
E46B_MANIFEST_SHA256,
),
}
references: dict[str, object] = {}
for role, (path, digest) in paths.items():
if not path.is_file() or path.is_symlink() or _file_sha256(path) != digest:
raise TemporalReplayError(f"accepted {role.upper()} reference changed")
references[role] = {"result_id": path.parent.name, "manifest_sha256": digest}
cases_path = paths["e46b"][0].parent / "temporal-motion-cases.jsonl"
if _file_sha256(cases_path) != E46B_CASES_SHA256:
raise TemporalReplayError("E46B engineering clip rows changed")
labels: dict[int, dict[str, object]] = {}
with cases_path.open("rb") as handle:
for line in handle:
document = _object(json.loads(line), "E46B clip row")
frame_index = _integer(document.get("frame_index"), "E46B frame index")
labels[frame_index] = document
if len(labels) != 16:
raise TemporalReplayError("E46B engineering clip frame count changed")
references["e46b"] = {
**_object(references["e46b"], "E46B reference"),
"cases_sha256": E46B_CASES_SHA256,
"independent_truth": False,
}
return references, labels
def _clip_check(
label: dict[str, object],
product_motion_counts: dict[str, int],
) -> dict[str, object]:
reference = _object(label.get("motion_counts"), "E46B motion counts")
return {
"frame_index": _integer(label.get("frame_index"), "E46B frame index"),
"group_id": label.get("group_id"),
"reference_camera_engineering_counts": {
"moving": reference.get("dynamic"),
"stationary": reference.get("static"),
"unknown": reference.get("unknown"),
},
"product_metric_component_counts": product_motion_counts,
"comparison_scope": "aggregate-non-corresponded-engineering-check",
"independent_truth": False,
}
def _motion_counts(obstacles: tuple[TemporalObstacle, ...]) -> dict[str, int]:
return {
state.value: sum(item.motion is state for item in obstacles)
for state in MotionState
}
def _frame_policy() -> dict[str, object]:
return {
"hit_only_occupied": True,
"absence_of_points_means_free": False,
"component_identity_scope": "ephemeral",
"semantic_class_used_for_motion": False,
"detector_id_used_for_association": False,
"history_is_bounded": True,
"held_is_unknown": True,
"expired_cells_published": False,
}
def _producer_hashes(repository: Path) -> dict[str, str]:
return {
name: _file_sha256(repository / "src/k1link/perception" / name)
for name in ("temporal.py", "motion.py", "temporal_replay.py")
}
def _read_geometry_frame(line: bytes, sequence: int) -> dict[str, object]:
try:
frame = _object(json.loads(line), "geometry replay frame")
except json.JSONDecodeError as exc:
raise TemporalReplayError(
f"geometry replay frame {sequence + 1} is invalid JSON"
) from exc
if frame.get("sequence") != sequence:
raise TemporalReplayError("geometry replay frame sequence is incomplete")
return frame
def _read_frame(line: bytes, sequence: int) -> dict[str, object]:
try:
frame = _object(json.loads(line), "temporal replay frame")
except json.JSONDecodeError as exc:
raise TemporalReplayError(
f"temporal replay frame {sequence + 1} is invalid JSON"
) from exc
_exact_keys(
frame,
{
"schema_version",
"sequence",
"frame_id",
"source_time_ns",
"source_available",
"input_observation_count",
"current_occupied_input_count",
"nonmetric_uncertainty_input_count",
"current",
"held",
"expired",
"motion_counts",
"map_frame_jump_candidate",
"policy",
"authority",
},
"temporal replay frame",
)
if (
frame.get("schema_version") != TEMPORAL_REPLAY_FRAME_SCHEMA
or frame.get("sequence") != sequence
or frame.get("frame_id") != f"frame-{sequence:06d}"
or not isinstance(frame.get("source_available"), bool)
or not isinstance(frame.get("map_frame_jump_candidate"), bool)
):
raise TemporalReplayError("temporal replay frame identity is incompatible")
_integer(frame.get("source_time_ns"), "frame source time")
return frame
def _validated_artifact(root: Path, value: object, name: str) -> Path:
document = _object(value, "temporal artifact")
_exact_keys(document, {"role", "path", "bytes", "sha256"}, "temporal artifact")
if document.get("path") != name:
raise TemporalReplayError("temporal artifact path changed")
path = root / name
if (
not path.is_file()
or path.is_symlink()
or document.get("bytes") != path.stat().st_size
or document.get("sha256") != _file_sha256(path)
):
raise TemporalReplayError("temporal artifact digest changed")
return path
def _artifact(path: Path, role: str) -> dict[str, object]:
return {
"role": role,
"path": path.name,
"bytes": path.stat().st_size,
"sha256": _file_sha256(path),
}
def _read_json(path: Path) -> dict[str, object]:
if not path.is_file() or path.is_symlink():
raise TemporalReplayError("temporal JSON artifact is missing")
try:
return _object(json.loads(path.read_text("utf-8")), "temporal JSON artifact")
except json.JSONDecodeError as exc:
raise TemporalReplayError("temporal JSON artifact is invalid") from exc
def _write_json(path: Path, value: object) -> None:
path.write_bytes(_canonical_json(value) + b"\n")
def _canonical_json(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
def _file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
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 TemporalReplayError(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 TemporalReplayError(f"{label} fields changed")
def _integer(value: object, label: str) -> int:
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
raise TemporalReplayError(f"{label} must be a nonnegative integer")
return value
def _false_authority() -> dict[str, bool]:
return {
"ground_truth": False,
"physical_live": False,
"commands_enabled": False,
"actuation_allowed": False,
"navigation_or_safety_accepted": False,
}
__all__ = [
"TEMPORAL_REPLAY_FRAME_SCHEMA",
"TEMPORAL_REPLAY_MANIFEST_NAME",
"TEMPORAL_REPLAY_REPORT_NAME",
"TEMPORAL_REPLAY_RESULT_PREFIX",
"TEMPORAL_REPLAY_SCHEMA",
"TemporalReplayError",
"TemporalReplayResult",
"build_temporal_replay",
"read_temporal_replay_result",
]
@@ -0,0 +1,39 @@
"""Command-line entrypoint for the local M4.5 temporal and motion replay."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from .temporal_replay import build_temporal_replay
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repository-root", type=Path, default=Path(__file__).resolve().parents[3])
parser.add_argument("--geometry-result", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
arguments = parser.parse_args(argv)
result = build_temporal_replay(
repository_root=arguments.repository_root,
geometry_result_root=arguments.geometry_result,
output_root=arguments.output_root,
)
print(
json.dumps(
{
"result_id": result.result_id,
"result_root": str(result.result_root),
"accepted": result.accepted,
"metrics": result.metrics,
},
sort_keys=True,
separators=(",", ":"),
)
)
return 0 if result.accepted else 1
if __name__ == "__main__":
raise SystemExit(main())
+22
View File
@@ -42,6 +42,16 @@ GEOMETRY_RUNTIME_MODULES = (
"providers.py",
"recorded_source.py",
)
TEMPORAL_RUNTIME_MODULES = (
"contracts.py",
"geometry.py",
"motion.py",
"providers.py",
"recorded_source.py",
"temporal.py",
"temporal_replay.py",
"temporal_replay_cli.py",
)
def _imports(path: Path) -> set[str]:
@@ -202,6 +212,18 @@ def test_geometry_runtime_closure_imports_no_legacy_compute_or_device_package()
assert {name: modules for name, modules in violations.items() if modules} == {}
def test_temporal_runtime_closure_imports_no_legacy_compute_or_device_package() -> None:
violations = {
name: sorted(
module
for module in _imports(PERCEPTION_ROOT / name)
if module.startswith(("k1link.compute", "k1link.device_plugins"))
)
for name in TEMPORAL_RUNTIME_MODULES
}
assert {name: modules for name, modules in violations.items() if modules} == {}
def test_new_perception_boundary_has_no_experiment_specific_imports() -> None:
violations: dict[str, str] = {}
for path in PERCEPTION_ROOT.glob("*.py"):
+241
View File
@@ -0,0 +1,241 @@
from __future__ import annotations
from pathlib import Path
import numpy as np
from k1link.perception.contracts import (
ClockBasis,
EvidenceBasis,
EvidenceCurrentness,
MetricGeometry,
ModalityOutcome,
ModalityStatus,
MotionState,
ObstacleObservation,
SourceEnvelope,
TemporalState,
TimestampBundle,
)
from k1link.perception.motion import ClassIndependentMotionEstimator
from k1link.perception.providers import SourcePacket
from k1link.perception.temporal import (
BoundedSpatialTemporalProvider,
load_temporal_motion_profile,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
PROFILE_PATH = REPOSITORY_ROOT / "config/perception/m4-temporal-motion-v1.json"
class _Points:
def __init__(self, frames: dict[int, list[list[float]]]) -> None:
self.frames = frames
def current_points(self, packet: SourcePacket) -> np.ndarray:
return np.asarray(self.frames[packet.envelope.sequence], dtype=np.float64)
def _status() -> ModalityStatus:
return ModalityStatus(True, ModalityOutcome.AVAILABLE, "test-available")
def _packet(sequence: int, seconds: float) -> SourcePacket:
frame_id = f"frame-{sequence:06d}"
return SourcePacket(
envelope=SourceEnvelope(
source_id="RAVNOVES00",
session_id="20260720T065719Z_viewer_live",
frame_id=frame_id,
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 _observation(
packet: SourcePacket,
point_index: int,
*,
identity: str,
hint: str | None = "object",
) -> ObstacleObservation:
return ObstacleObservation(
observation_id=f"{packet.envelope.frame_id}:{identity}",
occupancy_key=f"{packet.envelope.frame_id}:{identity}",
source_id=packet.envelope.source_id,
frame_id=packet.envelope.frame_id,
evidence_time_ns=packet.envelope.timestamps.source_ns,
basis=EvidenceBasis.FUSED,
currentness=EvidenceCurrentness.CURRENT,
occupied_support=True,
source_point_ids=(point_index,),
metric_geometry=MetricGeometry(
coordinate_frame="map",
centroid_xyz_m=(0.0, 0.0, 0.0),
range_m=1.0,
covariance_diagonal_m2=(0.0, 0.0, 0.0),
),
proposal_ids=(f"proposal-{identity}",),
semantic_hint=hint,
reason_codes=("test-current-support",),
)
def _camera_uncertainty(packet: SourcePacket) -> ObstacleObservation:
return ObstacleObservation(
observation_id=f"{packet.envelope.frame_id}:camera",
occupancy_key=f"{packet.envelope.frame_id}:camera",
source_id=packet.envelope.source_id,
frame_id=packet.envelope.frame_id,
evidence_time_ns=packet.envelope.timestamps.source_ns,
basis=EvidenceBasis.CAMERA,
currentness=EvidenceCurrentness.CURRENT,
occupied_support=False,
source_point_ids=(),
metric_geometry=None,
proposal_ids=("proposal-camera",),
semantic_hint="person",
reason_codes=("camera-without-current-points",),
)
def test_held_evidence_is_unknown_and_expires_without_cells() -> None:
profile = load_temporal_motion_profile(PROFILE_PATH)
points = _Points({0: [[1.0, 2.0, 0.5]], 1: [], 2: []})
temporal = BoundedSpatialTemporalProvider(point_resolver=points, profile=profile)
motion = ClassIndependentMotionEstimator(profile=profile)
first_packet = _packet(0, 0.0)
current = motion.estimate(
first_packet,
temporal.update(first_packet, (_observation(first_packet, 0, identity="a"),)),
)
held_packet = _packet(1, 0.5)
held = motion.estimate(held_packet, temporal.update(held_packet, ()))
expired_packet = _packet(2, 0.9)
expired = motion.estimate(expired_packet, temporal.update(expired_packet, ()))
assert current[0].state is TemporalState.CURRENT
assert held[0].state is TemporalState.HELD
assert held[0].motion is MotionState.UNKNOWN
assert held[0].motion_reason == "stale-support"
assert held[0].cells
assert expired[0].state is TemporalState.EXPIRED
assert expired[0].cells == ()
assert expired[0].coordinate_frame is None
assert temporal.snapshot().past_ttl_occupied_publications == 0
def test_detector_identity_and_semantic_changes_do_not_erase_spatial_component() -> None:
profile = load_temporal_motion_profile(PROFILE_PATH)
frames = {
index: [[index * 0.1, 0.0, 0.0]]
for index in range(4)
}
temporal = BoundedSpatialTemporalProvider(
point_resolver=_Points(frames),
profile=profile,
)
motion = ClassIndependentMotionEstimator(profile=profile)
component_ids: list[str] = []
result = ()
for index, hint in enumerate(("person", "truck", "car", "object")):
packet = _packet(index, index * 0.1)
obstacles = temporal.update(
packet,
(_observation(packet, 0, identity=f"changed-{index}", hint=hint),),
)
result = motion.estimate(packet, obstacles)
component_ids.append(result[0].component_id)
assert len(set(component_ids)) == 1
assert result[0].motion is MotionState.MOVING
assert result[0].motion_confidence == 0.75
snapshot = temporal.snapshot()
assert snapshot.detector_identity_changes_reassociated == 3
assert snapshot.spatial_reassociations == 3
def test_stationary_motion_decision_uses_only_bounded_map_history() -> None:
profile = load_temporal_motion_profile(PROFILE_PATH)
temporal = BoundedSpatialTemporalProvider(
point_resolver=_Points({index: [[1.0, 1.0, 0.0]] for index in range(4)}),
profile=profile,
)
motion = ClassIndependentMotionEstimator(profile=profile)
result = ()
for index in range(4):
packet = _packet(index, index * 0.1)
result = motion.estimate(
packet,
temporal.update(
packet,
(_observation(packet, 0, identity=f"id-{index}", hint=None),),
),
)
assert result[0].motion is MotionState.STATIONARY
assert len(result[0].history) == 4
assert result[0].motion_reason == "bounded-map-history-stationary"
def test_map_frame_discontinuity_forces_unknown_motion() -> None:
profile = load_temporal_motion_profile(PROFILE_PATH)
first_points = [[0.0, 0.0, 0.0], [3.0, 0.0, 0.0], [6.0, 0.0, 0.0], [9.0, 0.0, 0.0]]
shifted_points = [[x + 2.0, y, z] for x, y, z in first_points]
temporal = BoundedSpatialTemporalProvider(
point_resolver=_Points({0: first_points, 1: shifted_points}),
profile=profile,
)
motion = ClassIndependentMotionEstimator(profile=profile)
first = _packet(0, 0.0)
temporal.update(
first,
tuple(_observation(first, index, identity=f"a-{index}") for index in range(4)),
)
second = _packet(1, 0.1)
result = motion.estimate(
second,
temporal.update(
second,
tuple(_observation(second, index, identity=f"b-{index}") for index in range(4)),
),
)
current = tuple(item for item in result if item.state is TemporalState.CURRENT)
assert len(current) == 4
assert all(item.association_basis == "map-frame-discontinuity" for item in current)
assert all(item.motion is MotionState.UNKNOWN for item in current)
assert temporal.snapshot().map_frame_jump_candidates == 1
def test_camera_only_uncertainty_cannot_create_occupied_temporal_state() -> None:
profile = load_temporal_motion_profile(PROFILE_PATH)
temporal = BoundedSpatialTemporalProvider(
point_resolver=_Points({0: []}),
profile=profile,
)
packet = _packet(0, 0.0)
result = temporal.update(packet, (_camera_uncertainty(packet),))
assert result == ()
snapshot = temporal.snapshot()
assert snapshot.current_occupied_observations == 0
assert snapshot.nonmetric_uncertainty_observations == 1
+84
View File
@@ -0,0 +1,84 @@
from __future__ import annotations
from pathlib import Path
import pytest
from k1link.perception.temporal_replay import (
TemporalReplayResult,
read_temporal_replay_result,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
RESULT_ROOT = (
REPOSITORY_ROOT
/ ".runtime/perception-m4/temporal-results"
/ "m4-temporal-replay-9ed5dcd249ed3bcb81661dd18e2b854a7ffedf3fd2b92b9c994c3c70c34533f2"
)
@pytest.fixture(scope="module")
def result() -> TemporalReplayResult:
return read_temporal_replay_result(RESULT_ROOT)
def test_full_source_temporal_result_closes_m4_5_contract(
result: TemporalReplayResult,
) -> None:
assert result.accepted is True
assert result.metrics["frames"] == {"failed": 0, "total": 4489}
assert result.metrics["input_observations"] == 37457
temporal = result.metrics["temporal"]
assert isinstance(temporal, dict)
assert temporal["current_occupied_observations"] == 27299
assert temporal["nonmetric_uncertainty_observations"] == 10158
assert temporal["detector_identity_changes_reassociated"] == 22994
assert temporal["peak_active_components"] == 34
assert temporal["maximum_history_samples"] == 8
assert temporal["past_ttl_occupied_publications"] == 0
def test_temporal_result_measures_motion_retention_and_engineering_clips(
result: TemporalReplayResult,
) -> None:
motion = result.metrics["motion"]
retention = result.metrics["retention"]
clips = result.metrics["labeled_engineering_clip_checks"]
assert isinstance(motion, dict)
assert isinstance(retention, dict)
assert motion["moving"] == 11365
assert motion["stationary"] == 1451
assert motion["unknown"] == 52478
assert retention == {
"false_persistence_truth_available": False,
"ghost_occupancy_past_ttl_count": 0,
"maximum_expiry_deadline_delay_ns": 0,
"maximum_expiry_materialization_delay_ns": 283000000,
"maximum_held_age_ns": 750000000,
"past_ttl_occupied_publications": 0,
"ttl_ns": 750000000,
}
assert clips == {
"component_correspondence_available": False,
"frame_count": 16,
"independent_truth": False,
}
def test_temporal_result_is_digest_bound_to_m4_4_e34_e51_and_e46b(
result: TemporalReplayResult,
) -> None:
identity = result.manifest["identity"]
assert isinstance(identity, dict)
assert identity["geometry_result_id"] == (
"m4-geometry-replay-"
"8daf3109e3cf30b960b4b376032ff3b5ec58ca42a1e5899b841cf29fbcf14ad8"
)
assert identity["frames_sha256"] == (
"1bf1365bdb3f20214443d3f8b87a0fa88f9848af8ca0456b7ca364d37631c3fc"
)
references = identity["historical_references"]
assert isinstance(references, dict)
assert set(references) == {"e34", "e46b", "e51"}
assert references["e46b"]["independent_truth"] is False
assert len(identity["clip_checks"]) == 16