189 lines
6.2 KiB
Python
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
|