fix(perception): reconstruct rolling occupancy from K1 increments
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user