NODEDC_MISSION_CORE/tests/test_lidar_contract.py

189 lines
6.2 KiB
Python

from __future__ import annotations
from dataclasses import replace
import numpy as np
import pytest
from k1link.compute.lidar_contract import (
K1_LAB_LIDAR_PACK_V1_PROFILE,
K1_LIVE_LIDAR_PROFILE,
LidarContractError,
LidarCoordinateSpace,
LidarEvidenceProfile,
LidarPipelineStage,
LidarPointField,
LidarPoseStatus,
LidarQualityMonitor,
LidarReadiness,
LidarRepresentation,
LidarTimeBasis,
assess_lidar_profile,
lidar_readiness_document,
sensor_frame_xyzi,
)
from k1link.data_plane import (
ConsumerFrameContext,
DecodedPointCloudView,
DecodedPoseView,
)
def _context(sequence: int = 1) -> ConsumerFrameContext:
return ConsumerFrameContext(
sequence=sequence,
captured_at_epoch_ns=10,
received_monotonic_ns=20,
processing_started_monotonic_ns=21,
encoded_size_bytes=100,
live=True,
)
def _assessment(stage: LidarPipelineStage, *, lab_pack: bool = False) -> object:
profile = K1_LAB_LIDAR_PACK_V1_PROFILE if lab_pack else K1_LIVE_LIDAR_PROFILE
return next(item for item in assess_lidar_profile(profile) if item.stage is stage)
def test_k1_profiles_round_trip_and_do_not_claim_navigation_authority() -> None:
restored = type(K1_LIVE_LIDAR_PROFILE).from_dict(K1_LIVE_LIDAR_PROFILE.to_dict())
assert restored == K1_LIVE_LIDAR_PROFILE
document = lidar_readiness_document(restored)
assert document["authority"] == {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
}
def test_current_k1_live_source_is_detector_degraded_but_odometry_blocked() -> None:
detector = _assessment(LidarPipelineStage.LIDAR_3D_DETECTION)
odometry = _assessment(LidarPipelineStage.LIDAR_ODOMETRY)
lio = _assessment(LidarPipelineStage.LIDAR_INERTIAL_SLAM)
assert detector.readiness is LidarReadiness.DEGRADED
assert "pretrained-domain-expects-sensor-scan" in detector.reasons
assert odometry.readiness is LidarReadiness.BLOCKED
assert lio.readiness is LidarReadiness.BLOCKED
assert "per-point-time-unavailable" in lio.reasons
def test_lidar_pack_v1_is_blocked_for_pointpillars_because_it_dropped_intensity() -> None:
detector = _assessment(LidarPipelineStage.LIDAR_3D_DETECTION, lab_pack=True)
assert detector.readiness is LidarReadiness.BLOCKED
assert "admitted-pointpillars-baseline-requires-intensity" in detector.reasons
def test_nvblox_is_blocked_until_k1_scan_geometry_is_known() -> None:
assessment = _assessment(LidarPipelineStage.NVIDIA_NVBLOX)
assert assessment.readiness is LidarReadiness.BLOCKED
assert "lidar-intrinsics-or-scan-geometry-unknown" in assessment.reasons
def test_complete_sensor_profile_is_ready_for_all_admitted_stages() -> None:
profile = LidarEvidenceProfile(
profile_id="reference-raw-lidar-imu/v1",
representation=LidarRepresentation.SENSOR_SCAN,
coordinate_space=LidarCoordinateSpace.SENSOR,
coordinate_frame="lidar",
frame_time_basis=LidarTimeBasis.SENSOR,
point_fields=(
LidarPointField.XYZ,
LidarPointField.INTENSITY,
LidarPointField.RING,
LidarPointField.RELATIVE_TIME,
),
pose_status=LidarPoseStatus.SENSOR_SYNCHRONIZED,
scan_geometry_known=True,
imu_samples_available=True,
lidar_imu_extrinsic_available=True,
camera_extrinsic_available=True,
)
assert {
assessment.readiness for assessment in assess_lidar_profile(profile)
} == {LidarReadiness.READY}
assert len(assess_lidar_profile(profile)) == len(LidarPipelineStage)
def test_sensor_frame_xyzi_inverts_map_pose_and_normalizes_intensity() -> None:
cloud = DecodedPointCloudView(
context=_context(),
frame_id="map",
positions_xyz=((11.0, 2.0, 3.0), (10.0, 3.0, 3.0)),
intensities=bytes((0, 255)),
)
pose = DecodedPoseView(
context=_context(2),
frame_id="map",
child_frame_id="k1-lidar",
position_xyz=(10.0, 2.0, 3.0),
orientation_xyzw=(0.0, 0.0, 0.0, 1.0),
)
xyzi = sensor_frame_xyzi(cloud, pose)
np.testing.assert_allclose(
xyzi,
np.asarray(
[
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 1.0],
],
dtype=np.float32,
),
)
def test_sensor_frame_xyzi_rejects_missing_intensity_and_unbound_frames() -> None:
cloud = DecodedPointCloudView(
context=_context(),
frame_id="map",
positions_xyz=((1.0, 2.0, 3.0),),
)
with pytest.raises(LidarContractError, match="requires intensity"):
sensor_frame_xyzi(cloud)
with pytest.raises(LidarContractError, match="diagnostic-only"):
replace(K1_LIVE_LIDAR_PROFILE, navigation_or_safety_accepted=True)
def test_quality_monitor_is_bounded_and_reports_sensor_relative_range() -> None:
monitor = LidarQualityMonitor(
K1_LIVE_LIDAR_PROFILE,
frame_sample_capacity=2,
point_sample_capacity=256,
points_sampled_per_frame=2,
)
pose = DecodedPoseView(
context=_context(20),
frame_id="map",
child_frame_id="k1-lidar",
position_xyz=(10.0, 2.0, 3.0),
orientation_xyzw=(0.0, 0.0, 0.0, 1.0),
)
for sequence, capture_ns in enumerate((10, 110_000_010, 210_000_010), start=1):
context = replace(_context(sequence), captured_at_epoch_ns=capture_ns)
monitor.observe(
DecodedPointCloudView(
context=context,
frame_id="map",
positions_xyz=((11.0, 2.0, 3.0), (10.0, 4.0, 3.0)),
intensities=bytes((0, 255)),
),
pose=pose,
)
report = monitor.snapshot()
assert report["frames_observed"] == 3
assert report["points_observed"] == 6
assert report["point_count_per_frame"]["sample_count"] == 2
assert report["sensor_range_m"]["minimum"] == pytest.approx(1.0)
assert report["sensor_range_m"]["maximum"] == pytest.approx(2.0)
assert report["intensity_0_1"]["minimum"] == pytest.approx(0.0)
assert report["intensity_0_1"]["maximum"] == pytest.approx(1.0)
assert report["authority"]["navigation_or_safety_accepted"] is False