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