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
+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
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"
@@ -29,18 +29,19 @@ def test_full_source_threat_result_closes_m4_6_contract() -> None:
assert result.metrics["evidence"] == {
"camera-only": 10158,
"current-metric": 27299,
"rolling-map-retained": 69855,
"stale-or-held": 37995,
}
assert result.metrics["decisions"] == {
"not-threat": 10700,
"threat": 2716,
"unknown": 62036,
"threat": 6626,
"unknown": 127981,
}
assert result.metrics["fixtures"] == {
"critical": 4,
"critical": 5,
"critical_false_not_threat": 0,
"passed": 9,
"total": 9,
"passed": 10,
"total": 10,
}
@@ -50,10 +51,10 @@ def test_threat_result_is_content_bound_and_visual_evidence_is_complete() -> Non
assert isinstance(identity, dict)
assert identity["frames_sha256"] == (
"d55e7651f0b16a62c6b61c5cb2358dd8dff87dbfa57a59e9ec350bc38b156bc1"
"57219acd7dfe1cf04b12d4a415d1819e0a4947bc3173333bb56eea8fd2bd47b9"
)
assert identity["visuals_sha256"] == (
"957c35d46ae30143beb6b2f26f8f722853ef2a1e91a41d5dc1a03fbf723a54e0"
"bda54e144a1b4c878ba645a8dafedcdc0332e16a1489b62b7100670e8eae6f25"
)
visual = result.metrics["visual_evidence"]
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",
)
)
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"]
assert body_frame["qualified"] == 3861
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 [item["sequence"] for item in visuals["items"][:3]] == [62, 138, 274]
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["rig"] == {
"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,
"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["rollback"] == {
"durable_worker_action": "none",
+2
View File
@@ -48,6 +48,7 @@ TEMPORAL_RUNTIME_MODULES = (
"motion.py",
"providers.py",
"recorded_source.py",
"rolling_map.py",
"temporal.py",
"temporal_replay.py",
"temporal_replay_cli.py",
@@ -61,6 +62,7 @@ THREAT_RUNTIME_MODULES = (
"geometry_replay.py",
"providers.py",
"recorded_source.py",
"rolling_map.py",
"temporal_replay.py",
"threat.py",
"threat_replay.py",
+31 -1
View File
@@ -28,6 +28,7 @@ from k1link.perception.threat import (
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
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:
@@ -61,7 +62,7 @@ def _obstacle(
component_id=component_id,
identity_scope="ephemeral",
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,
age_ns=0 if state is TemporalState.CURRENT else 100_000_000,
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].closest_approach_m == values[1].closest_approach_m
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
import json
from pathlib import Path
import pytest
@@ -15,6 +16,11 @@ RESULT_ROOT = (
/ ".runtime/perception-m4/temporal-results"
/ "m4-temporal-replay-9ed5dcd249ed3bcb81661dd18e2b854a7ffedf3fd2b92b9c994c3c70c34533f2"
)
ROLLING_RESULT_ROOT = (
REPOSITORY_ROOT
/ ".runtime/perception-m4/temporal-results"
/ "m4-temporal-replay-b8611526dfcd2b9be9049560d751bbd23a9ad54b7dda8e9dc48a17374d46266e"
)
@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 references["e46b"]["independent_truth"] is False
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"])