feat(perception): add dual evidence replay threat

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 19:18:01 +03:00
parent 6b810952a4
commit b0d0bc8d7f
22 changed files with 3122 additions and 14 deletions
+2 -1
View File
@@ -127,7 +127,7 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
repository_root / "config" / "laboratories"
)
assert len(registry.definitions) == 31
assert len(registry.definitions) == 32
assert {item.work_id for item in registry.definitions} >= {
"e31-source-binding",
"e46j-raw-fisheye-realtime",
@@ -136,4 +136,5 @@ def test_product_registry_declares_every_advanced_evidence_source() -> None:
"l32-pointpillars-camera-review",
"l33-camera-first-detector-review",
"l34f-adjudicated-reference",
"m4-replay-threat",
}
+1
View File
@@ -90,6 +90,7 @@ def test_repository_registry_classifies_every_evidence_definition() -> None:
evidence, execution = _registries()
assert {row.work_id for row in execution.definitions} == {
"m4-replay-threat",
"e33-worker-shadow",
"e35-degradation-recovery",
"e46j-raw-fisheye-realtime",
@@ -80,11 +80,12 @@ def test_product_value_review_registry_covers_reviewed_laboratory_families() ->
root / "config" / "laboratory-value-review.json"
)
assert len(registry.entries) == 34
assert len(registry.entries) == 35
assert {entry.catalog_id for entry in registry.entries} >= {
"e28-local-surface",
"e46d-temporal-failure-audit",
"e46j-raw-fisheye-realtime",
"l31-pointpillars-ravnoves",
"l34f-adjudicated-reference",
"m4-replay-threat",
}
+113
View File
@@ -0,0 +1,113 @@
from __future__ import annotations
from pathlib import Path
from fastapi.routing import APIRoute
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-"
"7e1613a3ea35638b5ea7a3f7c1c78fe9eba1a3adae540b652dec167f815d45b2"
)
RESULTS_ROOT = REPOSITORY_ROOT / ".runtime/compute-experiments/m4/replay-threat"
def _endpoint(path: str):
router = build_m4_threat_replay_router(root_provider=lambda: RESULTS_ROOT)
return next(
route.endpoint
for route in router.routes
if isinstance(route, APIRoute) and route.path == path
)
def test_full_source_threat_result_closes_m4_6_contract() -> None:
result = read_threat_replay_result(RESULTS_ROOT / RESULT_ID)
assert result.accepted is True
assert result.metrics["frames"] == {"total": 4489, "failed": 0}
assert result.metrics["evidence"] == {
"camera-only": 10158,
"current-metric": 27299,
"stale-or-held": 37995,
}
assert result.metrics["decisions"] == {
"not-threat": 6610,
"threat": 8010,
"unknown": 60832,
}
assert result.metrics["fixtures"] == {
"critical": 4,
"critical_false_not_threat": 0,
"passed": 9,
"total": 9,
}
def test_threat_result_is_content_bound_and_visual_evidence_is_complete() -> None:
result = read_threat_replay_result(RESULTS_ROOT / RESULT_ID)
identity = result.manifest["identity"]
assert isinstance(identity, dict)
assert identity["frames_sha256"] == (
"bf690358efb45c323db7172251074b33c3ef7ede6ae99bd8d3da53cfba86b142"
)
assert identity["visuals_sha256"] == (
"fb022c6efd84f27c0916a6c87887443c9b43993ac4b1f9910332433152533dea"
)
visual = result.metrics["visual_evidence"]
assert isinstance(visual, dict)
assert visual["frame_count"] == 32
assert all(
visual[key] is True
for key in (
"video_overlay_available",
"camera_boxes_available",
"point_cloud_available",
"metric_distance_available",
"virtual_corridor_available",
)
)
def test_m4_6_lab_api_projects_report_and_exact_visual_frame() -> None:
list_results = _endpoint("/api/v1/laboratory/m4-threat/results")
list_visuals = _endpoint(
"/api/v1/laboratory/m4-threat/results/{result_id}/visuals"
)
get_visual = _endpoint(
"/api/v1/laboratory/m4-threat/results/{result_id}/visuals/{ordinal}"
)
catalog = list_results(limit=1)
assert catalog["items"][0]["result_id"] == RESULT_ID
assert catalog["items"][0]["authority"] == "replay-simulated"
visuals = list_visuals(RESULT_ID)
assert len(visuals["items"]) == 32
frame = get_visual(RESULT_ID, 1)
assert frame["schema_version"] == "missioncore.perception-threat-visual-frame/v1"
assert frame["point_cloud_sample_count"] > 0
assert frame["rig"] == {
"length_m": 1.0,
"nominal_sensor_height_m": 1.25,
"width_m": 0.6,
}
def test_m4_6_video_overlay_covers_the_exact_recorded_camera_timeline() -> None:
get_overlay = _endpoint(
"/api/v1/laboratory/m4-threat/results/{result_id}/video-overlay"
)
overlay = get_overlay(RESULT_ID)
assert overlay["frame_count"] == 4489
assert overlay["recorded_source"]["session_id"] == (
"20260720T065719Z_viewer_live"
)
assert overlay["frames"][0]["frame_index"] == 0
assert overlay["frames"][-1]["frame_index"] == 4488
assert overlay["authority"] == "replay-simulated"
+33
View File
@@ -52,6 +52,20 @@ TEMPORAL_RUNTIME_MODULES = (
"temporal_replay.py",
"temporal_replay_cli.py",
)
THREAT_RUNTIME_MODULES = (
"contracts.py",
"detector_replay_contracts.py",
"detector_replay_result.py",
"geometry.py",
"geometry_math.py",
"geometry_replay.py",
"providers.py",
"recorded_source.py",
"temporal_replay.py",
"threat.py",
"threat_replay.py",
"threat_replay_cli.py",
)
def _imports(path: Path) -> set[str]:
@@ -224,6 +238,25 @@ def test_temporal_runtime_closure_imports_no_legacy_compute_or_device_package()
assert {name: modules for name, modules in violations.items() if modules} == {}
def test_threat_runtime_closure_imports_no_legacy_compute_device_lab_or_web_package() -> None:
violations = {
name: sorted(
module
for module in _imports(PERCEPTION_ROOT / name)
if module.startswith(
(
"k1link.compute",
"k1link.device_plugins",
"k1link.laboratory",
"k1link.web",
)
)
)
for name in THREAT_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"):
+35 -3
View File
@@ -234,8 +234,7 @@ class _Threat:
provider_id = "test-threat/v1"
def assess(self, obstacle_map: LocalObstacleMap) -> tuple[ThreatAssessment, ...]:
occupied = obstacle_map.occupied
return tuple(
metric = tuple(
ThreatAssessment(
assessment_id=f"assessment-{item.component_id}",
component_id=item.component_id,
@@ -249,8 +248,41 @@ class _Threat:
decision=ThreatDecision.NOT_THREAT,
reason_codes=("qualified-corridor-clear",),
)
for item in occupied
for item in obstacle_map.occupied
)
unknown = tuple(
ThreatAssessment(
assessment_id=f"assessment-{item.component_id}",
component_id=item.component_id,
rig_profile_id="ravnoves00-virtual-rig/v1",
corridor_profile_id="ravnoves00-virtual-corridor/v1",
qualification=QualificationState.UNQUALIFIED,
relative_speed_mps=None,
closest_approach_m=None,
ttc_seconds=None,
corridor_intersection=CorridorIntersection.UNKNOWN,
decision=ThreatDecision.UNKNOWN,
reason_codes=("incomplete-evidence",),
)
for item in obstacle_map.unknown
)
camera = tuple(
ThreatAssessment(
assessment_id=f"assessment-{item.proposal_id}",
component_id=item.proposal_id,
rig_profile_id="ravnoves00-virtual-rig/v1",
corridor_profile_id="ravnoves00-virtual-corridor/v1",
qualification=QualificationState.UNQUALIFIED,
relative_speed_mps=None,
closest_approach_m=None,
ttc_seconds=None,
corridor_intersection=CorridorIntersection.UNKNOWN,
decision=ThreatDecision.UNKNOWN,
reason_codes=("camera-only",),
)
for item in obstacle_map.camera_uncertainty
)
return (*metric, *unknown, *camera)
def _config(
+277
View File
@@ -0,0 +1,277 @@
from __future__ import annotations
from pathlib import Path
from k1link.perception.contracts import (
BoundingRegion2D,
CorridorIntersection,
GridCell,
HistorySample,
LocalObstacleMap,
MotionState,
ObjectProposal2D,
SourceAccounting,
TemporalObstacle,
TemporalState,
ThreatDecision,
)
from k1link.perception.graph_validation import validate_threats
from k1link.perception.threat import (
DualEvidenceReplayThreatProvider,
ReplayPose,
load_replay_threat_profile,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
PROFILE_PATH = REPOSITORY_ROOT / "config/perception/m4-replay-threat-v1.json"
class _Poses:
def pose_for_frame(self, frame_id: str) -> ReplayPose:
return ReplayPose(
frame_id=frame_id,
position_map_xyz_m=(0.0, 0.0, 0.0),
orientation_map_from_lidar_xyzw=(0.0, 0.0, 0.0, 1.0),
)
def _obstacle(
component_id: str,
cell: GridCell,
*,
motion: MotionState,
history: tuple[tuple[str, int, tuple[float, float, float]], ...],
state: TemporalState = TemporalState.CURRENT,
semantic_hint: str | None = None,
) -> TemporalObstacle:
samples = tuple(
HistorySample(frame_id=frame_id, evidence_time_ns=time_ns, centroid_xyz_m=point)
for frame_id, time_ns, point in history
)
current = samples[-1]
return TemporalObstacle(
component_id=component_id,
identity_scope="ephemeral",
state=state,
ttl_ns=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",
history=samples,
cells=() if state is TemporalState.EXPIRED else (cell,),
coordinate_frame=None if state is TemporalState.EXPIRED else "map",
last_centroid_xyz_m=None if state is TemporalState.EXPIRED else current.centroid_xyz_m,
motion=MotionState.UNKNOWN if state is not TemporalState.CURRENT else motion,
motion_confidence=(
0.0
if state is not TemporalState.CURRENT or motion is MotionState.UNKNOWN
else 1.0
),
motion_reason=(
"stale-support"
if state is not TemporalState.CURRENT
else "bounded-map-history-moving"
if motion is MotionState.MOVING
else "bounded-map-history-stationary"
if motion is MotionState.STATIONARY
else "insufficient-history"
),
semantic_hint=semantic_hint,
)
def _proposal(frame_id: str = "frame-000002") -> ObjectProposal2D:
return ObjectProposal2D(
proposal_id="proposal-camera-only",
source_id="RAVNOVES00",
frame_id=frame_id,
region=BoundingRegion2D(10.0, 10.0, 20.0, 20.0),
objectness=0.8,
provider_id="test-detector/v1",
model_id="test-model/v1",
preprocess_id="test-preprocess/v1",
semantic_hint="person",
)
def _map(
*,
occupied: tuple[TemporalObstacle, ...] = (),
unknown: tuple[TemporalObstacle, ...] = (),
camera: tuple[ObjectProposal2D, ...] = (),
frame_id: str = "frame-000002",
) -> LocalObstacleMap:
return LocalObstacleMap(
source_id="RAVNOVES00",
session_id="20260720T065719Z_viewer_live",
frame_id=frame_id,
graph_id="reference-perception-graph/v1",
generated_monotonic_ns=0,
output_age_ns=0,
occupied=occupied,
unknown=unknown,
camera_uncertainty=camera,
accounting=SourceAccounting(1, 1, 0, 0),
)
def test_replay_threat_profile_freezes_virtual_authority_and_dual_evidence_policy() -> None:
profile = load_replay_threat_profile(PROFILE_PATH)
assert profile.rig.body_length_m == 1.0
assert profile.rig.body_width_m == 0.6
assert profile.rig.nominal_sensor_height_m == 1.25
assert profile.corridor.forward_length_m == 8.0
assert profile.calibration_content_sha256 == (
"05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
)
def test_static_crossing_approaching_and_geometry_only_critical_cases_are_never_safe() -> None:
provider = DualEvidenceReplayThreatProvider(
pose_resolver=_Poses(),
profile=load_replay_threat_profile(PROFILE_PATH),
)
current_frame = "frame-000002"
critical = (
_obstacle(
"static-in-corridor",
GridCell(6, 0, 0),
motion=MotionState.STATIONARY,
history=(
("frame-000000", 0, (2.925, 0.225, 0.225)),
(current_frame, 300_000_000, (2.925, 0.225, 0.225)),
),
),
_obstacle(
"crossing",
GridCell(6, 3, 0),
motion=MotionState.MOVING,
history=(
("frame-000000", 0, (2.925, 2.575, 0.225)),
(current_frame, 300_000_000, (2.925, 1.575, 0.225)),
),
),
_obstacle(
"approaching",
GridCell(9, 0, 0),
motion=MotionState.MOVING,
history=(
("frame-000000", 0, (6.275, 0.225, 0.225)),
(current_frame, 300_000_000, (4.275, 0.225, 0.225)),
),
semantic_hint="car",
),
_obstacle(
"geometry-only",
GridCell(4, 0, 0),
motion=MotionState.STATIONARY,
history=(
("frame-000000", 0, (2.025, 0.225, 0.225)),
(current_frame, 300_000_000, (2.025, 0.225, 0.225)),
),
semantic_hint=None,
),
)
result = provider.assess(_map(occupied=critical))
assert {item.decision for item in result} == {ThreatDecision.THREAT}
assert all(item.corridor_intersection is CorridorIntersection.INTERSECTS for item in result)
assert (
next(item for item in result if item.component_id == "approaching").ttc_seconds
is not None
)
assert "geometry-only-evidence" in next(
item for item in result if item.component_id == "geometry-only"
).reason_codes
def test_receding_and_static_outside_are_clear_but_incomplete_evidence_is_unknown() -> None:
provider = DualEvidenceReplayThreatProvider(
pose_resolver=_Poses(),
profile=load_replay_threat_profile(PROFILE_PATH),
)
current_frame = "frame-000002"
clear = (
_obstacle(
"static-outside",
GridCell(6, 7, 0),
motion=MotionState.STATIONARY,
history=(
("frame-000000", 0, (2.925, 3.375, 0.225)),
(current_frame, 300_000_000, (2.925, 3.375, 0.225)),
),
),
_obstacle(
"receding-behind",
GridCell(-5, 0, 0),
motion=MotionState.MOVING,
history=(
("frame-000000", 0, (-1.025, 0.225, 0.225)),
(current_frame, 300_000_000, (-2.025, 0.225, 0.225)),
),
),
)
incomplete = _obstacle(
"unknown-motion",
GridCell(6, 7, 0),
motion=MotionState.UNKNOWN,
history=((current_frame, 300_000_000, (2.925, 3.375, 0.225)),),
)
held = _obstacle(
"occluded-held",
GridCell(6, 0, 0),
motion=MotionState.UNKNOWN,
history=((current_frame, 300_000_000, (2.925, 0.225, 0.225)),),
state=TemporalState.HELD,
)
obstacle_map = _map(
occupied=(*clear, incomplete),
unknown=(held,),
camera=(_proposal(),),
)
result = provider.assess(obstacle_map)
by_id = {item.component_id: item for item in result}
assert by_id["static-outside"].decision is ThreatDecision.NOT_THREAT
assert by_id["receding-behind"].decision is ThreatDecision.NOT_THREAT
assert by_id["unknown-motion"].decision is ThreatDecision.UNKNOWN
assert by_id["occluded-held"].decision is ThreatDecision.UNKNOWN
assert by_id["proposal-camera-only"].decision is ThreatDecision.UNKNOWN
assert by_id["proposal-camera-only"].closest_approach_m is None
validate_threats(obstacle_map, result)
def test_semantic_hint_and_ephemeral_component_name_do_not_change_threat_geometry() -> None:
provider = DualEvidenceReplayThreatProvider(
pose_resolver=_Poses(),
profile=load_replay_threat_profile(PROFILE_PATH),
)
history = (
("frame-000000", 0, (2.925, 0.225, 0.225)),
("frame-000002", 300_000_000, (2.925, 0.225, 0.225)),
)
first = _obstacle(
"ephemeral-a",
GridCell(6, 0, 0),
motion=MotionState.STATIONARY,
history=history,
semantic_hint="car",
)
second = _obstacle(
"ephemeral-b",
GridCell(6, 0, 0),
motion=MotionState.STATIONARY,
history=history,
semantic_hint=None,
)
values = provider.assess(_map(occupied=(first, second)))
assert values[0].decision == values[1].decision
assert values[0].corridor_intersection == values[1].corridor_intersection
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