Files
NODEDC_MISSION_CORE/tests/test_replay_threat_provider.py
T

278 lines
9.1 KiB
Python

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