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
+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