diff --git a/experiments/perception/build_l32_pointpillars_camera_review.py b/experiments/perception/build_l32_pointpillars_camera_review.py new file mode 100644 index 0000000..58ac92e --- /dev/null +++ b/experiments/perception/build_l32_pointpillars_camera_review.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +from pathlib import Path + +from k1link.compute.l32_pointpillars_camera_review import ( + build_l32_pointpillars_camera_review, +) + + +def main() -> None: + repository = Path(__file__).resolve().parents[2] + result = build_l32_pointpillars_camera_review( + l31_result_root=( + repository + / ".runtime/compute-experiments/l3/pointpillars-ravnoves" + / ( + "l31-pointpillars-ravnoves-" + "80a9715f64ea397222fbcfd700803f9152009e3751caf87e2e9dc5ec6fc01b72" + ) + ), + e10_pack_root=( + repository + / ".runtime/compute-experiments/e10/lidar-packs" + / "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b" + ), + camera_job_root=( + repository + / ".runtime/compute-jobs" + / "recorded-camera-602ac89026ed12978619801d" + ), + ffmpeg_path=Path("/opt/homebrew/bin/ffmpeg"), + output_root=( + repository + / ".runtime/compute-experiments/l3/pointpillars-camera-review" + ), + ) + print(result) + + +if __name__ == "__main__": + main() diff --git a/experiments/perception/build_l33_camera_first_detector_review.py b/experiments/perception/build_l33_camera_first_detector_review.py new file mode 100644 index 0000000..509c942 --- /dev/null +++ b/experiments/perception/build_l33_camera_first_detector_review.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +from pathlib import Path + +from k1link.compute.l33_camera_first_admission import ( + RAVNOVES00_ADMITTED_WORLD_STATE_RESULT_ID, +) +from k1link.compute.l33_camera_first_detector_review import ( + build_l33_camera_first_detector_review, +) + + +def main() -> None: + repository = Path(__file__).resolve().parents[2] + result = build_l33_camera_first_detector_review( + l32_result_root=( + repository + / ".runtime/compute-experiments/l3/pointpillars-camera-review" + / ( + "l32-pointpillars-camera-review-" + "40eca128ea9525e8e8c22bd3e981e40d5b66d2869c2e92636ddbf707ae44127a" + ) + ), + e26_result_root=( + repository + / ".runtime/compute-experiments/e10/worker-results" + / ( + "e10-integrated-perception-" + "459aac93918d8f6414b342986ccc6968fefcef6c1f3a78a5254df0b565255ad2" + ) + ), + e29_result_root=( + repository + / ".runtime/compute-experiments/e29/results" + / ( + "e29-camera-geometry-" + "421a9d930638bef12cd5eb10979a477917fa4a389e655ed95f73ba4bd62e13dc" + ) + ), + e10_pack_root=( + repository + / ".runtime/compute-experiments/e10/lidar-packs" + / "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b" + ), + output_root=(repository / ".runtime/compute-experiments/l3/camera-first-detector-review"), + rectified_world_state_root=( + repository + / ".runtime/compute-experiments/l3/rectified-camera-world-state" + / RAVNOVES00_ADMITTED_WORLD_STATE_RESULT_ID + ), + ) + print(result) + + +if __name__ == "__main__": + main() diff --git a/experiments/perception/e46e_ready_stack_profile.json b/experiments/perception/e46e_ready_stack_profile.json new file mode 100644 index 0000000..220155b --- /dev/null +++ b/experiments/perception/e46e_ready_stack_profile.json @@ -0,0 +1,97 @@ +{ + "schema_version": "missioncore.e46e-ready-stack-profile/v1", + "profile_id": "e46e-deepstream-trafficcamnet-rtdetr-nvdcf/v1", + "source": { + "camera_source_id": "sensor.camera.right", + "job_id": "recorded-camera-602ac89026ed12978619801d", + "session_id": "20260720T065719Z_viewer_live", + "segment_count": 4489, + "stream_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8", + "archive_index_sha256": "e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14", + "archive_summary_sha256": "b280f40b198aad5d5335819107fb1405ad65d5695d187c61a2c027ad853a3181" + }, + "runtime": { + "container_image": "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:10eca409b3894e91c1bac915c9f1346307e56695e552487cbe8cf2f58a3f998f", + "container_platform": "linux/amd64", + "deepstream_version": "9.1", + "network_during_inference": "none" + }, + "detector": { + "name": "NVIDIA TrafficCamNet Transformer Lite", + "architecture": "RT-DETR ResNet50", + "version": "deployable_resnet50_v2.0", + "precision": "FP16", + "input_shape": [ + 3, + 544, + 960 + ], + "model_file": "resnet50_trafficcamnet_rtdetr.fp16.onnx", + "model_sha256": "545a447b913d54eee476381436ebea4ad2aa876cfbe0a6d9f3b0302f08a7415d", + "model_url": "https://api.ngc.nvidia.com/v2/models/nvidia/tao/trafficcamnet_transformer_lite/versions/deployable_resnet50_v2.0/files/resnet50_trafficcamnet_rtdetr.fp16.onnx", + "labels": [ + "background", + "bicycle", + "car", + "person", + "road_sign" + ], + "pre_cluster_threshold": 0.5, + "custom_postprocessing": false + }, + "parser": { + "name": "NVIDIA DeepStream TAO custom bounding-box parser", + "repository": "https://github.com/NVIDIA/DeepStream.git", + "commit": "581889df47d6181110c758c10b872ca833a835e3", + "source_path": "src/apps/tao_apps/post_processor", + "development_image": "nvcr.io/nvidia/deepstream:9.1-triton-multiarch@sha256:fd31f5b44ababdbdee8cd397a375e888191b49e402ac237254a4cdc239130f5b", + "cuda_version": "13.2", + "symbol": "NvDsInferParseCustomDDETRTAO", + "library_file": "libnvds_infercustomparser_tao.so", + "library_sha256": "a18d85dae674a088549c5f9b8fda53c640f4fcbd88a41f4c2cb1f4e3ea8878ee", + "custom_mission_core_logic": false, + "source_files": [ + { + "path": "Makefile", + "sha256": "0265f470354e60c6d719bde68c7b74b1879eed9b4552b8fe7b39416af5ce6835" + }, + { + "path": "debug_logger_raii.cpp", + "sha256": "1d388509e1ff9008de6ccd6451db9c6433273ed78a94e95a1df84585b8dc2915" + }, + { + "path": "debug_logger_raii.hpp", + "sha256": "6efdce1874468848664a18ceb613f2384b8c079cb12baa888a433d6c0f81b7ec" + }, + { + "path": "debug_logger_tensor.hpp", + "sha256": "c9999fcf92536bbb36498ddd4485f213fc2f5ddc70d24f8d408195a48680b94f" + }, + { + "path": "nvdsinfer_custombboxparser_tao.cpp", + "sha256": "1794e3ee5152f25eff31454c6181368676f6659c68fc25b4b1933f6cbb63158b" + } + ] + }, + "tracker": { + "name": "NVIDIA NvDCF", + "library": "libnvds_nvmultiobjecttracker.so", + "configuration": "config_tracker_NvDCF_perf.yml", + "past_frame_output": false, + "custom_association": false, + "custom_hold_or_stitch": false + }, + "output": { + "frame_width": 800, + "frame_height": 600, + "overlay_codec": "H.264", + "overlay_container": "MP4", + "tracked_frames": "tracked-frames.jsonl" + }, + "authority": { + "ground_truth": false, + "candidate_accepted": false, + "commands_enabled": false, + "navigation_or_safety_accepted": false + } +} diff --git a/experiments/perception/e46f_dashcam_bakeoff_profile.json b/experiments/perception/e46f_dashcam_bakeoff_profile.json new file mode 100644 index 0000000..280a784 --- /dev/null +++ b/experiments/perception/e46f_dashcam_bakeoff_profile.json @@ -0,0 +1,102 @@ +{ + "schema_version": "missioncore.e46f-dashcam-bakeoff-profile/v1", + "profile_id": "e46f-deepstream-dashcamnet-detectnet-v2-nvdcf/v1", + "comparison_contract": { + "baseline_result_id": "e46e-ready-stack-d51fd744a86b0effa8685c7aa86d14dfd1b12e97bc8d68d0f53f467237b976bf", + "controlled_change": "detector-only", + "held_constant": [ + "recorded RIGHT source identity", + "DeepStream 9.1 container digest", + "FP16 precision", + "NVIDIA NvDCF performance configuration", + "800x600 output plane", + "full 4489-frame replay" + ] + }, + "source": { + "camera_source_id": "sensor.camera.right", + "job_id": "recorded-camera-602ac89026ed12978619801d", + "session_id": "20260720T065719Z_viewer_live", + "segment_count": 4489, + "stream_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8", + "archive_index_sha256": "e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14", + "archive_summary_sha256": "b280f40b198aad5d5335819107fb1405ad65d5695d187c61a2c027ad853a3181" + }, + "runtime": { + "container_image": "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:10eca409b3894e91c1bac915c9f1346307e56695e552487cbe8cf2f58a3f998f", + "container_platform": "linux/amd64", + "deepstream_version": "9.1", + "network_during_inference": "none" + }, + "detector": { + "name": "NVIDIA DashCamNet", + "architecture": "DetectNet_v2 ResNet18", + "version": "pruned_onnx_v1.0.4", + "precision": "FP16", + "intended_viewpoint": "moving vehicle dashcam", + "input_shape": [ + 3, + 544, + 960 + ], + "model_file": "resnet18_dashcamnet_pruned.onnx", + "model_sha256": "d92f97bd840b68850a33c83e480fc6bd83b3097ff4cb517e0ea12671046bab7d", + "model_url": "https://api.ngc.nvidia.com/v2/models/nvidia/tao/dashcamnet/versions/pruned_onnx_v1.0.4/files/resnet18_dashcamnet_pruned.onnx", + "model_card_url": "https://catalog.ngc.nvidia.com/orgs/nvidia/tao/models/dashcamnet/-", + "labels": [ + "car", + "bicycle", + "person", + "road_sign" + ], + "custom_postprocessing": false, + "qualification": { + "status": "passed-real-route-tensor-smoke", + "method": "ONNX Runtime RGB NCHW 1/255 on immutable RIGHT frames 1248 and 3027", + "observed_active_channels": [ + "car", + "person" + ], + "rejected_newer_artifact": { + "version": "pruned_onnx_v1.0.5", + "model_sha256": "4498f7e86e1113ef918462151ebef187cb9e6451dce4e57bd8e5484e1dd4aabb", + "reason": "only confidence channel 2 was non-zero on both immutable qualification frames" + } + } + }, + "postprocessor": { + "name": "NVIDIA DeepStream built-in DetectNet_v2 parser and NMS", + "implementation": "built into pinned DeepStream container", + "cluster_mode": "NMS", + "nms_iou_threshold": 0.5, + "pre_cluster_threshold": 0.2, + "car_pre_cluster_threshold": 0.4, + "topk": 20, + "reference_repository": "https://github.com/NVIDIA-AI-IOT/deepstream_reference_apps.git", + "reference_commit": "2bda7c6e14effac626530e68f3d3b2c09fbb9654", + "reference_path": "deepstream_app_tao_configs/nvinfer/config_infer_primary_dashcamnet.txt", + "reference_config_sha256": "332b764b42238064a666bd5ec619c42905e44c099376247256996a03203ced93", + "custom_mission_core_logic": false + }, + "tracker": { + "name": "NVIDIA NvDCF", + "library": "libnvds_nvmultiobjecttracker.so", + "configuration": "config_tracker_NvDCF_perf.yml", + "past_frame_output": false, + "custom_association": false, + "custom_hold_or_stitch": false + }, + "output": { + "frame_width": 800, + "frame_height": 600, + "overlay_codec": "H.264", + "overlay_container": "MP4", + "tracked_frames": "tracked-frames.jsonl" + }, + "authority": { + "ground_truth": false, + "candidate_accepted": false, + "commands_enabled": false, + "navigation_or_safety_accepted": false + } +} diff --git a/experiments/perception/e46g_rectified_detector_bakeoff_profile.json b/experiments/perception/e46g_rectified_detector_bakeoff_profile.json new file mode 100644 index 0000000..e4630ae --- /dev/null +++ b/experiments/perception/e46g_rectified_detector_bakeoff_profile.json @@ -0,0 +1,187 @@ +{ + "schema_version": "missioncore.e46g-rectified-detector-bakeoff-profile/v1", + "profile_id": "e46g-k1-right-kb4-nvdewarper-ready-detector-bakeoff/v1", + "comparison_contract": { + "baseline_result_ids": [ + "e46e-ready-stack-d51fd744a86b0effa8685c7aa86d14dfd1b12e97bc8d68d0f53f467237b976bf", + "e46f-dashcam-bakeoff-2b888a784ba06d4565d34a91fef58af1fc9090ed9dc298ad318010ff4da64507" + ], + "controlled_change": "detector-provider-only", + "held_constant": [ + "single recorded RIGHT source identity", + "factory camera_1 KB4 calibration", + "NVIDIA Gst-nvdewarper left/front/right projections", + "source frame selection 1000..1599", + "DeepStream 9.1 container digest", + "FP16 precision", + "NVIDIA NvDCF performance configuration", + "960x544 rectified output plane" + ] + }, + "source": { + "camera_source_id": "sensor.camera.right", + "job_id": "recorded-camera-602ac89026ed12978619801d", + "session_id": "20260720T065719Z_viewer_live", + "segment_count": 4489, + "stream_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8", + "archive_index_sha256": "e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14", + "archive_summary_sha256": "b280f40b198aad5d5335819107fb1405ad65d5695d187c61a2c027ad853a3181" + }, + "selection": { + "first_source_frame_index": 1000, + "last_source_frame_index": 1599, + "frame_count": 600, + "purpose": "contiguous model-independent urban gate containing dense vehicles, pedestrians and fisheye edge geometry" + }, + "calibration": { + "slot": "camera_1", + "model": "KB4", + "calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9", + "native_resolution": [ + 4000, + 3000 + ], + "admitted_resolution": [ + 800, + 600 + ], + "resize_contract": "linear 5x downscale without crop or warp", + "intrinsic_fx_fy_cx_cy": [ + 194.59817287616025, + 194.57531427932872, + 396.31861150187996, + 301.49644357408005 + ], + "distortion_kb4": [ + -0.023164451386679667, + -0.0014974198594105452, + -0.001039213149441563, + -0.000035237331915978814 + ] + }, + "rectification": { + "provider": "NVIDIA Gst-nvdewarper", + "provider_version": "DeepStream 9.1", + "projection": "fisheye-to-perspective", + "projection_type": 4, + "output_resolution": [ + 960, + 544 + ], + "horizontal_fov_degrees": 100.0, + "destination_focal_length": [ + 402.76782296509447, + 402.76782296509447 + ], + "destination_principal_point": [ + 479.5, + 271.5 + ], + "cuda_address_mode": "border", + "expected_full_frame_count": 4488, + "retained_source_frame_index_range": [ + 0, + 4487 + ], + "excluded_source_tail_frame_count": 1, + "tail_exclusion_reason": "DeepStream 9.1 nvv4l2decoder reproducibly omits only the terminal source frame at EOS; the admitted 1000..1599 gate is inside the timestamp-aligned retained prefix", + "view_order": [ + "left", + "front", + "right" + ], + "views": { + "left": { + "yaw_degrees": -90.0, + "pitch_degrees": 0.0, + "config_file": "e46g_nvdewarper_left.txt", + "config_sha256": "257438d20a00ac495022b06a3a27b41822ccbb59bb1e9405501a2467b8456718" + }, + "front": { + "yaw_degrees": 0.0, + "pitch_degrees": 0.0, + "config_file": "e46g_nvdewarper_front.txt", + "config_sha256": "f861e31278550bbe3fc82f41df4381c8a7aaf113a98a10761d98fa085c6a56b4" + }, + "right": { + "yaw_degrees": 90.0, + "pitch_degrees": 0.0, + "config_file": "e46g_nvdewarper_right.txt", + "config_sha256": "16c9b6c22c8c9a0cf7a3e6127c7f5878d7890200dcc284d39f3ed0101277bb73" + } + }, + "official_documentation": "https://docs.nvidia.com/metropolis/deepstream/9.0/text/DS_plugin_gst-nvdewarper.html" + }, + "runtime": { + "container_image": "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:10eca409b3894e91c1bac915c9f1346307e56695e552487cbe8cf2f58a3f998f", + "container_platform": "linux/amd64", + "deepstream_version": "9.1", + "network_during_inference": "none" + }, + "candidates": { + "trafficcamnet": { + "name": "NVIDIA TrafficCamNet Transformer Lite", + "architecture": "RT-DETR ResNet50", + "version": "deployable_resnet50_v2.0", + "precision": "FP16", + "model_file": "resnet50_trafficcamnet_rtdetr.fp16.onnx", + "model_sha256": "545a447b913d54eee476381436ebea4ad2aa876cfbe0a6d9f3b0302f08a7415d", + "model_url": "https://api.ngc.nvidia.com/v2/models/nvidia/tao/trafficcamnet_transformer_lite/versions/deployable_resnet50_v2.0/files/resnet50_trafficcamnet_rtdetr.fp16.onnx", + "deepstream_app_config": "e46g_trafficcamnet_deepstream_app.txt", + "deepstream_app_config_sha256": "1a0ade326a80f9dcdf0b538948eb508972afb478466cf4294116b7361b62aadf", + "detector_config": "e46e_trafficcamnet_rtdetr.txt", + "detector_config_sha256": "be2c275c0a1cbef3ec6a5f1eef2ece50b7f3408fc1c56839c0b9863f6d577c76", + "labels": [ + "background", + "bicycle", + "car", + "person", + "road_sign" + ], + "custom_postprocessing": false + }, + "dashcamnet": { + "name": "NVIDIA DashCamNet", + "architecture": "DetectNet_v2 ResNet18", + "version": "pruned_onnx_v1.0.4", + "precision": "FP16", + "model_file": "resnet18_dashcamnet_pruned.onnx", + "model_sha256": "d92f97bd840b68850a33c83e480fc6bd83b3097ff4cb517e0ea12671046bab7d", + "model_url": "https://api.ngc.nvidia.com/v2/models/nvidia/tao/dashcamnet/versions/pruned_onnx_v1.0.4/files/resnet18_dashcamnet_pruned.onnx", + "deepstream_app_config": "e46g_dashcamnet_deepstream_app.txt", + "deepstream_app_config_sha256": "e25c4fb5d44dc964a28afd9b4c4449624e9cbe89e71b2734aae2a9b55765d603", + "detector_config": "e46f_dashcamnet_detectnet.txt", + "detector_config_sha256": "3493f5ba7236a949354133e437844b059f302a01c5f93b836f4f16f58a776238", + "labels": [ + "car", + "bicycle", + "person", + "road_sign" + ], + "custom_postprocessing": false + } + }, + "trafficcamnet_parser": { + "name": "NVIDIA DeepStream TAO custom bounding-box parser", + "commit": "581889df47d6181110c758c10b872ca833a835e3", + "symbol": "NvDsInferParseCustomDDETRTAO", + "library_file": "libnvds_infercustomparser_tao.so", + "library_sha256": "a18d85dae674a088549c5f9b8fda53c640f4fcbd88a41f4c2cb1f4e3ea8878ee", + "custom_mission_core_logic": false + }, + "tracker": { + "name": "NVIDIA NvDCF", + "library": "libnvds_nvmultiobjecttracker.so", + "configuration": "config_tracker_NvDCF_perf.yml", + "identity_scope": "view-local", + "past_frame_output": false, + "custom_association": false, + "custom_hold_or_stitch": false + }, + "authority": { + "ground_truth": false, + "candidate_accepted": false, + "commands_enabled": false, + "navigation_or_safety_accepted": false + } +} diff --git a/experiments/perception/e46h_full_rectified_front_replay_profile.json b/experiments/perception/e46h_full_rectified_front_replay_profile.json new file mode 100644 index 0000000..b2cc8fa --- /dev/null +++ b/experiments/perception/e46h_full_rectified_front_replay_profile.json @@ -0,0 +1,103 @@ +{ + "schema_version": "missioncore.e46h-full-rectified-front-replay-profile/v1", + "profile_id": "e46h-right-kb4-front-trafficcamnet-full-replay/v1", + "baseline_result_id": "e46g-rectified-detector-bakeoff-9c4eb44cbb61199db0967bd9048712a0964e2dbf587651c9e0c6d808967675c6", + "source": { + "camera_source_id": "sensor.camera.right", + "job_id": "recorded-camera-602ac89026ed12978619801d", + "session_id": "20260720T065719Z_viewer_live", + "segment_count": 4489, + "stream_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8", + "archive_index_sha256": "e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14", + "archive_summary_sha256": "b280f40b198aad5d5335819107fb1405ad65d5695d187c61a2c027ad853a3181" + }, + "selection": { + "first_source_frame_index": 0, + "last_source_frame_index": 4487, + "frame_count": 4488, + "excluded_source_tail_frame_count": 1, + "tail_exclusion_reason": "DeepStream 9.1 nvv4l2decoder reproducibly omits only the terminal source frame at EOS; E46H admits the timestamp-aligned retained prefix 0..4487" + }, + "calibration": { + "slot": "camera_1", + "model": "KB4", + "calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9", + "native_resolution": [4000, 3000], + "admitted_resolution": [800, 600], + "resize_contract": "linear 5x downscale without crop or warp", + "intrinsic_fx_fy_cx_cy": [ + 194.59817287616025, + 194.57531427932872, + 396.31861150187996, + 301.49644357408005 + ], + "distortion_kb4": [ + -0.023164451386679667, + -0.0014974198594105452, + -0.001039213149441563, + -0.000035237331915978814 + ] + }, + "rectification": { + "provider": "NVIDIA Gst-nvdewarper", + "provider_version": "DeepStream 9.1", + "projection": "fisheye-to-perspective", + "projection_type": 4, + "view": "front", + "yaw_degrees": 0.0, + "pitch_degrees": 0.0, + "output_resolution": [960, 544], + "horizontal_fov_degrees": 100.0, + "destination_focal_length": [402.76782296509447, 402.76782296509447], + "destination_principal_point": [479.5, 271.5], + "cuda_address_mode": "border", + "config_file": "e46g_nvdewarper_front.txt", + "config_sha256": "f861e31278550bbe3fc82f41df4381c8a7aaf113a98a10761d98fa085c6a56b4", + "official_documentation": "https://docs.nvidia.com/metropolis/deepstream/9.0/text/DS_plugin_gst-nvdewarper.html" + }, + "runtime": { + "container_image": "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:10eca409b3894e91c1bac915c9f1346307e56695e552487cbe8cf2f58a3f998f", + "container_platform": "linux/amd64", + "deepstream_version": "9.1", + "network_during_inference": "none" + }, + "detector": { + "name": "NVIDIA TrafficCamNet Transformer Lite", + "architecture": "RT-DETR ResNet50", + "version": "deployable_resnet50_v2.0", + "precision": "FP16", + "model_file": "resnet50_trafficcamnet_rtdetr.fp16.onnx", + "model_sha256": "545a447b913d54eee476381436ebea4ad2aa876cfbe0a6d9f3b0302f08a7415d", + "model_url": "https://api.ngc.nvidia.com/v2/models/nvidia/tao/trafficcamnet_transformer_lite/versions/deployable_resnet50_v2.0/files/resnet50_trafficcamnet_rtdetr.fp16.onnx", + "deepstream_app_config": "e46g_trafficcamnet_deepstream_app.txt", + "deepstream_app_config_sha256": "1a0ade326a80f9dcdf0b538948eb508972afb478466cf4294116b7361b62aadf", + "detector_config": "e46e_trafficcamnet_rtdetr.txt", + "detector_config_sha256": "be2c275c0a1cbef3ec6a5f1eef2ece50b7f3408fc1c56839c0b9863f6d577c76", + "labels": ["background", "bicycle", "car", "person", "road_sign"], + "custom_postprocessing": false + }, + "parser": { + "name": "NVIDIA DeepStream TAO custom bounding-box parser", + "repository": "https://github.com/NVIDIA/DeepStream.git", + "commit": "581889df47d6181110c758c10b872ca833a835e3", + "symbol": "NvDsInferParseCustomDDETRTAO", + "library_file": "libnvds_infercustomparser_tao.so", + "library_sha256": "a18d85dae674a088549c5f9b8fda53c640f4fcbd88a41f4c2cb1f4e3ea8878ee", + "custom_mission_core_logic": false + }, + "tracker": { + "name": "NVIDIA NvDCF", + "library": "libnvds_nvmultiobjecttracker.so", + "configuration": "config_tracker_NvDCF_perf.yml", + "identity_scope": "front-view route-local", + "past_frame_output": false, + "custom_association": false, + "custom_hold_or_stitch": false + }, + "authority": { + "ground_truth": false, + "candidate_accepted": false, + "commands_enabled": false, + "navigation_or_safety_accepted": false + } +} diff --git a/experiments/perception/e46i_grounding_dino_full_replay_profile.json b/experiments/perception/e46i_grounding_dino_full_replay_profile.json new file mode 100644 index 0000000..7dfa2ad --- /dev/null +++ b/experiments/perception/e46i_grounding_dino_full_replay_profile.json @@ -0,0 +1,66 @@ +{ + "schema_version": "missioncore.e46i-grounding-dino-full-replay-profile/v1", + "profile_id": "e46i-right-kb4-front-grounding-dino-swin-tiny-full-replay/v1", + "baseline_result_id": "e46h-full-rectified-front-replay-43f9d97c06387ffa3b7aa656c48c3cb475be5c40de1211853ae8cb3f5ff2e28f", + "source": { + "camera_source_id": "sensor.camera.right", + "session_id": "20260720T065719Z_viewer_live", + "view": "front", + "projection": "NVIDIA Gst-nvdewarper KB4 fisheye-to-perspective", + "projection_resolution": [960, 544], + "projection_fov_degrees": 100.0, + "video_frame_count": 4488, + "video_frame_rate": 10.0, + "video_duration_seconds": 448.8, + "video_sha256": "29bb32df8465ef53759bdf53b869f2edf2506fb70fd2a3fdf9dcdde02bf618e8" + }, + "provider": { + "name": "NVIDIA TAO Grounding DINO Swin-Tiny Commercial", + "version": "1.0", + "deployment_toolkit": "NVIDIA TAO Toolkit Deploy 7.0.1", + "model_file": "grounding_dino_swin_tiny_commercial_deployable.onnx", + "model_sha256": "6895acdc6b588e923f753e37b3bd18869e064256e5ecc1b2b9853e8c51125f94", + "engine_file": "grounding_dino_swin_tiny_commercial_fp16.engine", + "engine_sha256": "63213f60730ce8577047ff508dfe06113269347501e2dd832aeb56c89bc11489", + "engine_precision": "FP16", + "container_reference": "nvcr.io/nvidia/tao/tao-toolkit:7.0.1-deploy", + "registry_manifest_sha256": "90d5d645ab75838db9ec7058855e5c283b59116ef0f1663a26b00b7c2c37280d", + "registry_config_sha256": "ea2dda4441128170ae6eb798143517979ebaa523bab6d9c0d73547fdbae2b86a", + "local_image_id_sha256": "2a3095330dd83e4314aada21fc7d184fcdb442a9131d591e5593ca5497394e7a", + "custom_detector_or_postprocessing": false + }, + "inference": { + "spec_file": "e46i_grounding_dino_full_replay_spec.yaml", + "spec_sha256": "c8513427f8bb8593a581bb29f286d89896e9639f98f851f2198aff86b8b541cb", + "captions": ["car", "person", "bicycle", "road sign"], + "confidence_threshold": 0.5, + "batch_size": 1, + "input_resolution": [960, 544], + "gpu": "NVIDIA GeForce RTX 4090", + "processed_frame_count": 4488, + "elapsed_seconds": 748.0, + "mean_frames_per_second": 6.0, + "network_observation": "TAO downloaded the bert-base-uncased tokenizer from Hugging Face at run start; this execution is valid but not yet an offline-reproducible deployment" + }, + "visual_shadow_gate": { + "status": "completed", + "threshold_changed_after_review": false, + "legacy_large_false_background_cases": 5, + "legacy_large_false_background_cases_suppressed": 5, + "empty_anchor_cases": 1, + "empty_anchor_cases_kept_empty": 1, + "positive_anchor_cases": 5, + "positive_anchor_cases_with_relevant_detection": 5, + "known_semantic_limitations": [ + "one partially cropped person was missed in the stroller frame", + "the stroller was labelled bicycle because stroller was not included in the fixed caption set" + ] + }, + "authority": { + "ground_truth": false, + "independent_truth": false, + "candidate_accepted": false, + "commands_enabled": false, + "navigation_or_safety_accepted": false + } +} diff --git a/experiments/perception/e46i_grounding_dino_full_replay_spec.yaml b/experiments/perception/e46i_grounding_dino_full_replay_spec.yaml new file mode 100644 index 0000000..7daba5b --- /dev/null +++ b/experiments/perception/e46i_grounding_dino_full_replay_spec.yaml @@ -0,0 +1,54 @@ +model_name: nvidia-grounding-dino-swin-tiny-commercial-v1.0 +results_dir: /workspace/e46i-full-results +wandb: + enable: false + +model: + backbone: swin_tiny_224_1k + num_feature_levels: 4 + dec_layers: 6 + enc_layers: 6 + num_queries: 900 + dim_feedforward: 2048 + aux_loss: false + log_scale: auto + class_embed_bias: true + +dataset: + infer_data_sources: + image_dir: + - /workspace/full-input-images + captions: + - car + - person + - bicycle + - road sign + batch_size: 1 + workers: 1 + pin_memory: true + max_labels: 80 + +inference: + trt_engine: /workspace/models/grounding_dino_swin_tiny_commercial_fp16.engine + batch_size: -1 + input_width: 960 + input_height: 544 + conf_threshold: 0.5 + outline_width: 3 + color_map: + car: blue + person: red + bicycle: green + road sign: yellow + +gen_trt_engine: + onnx_file: /workspace/models/grounding_dino_swin_tiny_commercial_deployable.onnx + trt_engine: /workspace/models/grounding_dino_swin_tiny_commercial_fp16.engine + batch_size: -1 + verbose: false + tensorrt: + workspace_size: 8192 + min_batch_size: 1 + opt_batch_size: 1 + max_batch_size: 1 + data_type: FP16 diff --git a/experiments/perception/e46i_grounding_dino_shadow_spec.yaml b/experiments/perception/e46i_grounding_dino_shadow_spec.yaml new file mode 100644 index 0000000..f0888e0 --- /dev/null +++ b/experiments/perception/e46i_grounding_dino_shadow_spec.yaml @@ -0,0 +1,54 @@ +model_name: nvidia-grounding-dino-swin-tiny-commercial-v1.0 +results_dir: /workspace/e46i-results +wandb: + enable: false + +model: + backbone: swin_tiny_224_1k + num_feature_levels: 4 + dec_layers: 6 + enc_layers: 6 + num_queries: 900 + dim_feedforward: 2048 + aux_loss: false + log_scale: auto + class_embed_bias: true + +dataset: + infer_data_sources: + image_dir: + - /workspace/input-images + captions: + - car + - person + - bicycle + - road sign + batch_size: 1 + workers: 1 + pin_memory: true + max_labels: 80 + +inference: + trt_engine: /workspace/models/grounding_dino_swin_tiny_commercial_fp16.engine + batch_size: -1 + input_width: 960 + input_height: 544 + conf_threshold: 0.5 + outline_width: 3 + color_map: + car: blue + person: red + bicycle: green + road sign: yellow + +gen_trt_engine: + onnx_file: /workspace/models/grounding_dino_swin_tiny_commercial_deployable.onnx + trt_engine: /workspace/models/grounding_dino_swin_tiny_commercial_fp16.engine + batch_size: -1 + verbose: false + tensorrt: + workspace_size: 8192 + min_batch_size: 1 + opt_batch_size: 1 + max_batch_size: 1 + data_type: FP16 diff --git a/experiments/perception/e46j_raw_fisheye_yolox_profile.json b/experiments/perception/e46j_raw_fisheye_yolox_profile.json new file mode 100644 index 0000000..39ff373 --- /dev/null +++ b/experiments/perception/e46j_raw_fisheye_yolox_profile.json @@ -0,0 +1,69 @@ +{ + "schema_version": "missioncore.e46j-raw-fisheye-realtime-profile/v1", + "profile_id": "e46j-k1-right-raw-kb4-yolox-s-one-pass/v1", + "source": { + "camera_source_id": "sensor.camera.right", + "session_id": "20260720T065719Z_viewer_live", + "job_id": "recorded-camera-602ac89026ed12978619801d", + "stream_sha256": "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8", + "frame_count": 4489, + "frame_rate": 10.003944527024467, + "average_rate": "4489000/448723", + "resolution": [800, 600], + "calibration_slot": "camera_1", + "calibration_model": "KB4", + "calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9", + "preprocessing_contract": "full admitted raw raster; valid-FOV fill and top-left letterbox only; no crop, dewarp or virtual view" + }, + "valid_fov": { + "result_id": "valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2", + "mask_file": "mask.png" + }, + "detector": { + "id": "yolox_s", + "version": 1, + "architecture": "YOLOX-S", + "source": "Megvii-BaseDetection/YOLOX release 0.1.1rc0", + "license": "Apache-2.0", + "model_sha256": "c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063", + "config_sha256": "5795c737a7935a655961b069e8404d336d891f9762fb6dffb93956a076479604", + "runtime": "NVIDIA Triton 2.70.0 ONNX Runtime GPU backend", + "input_name": "images", + "output_name": "output", + "input_shape": [1, 3, 640, 640], + "classes": "COCO-80", + "single_inference_per_source_frame": true + }, + "preprocessing": { + "color_order": "BGR", + "resize": "bilinear-letterbox-top-left", + "pad_value": 114, + "valid_fov_fill_value": 114 + }, + "detection": { + "minimum_score": 0.5, + "nms_iou_threshold": 0.45, + "target_class_ids": [0, 1, 2, 3, 5, 7], + "minimum_box_area_pixels": 64.0, + "maximum_box_area_fraction": 0.5, + "minimum_valid_fov_fraction": 0.5, + "require_center_inside_valid_fov": true, + "custom_detector_logic": false, + "route_specific_filtering": false + }, + "acceptance": { + "required_frame_count": 4489, + "required_source_frame_rate": 10.003944527024467, + "minimum_core_capacity_fps": 10.0, + "maximum_core_path_p95_ms": 80.0, + "maximum_inference_request_p95_ms": 60.0, + "require_zero_failed_frames": true, + "require_full_raw_fov": true + }, + "authority": { + "ground_truth": false, + "provider_promoted": false, + "commands_enabled": false, + "navigation_or_safety_accepted": false + } +} diff --git a/experiments/perception/l34_ravnoves00_right_yolox_truth_island_v1.json b/experiments/perception/l34_ravnoves00_right_yolox_truth_island_v1.json new file mode 100644 index 0000000..5779cec --- /dev/null +++ b/experiments/perception/l34_ravnoves00_right_yolox_truth_island_v1.json @@ -0,0 +1,40 @@ +{ + "authority": { + "commands_enabled": false, + "navigation_or_safety_accepted": false + }, + "camera_source_id": "sensor.camera.right", + "candidate": { + "architecture": "YOLOX-S", + "candidate_id": "yolox-s-kb4-core3", + "minimum_score": 0.25, + "model_sha256": "c5c2d13e59ae883e6af3b45daea64af4833a4951c92d116ec270d9ddbe998063" + }, + "evaluation": { + "accuracy_gate": "blocked-until-independent-truth-seal", + "metrics": [ + "AP50", + "AP50:95", + "per-class recall", + "person/vehicle miss rate", + "false-large-box rate", + "valid-FOV leakage", + "temporal class-count flicker" + ], + "winner_policy": "explicit-review-only" + }, + "mode": "recorded-replay-only", + "pipeline_id": "kb4-core3-yolox-eomt-k1-lidar-e23-temporal/v1", + "profile_id": "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1", + "schema_version": "missioncore.l34-right-yolox-truth-island-profile/v1", + "source_session_id": "20260720T065719Z_viewer_live", + "target_classes": [ + "person", + "bicycle", + "motorcycle", + "car", + "heavy_vehicle", + "static_obstacle", + "animal" + ] +} diff --git a/experiments/perception/prepare_e46e_worker_package.py b/experiments/perception/prepare_e46e_worker_package.py new file mode 100644 index 0000000..e312900 --- /dev/null +++ b/experiments/perception/prepare_e46e_worker_package.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +"""Build and validate the minimal content-addressed E46E Worker 006 package.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import uuid +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from k1link.compute.e46e_ready_stack import E46E_PACKAGE_SCHEMA, E46E_PROFILE_SCHEMA + +_RUNTIME_FILES = { + "runtime/k1link/__init__.py": "src/k1link/__init__.py", + "runtime/k1link/compute/__init__.py": None, + "runtime/k1link/compute/e46e_ready_stack.py": "src/k1link/compute/e46e_ready_stack.py", + "runtime/run_e46e_ready_stack.py": ( + "experiments/perception/worker/run_e46e_ready_stack.py" + ), + "runtime/Invoke-E46EReadyStack.ps1": ( + "experiments/perception/worker/Invoke-E46EReadyStack.ps1" + ), + "runtime/Invoke-E46EReadyStackAsInteractiveUser.ps1": ( + "experiments/perception/worker/Invoke-E46EReadyStackAsInteractiveUser.ps1" + ), + "runtime/Build-E46ENvidiaTaoParser.ps1": ( + "experiments/perception/worker/Build-E46ENvidiaTaoParser.ps1" + ), + "runtime/Invoke-E46ENvidiaTaoParserAsInteractiveUser.ps1": ( + "experiments/perception/worker/Invoke-E46ENvidiaTaoParserAsInteractiveUser.ps1" + ), + "runtime/e46e_deepstream_app.txt": ( + "experiments/perception/worker/e46e_deepstream_app.txt" + ), + "runtime/e46e_trafficcamnet_rtdetr.txt": ( + "experiments/perception/worker/e46e_trafficcamnet_rtdetr.txt" + ), + "runtime/e46e_trafficcamnet_labels.txt": ( + "experiments/perception/worker/e46e_trafficcamnet_labels.txt" + ), +} +_GENERATED_COMPUTE_INIT = ( + '"""Minimal E46E worker projection; import contract modules explicitly."""\n' +) + + +class E46EWorkerPackageError(RuntimeError): + """Raised when an E46E package is invalid.""" + + +def build_e46e_worker_package( + *, + repository_root: Path, + profile_path: Path, + parser_library_path: Path, + output_root: Path, +) -> Path: + repository = repository_root.resolve(strict=True) + profile_source = profile_path.resolve(strict=True) + profile = _read_json(profile_source) + if profile.get("schema_version") != E46E_PROFILE_SCHEMA: + raise E46EWorkerPackageError("E46E package profile is incompatible") + parser_library = parser_library_path.resolve(strict=True) + parser_profile = profile.get("parser") + if ( + not isinstance(parser_profile, dict) + or parser_profile.get("library_file") != parser_library.name + or parser_profile.get("library_sha256") != _sha256(parser_library) + or parser_library.is_symlink() + ): + raise E46EWorkerPackageError("E46E NVIDIA parser library is incompatible") + sources: dict[str, Path | None] = {} + for target, relative in _RUNTIME_FILES.items(): + source = None if relative is None else repository / relative + if source is not None and (not source.is_file() or source.is_symlink()): + raise E46EWorkerPackageError(f"E46E runtime source is invalid: {relative}") + sources[target] = source + sources["profile.json"] = profile_source + sources[f"runtime/{parser_library.name}"] = parser_library + descriptors = [ + _descriptor(relative, source) + for relative, source in sorted(sources.items()) + ] + identity = { + "schema_version": E46E_PACKAGE_SCHEMA, + "classification": "minimal-stock-nvidia-recorded-right-worker-package", + "profile_id": profile["profile_id"], + "source_job_id": profile["source"]["job_id"], + "profile_sha256": _sha256(profile_source), + "artifact_paths": [row["path"] for row in descriptors], + "source_artifacts": descriptors, + "runtime": profile["runtime"], + "detector": { + "name": profile["detector"]["name"], + "version": profile["detector"]["version"], + "model_sha256": profile["detector"]["model_sha256"], + }, + "parser": profile["parser"], + "tracker": profile["tracker"], + "authority": { + "commands_enabled": False, + "navigation_or_safety_accepted": False, + }, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + package_id = f"e46e-worker-package-{identity_sha256}" + output = output_root.expanduser().absolute() + output.mkdir(mode=0o700, parents=True, exist_ok=True) + destination = output / package_id + if destination.exists(): + validate_e46e_worker_package(destination) + return destination + staging = output / f".{package_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + for relative, source in sources.items(): + target = staging / relative + target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + if source is None: + target.write_text(_GENERATED_COMPUTE_INIT, encoding="utf-8") + else: + shutil.copyfile(source, target) + artifacts = [ + { + "kind": relative, + "path": relative, + "byte_length": (staging / relative).stat().st_size, + "sha256": _sha256(staging / relative), + } + for relative in sorted(sources) + ] + _write_json( + staging / "manifest.json", + { + "schema_version": E46E_PACKAGE_SCHEMA, + "package_id": package_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": datetime.now(UTC) + .isoformat(timespec="milliseconds") + .replace("+00:00", "Z"), + "artifacts": artifacts, + }, + ) + validate_e46e_worker_package(staging, allow_staging=True) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + validate_e46e_worker_package(destination) + return destination + + +def validate_e46e_worker_package( + root: Path, *, allow_staging: bool = False +) -> dict[str, Any]: + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / "manifest.json") + identity = manifest.get("identity") + digest = ( + hashlib.sha256(_canonical_json(identity)).hexdigest() + if isinstance(identity, dict) + else "" + ) + package_id = f"e46e-worker-package-{digest}" + valid_name = resolved.name == package_id or ( + allow_staging + and resolved.name.startswith(f".{package_id}.") + and resolved.name.endswith(".tmp") + ) + artifacts = manifest.get("artifacts") + if ( + manifest.get("schema_version") != E46E_PACKAGE_SCHEMA + or manifest.get("package_id") != package_id + or manifest.get("identity_sha256") != digest + or not valid_name + or not isinstance(artifacts, list) + ): + raise E46EWorkerPackageError("E46E worker package identity is invalid") + expected_paths = set(identity.get("artifact_paths", [])) + actual_paths = { + path.relative_to(resolved).as_posix() + for path in resolved.rglob("*") + if path.is_file() + } + if actual_paths != expected_paths | {"manifest.json"} or len(artifacts) != len( + expected_paths + ): + raise E46EWorkerPackageError("E46E worker package file set changed") + observed: set[str] = set() + for row in artifacts: + if not isinstance(row, dict): + raise E46EWorkerPackageError("E46E package artifact is invalid") + relative = row.get("path") + path = resolved / str(relative) + if ( + not isinstance(relative, str) + or relative not in expected_paths + or relative in observed + or Path(relative).is_absolute() + or ".." in Path(relative).parts + or not path.is_file() + or path.is_symlink() + or row.get("kind") != relative + or row.get("byte_length") != path.stat().st_size + or row.get("sha256") != _sha256(path) + ): + raise E46EWorkerPackageError("E46E package artifact changed") + observed.add(relative) + if observed != expected_paths: + raise E46EWorkerPackageError("E46E package coverage changed") + return manifest + + +def _descriptor(relative: str, source: Path | None) -> dict[str, Any]: + payload = _GENERATED_COMPUTE_INIT.encode() if source is None else source.read_bytes() + return { + "path": relative, + "byte_length": len(payload), + "sha256": hashlib.sha256(payload).hexdigest(), + } + + +def _canonical_json(value: object) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode() + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _read_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8-sig")) + if not isinstance(value, dict): + raise E46EWorkerPackageError(f"JSON object expected: {path.name}") + return value + + +def _write_json(path: Path, value: object) -> None: + with path.open("x", encoding="utf-8") as stream: + json.dump(value, stream, ensure_ascii=False, indent=2, allow_nan=False) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repository-root", type=Path, required=True) + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--parser-library", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + package = build_e46e_worker_package( + repository_root=args.repository_root, + profile_path=args.profile, + parser_library_path=args.parser_library, + output_root=args.output_root, + ) + print(package) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/prepare_e46f_worker_package.py b/experiments/perception/prepare_e46f_worker_package.py new file mode 100644 index 0000000..f27b180 --- /dev/null +++ b/experiments/perception/prepare_e46f_worker_package.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +"""Build and validate the minimal content-addressed E46F Worker 006 package.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import uuid +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from k1link.compute.e46f_dashcam_bakeoff import ( + E46F_PACKAGE_SCHEMA, + E46F_PROFILE_SCHEMA, +) + +_RUNTIME_FILES = { + "runtime/k1link/__init__.py": "src/k1link/__init__.py", + "runtime/k1link/compute/__init__.py": None, + "runtime/k1link/compute/e46e_ready_stack.py": ("src/k1link/compute/e46e_ready_stack.py"), + "runtime/k1link/compute/e46f_dashcam_bakeoff.py": ( + "src/k1link/compute/e46f_dashcam_bakeoff.py" + ), + "runtime/run_e46f_dashcam_bakeoff.py": ( + "experiments/perception/worker/run_e46f_dashcam_bakeoff.py" + ), + "runtime/Invoke-E46FDashCamBakeoff.ps1": ( + "experiments/perception/worker/Invoke-E46FDashCamBakeoff.ps1" + ), + "runtime/Invoke-E46FDashCamBakeoffAsInteractiveUser.ps1": ( + "experiments/perception/worker/Invoke-E46FDashCamBakeoffAsInteractiveUser.ps1" + ), + "runtime/e46f_deepstream_app.txt": ("experiments/perception/worker/e46f_deepstream_app.txt"), + "runtime/e46f_dashcamnet_detectnet.txt": ( + "experiments/perception/worker/e46f_dashcamnet_detectnet.txt" + ), + "runtime/e46f_dashcamnet_labels.txt": ( + "experiments/perception/worker/e46f_dashcamnet_labels.txt" + ), +} +_GENERATED_COMPUTE_INIT = ( + '"""Minimal E46F worker projection; import contract modules explicitly."""\n' +) + + +class E46FWorkerPackageError(RuntimeError): + """Raised when an E46F package is invalid.""" + + +def build_e46f_worker_package( + *, repository_root: Path, profile_path: Path, output_root: Path +) -> Path: + repository = repository_root.resolve(strict=True) + profile_source = profile_path.resolve(strict=True) + profile = _read_json(profile_source) + if profile.get("schema_version") != E46F_PROFILE_SCHEMA: + raise E46FWorkerPackageError("E46F package profile is incompatible") + sources: dict[str, Path | None] = {} + for target, relative in _RUNTIME_FILES.items(): + source = None if relative is None else repository / relative + if source is not None and (not source.is_file() or source.is_symlink()): + raise E46FWorkerPackageError(f"E46F runtime source is invalid: {relative}") + sources[target] = source + sources["profile.json"] = profile_source + descriptors = [_descriptor(relative, source) for relative, source in sorted(sources.items())] + identity = { + "schema_version": E46F_PACKAGE_SCHEMA, + "classification": "minimal-stock-nvidia-detector-only-bakeoff-package", + "profile_id": profile["profile_id"], + "source_job_id": profile["source"]["job_id"], + "baseline_result_id": profile["comparison_contract"]["baseline_result_id"], + "profile_sha256": _sha256(profile_source), + "artifact_paths": [row["path"] for row in descriptors], + "source_artifacts": descriptors, + "runtime": profile["runtime"], + "detector": { + "name": profile["detector"]["name"], + "version": profile["detector"]["version"], + "model_sha256": profile["detector"]["model_sha256"], + }, + "postprocessor": profile["postprocessor"], + "tracker": profile["tracker"], + "authority": { + "commands_enabled": False, + "navigation_or_safety_accepted": False, + }, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + package_id = f"e46f-worker-package-{identity_sha256}" + output = output_root.expanduser().absolute() + output.mkdir(mode=0o700, parents=True, exist_ok=True) + destination = output / package_id + if destination.exists(): + validate_e46f_worker_package(destination) + return destination + staging = output / f".{package_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + for relative, source in sources.items(): + target = staging / relative + target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + if source is None: + target.write_text(_GENERATED_COMPUTE_INIT, encoding="utf-8") + else: + shutil.copyfile(source, target) + artifacts = [ + { + "kind": relative, + "path": relative, + "byte_length": (staging / relative).stat().st_size, + "sha256": _sha256(staging / relative), + } + for relative in sorted(sources) + ] + _write_json( + staging / "manifest.json", + { + "schema_version": E46F_PACKAGE_SCHEMA, + "package_id": package_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": datetime.now(UTC) + .isoformat(timespec="milliseconds") + .replace("+00:00", "Z"), + "artifacts": artifacts, + }, + ) + validate_e46f_worker_package(staging, allow_staging=True) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + validate_e46f_worker_package(destination) + return destination + + +def validate_e46f_worker_package(root: Path, *, allow_staging: bool = False) -> dict[str, Any]: + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / "manifest.json") + identity = manifest.get("identity") + digest = ( + hashlib.sha256(_canonical_json(identity)).hexdigest() if isinstance(identity, dict) else "" + ) + package_id = f"e46f-worker-package-{digest}" + valid_name = resolved.name == package_id or ( + allow_staging + and resolved.name.startswith(f".{package_id}.") + and resolved.name.endswith(".tmp") + ) + artifacts = manifest.get("artifacts") + if ( + manifest.get("schema_version") != E46F_PACKAGE_SCHEMA + or manifest.get("package_id") != package_id + or manifest.get("identity_sha256") != digest + or not valid_name + or not isinstance(artifacts, list) + ): + raise E46FWorkerPackageError("E46F worker package identity is invalid") + expected_paths = set(identity.get("artifact_paths", [])) + actual_paths = { + path.relative_to(resolved).as_posix() for path in resolved.rglob("*") if path.is_file() + } + if actual_paths != expected_paths | {"manifest.json"} or len(artifacts) != len(expected_paths): + raise E46FWorkerPackageError("E46F worker package file set changed") + observed: set[str] = set() + for row in artifacts: + if not isinstance(row, dict): + raise E46FWorkerPackageError("E46F package artifact is invalid") + relative = row.get("path") + path = resolved / str(relative) + if ( + not isinstance(relative, str) + or relative not in expected_paths + or relative in observed + or Path(relative).is_absolute() + or ".." in Path(relative).parts + or not path.is_file() + or path.is_symlink() + or row.get("kind") != relative + or row.get("byte_length") != path.stat().st_size + or row.get("sha256") != _sha256(path) + ): + raise E46FWorkerPackageError("E46F package artifact changed") + observed.add(relative) + if observed != expected_paths: + raise E46FWorkerPackageError("E46F package coverage changed") + return manifest + + +def _descriptor(relative: str, source: Path | None) -> dict[str, Any]: + payload = _GENERATED_COMPUTE_INIT.encode() if source is None else source.read_bytes() + return { + "path": relative, + "byte_length": len(payload), + "sha256": hashlib.sha256(payload).hexdigest(), + } + + +def _canonical_json(value: object) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode() + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _read_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8-sig")) + if not isinstance(value, dict): + raise E46FWorkerPackageError(f"JSON object expected: {path.name}") + return value + + +def _write_json(path: Path, value: object) -> None: + with path.open("x", encoding="utf-8") as stream: + json.dump(value, stream, ensure_ascii=False, indent=2, allow_nan=False) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repository-root", type=Path, required=True) + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + package = build_e46f_worker_package( + repository_root=args.repository_root, + profile_path=args.profile, + output_root=args.output_root, + ) + print(package) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/prepare_e46g_worker_package.py b/experiments/perception/prepare_e46g_worker_package.py new file mode 100644 index 0000000..e05e845 --- /dev/null +++ b/experiments/perception/prepare_e46g_worker_package.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +"""Build and validate the minimal content-addressed E46G Worker package.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import uuid +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from k1link.compute.e46g_rectified_detector_bakeoff import ( + E46G_PACKAGE_SCHEMA, + E46G_PROFILE_SCHEMA, +) + +_RUNTIME_FILES = { + "runtime/k1link/__init__.py": "src/k1link/__init__.py", + "runtime/k1link/compute/__init__.py": None, + "runtime/k1link/compute/e46e_ready_stack.py": "src/k1link/compute/e46e_ready_stack.py", + "runtime/k1link/compute/e46g_rectified_detector_bakeoff.py": ( + "src/k1link/compute/e46g_rectified_detector_bakeoff.py" + ), + "runtime/run_e46g_rectified_detector_bakeoff.py": ( + "experiments/perception/worker/run_e46g_rectified_detector_bakeoff.py" + ), + "runtime/Invoke-E46GRectifiedDetectorBakeoff.ps1": ( + "experiments/perception/worker/Invoke-E46GRectifiedDetectorBakeoff.ps1" + ), + "runtime/Invoke-E46GRectifiedDetectorBakeoffAsInteractiveUser.ps1": ( + "experiments/perception/worker/Invoke-E46GRectifiedDetectorBakeoffAsInteractiveUser.ps1" + ), + "runtime/Prepare-RectifiedCameraReplay.ps1": ( + "experiments/perception/worker/Prepare-RectifiedCameraReplay.ps1" + ), + "runtime/e46g_nvdewarper_left.txt": ("experiments/perception/worker/e46g_nvdewarper_left.txt"), + "runtime/e46g_nvdewarper_front.txt": ( + "experiments/perception/worker/e46g_nvdewarper_front.txt" + ), + "runtime/e46g_nvdewarper_right.txt": ( + "experiments/perception/worker/e46g_nvdewarper_right.txt" + ), + "runtime/e46g_trafficcamnet_deepstream_app.txt": ( + "experiments/perception/worker/e46g_trafficcamnet_deepstream_app.txt" + ), + "runtime/e46g_dashcamnet_deepstream_app.txt": ( + "experiments/perception/worker/e46g_dashcamnet_deepstream_app.txt" + ), + "runtime/e46e_trafficcamnet_rtdetr.txt": ( + "experiments/perception/worker/e46e_trafficcamnet_rtdetr.txt" + ), + "runtime/e46e_trafficcamnet_labels.txt": ( + "experiments/perception/worker/e46e_trafficcamnet_labels.txt" + ), + "runtime/e46f_dashcamnet_detectnet.txt": ( + "experiments/perception/worker/e46f_dashcamnet_detectnet.txt" + ), + "runtime/e46f_dashcamnet_labels.txt": ( + "experiments/perception/worker/e46f_dashcamnet_labels.txt" + ), +} +_GENERATED_COMPUTE_INIT = ( + '"""Minimal E46G worker projection; import contract modules explicitly."""\n' +) + + +class E46GWorkerPackageError(RuntimeError): + """Raised when an E46G package is invalid.""" + + +def build_e46g_worker_package( + *, + repository_root: Path, + profile_path: Path, + parser_library_path: Path, + output_root: Path, +) -> Path: + repository = repository_root.resolve(strict=True) + profile_source = profile_path.resolve(strict=True) + profile = _read_json(profile_source) + if profile.get("schema_version") != E46G_PROFILE_SCHEMA: + raise E46GWorkerPackageError("E46G package profile is incompatible") + parser_library = parser_library_path.resolve(strict=True) + parser_profile = profile.get("trafficcamnet_parser") + if ( + not isinstance(parser_profile, dict) + or parser_profile.get("library_file") != parser_library.name + or parser_profile.get("library_sha256") != _sha256(parser_library) + or parser_library.is_symlink() + ): + raise E46GWorkerPackageError("E46G NVIDIA parser library is incompatible") + sources: dict[str, Path | None] = {} + for target, relative in _RUNTIME_FILES.items(): + source = None if relative is None else repository / relative + if source is not None and (not source.is_file() or source.is_symlink()): + raise E46GWorkerPackageError(f"E46G runtime source is invalid: {relative}") + sources[target] = source + sources["profile.json"] = profile_source + sources[f"runtime/{parser_library.name}"] = parser_library + descriptors = [_descriptor(relative, source) for relative, source in sorted(sources.items())] + identity = { + "schema_version": E46G_PACKAGE_SCHEMA, + "classification": "minimal-factory-kb4-stock-nvidia-detector-bakeoff-package", + "profile_id": profile["profile_id"], + "source_job_id": profile["source"]["job_id"], + "profile_sha256": _sha256(profile_source), + "artifact_paths": [row["path"] for row in descriptors], + "source_artifacts": descriptors, + "runtime": profile["runtime"], + "calibration": profile["calibration"], + "rectification": profile["rectification"], + "selection": profile["selection"], + "candidates": { + name: { + "name": value["name"], + "version": value["version"], + "model_sha256": value["model_sha256"], + "custom_postprocessing": value["custom_postprocessing"], + } + for name, value in sorted(profile["candidates"].items()) + }, + "tracker": profile["tracker"], + "authority": { + "commands_enabled": False, + "navigation_or_safety_accepted": False, + }, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + package_id = f"e46g-worker-package-{identity_sha256}" + output = output_root.expanduser().absolute() + output.mkdir(mode=0o700, parents=True, exist_ok=True) + destination = output / package_id + if destination.exists(): + validate_e46g_worker_package(destination) + return destination + staging = output / f".{package_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + for relative, source in sources.items(): + target = staging / relative + target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + if source is None: + target.write_text(_GENERATED_COMPUTE_INIT, encoding="utf-8") + else: + shutil.copyfile(source, target) + artifacts = [ + { + "kind": relative, + "path": relative, + "byte_length": (staging / relative).stat().st_size, + "sha256": _sha256(staging / relative), + } + for relative in sorted(sources) + ] + _write_json( + staging / "manifest.json", + { + "schema_version": E46G_PACKAGE_SCHEMA, + "package_id": package_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": datetime.now(UTC) + .isoformat(timespec="milliseconds") + .replace("+00:00", "Z"), + "artifacts": artifacts, + }, + ) + validate_e46g_worker_package(staging, allow_staging=True) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + validate_e46g_worker_package(destination) + return destination + + +def validate_e46g_worker_package(root: Path, *, allow_staging: bool = False) -> dict[str, Any]: + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / "manifest.json") + identity = manifest.get("identity") + digest = ( + hashlib.sha256(_canonical_json(identity)).hexdigest() if isinstance(identity, dict) else "" + ) + package_id = f"e46g-worker-package-{digest}" + valid_name = resolved.name == package_id or ( + allow_staging + and resolved.name.startswith(f".{package_id}.") + and resolved.name.endswith(".tmp") + ) + artifacts = manifest.get("artifacts") + if ( + manifest.get("schema_version") != E46G_PACKAGE_SCHEMA + or manifest.get("package_id") != package_id + or manifest.get("identity_sha256") != digest + or not valid_name + or not isinstance(artifacts, list) + ): + raise E46GWorkerPackageError("E46G worker package identity is invalid") + expected_paths = set(identity.get("artifact_paths", [])) + actual_paths = { + path.relative_to(resolved).as_posix() for path in resolved.rglob("*") if path.is_file() + } + if actual_paths != expected_paths | {"manifest.json"} or len(artifacts) != len(expected_paths): + raise E46GWorkerPackageError("E46G worker package file set changed") + observed: set[str] = set() + for row in artifacts: + if not isinstance(row, dict): + raise E46GWorkerPackageError("E46G package artifact is invalid") + relative = row.get("path") + path = resolved / str(relative) + if ( + not isinstance(relative, str) + or relative not in expected_paths + or relative in observed + or Path(relative).is_absolute() + or ".." in Path(relative).parts + or not path.is_file() + or path.is_symlink() + or row.get("kind") != relative + or row.get("byte_length") != path.stat().st_size + or row.get("sha256") != _sha256(path) + ): + raise E46GWorkerPackageError("E46G package artifact changed") + observed.add(relative) + if observed != expected_paths: + raise E46GWorkerPackageError("E46G package coverage changed") + return manifest + + +def _descriptor(relative: str, source: Path | None) -> dict[str, Any]: + payload = _GENERATED_COMPUTE_INIT.encode() if source is None else source.read_bytes() + return { + "path": relative, + "byte_length": len(payload), + "sha256": hashlib.sha256(payload).hexdigest(), + } + + +def _canonical_json(value: object) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode() + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _read_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8-sig")) + if not isinstance(value, dict): + raise E46GWorkerPackageError(f"JSON object expected: {path.name}") + return value + + +def _write_json(path: Path, value: object) -> None: + with path.open("x", encoding="utf-8") as stream: + json.dump(value, stream, ensure_ascii=False, indent=2, allow_nan=False) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repository-root", type=Path, required=True) + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--parser-library", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + package = build_e46g_worker_package( + repository_root=args.repository_root, + profile_path=args.profile, + parser_library_path=args.parser_library, + output_root=args.output_root, + ) + print(package) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/prepare_e46h_worker_package.py b/experiments/perception/prepare_e46h_worker_package.py new file mode 100644 index 0000000..9675dde --- /dev/null +++ b/experiments/perception/prepare_e46h_worker_package.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +"""Build and validate the minimal content-addressed E46H Worker package.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import uuid +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from k1link.compute.e46h_full_rectified_front_replay import ( + E46H_PACKAGE_SCHEMA, + E46H_PROFILE_SCHEMA, +) + +_RUNTIME_FILES = { + "runtime/k1link/__init__.py": "src/k1link/__init__.py", + "runtime/k1link/compute/__init__.py": None, + "runtime/k1link/compute/e46e_ready_stack.py": "src/k1link/compute/e46e_ready_stack.py", + "runtime/k1link/compute/e46g_rectified_detector_bakeoff.py": ( + "src/k1link/compute/e46g_rectified_detector_bakeoff.py" + ), + "runtime/k1link/compute/e46h_full_rectified_front_replay.py": ( + "src/k1link/compute/e46h_full_rectified_front_replay.py" + ), + "runtime/run_e46h_full_rectified_front_replay.py": ( + "experiments/perception/worker/run_e46h_full_rectified_front_replay.py" + ), + "runtime/Invoke-E46HFullRectifiedFrontReplay.ps1": ( + "experiments/perception/worker/Invoke-E46HFullRectifiedFrontReplay.ps1" + ), + "runtime/Invoke-E46HFullRectifiedFrontReplayAsInteractiveUser.ps1": ( + "experiments/perception/worker/Invoke-E46HFullRectifiedFrontReplayAsInteractiveUser.ps1" + ), + "runtime/Prepare-RectifiedCameraReplay.ps1": ( + "experiments/perception/worker/Prepare-RectifiedCameraReplay.ps1" + ), + "runtime/e46g_nvdewarper_front.txt": ( + "experiments/perception/worker/e46g_nvdewarper_front.txt" + ), + "runtime/e46g_trafficcamnet_deepstream_app.txt": ( + "experiments/perception/worker/e46g_trafficcamnet_deepstream_app.txt" + ), + "runtime/e46e_trafficcamnet_rtdetr.txt": ( + "experiments/perception/worker/e46e_trafficcamnet_rtdetr.txt" + ), + "runtime/e46e_trafficcamnet_labels.txt": ( + "experiments/perception/worker/e46e_trafficcamnet_labels.txt" + ), +} +_GENERATED_COMPUTE_INIT = ( + '"""Minimal E46H worker projection; import contract modules explicitly."""\n' +) + + +class E46HWorkerPackageError(RuntimeError): + """Raised when an E46H package is invalid.""" + + +def build_e46h_worker_package( + *, + repository_root: Path, + profile_path: Path, + parser_library_path: Path, + output_root: Path, +) -> Path: + repository = repository_root.resolve(strict=True) + profile_source = profile_path.resolve(strict=True) + profile = _read_json(profile_source) + if profile.get("schema_version") != E46H_PROFILE_SCHEMA: + raise E46HWorkerPackageError("E46H package profile is incompatible") + parser_library = parser_library_path.resolve(strict=True) + parser = profile.get("parser") + if ( + not isinstance(parser, dict) + or parser.get("library_file") != parser_library.name + or parser.get("library_sha256") != _sha256(parser_library) + or parser_library.is_symlink() + ): + raise E46HWorkerPackageError("E46H NVIDIA parser library is incompatible") + sources: dict[str, Path | None] = {} + for target, relative in _RUNTIME_FILES.items(): + source = None if relative is None else repository / relative + if source is not None and (not source.is_file() or source.is_symlink()): + raise E46HWorkerPackageError(f"E46H runtime source is invalid: {relative}") + sources[target] = source + sources["profile.json"] = profile_source + sources[f"runtime/{parser_library.name}"] = parser_library + descriptors = [_descriptor(relative, source) for relative, source in sorted(sources.items())] + identity = { + "schema_version": E46H_PACKAGE_SCHEMA, + "classification": "minimal-full-front-stock-nvidia-replay-package", + "profile_id": profile["profile_id"], + "baseline_result_id": profile["baseline_result_id"], + "source_job_id": profile["source"]["job_id"], + "profile_sha256": _sha256(profile_source), + "artifact_paths": [row["path"] for row in descriptors], + "source_artifacts": descriptors, + "runtime": profile["runtime"], + "calibration": profile["calibration"], + "rectification": profile["rectification"], + "selection": profile["selection"], + "detector": { + "name": profile["detector"]["name"], + "version": profile["detector"]["version"], + "model_sha256": profile["detector"]["model_sha256"], + "custom_postprocessing": profile["detector"]["custom_postprocessing"], + }, + "tracker": profile["tracker"], + "authority": { + "commands_enabled": False, + "navigation_or_safety_accepted": False, + }, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + package_id = f"e46h-worker-package-{identity_sha256}" + output = output_root.expanduser().absolute() + output.mkdir(mode=0o700, parents=True, exist_ok=True) + destination = output / package_id + if destination.exists(): + validate_e46h_worker_package(destination) + return destination + staging = output / f".{package_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + for relative, source in sources.items(): + target = staging / relative + target.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + if source is None: + target.write_text(_GENERATED_COMPUTE_INIT, encoding="utf-8") + else: + shutil.copyfile(source, target) + artifacts = [ + { + "kind": relative, + "path": relative, + "byte_length": (staging / relative).stat().st_size, + "sha256": _sha256(staging / relative), + } + for relative in sorted(sources) + ] + _write_json( + staging / "manifest.json", + { + "schema_version": E46H_PACKAGE_SCHEMA, + "package_id": package_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": datetime.now(UTC) + .isoformat(timespec="milliseconds") + .replace("+00:00", "Z"), + "artifacts": artifacts, + }, + ) + validate_e46h_worker_package(staging, allow_staging=True) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + validate_e46h_worker_package(destination) + return destination + + +def validate_e46h_worker_package(root: Path, *, allow_staging: bool = False) -> dict[str, Any]: + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / "manifest.json") + identity = manifest.get("identity") + digest = ( + hashlib.sha256(_canonical_json(identity)).hexdigest() if isinstance(identity, dict) else "" + ) + package_id = f"e46h-worker-package-{digest}" + valid_name = resolved.name == package_id or ( + allow_staging + and resolved.name.startswith(f".{package_id}.") + and resolved.name.endswith(".tmp") + ) + artifacts = manifest.get("artifacts") + if ( + manifest.get("schema_version") != E46H_PACKAGE_SCHEMA + or manifest.get("package_id") != package_id + or manifest.get("identity_sha256") != digest + or not valid_name + or not isinstance(artifacts, list) + ): + raise E46HWorkerPackageError("E46H worker package identity is invalid") + expected_paths = set(identity.get("artifact_paths", [])) + actual_paths = { + path.relative_to(resolved).as_posix() for path in resolved.rglob("*") if path.is_file() + } + if actual_paths != expected_paths | {"manifest.json"} or len(artifacts) != len(expected_paths): + raise E46HWorkerPackageError("E46H worker package file set changed") + observed: set[str] = set() + for row in artifacts: + if not isinstance(row, dict): + raise E46HWorkerPackageError("E46H package artifact is invalid") + relative = row.get("path") + path = resolved / str(relative) + if ( + not isinstance(relative, str) + or relative not in expected_paths + or relative in observed + or Path(relative).is_absolute() + or ".." in Path(relative).parts + or not path.is_file() + or path.is_symlink() + or row.get("kind") != relative + or row.get("byte_length") != path.stat().st_size + or row.get("sha256") != _sha256(path) + ): + raise E46HWorkerPackageError("E46H package artifact changed") + observed.add(relative) + if observed != expected_paths: + raise E46HWorkerPackageError("E46H package coverage changed") + return manifest + + +def _descriptor(relative: str, source: Path | None) -> dict[str, Any]: + payload = _GENERATED_COMPUTE_INIT.encode() if source is None else source.read_bytes() + return { + "path": relative, + "byte_length": len(payload), + "sha256": hashlib.sha256(payload).hexdigest(), + } + + +def _canonical_json(value: object) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode() + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _read_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8-sig")) + if not isinstance(value, dict): + raise E46HWorkerPackageError(f"JSON object expected: {path.name}") + return value + + +def _write_json(path: Path, value: object) -> None: + with path.open("x", encoding="utf-8") as stream: + json.dump(value, stream, ensure_ascii=False, indent=2, allow_nan=False) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repository-root", type=Path, required=True) + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--parser-library", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + package = build_e46h_worker_package( + repository_root=args.repository_root, + profile_path=args.profile, + parser_library_path=args.parser_library, + output_root=args.output_root, + ) + print(package) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/run_e46b_temporal_motion.py b/experiments/perception/run_e46b_temporal_motion.py new file mode 100644 index 0000000..4adc8cf --- /dev/null +++ b/experiments/perception/run_e46b_temporal_motion.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Build the immutable E46B recorded RIGHT temporal-motion result.""" + +from pathlib import Path + +from k1link.compute.e46b_temporal_motion import build_e46b_temporal_motion + + +def main() -> None: + root = Path(__file__).resolve().parents[2] + result = build_e46b_temporal_motion( + e46a_root=( + root + / ".runtime/compute-experiments/e46a/ai-engineering-preannotations" + / ( + "e46a-ai-engineering-preannotation-" + "37cab05e1168cd6004202b890f2c7a877ab02d036afd0891a11a4df61d4d26bf" + ) + ), + e26_root=( + root + / ".runtime/compute-experiments/e10/worker-results" + / ( + "e10-integrated-perception-" + "459aac93918d8f6414b342986ccc6968fefcef6c1f3a78a5254df0b565255ad2" + ) + ), + output_root=root / ".runtime/compute-experiments/e46b/temporal-motion", + ) + print(result["result_id"]) + + +if __name__ == "__main__": + main() diff --git a/experiments/perception/run_e46c_full_replay_world_tracks.py b/experiments/perception/run_e46c_full_replay_world_tracks.py new file mode 100644 index 0000000..370fb17 --- /dev/null +++ b/experiments/perception/run_e46c_full_replay_world_tracks.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Build the immutable E46C full recorded route/world-track result.""" + +from pathlib import Path + +from k1link.compute.e46c_full_replay_world_tracks import ( + build_e46c_full_replay_world_tracks, +) + + +def main() -> None: + root = Path(__file__).resolve().parents[2] + result = build_e46c_full_replay_world_tracks( + e46a_root=( + root + / ".runtime/compute-experiments/e46a/ai-engineering-preannotations" + / ( + "e46a-ai-engineering-preannotation-" + "37cab05e1168cd6004202b890f2c7a877ab02d036afd0891a11a4df61d4d26bf" + ) + ), + e26_root=( + root + / ".runtime/compute-experiments/e10/worker-results" + / ( + "e10-integrated-perception-" + "459aac93918d8f6414b342986ccc6968fefcef6c1f3a78a5254df0b565255ad2" + ) + ), + output_root=( + root / ".runtime/compute-experiments/e46c/full-replay-world-tracks" + ), + ) + print(result["result_id"]) + + +if __name__ == "__main__": + main() diff --git a/experiments/perception/run_e46d_temporal_failure_audit.py b/experiments/perception/run_e46d_temporal_failure_audit.py new file mode 100644 index 0000000..52f60eb --- /dev/null +++ b/experiments/perception/run_e46d_temporal_failure_audit.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Build the immutable E46D full-route temporal-failure audit.""" + +from pathlib import Path + +from k1link.compute.e46d_temporal_failure_audit import ( + build_e46d_temporal_failure_audit, +) + + +def main() -> None: + root = Path(__file__).resolve().parents[2] + result = build_e46d_temporal_failure_audit( + e46c_root=( + root + / ".runtime/compute-experiments/e46c/full-replay-world-tracks" + / ( + "e46c-full-replay-world-tracks-" + "98ca4aeb9839082be64c1ce375ea773cff290e08cf085d8d24bb29e11d6fba8d" + ) + ), + e26_results_root=root / ".runtime/compute-experiments/e10/worker-results", + output_root=root / ".runtime/compute-experiments/e46d/temporal-failure-audits", + ) + print(result["result_id"]) + + +if __name__ == "__main__": + main() diff --git a/experiments/perception/run_e46i_grounding_dino_full_replay.py b/experiments/perception/run_e46i_grounding_dino_full_replay.py new file mode 100644 index 0000000..197ca50 --- /dev/null +++ b/experiments/perception/run_e46i_grounding_dino_full_replay.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Freeze validated E46I Grounding DINO full-route evidence.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from k1link.compute.e46i_grounding_dino_full_replay import ( + build_e46i_grounding_dino_full_replay, +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--raw-root", type=Path, required=True) + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = build_e46i_grounding_dino_full_replay( + raw_root=args.raw_root, + profile_path=args.profile, + output_root=args.output_root, + ) + print(result["result_root"]) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/run_e46j_raw_fisheye_realtime.py b/experiments/perception/run_e46j_raw_fisheye_realtime.py new file mode 100644 index 0000000..a1b624a --- /dev/null +++ b/experiments/perception/run_e46j_raw_fisheye_realtime.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Freeze one validated E46J worker run as an immutable LAB result.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from k1link.compute.e46j_raw_fisheye_realtime import ( + build_e46j_raw_fisheye_realtime, +) + + +def arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--raw-root", type=Path, required=True) + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + return parser.parse_args() + + +def main() -> int: + args = arguments() + frozen = build_e46j_raw_fisheye_realtime( + raw_root=args.raw_root, + profile_path=args.profile, + output_root=args.output_root, + ) + print( + json.dumps( + { + "result_id": frozen["result_id"], + "result_root": str(frozen["result_root"]), + "status": frozen["report"]["status"], + }, + ensure_ascii=False, + indent=2, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/run_l34_right_yolox_truth_island_freeze.py b/experiments/perception/run_l34_right_yolox_truth_island_freeze.py new file mode 100644 index 0000000..9b6171a --- /dev/null +++ b/experiments/perception/run_l34_right_yolox_truth_island_freeze.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Freeze the first recorded right-camera benchmark candidate.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from k1link.compute.l34_right_yolox_truth_island_freeze import ( + build_l34_right_yolox_truth_island_freeze, +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--truth-island-root", type=Path, required=True) + parser.add_argument("--detector-qualification-root", type=Path, required=True) + parser.add_argument("--l33-result-root", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = build_l34_right_yolox_truth_island_freeze( + profile_path=args.profile, + truth_island_root=args.truth_island_root, + detector_qualification_root=args.detector_qualification_root, + l33_result_root=args.l33_result_root, + output_root=args.output_root, + ) + print( + json.dumps( + { + "result_id": result.result_id, + "result_root": str(result.result_root), + "status": result.report["status"], + "metrics": result.report["metrics"], + }, + ensure_ascii=False, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/run_l34a_assisted_yolox_error_audit.py b/experiments/perception/run_l34a_assisted_yolox_error_audit.py new file mode 100644 index 0000000..d555d29 --- /dev/null +++ b/experiments/perception/run_l34a_assisted_yolox_error_audit.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Build the non-truth assisted error audit for the exact L3.4 freeze.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from k1link.compute.l34a_assisted_yolox_error_audit import ( + build_l34a_assisted_yolox_error_audit, +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--l34-freeze-root", type=Path, required=True) + parser.add_argument("--annotation-session", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = build_l34a_assisted_yolox_error_audit( + l34_freeze_root=args.l34_freeze_root, + annotation_session_path=args.annotation_session, + output_root=args.output_root, + ) + report = result["report"] + print( + json.dumps( + { + "result_id": result["result_id"], + "result_root": str(result["result_root"]), + "status": report["status"], + "metrics": report["metrics"], + "ground_truth": report["ground_truth"], + }, + ensure_ascii=False, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/run_l34b_nested_box_consolidation_shadow.py b/experiments/perception/run_l34b_nested_box_consolidation_shadow.py new file mode 100644 index 0000000..793af2a --- /dev/null +++ b/experiments/perception/run_l34b_nested_box_consolidation_shadow.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Build the L3.4B nested-box consolidation shadow.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from k1link.compute.l34b_nested_box_consolidation_shadow import ( + build_l34b_nested_box_consolidation_shadow, +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--l34-freeze-root", type=Path, required=True) + parser.add_argument("--l34a-audit-root", type=Path, required=True) + parser.add_argument("--annotation-session", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = build_l34b_nested_box_consolidation_shadow( + l34_freeze_root=args.l34_freeze_root, + l34a_audit_root=args.l34a_audit_root, + annotation_session_path=args.annotation_session, + output_root=args.output_root, + ) + report = result["report"] + print(json.dumps({ + "result_id": result["result_id"], + "result_root": str(result["result_root"]), + "status": report["status"], + "metrics": report["metrics"], + "ground_truth": report["ground_truth"], + }, ensure_ascii=False, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/run_l34c_tile_seam_stitch_shadow.py b/experiments/perception/run_l34c_tile_seam_stitch_shadow.py new file mode 100644 index 0000000..1f42256 --- /dev/null +++ b/experiments/perception/run_l34c_tile_seam_stitch_shadow.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Build the immutable L3.4C temporal tile-seam stitch shadow.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from k1link.compute.l34c_tile_seam_stitch_shadow import ( + build_l34c_tile_seam_stitch_shadow, +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--l34-freeze-root", type=Path, required=True) + parser.add_argument("--l34a-audit-root", type=Path, required=True) + parser.add_argument("--annotation-session", type=Path, required=True) + parser.add_argument("--detector-qualification-root", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = build_l34c_tile_seam_stitch_shadow( + l34_freeze_root=args.l34_freeze_root, + l34a_audit_root=args.l34a_audit_root, + annotation_session_path=args.annotation_session, + detector_qualification_root=args.detector_qualification_root, + output_root=args.output_root, + ) + report = result["report"] + print( + json.dumps( + { + "result_id": result["result_id"], + "result_root": str(result["result_root"]), + "status": report["status"], + "metrics": report["metrics"], + "ground_truth": report["ground_truth"], + }, + ensure_ascii=False, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/run_l34d_cumulative_postprocessing_candidate.py b/experiments/perception/run_l34d_cumulative_postprocessing_candidate.py new file mode 100644 index 0000000..22e65ca --- /dev/null +++ b/experiments/perception/run_l34d_cumulative_postprocessing_candidate.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Build the immutable L3.4D cumulative post-processing candidate.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from k1link.compute.l34d_cumulative_postprocessing_candidate import ( + build_l34d_cumulative_postprocessing_candidate, +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--l34a-audit-root", type=Path, required=True) + parser.add_argument("--l34b-shadow-root", type=Path, required=True) + parser.add_argument("--l34c-shadow-root", type=Path, required=True) + parser.add_argument("--annotation-session", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = build_l34d_cumulative_postprocessing_candidate( + l34a_audit_root=args.l34a_audit_root, + l34b_shadow_root=args.l34b_shadow_root, + l34c_shadow_root=args.l34c_shadow_root, + annotation_session_path=args.annotation_session, + output_root=args.output_root, + ) + report = result["report"] + print( + json.dumps( + { + "result_id": result["result_id"], + "result_root": str(result["result_root"]), + "status": report["status"], + "metrics": report["metrics"], + "ground_truth": report["ground_truth"], + }, + ensure_ascii=False, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/run_l34e_self_review_diagnostic.py b/experiments/perception/run_l34e_self_review_diagnostic.py new file mode 100644 index 0000000..00d8698 --- /dev/null +++ b/experiments/perception/run_l34e_self_review_diagnostic.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Build the non-truth L3.4E manual self-review diagnostic.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from k1link.compute.l34e_self_review_diagnostic import ( + build_l34e_self_review_diagnostic, +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--l34d-candidate-root", type=Path, required=True) + parser.add_argument("--annotation-session", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = build_l34e_self_review_diagnostic( + l34d_candidate_root=args.l34d_candidate_root, + annotation_session_path=args.annotation_session, + output_root=args.output_root, + ) + report = result["report"] + print( + json.dumps( + { + "result_id": result["result_id"], + "result_root": str(result["result_root"]), + "status": report["status"], + "metrics": report["metrics"], + "decision": report["decision"], + "ground_truth": report["ground_truth"], + }, + ensure_ascii=False, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/run_l35_right_yolox_truth_evaluation.py b/experiments/perception/run_l35_right_yolox_truth_evaluation.py new file mode 100644 index 0000000..7b02da2 --- /dev/null +++ b/experiments/perception/run_l35_right_yolox_truth_evaluation.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Evaluate exact L3.4 predictions after the independent E48 truth seal.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from k1link.compute.l35_right_yolox_truth_evaluation import ( + build_l35_right_yolox_truth_evaluation, +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--truth-island-root", type=Path, required=True) + parser.add_argument("--truth-seal-root", type=Path, required=True) + parser.add_argument("--l34-freeze-root", type=Path, required=True) + parser.add_argument("--valid-fov-root", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = build_l35_right_yolox_truth_evaluation( + truth_island_root=args.truth_island_root, + truth_seal_root=args.truth_seal_root, + l34_freeze_root=args.l34_freeze_root, + valid_fov_root=args.valid_fov_root, + output_root=args.output_root, + ) + print( + json.dumps( + { + "result_id": result["result_id"], + "result_root": str(result["result_root"]), + "status": result["report"]["status"], + "candidates": result["report"]["candidates"], + }, + ensure_ascii=False, + sort_keys=True, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/run_rectified_camera_lidar_fusion.py b/experiments/perception/run_rectified_camera_lidar_fusion.py new file mode 100644 index 0000000..d712f7a --- /dev/null +++ b/experiments/perception/run_rectified_camera_lidar_fusion.py @@ -0,0 +1,509 @@ +#!/usr/bin/env python3 +"""Attach canonical E29 LiDAR geometry to rectified YOLOX ByteTrack objects.""" + +from __future__ import annotations + +import argparse +import hashlib +import importlib +import json +import math +import os +import resource +import shutil +import sys +import time +import types +import uuid +from collections import Counter +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +import numpy as np + + +def _install_compute_import_boundary() -> None: + """Load pure compute modules without importing the application/Rerun graph. + + The product packages still expose legacy eager ``__init__`` imports. A bounded + worker replay must not acquire MQTT, Rerun or web dependencies merely to read + immutable LiDAR arrays and run calibrated geometry. These namespace packages + preserve normal submodule loading while deliberately skipping those facades. + """ + + import k1link + import k1link.device_plugins + + k1link_root = Path(next(iter(k1link.__path__))).resolve(strict=True) + + def namespace(name: str, path: Path) -> types.ModuleType: + module = types.ModuleType(name) + module.__package__ = name + module.__path__ = [str(path)] # type: ignore[attr-defined] + sys.modules[name] = module + parent_name, _, child_name = name.rpartition(".") + if parent_name: + setattr(sys.modules[parent_name], child_name, module) + return module + + namespace("k1link.compute", k1link_root / "compute") + xgrids = namespace( + "k1link.device_plugins.xgrids_k1", + k1link_root / "device_plugins" / "xgrids_k1", + ) + analyze = namespace( + "k1link.device_plugins.xgrids_k1.analyze", + k1link_root / "device_plugins" / "xgrids_k1" / "analyze", + ) + + replay = types.ModuleType("k1link.compute.lidar_replay") + replay.LIDAR_REPLAY_PACK_SCHEMA = "missioncore.lidar-replay-pack/v2" + replay.LidarReplayPackV2 = type("LidarReplayPackV2", (), {}) + sys.modules[replay.__name__] = replay + + projection = importlib.import_module( + "k1link.device_plugins.xgrids_k1.analyze.calibrated_projection" + ) + for export in ( + "CalibratedProjectionError", + "Kb4ProjectionProfile", + "ProjectedPointCloud", + "map_points_to_lidar", + "project_map_points_kb4", + ): + setattr(analyze, export, getattr(projection, export)) + setattr(xgrids, "analyze", analyze) + + +_install_compute_import_boundary() + +from k1link.compute.lidar_field_review import E10LidarFieldSource +from k1link.compute.lidar_local_surface import K1LocalSurfaceV1 +from k1link.compute.semantic_geometry_fusion import ( + DEFAULT_CAMERA_GEOMETRY_FUSION_PROFILE, + evaluate_camera_geometry_frame, + projection_profile_from_source, +) +from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import ( + project_map_points_kb4, +) + + +RESULT_SCHEMA = "missioncore.rectified-camera-lidar-fusion/v1" +FRAME_SCHEMA = "missioncore.rectified-camera-lidar-frame/v1" +AUTHORITY = { + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} +LABEL_GROUP = { + "person": "person", + "bicycle": "two-wheel", + "motorcycle": "two-wheel", + "car": "vehicle", + "bus": "vehicle", + "truck": "vehicle", +} + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--detector-result", type=Path, required=True) + parser.add_argument("--source-pack", type=Path, required=True) + parser.add_argument("--local-surface", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + return parser.parse_args() + + +def _read_object(path: Path) -> dict[str, Any]: + value = json.loads(path.resolve(strict=True).read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise RuntimeError(f"JSON root is not an object: {path}") + return value + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _canonical(value: object) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +def _distribution(values: list[float]) -> dict[str, float]: + ordered = np.asarray(values, dtype=np.float64) + return { + "minimum": round(float(np.min(ordered)), 6), + "mean": round(float(np.mean(ordered)), 6), + "p50": round(float(np.percentile(ordered, 50)), 6), + "p95": round(float(np.percentile(ordered, 95)), 6), + "p99": round(float(np.percentile(ordered, 99)), 6), + "maximum": round(float(np.max(ordered)), 6), + } + + +def _process_peak_rss_mib() -> float: + value = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) + divisor = 1024.0 * 1024.0 if sys.platform == "darwin" else 1024.0 + return value / divisor + + +def _track_objects(frame: dict[str, Any]) -> list[dict[str, Any]]: + tracks = frame.get("tracks") + if not isinstance(tracks, list): + raise RuntimeError("Rectified detector frame has no track list") + objects: list[dict[str, Any]] = [] + for track in tracks: + if not isinstance(track, dict): + raise RuntimeError("Rectified detector track is invalid") + label = track.get("label") + track_id = track.get("track_id") + bbox = track.get("bbox_xyxy") + score = track.get("score") + if ( + label not in LABEL_GROUP + or not isinstance(track_id, int) + or isinstance(track_id, bool) + or not isinstance(bbox, list) + or len(bbox) != 4 + or any( + not isinstance(value, int | float) + or isinstance(value, bool) + or not math.isfinite(float(value)) + for value in bbox + ) + or not isinstance(score, int | float) + or isinstance(score, bool) + or not math.isfinite(float(score)) + ): + raise RuntimeError("Rectified detector track contract changed") + objects.append( + { + "source_track_id": track_id, + "track_id": track_id, + "label": label, + "association_group": LABEL_GROUP[label], + "score": float(score), + "bbox_xyxy": [float(value) for value in bbox], + "cuboid_status": "camera-track-current-no-cuboid", + "camera_motion_state": "bytetrack-current", + "camera_motion_confidence": None, + "motion_state": "camera-track-current", + "motion_status": "metric-velocity-not-yet-published", + } + ) + return objects + + +def main() -> None: + arguments = _arguments() + detector_root = arguments.detector_result.resolve(strict=True) + if detector_root.is_symlink(): + raise RuntimeError("Detector result cannot be a symlink") + qualification_path = detector_root / "qualification.json" + detector_frames_path = detector_root / "frames.jsonl" + qualification = _read_object(qualification_path) + if ( + qualification.get("schema_version") + != "missioncore.rectified-yolox-qualification/v1" + or qualification.get("state") != "accepted" + or qualification.get("source", {}).get("frame_count") != 4489 + or qualification.get("source", {}).get("session_id") + != "20260720T065719Z_viewer_live" + or qualification.get("pipeline", {}).get("id") + != "k1-kb4-core3-yolox-bytetrack/v1" + or qualification.get("acceptance", {}).get("accepted") is not True + ): + raise RuntimeError("Rectified detector qualification is not accepted") + + source = E10LidarFieldSource(arguments.source_pack) + surface = K1LocalSurfaceV1(arguments.local_surface) + try: + if ( + source.frame_count != 4489 + or source.identity.get("session_id") + != qualification["source"]["session_id"] + or source.identity.get("source_id") + != qualification["source"]["source_id"] + or surface.identity.get("source_pack_id") != source.pack_id + or surface.identity.get("frame_count") != source.frame_count + ): + raise RuntimeError("Detector, LiDAR and local surface are not source-aligned") + identity = { + "schema_version": RESULT_SCHEMA, + "source": { + "session_id": source.identity["session_id"], + "source_pack_id": source.pack_id, + "source_artifact_sha256": source.manifest["artifact"]["sha256"], + "local_surface_model_id": surface.model_id, + "local_surface_logical_sha256": surface.identity[ + "logical_content_sha256" + ], + "detector_qualification_sha256": _sha256(qualification_path), + "detector_frames_sha256": _sha256(detector_frames_path), + }, + "pipeline": qualification["pipeline"], + "association": DEFAULT_CAMERA_GEOMETRY_FUSION_PROFILE.to_dict(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical(identity)).hexdigest() + result_id = f"rectified-camera-lidar-fusion-{identity_sha256}" + destination = arguments.output_root.expanduser().absolute() / result_id + if destination.exists(): + print(json.dumps({"result_id": result_id, "result_root": str(destination)})) + return + destination.parent.mkdir(parents=True, mode=0o700, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700) + started = time.perf_counter() + rss_start = _process_peak_rss_mib() + arrays = source.arrays + offsets = arrays["cloud_offsets"] + points_map = arrays["cloud_points_map"] + sample_available = arrays["sample_available"] + source_frame_indices = arrays["source_frame_indices"] + session_seconds = arrays["session_seconds"] + pose_positions = arrays["pose_positions_map"] + pose_orientations = arrays["pose_quaternions_map_from_lidar"] + point_class = surface.arrays["point_class"] + point_height = surface.arrays["point_height_m"] + surface_frame_valid = surface.arrays["frame_valid"] + projection = projection_profile_from_source(source) + latencies: list[float] = [] + status_counts: Counter[str] = Counter() + ranged_by_label: Counter[str] = Counter() + observations_by_label: Counter[str] = Counter() + observation_count = 0 + ranged_count = 0 + range_estimate_count = 0 + unqualified_range_estimates_withheld = 0 + unqualified_metric_range_count = 0 + frames_with_tracks = 0 + frame_count = 0 + frames_path = staging / "camera-lidar-frames.jsonl" + try: + with ( + detector_frames_path.open("r", encoding="utf-8") as detector_stream, + frames_path.open("x", encoding="utf-8", newline="\n") as output, + ): + for line in detector_stream: + frame_started = time.perf_counter() + detector = json.loads(line) + if ( + not isinstance(detector, dict) + or detector.get("schema_version") + != "missioncore.rectified-yolox-frame/v1" + or detector.get("frame_index") != frame_count + ): + raise RuntimeError("Detector frame order or schema changed") + objects = _track_objects(detector) + frames_with_tracks += int(bool(objects)) + start = int(offsets[frame_count]) + end = int(offsets[frame_count + 1]) + points = np.asarray(points_map[start:end], dtype=np.float64) + source_available = bool(sample_available[frame_count]) + surface_valid = bool(surface_frame_valid[frame_count]) + position = np.asarray(pose_positions[frame_count], dtype=np.float64) + if source_available and surface_valid: + orientation = pose_orientations[frame_count] + projected = project_map_points_kb4( + points, + position_map_xyz=tuple(float(value) for value in position), + orientation_map_from_lidar_xyzw=tuple( + float(value) for value in orientation + ), + profile=projection, + ) + else: + projected = None + evaluation = evaluate_camera_geometry_frame( + objects=objects, + projected=projected, + frame_points_map=points, + point_class=point_class[start:end], + point_height_m=point_height[start:end], + sensor_position_map=position, + source_available=source_available, + surface_valid=surface_valid, + profile=DEFAULT_CAMERA_GEOMETRY_FUSION_PROFILE, + include_geometry_only=False, + ) + observations = list(evaluation.semantic_observations) + observation_count += len(observations) + for observation in observations: + label = str(observation["label"]) + observations_by_label[label] += 1 + status_counts[str(observation["geometry_status"])] += 1 + estimate_available = bool( + observation["range_estimate_available"] + ) + support_qualified = bool( + observation["range_support_qualified"] + ) + range_estimate_count += int(estimate_available) + unqualified_range_estimates_withheld += int( + estimate_available and not support_qualified + ) + unqualified_metric_range_count += int( + observation["range_m"] is not None + and not support_qualified + ) + if observation["range_m"] is not None: + ranged_count += 1 + ranged_by_label[label] += 1 + document = { + "schema_version": FRAME_SCHEMA, + "frame_index": frame_count, + "source_frame_index": int(source_frame_indices[frame_count]), + "session_seconds": float(session_seconds[frame_count]), + "source_available": source_available, + "local_surface_valid": surface_valid, + "semantic_observations": observations, + "policy": { + "camera_owns_semantics": True, + "lidar_owns_metric_geometry": True, + "track_id_preserved": True, + "absence_of_points_means_free": False, + "commands_enabled": False, + }, + "authority": AUTHORITY, + } + output.write(_canonical(document).decode() + "\n") + frame_count += 1 + latencies.append((time.perf_counter() - frame_started) * 1000.0) + if frame_count % 500 == 0: + output.flush() + print(f"PHASE=camera-lidar-fusion FRAMES={frame_count}", flush=True) + rss_end = _process_peak_rss_mib() + distribution = _distribution(latencies) + checks = { + "complete_frame_accounting": frame_count == source.frame_count, + "source_alignment_complete": frame_count == source.frame_count, + "canonical_e29_association_reused": True, + "camera_semantic_ownership_preserved": True, + "track_identity_preserved": True, + "absence_of_points_not_free": True, + "unqualified_metric_range_absent": unqualified_metric_range_count == 0, + "maximum_postprocess_p95_ms_50": distribution["p95"] <= 50.0, + "maximum_rss_growth_mib_512": max(0.0, rss_end - rss_start) <= 512.0, + } + report = { + "schema_version": RESULT_SCHEMA, + "result_id": result_id, + "state": "accepted" if all(checks.values()) else "rejected", + "identity_sha256": identity_sha256, + "created_at_utc": datetime.now(UTC) + .isoformat(timespec="milliseconds") + .replace("+00:00", "Z"), + "metrics": { + "frames_processed": frame_count, + "frames_with_confirmed_tracks": frames_with_tracks, + "semantic_observations": observation_count, + "observations_by_label": dict(sorted(observations_by_label.items())), + "with_metric_range": ranged_count, + "range_fraction": ( + ranged_count / observation_count if observation_count else 0.0 + ), + "qualified_range_fraction": ( + ranged_count / observation_count if observation_count else 0.0 + ), + "with_range_estimate": range_estimate_count, + "range_estimate_fraction": ( + range_estimate_count / observation_count + if observation_count else 0.0 + ), + "unqualified_range_estimates_withheld": ( + unqualified_range_estimates_withheld + ), + "ranged_by_label": dict(sorted(ranged_by_label.items())), + "geometry_status": dict(sorted(status_counts.items())), + "postprocess_latency_ms": distribution, + "elapsed_ms": (time.perf_counter() - started) * 1000.0, + "rss_growth_mib": max(0.0, rss_end - rss_start), + }, + "acceptance": {"accepted": all(checks.values()), "checks": checks}, + "decision": { + "detector_tracks_connected_to_lidar_range": True, + "runtime_promoted": False, + "next_gate": "feed observations into existing inline temporal world state", + }, + "limitations": [ + "RAVNOVES00 has no exhaustive independent object ground truth.", + "Metric range is published only with qualified occupied LiDAR support.", + "Camera-only objects remain visible without invented range.", + ], + "authority": AUTHORITY, + } + report_path = staging / "run-report.json" + report_path.write_text( + json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", + encoding="utf-8", + ) + manifest = { + "schema_version": RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "acceptance_state": report["state"], + "ground_truth": False, + "artifacts": [ + { + "path": frames_path.name, + "role": "camera-lidar-frames", + "byte_length": frames_path.stat().st_size, + "sha256": _sha256(frames_path), + }, + { + "path": report_path.name, + "role": "run-report", + "byte_length": report_path.stat().st_size, + "sha256": _sha256(report_path), + }, + ], + "authority": AUTHORITY, + } + (staging / "manifest.json").write_text( + json.dumps(manifest, ensure_ascii=False, sort_keys=True, indent=2) + "\n", + encoding="utf-8", + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + print( + json.dumps( + { + "result_id": result_id, + "result_root": str(destination), + "accepted": report["acceptance"]["accepted"], + "frames_processed": frame_count, + "semantic_observations": observation_count, + "with_metric_range": ranged_count, + "range_fraction": report["metrics"]["range_fraction"], + "with_range_estimate": range_estimate_count, + "unqualified_range_estimates_withheld": ( + unqualified_range_estimates_withheld + ), + "postprocess_p95_ms": distribution["p95"], + }, + sort_keys=True, + ) + ) + finally: + source.close() + surface.close() + + +if __name__ == "__main__": + main() diff --git a/experiments/perception/run_rectified_camera_world_state.py b/experiments/perception/run_rectified_camera_world_state.py new file mode 100644 index 0000000..a1f7390 --- /dev/null +++ b/experiments/perception/run_rectified_camera_world_state.py @@ -0,0 +1,631 @@ +#!/usr/bin/env python3 +"""Compose rectified camera tracks, EoMT semantics and K1 LiDAR world state. + +This is a source-bound replay composition, not a new perception algorithm. It +reuses the accepted E10 fusion/cuboid implementation and E23 bounded temporal +state while replacing only the raw-fisheye detector input with the accepted +three-view KB4 rectified YOLOX/ByteTrack result. +""" + +from __future__ import annotations + +import argparse +import hashlib +import inspect +import json +import math +import os +import resource +import shutil +import sys +import time +import uuid +from collections import Counter +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +import numpy as np + +from e10_fusion_runtime import ( + CuboidCompletionTracker, + LidarReplayPack, + WorldStateProjector, + clearance, + distance_history, + fuse_tracks, + fusion_document, + project_points, +) +from inline_temporal import ( + TemporalStabilizer, + read_inline_profile, + stabilize_world_state, +) + + +RESULT_SCHEMA = "missioncore.rectified-camera-world-state/v1" +FUSION_FRAME_SCHEMA = "missioncore.rectified-camera-temporal-fusion-frame/v1" +PIPELINE_ID = "kb4-core3-yolox-eomt-k1-lidar-e23-temporal/v1" +AUTHORITY = { + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--detector-result", type=Path, required=True) + parser.add_argument("--semantic-source", type=Path, required=True) + parser.add_argument("--lidar-pack", type=Path, required=True) + parser.add_argument("--fusion-profile", type=Path, required=True) + parser.add_argument("--temporal-profile", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + return parser.parse_args() + + +def _read_object(path: Path) -> dict[str, Any]: + value = json.loads(path.resolve(strict=True).read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise RuntimeError(f"JSON root is not an object: {path}") + return value + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _canonical(value: object) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +def _distribution(values: list[float]) -> dict[str, float]: + samples = np.asarray(values, dtype=np.float64) + if samples.size == 0: + return {key: 0.0 for key in ("minimum", "mean", "p50", "p95", "p99", "maximum")} + return { + "minimum": round(float(np.min(samples)), 6), + "mean": round(float(np.mean(samples)), 6), + "p50": round(float(np.percentile(samples, 50)), 6), + "p95": round(float(np.percentile(samples, 95)), 6), + "p99": round(float(np.percentile(samples, 99)), 6), + "maximum": round(float(np.max(samples)), 6), + } + + +def _process_peak_rss_mib() -> float: + value = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss) + divisor = 1024.0 * 1024.0 if sys.platform == "darwin" else 1024.0 + return value / divisor + + +def _artifact(result: dict[str, Any], kind: str, root: Path) -> Path: + artifacts = result.get("artifacts") + if not isinstance(artifacts, list): + raise RuntimeError("Semantic source artifact list is invalid") + matches = [item for item in artifacts if isinstance(item, dict) and item.get("kind") == kind] + if len(matches) != 1: + raise RuntimeError(f"Semantic source has no unique {kind} artifact") + item = matches[0] + path = (root / str(item.get("path"))).resolve(strict=True) + if ( + path.is_symlink() + or path.parent != root + or path.stat().st_size != item.get("byte_length") + or _sha256(path) != item.get("sha256") + ): + raise RuntimeError(f"Semantic source {kind} artifact changed") + return path + + +def _validated_sources(arguments: argparse.Namespace) -> tuple[ + dict[str, Any], + Path, + dict[str, Any], + Path, + dict[str, Any], + str, +]: + detector_root = arguments.detector_result.resolve(strict=True) + semantic_root = arguments.semantic_source.resolve(strict=True) + if detector_root.is_symlink() or semantic_root.is_symlink(): + raise RuntimeError("Perception source root cannot be a symlink") + detector_report_path = detector_root / "qualification.json" + detector_frames = detector_root / "frames.jsonl" + detector = _read_object(detector_report_path) + if ( + detector.get("schema_version") != "missioncore.rectified-yolox-qualification/v1" + or detector.get("state") != "accepted" + or detector.get("acceptance", {}).get("accepted") is not True + or detector.get("source", {}).get("frame_count") != 4489 + or detector.get("pipeline", {}).get("id") != "k1-kb4-core3-yolox-bytetrack/v1" + ): + raise RuntimeError("Rectified detector result is not accepted") + semantic_report_path = semantic_root / "run-report.json" + semantic_result_path = semantic_root / "result.json" + semantic_report = _read_object(semantic_report_path) + semantic_result = _read_object(semantic_result_path) + configuration = semantic_report.get("identity", {}).get("configuration", {}) + profile = configuration.get("profile") + if ( + semantic_report.get("schema_version") + != "missioncore.e10-integrated-perception-report/v1" + or semantic_report.get("state") != "accepted" + or semantic_report.get("acceptance", {}).get("accepted") is not True + or configuration.get("pipeline") + != "source-paced-yolox-eomt-kb4-lidar-world-state/v1" + or not isinstance(profile, dict) + or profile.get("selection", {}).get("required_frame_count") != 4489 + or semantic_report.get("metrics", {}).get("semantic", {}).get("frames_processed") + != 898 + or semantic_result.get("acceptance_state") != "accepted" + ): + raise RuntimeError("Full-session E10 semantic source is not accepted") + semantic_identity = semantic_report.get("identity", {}) + if ( + semantic_identity.get("job_id") != detector.get("source", {}).get("job_id") + or semantic_identity.get("session_id") + != detector.get("source", {}).get("session_id") + or semantic_identity.get("source_id") != detector.get("source", {}).get("source_id") + ): + raise RuntimeError("Detector and semantic source identities disagree") + semantic_arrays = _artifact(semantic_result, "e10-transient-perception", semantic_root) + temporal, temporal_sha256 = read_inline_profile(arguments.temporal_profile) + return detector, detector_frames, semantic_report, semantic_arrays, temporal, temporal_sha256 + + +def _validated_fusion_profile( + path: Path, + *, + semantic_source_profile: dict[str, Any], +) -> tuple[dict[str, Any], str]: + resolved = path.resolve(strict=True) + if resolved.is_symlink() or not resolved.is_file(): + raise RuntimeError("Fusion profile must be a regular immutable file") + profile = _read_object(resolved) + association = profile.get("association") + cuboid_completion = profile.get("cuboid_completion") + if ( + profile.get("schema_version") + != "missioncore.e10-integrated-perception-profile/v1" + or profile.get("profile_id") != "lab-e19-ground-aware-cuboids-v1" + or profile.get("source") != semantic_source_profile.get("source") + or profile.get("selection") != semantic_source_profile.get("selection") + or not isinstance(association, dict) + or association.get("support_duplicate_overlap_threshold") != 0.6 + or association.get("object_support_ground_filter", {}).get("mode") + != "local-ground-relative-object-support-v1" + or not isinstance(cuboid_completion, dict) + or cuboid_completion.get("mode") != "class-prior-amodal-v1" + ): + raise RuntimeError("Fusion profile is not the canonical E19 ground-aware profile") + return profile, _sha256(resolved) + + +def main() -> None: + arguments = _arguments() + ( + detector, + detector_frames_path, + semantic_report, + semantic_arrays_path, + temporal_profile, + temporal_profile_sha256, + ) = _validated_sources(arguments) + semantic_source_profile = semantic_report["identity"]["configuration"]["profile"] + profile, fusion_profile_sha256 = _validated_fusion_profile( + arguments.fusion_profile, + semantic_source_profile=semantic_source_profile, + ) + job_id = str(detector["source"]["job_id"]) + lidar = LidarReplayPack(arguments.lidar_pack, expected_job_id=job_id) + try: + if lidar.frame_count != 4489: + raise RuntimeError("LiDAR replay frame count changed") + identity = { + "schema_version": RESULT_SCHEMA, + "pipeline_id": PIPELINE_ID, + "source": { + "job_id": job_id, + "input_sha256": semantic_report["identity"]["input_sha256"], + "session_id": detector["source"]["session_id"], + "source_id": detector["source"]["source_id"], + "detector_qualification_sha256": _sha256( + arguments.detector_result / "qualification.json" + ), + "detector_frames_sha256": _sha256(detector_frames_path), + "semantic_report_sha256": _sha256(arguments.semantic_source / "run-report.json"), + "semantic_arrays_sha256": _sha256(semantic_arrays_path), + "lidar_pack_id": lidar.pack_id, + }, + "composition": { + "detector": detector["pipeline"], + "semantic": semantic_report["identity"]["models"]["semantic"], + "fusion_profile": profile["association"], + "cuboid_completion_profile": profile["cuboid_completion"], + "fusion_profile_sha256": fusion_profile_sha256, + "temporal_profile_sha256": temporal_profile_sha256, + }, + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "fusion_runtime_sha256": _sha256(Path(__file__).with_name("e10_fusion_runtime.py")), + "inline_temporal_sha256": _sha256( + Path(inspect.getfile(TemporalStabilizer)).resolve(strict=True) + ), + "authority": AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical(identity)).hexdigest() + result_id = f"rectified-camera-world-state-{identity_sha256}" + output_root = arguments.output_root.expanduser().absolute() + destination = output_root / result_id + if destination.exists(): + print(json.dumps({"result_id": result_id, "result_root": str(destination)})) + return + output_root.mkdir(parents=True, exist_ok=True) + staging = output_root / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700) + started = time.perf_counter() + rss_start = _process_peak_rss_mib() + detector_latencies: list[float] = [] + projection_latencies: list[float] = [] + association_latencies: list[float] = [] + temporal_latencies: list[float] = [] + composition_latencies: list[float] = [] + fusion_state_counts: Counter[str] = Counter() + rejection_counts: Counter[str] = Counter() + ranged_by_label: Counter[str] = Counter() + temporal = TemporalStabilizer(temporal_profile) + world_memory: dict[int, dict[str, Any]] = {} + projector = WorldStateProjector(float(profile["world_state"]["velocity_history_limit_s"])) + completion = CuboidCompletionTracker(profile["cuboid_completion"]) + history = distance_history(int(profile["association"]["distance_history_frames"])) + semantic_fresh_frames = 0 + fused_frames = 0 + raw_accepted_cuboids = 0 + temporal_accepted_cuboids = 0 + world_object_observations = 0 + frames_processed = 0 + support_offsets = [0] + support_points: list[np.ndarray] = [] + support_track_ids: list[np.ndarray] = [] + box_offsets = [0] + box_centers: list[list[float]] = [] + box_half_sizes: list[list[float]] = [] + box_quaternions: list[list[float]] = [] + box_track_ids: list[int] = [] + frames_path = staging / "fusion-frames.jsonl" + world_path = staging / "world-state.jsonl" + with ( + np.load(semantic_arrays_path, allow_pickle=False) as semantic_arrays, + detector_frames_path.open("r", encoding="utf-8") as detector_stream, + frames_path.open("x", encoding="utf-8", newline="\n") as fusion_stream, + world_path.open("x", encoding="utf-8", newline="\n") as world_stream, + ): + semantic_indices = semantic_arrays["semantic_frame_indices"] + semantic_masks = semantic_arrays["semantic_masks"] + if ( + semantic_indices.shape != (898,) + or semantic_masks.shape != (898, 600, 800) + or semantic_masks.dtype != np.uint8 + or int(semantic_indices[0]) != 0 + or int(semantic_indices[-1]) != 4485 + or np.any(np.diff(semantic_indices) != 5) + ): + raise RuntimeError("Full-session semantic arrays changed") + semantic_slot = 0 + for expected_frame, line in enumerate(detector_stream): + frame_started = time.perf_counter() + frame = json.loads(line) + if ( + not isinstance(frame, dict) + or frame.get("schema_version") != "missioncore.rectified-yolox-frame/v1" + or frame.get("frame_index") != expected_frame + ): + raise RuntimeError("Rectified detector frame stream changed") + while ( + semantic_slot + 1 < semantic_indices.size + and int(semantic_indices[semantic_slot + 1]) <= expected_frame + ): + semantic_slot += 1 + semantic_frame_index = int(semantic_indices[semantic_slot]) + semantic_age_s = float( + lidar.session_seconds[expected_frame] + - lidar.session_seconds[semantic_frame_index] + ) + semantic_fresh = 0.0 <= semantic_age_s <= ( + float(profile["replay"]["semantic_ttl_ms"]) / 1000.0 + ) + if semantic_fresh: + semantic_fresh_frames += 1 + lidar_frame = lidar.frame(expected_frame) + fusions = () + points_lidar = np.empty((0, 3), dtype=np.float64) + fusion_state = "depth-unavailable" + projection_ms = 0.0 + association_ms = 0.0 + if lidar_frame is not None and semantic_fresh: + points_map, position, quaternion = lidar_frame + stage_started = time.perf_counter() + pixels, depths, source_indices, points_lidar = project_points( + points_map, position, quaternion, lidar.profile + ) + projection_ms = (time.perf_counter() - stage_started) * 1000.0 + stage_started = time.perf_counter() + fusions = fuse_tracks( + tracks=frame["tracks"], + semantic_map=semantic_masks[semantic_slot], + pixels=pixels, + depths=depths, + source_indices=source_indices, + points_map=points_map, + points_lidar=points_lidar, + association=profile["association"], + distance_history=history, + completion_tracker=completion, + sensor_position_map=position, + session_seconds=float(lidar.session_seconds[expected_frame]), + ) + association_ms = (time.perf_counter() - stage_started) * 1000.0 + fusion_state = "fused" + fused_frames += 1 + elif not semantic_fresh: + fusion_state = "semantic-stale" + fusion_state_counts[fusion_state] += 1 + raw_objects = [fusion_document(item) for item in fusions] + raw_accepted = [item for item in fusions if item.cuboid is not None] + raw_accepted_cuboids += len(raw_accepted) + for item in fusions: + rejection_counts[item.status] += 1 + if item.distance_smoothed_m is not None: + ranged_by_label[item.label] += 1 + stage_started = time.perf_counter() + stabilized = temporal.update( + frame_index=expected_frame, + session_seconds=float(lidar.session_seconds[expected_frame]), + objects=raw_objects, + ) + temporal_ms = (time.perf_counter() - stage_started) * 1000.0 + temporal_boxes = [ + item + for item in stabilized + if str(item.get("cuboid_status", "")).startswith("accepted-") + ] + temporal_accepted_cuboids += len(temporal_boxes) + clearance_state = clearance(points_lidar, profile["world_state"]["clearance"]) + world = projector.project( + frame_index=expected_frame, + source_frame_index=int(lidar.source_frame_indices[expected_frame]), + session_seconds=float(lidar.session_seconds[expected_frame]), + fusion_state=fusion_state, + fusions=fusions, + points_lidar=points_lidar, + clearance_state=clearance_state, + delivery={ + "health": "healthy" if fusion_state == "fused" else "degraded", + "semantic_status": "fresh" if semantic_fresh else "stale", + "semantic_source_age_ms": semantic_age_s * 1000.0, + "replay_composition": True, + }, + ) + world = stabilize_world_state(world, stabilized, world_memory) + world_object_observations += int(world["object_count"]) + support_for_frame: list[np.ndarray] = [] + support_ids_for_frame: list[np.ndarray] = [] + if lidar_frame is not None: + points_map = lidar_frame[0] + for item in raw_accepted: + values = points_map[item.source_indices].astype(np.float32) + support_for_frame.append(values) + support_ids_for_frame.append( + np.full(values.shape[0], item.track_id, dtype=np.int32) + ) + if support_for_frame: + points = np.concatenate(support_for_frame) + ids = np.concatenate(support_ids_for_frame) + support_points.append(points) + support_track_ids.append(ids) + support_offsets.append(support_offsets[-1] + int(points.shape[0])) + else: + support_offsets.append(support_offsets[-1]) + for item in temporal_boxes: + box_centers.append([float(value) for value in item["cuboid_center_map"]]) + box_half_sizes.append([float(value) for value in item["cuboid_half_size"]]) + box_quaternions.append( + [float(value) for value in item["cuboid_quaternion_xyzw"]] + ) + box_track_ids.append(int(item["track_id"])) + box_offsets.append(box_offsets[-1] + len(temporal_boxes)) + fusion_stream.write( + json.dumps( + { + "schema_version": FUSION_FRAME_SCHEMA, + "frame_index": expected_frame, + "source_frame_index": int(lidar.source_frame_indices[expected_frame]), + "session_seconds": float(lidar.session_seconds[expected_frame]), + "fusion_state": fusion_state, + "semantic_source_frame_index": semantic_frame_index, + "objects": stabilized, + "authority": AUTHORITY, + }, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ) + + "\n" + ) + world["authority"] = AUTHORITY + world_stream.write( + json.dumps(world, sort_keys=True, separators=(",", ":"), allow_nan=False) + + "\n" + ) + detector_ms = float(frame["processing_ms"]) + detector_latencies.append(detector_ms) + projection_latencies.append(projection_ms) + association_latencies.append(association_ms) + temporal_latencies.append(temporal_ms) + composition_latencies.append( + detector_ms + projection_ms + association_ms + temporal_ms + ) + frames_processed += 1 + if frames_processed % 500 == 0: + print(f"PHASE=rectified-world-state FRAMES={frames_processed}", flush=True) + if frames_processed != 4489: + raise RuntimeError("Rectified detector frame stream is incomplete") + arrays_path = staging / "visual-geometry.npz" + np.savez_compressed( + arrays_path, + frame_times_ns=np.rint(lidar.session_seconds * 1e9).astype(np.int64), + sensor_positions_map=np.asarray(lidar.positions, dtype=np.float64), + support_offsets=np.asarray(support_offsets, dtype=np.int64), + support_points=( + np.concatenate(support_points) + if support_points + else np.empty((0, 3), dtype=np.float32) + ), + support_track_ids=( + np.concatenate(support_track_ids) + if support_track_ids + else np.empty((0,), dtype=np.int32) + ), + box_offsets=np.asarray(box_offsets, dtype=np.int64), + box_centers=np.asarray(box_centers, dtype=np.float32).reshape((-1, 3)), + box_half_sizes=np.asarray(box_half_sizes, dtype=np.float32).reshape((-1, 3)), + box_quaternions=np.asarray(box_quaternions, dtype=np.float32).reshape((-1, 4)), + box_track_ids=np.asarray(box_track_ids, dtype=np.int32), + ) + detector_summary = _distribution(detector_latencies) + projection_summary = _distribution(projection_latencies) + association_summary = _distribution(association_latencies) + temporal_summary = _distribution(temporal_latencies) + composition_summary = _distribution(composition_latencies) + effective_fps = 1000.0 / max(composition_summary["mean"], 1e-9) + semantic_metrics = semantic_report["metrics"]["semantic"] + checks = { + "complete_frame_accounting": frames_processed == 4489, + "rectified_detector_accepted": detector["acceptance"]["accepted"] is True, + "semantic_source_accepted": semantic_report["acceptance"]["accepted"] is True, + "semantic_multirate_fresh_coverage": semantic_fresh_frames / frames_processed >= 0.9, + "minimum_lidar_fused_frames": fused_frames >= 3500, + "minimum_raw_accepted_cuboids": raw_accepted_cuboids >= 1500, + "minimum_temporal_accepted_cuboids": temporal_accepted_cuboids >= 1500, + "minimum_effective_composed_fps": effective_fps >= 9.5, + "maximum_composed_p95_ms": composition_summary["p95"] <= 175.0, + "maximum_temporal_p95_ms": temporal_summary["p95"] <= 5.0, + "track_identity_preserved": True, + "camera_semantic_ownership_preserved": True, + "lidar_metric_geometry_ownership_preserved": True, + } + accepted = all(checks.values()) + report = { + "schema_version": RESULT_SCHEMA, + "result_id": result_id, + "created_at_utc": datetime.now(UTC).isoformat(timespec="milliseconds").replace( + "+00:00", "Z" + ), + "state": "accepted" if accepted else "rejected", + "identity_sha256": identity_sha256, + "identity": identity, + "metrics": { + "frames_processed": frames_processed, + "semantic_frames_reused": int(semantic_metrics["frames_processed"]), + "semantic_effective_fps": float(semantic_metrics["effective_fps"]), + "semantic_fresh_frames": semantic_fresh_frames, + "semantic_fresh_fraction": semantic_fresh_frames / frames_processed, + "lidar_fused_frames": fused_frames, + "fusion_state_counts": dict(sorted(fusion_state_counts.items())), + "raw_accepted_cuboids": raw_accepted_cuboids, + "temporal_accepted_cuboids": temporal_accepted_cuboids, + "world_object_observations": world_object_observations, + "ranged_by_label": dict(sorted(ranged_by_label.items())), + "rejection_counts": dict(sorted(rejection_counts.items())), + "latency_ms": { + "rectified_detector": detector_summary, + "lidar_projection": projection_summary, + "semantic_lidar_association_and_cuboid": association_summary, + "temporal_world_state": temporal_summary, + "composed_compute": composition_summary, + }, + "effective_composed_fps": effective_fps, + "elapsed_ms": (time.perf_counter() - started) * 1000.0, + "rss_growth_mib": _process_peak_rss_mib() - rss_start, + "temporal_state": temporal.snapshot(), + }, + "acceptance": {"accepted": accepted, "checks": checks}, + "decision": { + "camera_first_world_state_composed": accepted, + "runtime_promoted": False, + "next_gate": "wire the same bounded composition into the warm worker", + }, + "limitations": [ + "Recorded source-bound replay is not a live transport proof.", + "EoMT runs at the accepted 2 Hz multi-rate cadence, not once per camera frame.", + "RAVNOVES00 has no exhaustive independent object ground truth.", + "A camera-only object remains visible when LiDAR range is unavailable.", + "Completed cuboids include bounded class-size priors for unobserved volume.", + ], + "authority": AUTHORITY, + } + report_path = staging / "run-report.json" + report_path.write_text( + json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True, allow_nan=False) + + "\n", + encoding="utf-8", + ) + manifest = { + "schema_version": RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": report["created_at_utc"], + "classification": "private-derived-perception-qualification", + "artifacts": [ + { + "kind": kind, + "path": path.name, + "byte_length": path.stat().st_size, + "sha256": _sha256(path), + } + for kind, path in ( + ("temporal-fusion-frames", frames_path), + ("world-state", world_path), + ("visual-geometry", arrays_path), + ("run-report", report_path), + ) + ], + "authority": AUTHORITY, + } + (staging / "manifest.json").write_text( + json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True, allow_nan=False) + + "\n", + encoding="utf-8", + ) + os.replace(staging, destination) + print( + json.dumps( + { + "accepted": accepted, + "effective_composed_fps": effective_fps, + "fused_frames": fused_frames, + "raw_accepted_cuboids": raw_accepted_cuboids, + "temporal_accepted_cuboids": temporal_accepted_cuboids, + "result_id": result_id, + "result_root": str(destination), + }, + sort_keys=True, + ) + ) + finally: + lidar.close() + + +if __name__ == "__main__": + main() diff --git a/experiments/perception/worker/Build-E46ENvidiaTaoParser.ps1 b/experiments/perception/worker/Build-E46ENvidiaTaoParser.ps1 new file mode 100644 index 0000000..62784b4 --- /dev/null +++ b/experiments/perception/worker/Build-E46ENvidiaTaoParser.ps1 @@ -0,0 +1,135 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$SourceRoot, + + [Parameter(Mandatory = $true)] + [string]$OutputRoot, + + [string]$LogPath = '' +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +$sourceCommit = '581889df47d6181110c758c10b872ca833a835e3' +$developmentImage = 'nvcr.io/nvidia/deepstream:9.1-triton-multiarch@sha256:fd31f5b44ababdbdee8cd397a375e888191b49e402ac237254a4cdc239130f5b' +$runtimeImage = 'nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:10eca409b3894e91c1bac915c9f1346307e56695e552487cbe8cf2f58a3f998f' +$libraryName = 'libnvds_infercustomparser_tao.so' +$expectedSources = [ordered]@{ + 'Makefile' = '0265f470354e60c6d719bde68c7b74b1879eed9b4552b8fe7b39416af5ce6835' + 'debug_logger_raii.cpp' = '1d388509e1ff9008de6ccd6451db9c6433273ed78a94e95a1df84585b8dc2915' + 'debug_logger_raii.hpp' = '6efdce1874468848664a18ceb613f2384b8c079cb12baa888a433d6c0f81b7ec' + 'debug_logger_tensor.hpp' = 'c9999fcf92536bbb36498ddd4485f213fc2f5ddc70d24f8d408195a48680b94f' + 'nvdsinfer_custombboxparser_tao.cpp' = '1794e3ee5152f25eff31454c6181368676f6659c68fc25b4b1933f6cbb63158b' +} + +function Get-Sha256([string]$Path) { + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() +} + +function Invoke-Docker([string[]]$Arguments, [string]$Label) { + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & docker @Arguments + $dockerExitCode = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($dockerExitCode -ne 0) { + throw "$Label failed with exit code $dockerExitCode" + } +} + +$source = (Resolve-Path -LiteralPath $SourceRoot).Path +New-Item -ItemType Directory -Force -Path $OutputRoot | Out-Null +$output = (Resolve-Path -LiteralPath $OutputRoot).Path +if ($LogPath) { + $logParent = Split-Path -Parent $LogPath + if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null } + Start-Transcript -LiteralPath $LogPath -Append | Out-Null +} + +try { + $sourceLibrary = Join-Path $source $libraryName + if (Test-Path -LiteralPath $sourceLibrary -PathType Leaf) { + Remove-Item -LiteralPath $sourceLibrary -Force + } + $actualSourceFiles = @(Get-ChildItem -LiteralPath $source -File | ForEach-Object { $_.Name }) + if (@($actualSourceFiles | Where-Object { -not $expectedSources.Contains($_) }).Count -ne 0 -or + @($expectedSources.Keys | Where-Object { $actualSourceFiles -notcontains $_ }).Count -ne 0) { + throw 'NVIDIA TAO parser source inventory changed.' + } + foreach ($entry in $expectedSources.GetEnumerator()) { + $path = Join-Path $source $entry.Key + $actualSha = Get-Sha256 $path + if ($actualSha -ne $entry.Value) { + throw "NVIDIA TAO parser source changed: $($entry.Key)" + } + } + + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & docker image inspect $developmentImage *> $null + $developmentImageCached = $LASTEXITCODE -eq 0 + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if (-not $developmentImageCached) { + Write-Host 'Pulling exact NVIDIA DeepStream 9.1 Triton development image...' + Invoke-Docker @('pull', $developmentImage) 'DeepStream development image pull' + } + + $containerSource = '/opt/nvidia/deepstream/deepstream/sources/apps/sample_apps/deepstream_tao_apps/post_processor' + Invoke-Docker @( + 'run', '--rm', '--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', + '--mount', "type=bind,src=$source,dst=$containerSource", + '--workdir', $containerSource, + '--entrypoint', 'make', $developmentImage, 'CUDA_VER=13.2' + ) 'official NVIDIA TAO parser build' + if (-not (Test-Path -LiteralPath $sourceLibrary -PathType Leaf) -or + (Get-Item -LiteralPath $sourceLibrary).Length -eq 0) { + throw 'NVIDIA TAO parser build did not produce a library.' + } + + $destination = Join-Path $output $libraryName + $temporary = "$destination.$([Guid]::NewGuid().ToString('N')).tmp" + Copy-Item -LiteralPath $sourceLibrary -Destination $temporary + Move-Item -LiteralPath $temporary -Destination $destination -Force + $librarySha = Get-Sha256 $destination + + Invoke-Docker @( + 'run', '--rm', '--gpus', 'all', '--network', 'none', '--read-only', + '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges', + '--mount', "type=bind,src=$output,dst=/workspace/parser,readonly", + '--entrypoint', '/bin/bash', $runtimeImage, '-lc', + "ldd /workspace/parser/$libraryName && nm -D /workspace/parser/$libraryName | grep -q NvDsInferParseCustomDDETRTAO" + ) 'NVIDIA TAO parser runtime verification' + + $manifest = [ordered]@{ + schema_version = 'missioncore.e46e-nvidia-tao-parser/v1' + status = 'completed' + source_repository = 'https://github.com/NVIDIA/DeepStream.git' + source_commit = $sourceCommit + source_files = @($expectedSources.GetEnumerator() | ForEach-Object { + [ordered]@{ path = $_.Key; sha256 = $_.Value } + }) + development_image = $developmentImage + runtime_image = $runtimeImage + cuda_version = '13.2' + symbol = 'NvDsInferParseCustomDDETRTAO' + library_file = $libraryName + library_sha256 = $librarySha + completed_at_utc = (Get-Date).ToUniversalTime().ToString('o') + } + $manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $output 'parser-runtime.json') -Encoding UTF8 + Write-Host "E46E_NVIDIA_TAO_PARSER_COMPLETED sha256=$librarySha output=$output" +} +finally { + if ($LogPath) { Stop-Transcript | Out-Null } +} diff --git a/experiments/perception/worker/Invoke-E46ENvidiaTaoParserAsInteractiveUser.ps1 b/experiments/perception/worker/Invoke-E46ENvidiaTaoParserAsInteractiveUser.ps1 new file mode 100644 index 0000000..49b0d3b --- /dev/null +++ b/experiments/perception/worker/Invoke-E46ENvidiaTaoParserAsInteractiveUser.ps1 @@ -0,0 +1,43 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$BuildScript, + + [Parameter(Mandatory = $true)] + [string]$SourceRoot, + + [Parameter(Mandatory = $true)] + [string]$OutputRoot +) + +$ErrorActionPreference = 'Stop' +$taskName = 'MissionCore-E46ENvidiaTaoParser' +$script = (Resolve-Path -LiteralPath $BuildScript).Path +$source = (Resolve-Path -LiteralPath $SourceRoot).Path +New-Item -ItemType Directory -Force -Path $OutputRoot | Out-Null +$output = (Resolve-Path -LiteralPath $OutputRoot).Path +$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue +if ($existing -and $existing.State -eq 'Running') { + throw "$taskName is already running." +} +$logsRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e\logs' +New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null +$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ') +$logPath = Join-Path $logsRoot "e46e-parser-build-$stamp.log" +$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" +$arguments = @( + '-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', + '-File', "`"$script`"", '-SourceRoot', "`"$source`"", + '-OutputRoot', "`"$output`"", '-LogPath', "`"$logPath`"" +) -join ' ' +$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name +$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $source +$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited +$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30)) +$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries ` + -StartWhenAvailable -ExecutionTimeLimit ([TimeSpan]::FromHours(3)) +Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal ` + -Trigger $trigger -Settings $settings ` + -Description 'Build the pinned official NVIDIA DeepStream TAO RT-DETR parser.' -Force | Out-Null +Start-ScheduledTask -TaskName $taskName +Write-Host "E46E_PARSER_TASK_STARTED task=$taskName log=$logPath" diff --git a/experiments/perception/worker/Invoke-E46EReadyStack.ps1 b/experiments/perception/worker/Invoke-E46EReadyStack.ps1 new file mode 100644 index 0000000..4ced689 --- /dev/null +++ b/experiments/perception/worker/Invoke-E46EReadyStack.ps1 @@ -0,0 +1,319 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$PackageRoot, + + [string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d', + + [string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e', + + [string]$LogPath = '' +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +function Get-Sha256([string]$Path) { + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() +} + +function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label) { + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + throw "$Label is missing: $Path" + } + $actual = Get-Sha256 $Path + if ($actual -ne $Expected) { + throw "$Label SHA-256 changed: expected $Expected, got $actual" + } +} + +function Invoke-Docker([string[]]$Arguments, [string]$Label) { + & docker @Arguments + if ($LASTEXITCODE -ne 0) { + throw "$Label failed with exit code $LASTEXITCODE" + } +} + +$package = (Resolve-Path -LiteralPath $PackageRoot).Path +$sourceJob = (Resolve-Path -LiteralPath $SourceJobRoot).Path +$profilePath = Join-Path $package 'profile.json' +$manifestPath = Join-Path $package 'manifest.json' +if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf) -or + -not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw 'E46E package is incomplete.' +} +$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ($manifest.schema_version -ne 'missioncore.e46e-worker-package/v1' -or + $manifest.package_id -ne (Split-Path -Leaf $package)) { + throw 'E46E package identity is invalid.' +} +$expectedPaths = @($manifest.identity.artifact_paths) +foreach ($artifact in @($manifest.artifacts)) { + if ($expectedPaths -notcontains [string]$artifact.path) { + throw "Unexpected E46E package artifact: $($artifact.path)" + } + $artifactPath = Join-Path $package ([string]$artifact.path) + Assert-Sha256 $artifactPath ([string]$artifact.sha256) "package artifact $($artifact.path)" + if ((Get-Item -LiteralPath $artifactPath).Length -ne [int64]$artifact.byte_length) { + throw "Package artifact length changed: $($artifact.path)" + } +} +$actualPaths = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object { + $_.FullName.Substring($package.Length + 1).Replace('\', '/') +}) +if (@($actualPaths | Where-Object { $_ -ne 'manifest.json' -and $expectedPaths -notcontains $_ }).Count -ne 0 -or + @($expectedPaths | Where-Object { $actualPaths -notcontains $_ }).Count -ne 0) { + throw 'E46E package file set changed.' +} + +$profile = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json +if ($profile.schema_version -ne 'missioncore.e46e-ready-stack-profile/v1') { + throw 'E46E profile is incompatible.' +} +$image = [string]$profile.runtime.container_image +$imageDigestMatch = [regex]::Match($image, '@sha256:([0-9a-f]{64})$') +if (-not $imageDigestMatch.Success) { + throw 'E46E runtime image must be pinned by a full SHA-256 digest.' +} +$imageDigest = $imageDigestMatch.Groups[1].Value +$modelSha = [string]$profile.detector.model_sha256 +$streamSha = [string]$profile.source.stream_sha256 +$modelFile = [string]$profile.detector.model_file +$modelUrl = [string]$profile.detector.model_url +$parserFile = [string]$profile.parser.library_file +$parserSha = [string]$profile.parser.library_sha256 +$parserLibraryPath = Join-Path $package "runtime\$parserFile" + +New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null +$logsRoot = Join-Path $RuntimeRoot 'logs' +$modelsRoot = Join-Path $RuntimeRoot 'models\trafficcamnet_transformer_lite\deployable_resnet50_v2.0' +$inputsRoot = Join-Path $RuntimeRoot 'inputs' +$runsRoot = Join-Path $RuntimeRoot 'runs' +$resultsRoot = Join-Path $RuntimeRoot 'ready-stack-results' +foreach ($path in @($logsRoot, $modelsRoot, $inputsRoot, $runsRoot, $resultsRoot)) { + New-Item -ItemType Directory -Force -Path $path | Out-Null +} +if ($LogPath) { + $logParent = Split-Path -Parent $LogPath + if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null } + Start-Transcript -LiteralPath $LogPath -Append | Out-Null +} + +try { + Write-Host "E46E package: $($manifest.package_id)" + Write-Host "E46E source: $sourceJob" + Write-Host "E46E image: $image" + Assert-Sha256 $parserLibraryPath $parserSha 'official NVIDIA DeepStream TAO parser' + + $modelPath = Join-Path $modelsRoot $modelFile + if (Test-Path -LiteralPath $modelPath -PathType Leaf) { + Assert-Sha256 $modelPath $modelSha 'TrafficCamNet Transformer Lite model' + } + else { + $modelTemp = "$modelPath.$([Guid]::NewGuid().ToString('N')).download" + Write-Host 'Downloading exact NVIDIA TrafficCamNet Transformer Lite model...' + & curl.exe --fail --location --retry 3 --output $modelTemp $modelUrl + if ($LASTEXITCODE -ne 0) { + throw "NVIDIA model download failed with exit code $LASTEXITCODE" + } + Assert-Sha256 $modelTemp $modelSha 'downloaded model' + Move-Item -LiteralPath $modelTemp -Destination $modelPath + } + + $jobPath = Join-Path $sourceJob 'job.json' + $job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json + if ($job.job_id -ne $profile.source.job_id -or + $job.input.archive_index_sha256 -ne $profile.source.archive_index_sha256 -or + $job.input.archive_summary_sha256 -ne $profile.source.archive_summary_sha256) { + throw 'Exact E46E source job binding changed.' + } + $cameraRoot = Join-Path $sourceJob 'input\camera\sensor.camera.right\epoch-1' + $indexPath = Join-Path $cameraRoot 'index.jsonl' + $summaryPath = Join-Path $cameraRoot 'summary.json' + Assert-Sha256 $indexPath ([string]$profile.source.archive_index_sha256) 'source index' + Assert-Sha256 $summaryPath ([string]$profile.source.archive_summary_sha256) 'source summary' + $summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json + if ($summary.stream_sha256 -ne $streamSha -or + [int]$summary.segment_count -ne [int]$profile.source.segment_count) { + throw 'Source stream identity changed.' + } + + $inputPath = Join-Path $inputsRoot "right-$streamSha.mp4" + if (Test-Path -LiteralPath $inputPath -PathType Leaf) { + Assert-Sha256 $inputPath $streamSha 'reconstructed RIGHT stream' + } + else { + $inputTemp = "$inputPath.$([Guid]::NewGuid().ToString('N')).tmp" + $destinationStream = [System.IO.File]::Open( + $inputTemp, + [System.IO.FileMode]::CreateNew, + [System.IO.FileAccess]::Write, + [System.IO.FileShare]::None + ) + $incremental = [System.Security.Cryptography.IncrementalHash]::CreateHash( + [System.Security.Cryptography.HashAlgorithmName]::SHA256 + ) + try { + $sourceParts = [System.Collections.Generic.List[string]]::new() + $sourceParts.Add((Join-Path $cameraRoot 'init.mp4')) + foreach ($line in [System.IO.File]::ReadLines($indexPath)) { + $row = $line | ConvertFrom-Json + $sourceParts.Add((Join-Path $cameraRoot ([string]$row.path))) + } + if ($sourceParts.Count -ne ([int]$profile.source.segment_count + 1)) { + throw 'Source stream part count changed.' + } + $buffer = New-Object byte[] (4MB) + foreach ($part in $sourceParts) { + $inputStream = [System.IO.File]::OpenRead($part) + try { + while (($read = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0) { + $destinationStream.Write($buffer, 0, $read) + $incremental.AppendData($buffer, 0, $read) + } + } + finally { + $inputStream.Dispose() + } + } + $destinationStream.Flush($true) + $actualStreamSha = ([BitConverter]::ToString( + $incremental.GetHashAndReset() + )).Replace('-', '').ToLowerInvariant() + } + finally { + $incremental.Dispose() + $destinationStream.Dispose() + } + if ($actualStreamSha -ne $streamSha) { + throw "Reconstructed stream SHA-256 changed: $actualStreamSha" + } + Move-Item -LiteralPath $inputTemp -Destination $inputPath + } + + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & docker image inspect $image *> $null + $imageCached = $LASTEXITCODE -eq 0 + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($imageCached) { + Write-Host 'Using the exact cached NVIDIA DeepStream image.' + } + else { + Write-Host 'Pulling exact NVIDIA DeepStream image in the interactive user session...' + Invoke-Docker @('pull', $image) 'DeepStream image pull' + } + + $runId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ') + $runRoot = Join-Path $runsRoot $runId + $rawRoot = Join-Path $runRoot 'raw' + $inputMount = Join-Path $runRoot 'input' + New-Item -ItemType Directory -Force -Path $rawRoot | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $rawRoot 'detections') | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $rawRoot 'tracks') | Out-Null + New-Item -ItemType Directory -Force -Path $inputMount | Out-Null + Copy-Item -LiteralPath $inputPath -Destination (Join-Path $inputMount 'right.mp4') + $deepstreamLog = Join-Path $rawRoot 'deepstream.log' + $trackerCopy = Join-Path $rawRoot 'tracker-config.yml' + $startedAt = (Get-Date).ToUniversalTime().ToString('o') + + $containerCommand = @" +set -euo pipefail +cp /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml /workspace/output/tracker-config.yml +deepstream-app -c /workspace/package/runtime/e46e_deepstream_app.txt +"@ + $dockerArguments = @( + 'run', '--rm', '--name', "ndc-mission-core-deepstream-e46e-$runId", + '--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--shm-size', '4g', + '--label', 'com.nodedc.product=mission-core', + '--label', 'com.nodedc.stack=perception', + '--label', 'com.nodedc.role=deepstream-ready-stack-e46e', + '--label', 'com.nodedc.managed-by=mission-core-worker', + '--mount', "type=bind,src=$inputMount,dst=/workspace/input,readonly", + '--mount', "type=bind,src=$package,dst=/workspace/package,readonly", + '--mount', "type=bind,src=$modelsRoot,dst=/workspace/model", + '--mount', "type=bind,src=$rawRoot,dst=/workspace/output", + '--entrypoint', '/bin/bash', $image, '-lc', $containerCommand + ) + Write-Host 'Running full 4489-frame NVIDIA detector + NvDCF replay...' + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & docker @dockerArguments 2>&1 | Tee-Object -LiteralPath $deepstreamLog + $deepstreamExit = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($deepstreamExit -ne 0) { + throw "DeepStream replay failed with exit code $deepstreamExit" + } + + $overlayPath = Join-Path $rawRoot 'overlay.mp4' + $enginePath = Join-Path $modelsRoot "$modelFile`_b1_gpu0_fp16.engine" + if (-not (Test-Path -LiteralPath $overlayPath -PathType Leaf) -or + (Get-Item -LiteralPath $overlayPath).Length -eq 0) { + throw 'DeepStream did not produce an overlay video.' + } + if (-not (Test-Path -LiteralPath $enginePath -PathType Leaf)) { + throw 'DeepStream did not produce the exact TensorRT engine.' + } + $detectionFiles = @(Get-ChildItem -LiteralPath (Join-Path $rawRoot 'detections') -File) + $trackFiles = @(Get-ChildItem -LiteralPath (Join-Path $rawRoot 'tracks') -File) + if ($detectionFiles.Count -ne [int]$profile.source.segment_count -or + $trackFiles.Count -ne [int]$profile.source.segment_count) { + throw "DeepStream frame coverage changed: detections=$($detectionFiles.Count), tracks=$($trackFiles.Count)" + } + $runtime = [ordered]@{ + schema_version = 'missioncore.e46e-deepstream-runtime/v1' + status = 'completed' + worker_host = $env:COMPUTERNAME + gpu_name = ((& nvidia-smi --query-gpu=name --format=csv,noheader | Select-Object -First 1).Trim()) + started_at_utc = $startedAt + completed_at_utc = (Get-Date).ToUniversalTime().ToString('o') + container_image = $image + container_image_digest = $imageDigest + model_sha256 = Get-Sha256 $modelPath + model_engine_sha256 = Get-Sha256 $enginePath + deepstream_config_sha256 = Get-Sha256 (Join-Path $package 'runtime\e46e_deepstream_app.txt') + detector_config_sha256 = Get-Sha256 (Join-Path $package 'runtime\e46e_trafficcamnet_rtdetr.txt') + parser_library_sha256 = Get-Sha256 $parserLibraryPath + tracker_config_sha256 = Get-Sha256 $trackerCopy + input_stream_sha256 = Get-Sha256 $inputPath + overlay_sha256 = Get-Sha256 $overlayPath + frame_count = [int]$profile.source.segment_count + deepstream_exit_code = $deepstreamExit + } + $runtimePath = Join-Path $rawRoot 'runtime.json' + $runtime | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $runtimePath -Encoding UTF8 + + $consolidatorImage = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794' + $consolidatorArguments = @( + 'run', '--rm', '--name', "ndc-mission-core-e46e-consolidator-$runId", + '--network', 'none', '--read-only', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m', + '--mount', "type=bind,src=$package,dst=/workspace/package,readonly", + '--mount', "type=bind,src=$sourceJob,dst=/workspace/source-job,readonly", + '--mount', "type=bind,src=$rawRoot,dst=/workspace/raw,readonly", + '--mount', "type=bind,src=$resultsRoot,dst=/workspace/results", + '-e', 'PYTHONPATH=/workspace/package/runtime', + '-e', 'PYTHONDONTWRITEBYTECODE=1', + $consolidatorImage, + 'python3', '/workspace/package/runtime/run_e46e_ready_stack.py', + '--source-job', '/workspace/source-job', + '--raw-root', '/workspace/raw', + '--profile', '/workspace/package/profile.json', + '--output-root', '/workspace/results' + ) + Write-Host 'Freezing immutable E46E evidence...' + Invoke-Docker $consolidatorArguments 'E46E consolidation' + Write-Host "E46E_READY_STACK_COMPLETED run=$runId results=$resultsRoot" +} +finally { + if ($LogPath) { Stop-Transcript | Out-Null } +} diff --git a/experiments/perception/worker/Invoke-E46EReadyStackAsInteractiveUser.ps1 b/experiments/perception/worker/Invoke-E46EReadyStackAsInteractiveUser.ps1 new file mode 100644 index 0000000..4b4e5bc --- /dev/null +++ b/experiments/perception/worker/Invoke-E46EReadyStackAsInteractiveUser.ps1 @@ -0,0 +1,53 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$PackageRoot, + + [string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d', + + [string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e' +) + +$ErrorActionPreference = 'Stop' +$taskName = 'MissionCore-E46EReadyStack' +$package = (Resolve-Path -LiteralPath $PackageRoot).Path +$script = Join-Path $package 'runtime\Invoke-E46EReadyStack.ps1' +if (-not (Test-Path -LiteralPath $script -PathType Leaf)) { + throw "E46E runner is missing: $script" +} +$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue +if ($existing -and $existing.State -eq 'Running') { + throw "$taskName is already running." +} +$logsRoot = Join-Path $RuntimeRoot 'logs' +New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null +$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ') +$logPath = Join-Path $logsRoot "e46e-ready-stack-$stamp.log" +$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" +$arguments = @( + '-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', + '-File', "`"$script`"", + '-PackageRoot', "`"$package`"", + '-SourceJobRoot', "`"$SourceJobRoot`"", + '-RuntimeRoot', "`"$RuntimeRoot`"", + '-LogPath', "`"$logPath`"" +) -join ' ' +$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name +$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $package +$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited +$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30)) +$settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -ExecutionTimeLimit ([TimeSpan]::FromHours(6)) +Register-ScheduledTask ` + -TaskName $taskName ` + -Action $action ` + -Principal $principal ` + -Trigger $trigger ` + -Settings $settings ` + -Description 'One-shot Mission Core E46E stock NVIDIA RT-DETR plus NvDCF recorded RIGHT replay.' ` + -Force | Out-Null +Start-ScheduledTask -TaskName $taskName +Write-Host "E46E_TASK_STARTED task=$taskName log=$logPath" diff --git a/experiments/perception/worker/Invoke-E46FDashCamBakeoff.ps1 b/experiments/perception/worker/Invoke-E46FDashCamBakeoff.ps1 new file mode 100644 index 0000000..86fa05d --- /dev/null +++ b/experiments/perception/worker/Invoke-E46FDashCamBakeoff.ps1 @@ -0,0 +1,329 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$PackageRoot, + + [string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d', + + [string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46f', + + [string]$LogPath = '' +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +function Get-Sha256([string]$Path) { + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() +} + +function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label) { + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + throw "$Label is missing: $Path" + } + $actual = Get-Sha256 $Path + if ($actual -ne $Expected) { + throw "$Label SHA-256 changed: expected $Expected, got $actual" + } +} + +function Invoke-Docker([string[]]$Arguments, [string]$Label) { + & docker @Arguments + if ($LASTEXITCODE -ne 0) { + throw "$Label failed with exit code $LASTEXITCODE" + } +} + +$package = (Resolve-Path -LiteralPath $PackageRoot).Path +$sourceJob = (Resolve-Path -LiteralPath $SourceJobRoot).Path +$profilePath = Join-Path $package 'profile.json' +$manifestPath = Join-Path $package 'manifest.json' +if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf) -or + -not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw 'E46F package is incomplete.' +} +$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ($manifest.schema_version -ne 'missioncore.e46f-worker-package/v1' -or + $manifest.package_id -ne (Split-Path -Leaf $package)) { + throw 'E46F package identity is invalid.' +} +$expectedPaths = @($manifest.identity.artifact_paths) +foreach ($artifact in @($manifest.artifacts)) { + if ($expectedPaths -notcontains [string]$artifact.path) { + throw "Unexpected E46F package artifact: $($artifact.path)" + } + $artifactPath = Join-Path $package ([string]$artifact.path) + Assert-Sha256 $artifactPath ([string]$artifact.sha256) "package artifact $($artifact.path)" + if ((Get-Item -LiteralPath $artifactPath).Length -ne [int64]$artifact.byte_length) { + throw "Package artifact length changed: $($artifact.path)" + } +} +$actualPaths = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object { + $_.FullName.Substring($package.Length + 1).Replace('\', '/') +}) +if (@($actualPaths | Where-Object { $_ -ne 'manifest.json' -and $expectedPaths -notcontains $_ }).Count -ne 0 -or + @($expectedPaths | Where-Object { $actualPaths -notcontains $_ }).Count -ne 0) { + throw 'E46F package file set changed.' +} + +$profile = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json +if ($profile.schema_version -ne 'missioncore.e46f-dashcam-bakeoff-profile/v1' -or + $profile.comparison_contract.controlled_change -ne 'detector-only') { + throw 'E46F profile is incompatible.' +} +$image = [string]$profile.runtime.container_image +$imageDigestMatch = [regex]::Match($image, '@sha256:([0-9a-f]{64})$') +if (-not $imageDigestMatch.Success) { + throw 'E46F runtime image must be pinned by a full SHA-256 digest.' +} +$imageDigest = $imageDigestMatch.Groups[1].Value +$modelSha = [string]$profile.detector.model_sha256 +$streamSha = [string]$profile.source.stream_sha256 +$modelFile = [string]$profile.detector.model_file +$modelUrl = [string]$profile.detector.model_url + +New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null +$logsRoot = Join-Path $RuntimeRoot 'logs' +$modelsRoot = Join-Path $RuntimeRoot "models\dashcamnet\$([string]$profile.detector.version)" +$inputsRoot = Join-Path $RuntimeRoot 'inputs' +$runsRoot = Join-Path $RuntimeRoot 'runs' +$resultsRoot = Join-Path $RuntimeRoot 'dashcam-bakeoff-results' +foreach ($path in @($logsRoot, $modelsRoot, $inputsRoot, $runsRoot, $resultsRoot)) { + New-Item -ItemType Directory -Force -Path $path | Out-Null +} +if ($LogPath) { + $logParent = Split-Path -Parent $LogPath + if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null } + Start-Transcript -LiteralPath $LogPath -Append | Out-Null +} + +try { + Write-Host "E46F package: $($manifest.package_id)" + Write-Host "E46F source: $sourceJob" + Write-Host "E46F image: $image" + + $modelPath = Join-Path $modelsRoot $modelFile + if (Test-Path -LiteralPath $modelPath -PathType Leaf) { + Assert-Sha256 $modelPath $modelSha 'DashCamNet model' + } + else { + $modelTemp = "$modelPath.$([Guid]::NewGuid().ToString('N')).download" + Write-Host 'Downloading exact NVIDIA DashCamNet model...' + & curl.exe --fail --location --retry 3 --output $modelTemp $modelUrl + if ($LASTEXITCODE -ne 0) { + throw "NVIDIA model download failed with exit code $LASTEXITCODE" + } + Assert-Sha256 $modelTemp $modelSha 'downloaded DashCamNet model' + Move-Item -LiteralPath $modelTemp -Destination $modelPath + } + + $jobPath = Join-Path $sourceJob 'job.json' + $job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json + if ($job.job_id -ne $profile.source.job_id -or + $job.input.archive_index_sha256 -ne $profile.source.archive_index_sha256 -or + $job.input.archive_summary_sha256 -ne $profile.source.archive_summary_sha256) { + throw 'Exact E46F source job binding changed.' + } + $cameraRoot = Join-Path $sourceJob 'input\camera\sensor.camera.right\epoch-1' + $indexPath = Join-Path $cameraRoot 'index.jsonl' + $summaryPath = Join-Path $cameraRoot 'summary.json' + Assert-Sha256 $indexPath ([string]$profile.source.archive_index_sha256) 'source index' + Assert-Sha256 $summaryPath ([string]$profile.source.archive_summary_sha256) 'source summary' + $summary = Get-Content -LiteralPath $summaryPath -Raw | ConvertFrom-Json + if ($summary.stream_sha256 -ne $streamSha -or + [int]$summary.segment_count -ne [int]$profile.source.segment_count) { + throw 'Source stream identity changed.' + } + + $inputPath = Join-Path $inputsRoot "right-$streamSha.mp4" + $e46eInputPath = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-$streamSha.mp4" + if (Test-Path -LiteralPath $inputPath -PathType Leaf) { + Assert-Sha256 $inputPath $streamSha 'E46F reconstructed RIGHT stream' + } + elseif (Test-Path -LiteralPath $e46eInputPath -PathType Leaf) { + Assert-Sha256 $e46eInputPath $streamSha 'E46E controlled RIGHT stream' + Copy-Item -LiteralPath $e46eInputPath -Destination $inputPath + Assert-Sha256 $inputPath $streamSha 'E46F copied RIGHT stream' + } + else { + $inputTemp = "$inputPath.$([Guid]::NewGuid().ToString('N')).tmp" + $destinationStream = [System.IO.File]::Open( + $inputTemp, + [System.IO.FileMode]::CreateNew, + [System.IO.FileAccess]::Write, + [System.IO.FileShare]::None + ) + $incremental = [System.Security.Cryptography.IncrementalHash]::CreateHash( + [System.Security.Cryptography.HashAlgorithmName]::SHA256 + ) + try { + $sourceParts = [System.Collections.Generic.List[string]]::new() + $sourceParts.Add((Join-Path $cameraRoot 'init.mp4')) + foreach ($line in [System.IO.File]::ReadLines($indexPath)) { + $row = $line | ConvertFrom-Json + $sourceParts.Add((Join-Path $cameraRoot ([string]$row.path))) + } + if ($sourceParts.Count -ne ([int]$profile.source.segment_count + 1)) { + throw 'Source stream part count changed.' + } + $buffer = New-Object byte[] (4MB) + foreach ($part in $sourceParts) { + $inputStream = [System.IO.File]::OpenRead($part) + try { + while (($read = $inputStream.Read($buffer, 0, $buffer.Length)) -gt 0) { + $destinationStream.Write($buffer, 0, $read) + $incremental.AppendData($buffer, 0, $read) + } + } + finally { + $inputStream.Dispose() + } + } + $destinationStream.Flush($true) + $actualStreamSha = ([BitConverter]::ToString( + $incremental.GetHashAndReset() + )).Replace('-', '').ToLowerInvariant() + } + finally { + $incremental.Dispose() + $destinationStream.Dispose() + } + if ($actualStreamSha -ne $streamSha) { + throw "Reconstructed stream SHA-256 changed: $actualStreamSha" + } + Move-Item -LiteralPath $inputTemp -Destination $inputPath + } + + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & docker image inspect $image *> $null + $imageCached = $LASTEXITCODE -eq 0 + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($imageCached) { + Write-Host 'Using the exact cached NVIDIA DeepStream image.' + } + else { + Write-Host 'Pulling exact NVIDIA DeepStream image in the interactive user session...' + Invoke-Docker @('pull', $image) 'DeepStream image pull' + } + + $runId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ') + $runRoot = Join-Path $runsRoot $runId + $rawRoot = Join-Path $runRoot 'raw' + $inputMount = Join-Path $runRoot 'input' + New-Item -ItemType Directory -Force -Path $rawRoot | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $rawRoot 'detections') | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $rawRoot 'tracks') | Out-Null + New-Item -ItemType Directory -Force -Path $inputMount | Out-Null + Copy-Item -LiteralPath $inputPath -Destination (Join-Path $inputMount 'right.mp4') + $deepstreamLog = Join-Path $rawRoot 'deepstream.log' + $trackerCopy = Join-Path $rawRoot 'tracker-config.yml' + $startedAt = (Get-Date).ToUniversalTime().ToString('o') + + $containerCommand = @" +set -euo pipefail +cp /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml /workspace/output/tracker-config.yml +deepstream-app -c /workspace/package/runtime/e46f_deepstream_app.txt +"@ + $dockerArguments = @( + 'run', '--rm', '--name', "ndc-mission-core-deepstream-e46f-$runId", + '--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--shm-size', '4g', + '--label', 'com.nodedc.product=mission-core', + '--label', 'com.nodedc.stack=perception', + '--label', 'com.nodedc.role=deepstream-dashcam-bakeoff-e46f', + '--label', 'com.nodedc.managed-by=mission-core-worker', + '--mount', "type=bind,src=$inputMount,dst=/workspace/input,readonly", + '--mount', "type=bind,src=$package,dst=/workspace/package,readonly", + '--mount', "type=bind,src=$modelsRoot,dst=/workspace/model", + '--mount', "type=bind,src=$rawRoot,dst=/workspace/output", + '--entrypoint', '/bin/bash', $image, '-lc', $containerCommand + ) + Write-Host 'Running full 4489-frame NVIDIA DashCamNet + NvDCF bake-off...' + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & docker @dockerArguments 2>&1 | Tee-Object -LiteralPath $deepstreamLog + $deepstreamExit = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($deepstreamExit -ne 0) { + throw "DeepStream replay failed with exit code $deepstreamExit" + } + $invalidOutputBinding = Select-String ` + -LiteralPath $deepstreamLog ` + -Pattern 'Could not find output layer','Given invalid tensor name' ` + -SimpleMatch ` + -Quiet + if ($invalidOutputBinding) { + throw 'DeepStream accepted the process but rejected the configured detector output bindings.' + } + + $overlayPath = Join-Path $rawRoot 'overlay.mp4' + $enginePath = Join-Path $modelsRoot "$modelFile`_b1_gpu0_fp16.engine" + if (-not (Test-Path -LiteralPath $overlayPath -PathType Leaf) -or + (Get-Item -LiteralPath $overlayPath).Length -eq 0) { + throw 'DeepStream did not produce an overlay video.' + } + if (-not (Test-Path -LiteralPath $enginePath -PathType Leaf)) { + throw 'DeepStream did not produce the exact TensorRT engine.' + } + $detectionFiles = @(Get-ChildItem -LiteralPath (Join-Path $rawRoot 'detections') -File) + $trackFiles = @(Get-ChildItem -LiteralPath (Join-Path $rawRoot 'tracks') -File) + if ($detectionFiles.Count -ne [int]$profile.source.segment_count -or + $trackFiles.Count -ne [int]$profile.source.segment_count) { + throw "DeepStream frame coverage changed: detections=$($detectionFiles.Count), tracks=$($trackFiles.Count)" + } + $runtime = [ordered]@{ + schema_version = 'missioncore.e46f-dashcam-deepstream-runtime/v1' + status = 'completed' + worker_host = $env:COMPUTERNAME + gpu_name = ((& nvidia-smi --query-gpu=name --format=csv,noheader | Select-Object -First 1).Trim()) + started_at_utc = $startedAt + completed_at_utc = (Get-Date).ToUniversalTime().ToString('o') + container_image = $image + container_image_digest = $imageDigest + model_sha256 = Get-Sha256 $modelPath + model_engine_sha256 = Get-Sha256 $enginePath + deepstream_config_sha256 = Get-Sha256 (Join-Path $package 'runtime\e46f_deepstream_app.txt') + detector_config_sha256 = Get-Sha256 (Join-Path $package 'runtime\e46f_dashcamnet_detectnet.txt') + tracker_config_sha256 = Get-Sha256 $trackerCopy + input_stream_sha256 = Get-Sha256 $inputPath + overlay_sha256 = Get-Sha256 $overlayPath + frame_count = [int]$profile.source.segment_count + deepstream_exit_code = $deepstreamExit + } + $runtimePath = Join-Path $rawRoot 'runtime.json' + $runtime | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $runtimePath -Encoding UTF8 + + $consolidatorImage = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794' + $consolidatorArguments = @( + 'run', '--rm', '--name', "ndc-mission-core-e46f-consolidator-$runId", + '--network', 'none', '--read-only', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m', + '--mount', "type=bind,src=$package,dst=/workspace/package,readonly", + '--mount', "type=bind,src=$sourceJob,dst=/workspace/source-job,readonly", + '--mount', "type=bind,src=$rawRoot,dst=/workspace/raw,readonly", + '--mount', "type=bind,src=$resultsRoot,dst=/workspace/results", + '-e', 'PYTHONPATH=/workspace/package/runtime', + '-e', 'PYTHONDONTWRITEBYTECODE=1', + $consolidatorImage, + 'python3', '/workspace/package/runtime/run_e46f_dashcam_bakeoff.py', + '--source-job', '/workspace/source-job', + '--raw-root', '/workspace/raw', + '--profile', '/workspace/package/profile.json', + '--output-root', '/workspace/results' + ) + Write-Host 'Freezing immutable E46F evidence...' + Invoke-Docker $consolidatorArguments 'E46F consolidation' + Write-Host "E46F_DASHCAM_BAKEOFF_COMPLETED run=$runId results=$resultsRoot" +} +finally { + if ($LogPath) { Stop-Transcript | Out-Null } +} diff --git a/experiments/perception/worker/Invoke-E46FDashCamBakeoffAsInteractiveUser.ps1 b/experiments/perception/worker/Invoke-E46FDashCamBakeoffAsInteractiveUser.ps1 new file mode 100644 index 0000000..3ec5d2a --- /dev/null +++ b/experiments/perception/worker/Invoke-E46FDashCamBakeoffAsInteractiveUser.ps1 @@ -0,0 +1,53 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$PackageRoot, + + [string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d', + + [string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46f' +) + +$ErrorActionPreference = 'Stop' +$taskName = 'MissionCore-E46FDashCamBakeoff' +$package = (Resolve-Path -LiteralPath $PackageRoot).Path +$script = Join-Path $package 'runtime\Invoke-E46FDashCamBakeoff.ps1' +if (-not (Test-Path -LiteralPath $script -PathType Leaf)) { + throw "E46F runner is missing: $script" +} +$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue +if ($existing -and $existing.State -eq 'Running') { + throw "$taskName is already running." +} +$logsRoot = Join-Path $RuntimeRoot 'logs' +New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null +$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ') +$logPath = Join-Path $logsRoot "e46f-dashcam-bakeoff-$stamp.log" +$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" +$arguments = @( + '-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', + '-File', "`"$script`"", + '-PackageRoot', "`"$package`"", + '-SourceJobRoot', "`"$SourceJobRoot`"", + '-RuntimeRoot', "`"$RuntimeRoot`"", + '-LogPath', "`"$logPath`"" +) -join ' ' +$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name +$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $package +$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited +$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30)) +$settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -ExecutionTimeLimit ([TimeSpan]::FromHours(6)) +Register-ScheduledTask ` + -TaskName $taskName ` + -Action $action ` + -Principal $principal ` + -Trigger $trigger ` + -Settings $settings ` + -Description 'One-shot Mission Core E46F stock NVIDIA DashCamNet plus NvDCF detector-only bake-off.' ` + -Force | Out-Null +Start-ScheduledTask -TaskName $taskName +Write-Host "E46F_TASK_STARTED task=$taskName log=$logPath" diff --git a/experiments/perception/worker/Invoke-E46GRectifiedDetectorBakeoff.ps1 b/experiments/perception/worker/Invoke-E46GRectifiedDetectorBakeoff.ps1 new file mode 100644 index 0000000..eca2101 --- /dev/null +++ b/experiments/perception/worker/Invoke-E46GRectifiedDetectorBakeoff.ps1 @@ -0,0 +1,440 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$PackageRoot, + + [string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d', + + [string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46g', + + [string]$LogPath = '' +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +function Get-Sha256([string]$Path) { + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() +} + +function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label) { + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + throw "$Label is missing: $Path" + } + $actual = Get-Sha256 $Path + if ($actual -ne $Expected) { + throw "$Label SHA-256 changed: expected $Expected, got $actual" + } +} + +function Invoke-Docker([string[]]$Arguments, [string]$Label) { + & docker @Arguments + if ($LASTEXITCODE -ne 0) { + throw "$Label failed with exit code $LASTEXITCODE" + } +} + +function Get-VideoFrameCount([string]$Path) { + $probe = & ffprobe -v error -select_streams v:0 -count_frames ` + -show_entries stream=nb_read_frames -of json $Path | ConvertFrom-Json + if ($LASTEXITCODE -ne 0) { + throw "ffprobe failed: $Path" + } + return [int]@($probe.streams)[0].nb_read_frames +} + +function Ensure-Model( + [pscustomobject]$Candidate, + [string]$ModelRoot, + [string]$Label +) { + New-Item -ItemType Directory -Force -Path $ModelRoot | Out-Null + $modelPath = Join-Path $ModelRoot ([string]$Candidate.model_file) + if (Test-Path -LiteralPath $modelPath -PathType Leaf) { + Assert-Sha256 $modelPath ([string]$Candidate.model_sha256) $Label + return $modelPath + } + $temporary = "$modelPath.$([Guid]::NewGuid().ToString('N')).download" + & curl.exe --fail --location --retry 3 --output $temporary ([string]$Candidate.model_url) + if ($LASTEXITCODE -ne 0) { + throw "$Label download failed with exit code $LASTEXITCODE" + } + Assert-Sha256 $temporary ([string]$Candidate.model_sha256) "downloaded $Label" + Move-Item -LiteralPath $temporary -Destination $modelPath + return $modelPath +} + +$package = (Resolve-Path -LiteralPath $PackageRoot).Path +$sourceJob = (Resolve-Path -LiteralPath $SourceJobRoot).Path +$profilePath = Join-Path $package 'profile.json' +$manifestPath = Join-Path $package 'manifest.json' +if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf) -or + -not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw 'E46G package is incomplete.' +} +$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ($manifest.schema_version -ne 'missioncore.e46g-worker-package/v1' -or + $manifest.package_id -ne (Split-Path -Leaf $package)) { + throw 'E46G package identity is invalid.' +} +$expectedPaths = @($manifest.identity.artifact_paths) +foreach ($artifact in @($manifest.artifacts)) { + if ($expectedPaths -notcontains [string]$artifact.path) { + throw "Unexpected E46G package artifact: $($artifact.path)" + } + $artifactPath = Join-Path $package ([string]$artifact.path) + Assert-Sha256 $artifactPath ([string]$artifact.sha256) "package artifact $($artifact.path)" + if ((Get-Item -LiteralPath $artifactPath).Length -ne [int64]$artifact.byte_length) { + throw "Package artifact length changed: $($artifact.path)" + } +} +$actualPaths = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object { + $_.FullName.Substring($package.Length + 1).Replace('\', '/') +}) +if (@($actualPaths | Where-Object { $_ -ne 'manifest.json' -and $expectedPaths -notcontains $_ }).Count -ne 0 -or + @($expectedPaths | Where-Object { $actualPaths -notcontains $_ }).Count -ne 0) { + throw 'E46G package file set changed.' +} + +$profile = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json +if ($profile.schema_version -ne 'missioncore.e46g-rectified-detector-bakeoff-profile/v1' -or + $profile.source.camera_source_id -ne 'sensor.camera.right' -or + $profile.rectification.provider -ne 'NVIDIA Gst-nvdewarper') { + throw 'E46G profile is incompatible.' +} +$image = [string]$profile.runtime.container_image +$imageDigestMatch = [regex]::Match($image, '@sha256:([0-9a-f]{64})$') +if (-not $imageDigestMatch.Success) { + throw 'E46G runtime image must be pinned by a full SHA-256 digest.' +} +$imageDigest = $imageDigestMatch.Groups[1].Value +$streamSha = [string]$profile.source.stream_sha256 +$sampleFrameCount = [int]$profile.selection.frame_count +$firstSourceFrame = [int]$profile.selection.first_source_frame_index +$lastSourceFrame = [int]$profile.selection.last_source_frame_index +$expectedFullFrameCount = [int]$profile.rectification.expected_full_frame_count +$retainedSourceRange = @($profile.rectification.retained_source_frame_index_range) +if ($retainedSourceRange.Count -ne 2 -or + $firstSourceFrame -lt [int]$retainedSourceRange[0] -or + $lastSourceFrame -gt [int]$retainedSourceRange[1]) { + throw 'E46G selection is outside the admitted NVIDIA-decoded source prefix.' +} +$parserPath = Join-Path $package "runtime\$([string]$profile.trafficcamnet_parser.library_file)" +Assert-Sha256 $parserPath ([string]$profile.trafficcamnet_parser.library_sha256) ` + 'official NVIDIA DeepStream TAO parser' + +if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue) -or + -not (Get-Command ffprobe -ErrorAction SilentlyContinue)) { + throw 'E46G requires the existing Worker ffmpeg/ffprobe installation.' +} + +New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null +$inputsRoot = Join-Path $RuntimeRoot 'inputs' +$runsRoot = Join-Path $RuntimeRoot 'runs' +$resultsRoot = Join-Path $RuntimeRoot 'rectified-detector-bakeoff-results' +foreach ($path in @($inputsRoot, $runsRoot, $resultsRoot)) { + New-Item -ItemType Directory -Force -Path $path | Out-Null +} +if ($LogPath) { + $logParent = Split-Path -Parent $LogPath + if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null } + Start-Transcript -LiteralPath $LogPath -Append | Out-Null +} + +try { + $jobPath = Join-Path $sourceJob 'job.json' + $job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json + if ($job.job_id -ne $profile.source.job_id -or + $job.input.archive_index_sha256 -ne $profile.source.archive_index_sha256 -or + $job.input.archive_summary_sha256 -ne $profile.source.archive_summary_sha256) { + throw 'Exact E46G source job binding changed.' + } + $inputPath = Join-Path $inputsRoot "right-$streamSha.mp4" + $e46eInput = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-$streamSha.mp4" + $e46fInput = "D:\NDC_MISSIONCORE\runtime\experiments\e46f\inputs\right-$streamSha.mp4" + if (Test-Path -LiteralPath $inputPath -PathType Leaf) { + Assert-Sha256 $inputPath $streamSha 'E46G controlled RIGHT stream' + } + elseif (Test-Path -LiteralPath $e46eInput -PathType Leaf) { + Assert-Sha256 $e46eInput $streamSha 'E46E controlled RIGHT stream' + Copy-Item -LiteralPath $e46eInput -Destination $inputPath + } + elseif (Test-Path -LiteralPath $e46fInput -PathType Leaf) { + Assert-Sha256 $e46fInput $streamSha 'E46F controlled RIGHT stream' + Copy-Item -LiteralPath $e46fInput -Destination $inputPath + } + else { + & (Join-Path $package 'runtime\Prepare-RectifiedCameraReplay.ps1') ` + -JobRoot $sourceJob -OutputPath $inputPath + } + Assert-Sha256 $inputPath $streamSha 'E46G reconstructed RIGHT stream' + + $trafficModelRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e\models\trafficcamnet_transformer_lite\deployable_resnet50_v2.0' + $dashModelRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46f\models\dashcamnet\pruned_onnx_v1.0.4' + $trafficModel = Ensure-Model $profile.candidates.trafficcamnet $trafficModelRoot 'TrafficCamNet model' + $dashModel = Ensure-Model $profile.candidates.dashcamnet $dashModelRoot 'DashCamNet model' + + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & docker image inspect $image *> $null + $imageCached = $LASTEXITCODE -eq 0 + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if (-not $imageCached) { + Invoke-Docker @('pull', $image) 'DeepStream image pull' + } + + $runId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ') + $runRoot = Join-Path $runsRoot $runId + $rawRoot = Join-Path $runRoot 'raw' + $sourceInputMount = Join-Path $runRoot 'source-input' + $geometryRoot = Join-Path $rawRoot 'geometry' + $samplesRoot = Join-Path $rawRoot 'samples' + $comparisonRoot = Join-Path $rawRoot 'comparison' + foreach ($path in @( + $rawRoot, + $sourceInputMount, + $geometryRoot, + $samplesRoot, + $comparisonRoot + )) { + New-Item -ItemType Directory -Force -Path $path | Out-Null + } + Copy-Item -LiteralPath $inputPath -Destination (Join-Path $sourceInputMount 'right.mp4') + $workerLog = Join-Path $rawRoot 'worker.log' + "E46G run $runId`nsource=$streamSha`nselection=$firstSourceFrame..$lastSourceFrame" | + Set-Content -LiteralPath $workerLog -Encoding UTF8 + $startedAt = (Get-Date).ToUniversalTime().ToString('o') + + $geometryRuntime = [ordered]@{} + foreach ($view in @('left', 'front', 'right')) { + $viewProfile = $profile.rectification.views.$view + $configName = [string]$viewProfile.config_file + $configPath = Join-Path $package "runtime\$configName" + Assert-Sha256 $configPath ([string]$viewProfile.config_sha256) "$view dewarper config" + $outputPath = Join-Path $geometryRoot "$view.mp4" + $containerCommand = @" +set -euo pipefail +gst-launch-1.0 -e filesrc location=/workspace/input/right.mp4 ! qtdemux ! h264parse ! nvv4l2decoder ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=RGBA' ! nvdewarper config-file=/workspace/package/runtime/$configName source-id=0 num-batch-buffers=1 ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=NV12' ! nvv4l2h264enc bitrate=6000000 ! h264parse ! qtmux ! filesink location=/workspace/output/$view.mp4 +"@ + $dewarperLog = Join-Path $geometryRoot "$view.log" + $dockerArguments = @( + 'run', '--rm', '--name', "ndc-mission-core-e46g-dewarper-$view-$runId", + '--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--shm-size', '4g', + '--label', 'com.nodedc.product=mission-core', + '--label', 'com.nodedc.stack=perception', + '--label', 'com.nodedc.role=nvdewarper-e46g', + '--mount', "type=bind,src=$sourceInputMount,dst=/workspace/input,readonly", + '--mount', "type=bind,src=$package,dst=/workspace/package,readonly", + '--mount', "type=bind,src=$geometryRoot,dst=/workspace/output", + '--entrypoint', '/bin/bash', $image, '-lc', $containerCommand + ) + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & docker @dockerArguments 2>&1 | Tee-Object -LiteralPath $dewarperLog + $dewarperExit = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($dewarperExit -ne 0 -or -not (Test-Path -LiteralPath $outputPath -PathType Leaf)) { + throw "NVIDIA nvdewarper failed for $view with exit code $dewarperExit" + } + if ((Get-VideoFrameCount $outputPath) -ne $expectedFullFrameCount) { + throw "Full rectified $view frame coverage changed." + } + + $samplePath = Join-Path $samplesRoot "$view.mp4" + $filter = "select='between(n\,$firstSourceFrame\,$lastSourceFrame)',setpts=N/(10*TB)" + & ffmpeg -hide_banner -loglevel error -y -i $outputPath -vf $filter -an ` + -c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p -r 10 $samplePath + if ($LASTEXITCODE -ne 0 -or (Get-VideoFrameCount $samplePath) -ne $sampleFrameCount) { + throw "Exact E46G sample extraction failed for $view." + } + $geometryRuntime[$view] = [ordered]@{ + dewarper_config_sha256 = Get-Sha256 $configPath + dewarper_log_sha256 = Get-Sha256 $dewarperLog + full_rectified_video_path = "geometry/$view.mp4" + full_rectified_video_sha256 = Get-Sha256 $outputPath + sample_video_path = "samples/$view.mp4" + sample_video_sha256 = Get-Sha256 $samplePath + full_frame_count = $expectedFullFrameCount + retained_source_frame_index_range = @( + [int]$retainedSourceRange[0], + [int]$retainedSourceRange[1] + ) + excluded_source_tail_frame_count = [int]$profile.rectification.excluded_source_tail_frame_count + sample_frame_count = $sampleFrameCount + } + } + + $candidateRuntime = [ordered]@{} + $candidateDefinitions = @( + [pscustomobject]@{ + Name = 'trafficcamnet' + ModelRoot = $trafficModelRoot + ModelPath = $trafficModel + }, + [pscustomobject]@{ + Name = 'dashcamnet' + ModelRoot = $dashModelRoot + ModelPath = $dashModel + } + ) + foreach ($definition in $candidateDefinitions) { + $candidate = [string]$definition.Name + $candidateProfile = $profile.candidates.$candidate + $appConfigPath = Join-Path $package "runtime\$([string]$candidateProfile.deepstream_app_config)" + $detectorConfigPath = Join-Path $package "runtime\$([string]$candidateProfile.detector_config)" + Assert-Sha256 $appConfigPath ([string]$candidateProfile.deepstream_app_config_sha256) ` + "$candidate DeepStream app config" + Assert-Sha256 $detectorConfigPath ([string]$candidateProfile.detector_config_sha256) ` + "$candidate detector config" + $runs = [ordered]@{} + foreach ($view in @('left', 'front', 'right')) { + $viewRoot = Join-Path $rawRoot "runs\$candidate\$view" + $inputMount = Join-Path $viewRoot 'input' + foreach ($path in @( + $viewRoot, + $inputMount, + (Join-Path $viewRoot 'detections'), + (Join-Path $viewRoot 'tracks') + )) { + New-Item -ItemType Directory -Force -Path $path | Out-Null + } + Copy-Item -LiteralPath (Join-Path $samplesRoot "$view.mp4") ` + -Destination (Join-Path $inputMount 'view.mp4') + $deepstreamLog = Join-Path $viewRoot 'deepstream.log' + $containerCommand = @" +set -euo pipefail +cp /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml /workspace/output/tracker-config.yml +deepstream-app -c /workspace/package/runtime/$([string]$candidateProfile.deepstream_app_config) +"@ + $dockerArguments = @( + 'run', '--rm', '--name', "ndc-mission-core-e46g-$candidate-$view-$runId", + '--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--shm-size', '4g', + '--label', 'com.nodedc.product=mission-core', + '--label', 'com.nodedc.stack=perception', + '--label', "com.nodedc.role=e46g-$candidate-$view", + '--mount', "type=bind,src=$inputMount,dst=/workspace/input,readonly", + '--mount', "type=bind,src=$package,dst=/workspace/package,readonly", + '--mount', "type=bind,src=$($definition.ModelRoot),dst=/workspace/model", + '--mount', "type=bind,src=$viewRoot,dst=/workspace/output", + '--entrypoint', '/bin/bash', $image, '-lc', $containerCommand + ) + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & docker @dockerArguments 2>&1 | Tee-Object -LiteralPath $deepstreamLog + $deepstreamExit = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($deepstreamExit -ne 0) { + throw "DeepStream failed for $candidate/$view with exit code $deepstreamExit" + } + $overlayPath = Join-Path $viewRoot 'overlay.mp4' + $trackerPath = Join-Path $viewRoot 'tracker-config.yml' + $detections = @(Get-ChildItem -LiteralPath (Join-Path $viewRoot 'detections') -File) + $tracks = @(Get-ChildItem -LiteralPath (Join-Path $viewRoot 'tracks') -File) + if (-not (Test-Path -LiteralPath $overlayPath -PathType Leaf) -or + $detections.Count -ne $sampleFrameCount -or $tracks.Count -ne $sampleFrameCount) { + throw "DeepStream output coverage changed for $candidate/$view." + } + $enginePath = "$($definition.ModelPath)_b1_gpu0_fp16.engine" + if (-not (Test-Path -LiteralPath $enginePath -PathType Leaf)) { + throw "TensorRT engine is missing for $candidate." + } + $runs[$view] = [ordered]@{ + overlay_path = "runs/$candidate/$view/overlay.mp4" + overlay_sha256 = Get-Sha256 $overlayPath + deepstream_log_path = "runs/$candidate/$view/deepstream.log" + deepstream_log_sha256 = Get-Sha256 $deepstreamLog + tracker_config_sha256 = Get-Sha256 $trackerPath + model_engine_sha256 = Get-Sha256 $enginePath + frame_count = $sampleFrameCount + deepstream_exit_code = $deepstreamExit + } + } + $candidateRuntime[$candidate] = [ordered]@{ + model_sha256 = Get-Sha256 $definition.ModelPath + deepstream_app_config_sha256 = Get-Sha256 $appConfigPath + detector_config_sha256 = Get-Sha256 $detectorConfigPath + parser_library_sha256 = $(if ($candidate -eq 'trafficcamnet') { + Get-Sha256 $parserPath + } else { $null }) + runs = $runs + } + } + + $comparisonRuntime = [ordered]@{} + foreach ($candidate in @('trafficcamnet', 'dashcamnet')) { + $left = Join-Path $rawRoot "runs\$candidate\left\overlay.mp4" + $front = Join-Path $rawRoot "runs\$candidate\front\overlay.mp4" + $right = Join-Path $rawRoot "runs\$candidate\right\overlay.mp4" + $output = Join-Path $comparisonRoot "$candidate.mp4" + & ffmpeg -hide_banner -loglevel error -y -i $left -i $front -i $right ` + -filter_complex '[0:v][1:v][2:v]hstack=inputs=3[v]' -map '[v]' -an ` + -c:v libx264 -preset fast -crf 20 -pix_fmt yuv420p -movflags +faststart $output + if ($LASTEXITCODE -ne 0 -or (Get-VideoFrameCount $output) -ne $sampleFrameCount) { + throw "E46G synchronized comparison video failed for $candidate." + } + $comparisonRuntime[$candidate] = [ordered]@{ + video_path = "comparison/$candidate.mp4" + video_sha256 = Get-Sha256 $output + frame_count = $sampleFrameCount + view_order = @('left', 'front', 'right') + } + } + + $runtime = [ordered]@{ + schema_version = 'missioncore.e46g-rectified-detector-runtime/v1' + status = 'completed' + worker_host = $env:COMPUTERNAME + gpu_name = ((& nvidia-smi --query-gpu=name --format=csv,noheader | Select-Object -First 1).Trim()) + started_at_utc = $startedAt + completed_at_utc = (Get-Date).ToUniversalTime().ToString('o') + container_image = $image + container_image_digest = $imageDigest + source_stream_sha256 = Get-Sha256 $inputPath + first_source_frame_index = $firstSourceFrame + sample_frame_count = $sampleFrameCount + geometry = $geometryRuntime + candidates = $candidateRuntime + comparison = $comparisonRuntime + } + $runtime | ConvertTo-Json -Depth 16 | + Set-Content -LiteralPath (Join-Path $rawRoot 'runtime.json') -Encoding UTF8 + Add-Content -LiteralPath $workerLog -Value "completed=$(Get-Date -Format o)" + + $consolidatorImage = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794' + $consolidatorArguments = @( + 'run', '--rm', '--name', "ndc-mission-core-e46g-consolidator-$runId", + '--network', 'none', '--read-only', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m', + '--mount', "type=bind,src=$package,dst=/workspace/package,readonly", + '--mount', "type=bind,src=$sourceJob,dst=/workspace/source-job,readonly", + '--mount', "type=bind,src=$rawRoot,dst=/workspace/raw,readonly", + '--mount', "type=bind,src=$resultsRoot,dst=/workspace/results", + '-e', 'PYTHONPATH=/workspace/package/runtime', + '-e', 'PYTHONDONTWRITEBYTECODE=1', + $consolidatorImage, + 'python3', '/workspace/package/runtime/run_e46g_rectified_detector_bakeoff.py', + '--source-job', '/workspace/source-job', + '--raw-root', '/workspace/raw', + '--profile', '/workspace/package/profile.json', + '--output-root', '/workspace/results' + ) + Invoke-Docker $consolidatorArguments 'E46G consolidation' + Write-Host "E46G_RECTIFIED_DETECTOR_BAKEOFF_COMPLETED run=$runId results=$resultsRoot" +} +finally { + if ($LogPath) { Stop-Transcript | Out-Null } +} diff --git a/experiments/perception/worker/Invoke-E46GRectifiedDetectorBakeoffAsInteractiveUser.ps1 b/experiments/perception/worker/Invoke-E46GRectifiedDetectorBakeoffAsInteractiveUser.ps1 new file mode 100644 index 0000000..223c41f --- /dev/null +++ b/experiments/perception/worker/Invoke-E46GRectifiedDetectorBakeoffAsInteractiveUser.ps1 @@ -0,0 +1,53 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$PackageRoot, + + [string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d', + + [string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46g' +) + +$ErrorActionPreference = 'Stop' +$taskName = 'MissionCore-E46GRectifiedDetectorBakeoff' +$package = (Resolve-Path -LiteralPath $PackageRoot).Path +$script = Join-Path $package 'runtime\Invoke-E46GRectifiedDetectorBakeoff.ps1' +if (-not (Test-Path -LiteralPath $script -PathType Leaf)) { + throw "E46G runner is missing: $script" +} +$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue +if ($existing -and $existing.State -eq 'Running') { + throw "$taskName is already running." +} +$logsRoot = Join-Path $RuntimeRoot 'logs' +New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null +$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ') +$logPath = Join-Path $logsRoot "e46g-rectified-detector-bakeoff-$stamp.log" +$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" +$arguments = @( + '-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', + '-File', "`"$script`"", + '-PackageRoot', "`"$package`"", + '-SourceJobRoot', "`"$SourceJobRoot`"", + '-RuntimeRoot', "`"$RuntimeRoot`"", + '-LogPath', "`"$logPath`"" +) -join ' ' +$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name +$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $package +$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited +$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30)) +$settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -ExecutionTimeLimit ([TimeSpan]::FromHours(6)) +Register-ScheduledTask ` + -TaskName $taskName ` + -Action $action ` + -Principal $principal ` + -Trigger $trigger ` + -Settings $settings ` + -Description 'One-shot E46G factory-KB4 NVIDIA nvdewarper detector A/B.' ` + -Force | Out-Null +Start-ScheduledTask -TaskName $taskName +Write-Host "E46G_TASK_STARTED task=$taskName log=$logPath" diff --git a/experiments/perception/worker/Invoke-E46HFullRectifiedFrontReplay.ps1 b/experiments/perception/worker/Invoke-E46HFullRectifiedFrontReplay.ps1 new file mode 100644 index 0000000..c9b1cc1 --- /dev/null +++ b/experiments/perception/worker/Invoke-E46HFullRectifiedFrontReplay.ps1 @@ -0,0 +1,369 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$PackageRoot, + + [string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d', + + [string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46h', + + [string]$LogPath = '' +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +function Get-Sha256([string]$Path) { + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() +} + +function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label) { + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { + throw "$Label is missing: $Path" + } + $actual = Get-Sha256 $Path + if ($actual -ne $Expected) { + throw "$Label SHA-256 changed: expected $Expected, got $actual" + } +} + +function Invoke-Docker([string[]]$Arguments, [string]$Label) { + & docker @Arguments + if ($LASTEXITCODE -ne 0) { + throw "$Label failed with exit code $LASTEXITCODE" + } +} + +function Get-VideoFrameCount([string]$Path) { + $probe = & ffprobe -v error -select_streams v:0 -count_frames ` + -show_entries stream=nb_read_frames -of json $Path | ConvertFrom-Json + if ($LASTEXITCODE -ne 0) { + throw "ffprobe failed: $Path" + } + return [int]@($probe.streams)[0].nb_read_frames +} + +function Ensure-Model([pscustomobject]$Detector, [string]$ModelRoot) { + New-Item -ItemType Directory -Force -Path $ModelRoot | Out-Null + $modelPath = Join-Path $ModelRoot ([string]$Detector.model_file) + if (Test-Path -LiteralPath $modelPath -PathType Leaf) { + Assert-Sha256 $modelPath ([string]$Detector.model_sha256) 'TrafficCamNet model' + return $modelPath + } + $temporary = "$modelPath.$([Guid]::NewGuid().ToString('N')).download" + & curl.exe --fail --location --retry 3 --output $temporary ([string]$Detector.model_url) + if ($LASTEXITCODE -ne 0) { + throw "TrafficCamNet download failed with exit code $LASTEXITCODE" + } + Assert-Sha256 $temporary ([string]$Detector.model_sha256) 'downloaded TrafficCamNet model' + Move-Item -LiteralPath $temporary -Destination $modelPath + return $modelPath +} + +$package = (Resolve-Path -LiteralPath $PackageRoot).Path +$sourceJob = (Resolve-Path -LiteralPath $SourceJobRoot).Path +$profilePath = Join-Path $package 'profile.json' +$manifestPath = Join-Path $package 'manifest.json' +if (-not (Test-Path -LiteralPath $profilePath -PathType Leaf) -or + -not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { + throw 'E46H package is incomplete.' +} +$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json +if ($manifest.schema_version -ne 'missioncore.e46h-worker-package/v1' -or + $manifest.package_id -ne (Split-Path -Leaf $package)) { + throw 'E46H package identity is invalid.' +} +$expectedPaths = @($manifest.identity.artifact_paths) +foreach ($artifact in @($manifest.artifacts)) { + if ($expectedPaths -notcontains [string]$artifact.path) { + throw "Unexpected E46H package artifact: $($artifact.path)" + } + $artifactPath = Join-Path $package ([string]$artifact.path) + Assert-Sha256 $artifactPath ([string]$artifact.sha256) "package artifact $($artifact.path)" + if ((Get-Item -LiteralPath $artifactPath).Length -ne [int64]$artifact.byte_length) { + throw "Package artifact length changed: $($artifact.path)" + } +} +$actualPaths = @(Get-ChildItem -LiteralPath $package -Recurse -File | ForEach-Object { + $_.FullName.Substring($package.Length + 1).Replace('\', '/') +}) +if (@($actualPaths | Where-Object { $_ -ne 'manifest.json' -and $expectedPaths -notcontains $_ }).Count -ne 0 -or + @($expectedPaths | Where-Object { $actualPaths -notcontains $_ }).Count -ne 0) { + throw 'E46H package file set changed.' +} + +$profile = Get-Content -LiteralPath $profilePath -Raw | ConvertFrom-Json +if ($profile.schema_version -ne 'missioncore.e46h-full-rectified-front-replay-profile/v1' -or + $profile.source.camera_source_id -ne 'sensor.camera.right' -or + $profile.rectification.view -ne 'front' -or + $profile.detector.name -ne 'NVIDIA TrafficCamNet Transformer Lite') { + throw 'E46H profile is incompatible.' +} +$image = [string]$profile.runtime.container_image +$imageDigestMatch = [regex]::Match($image, '@sha256:([0-9a-f]{64})$') +if (-not $imageDigestMatch.Success) { + throw 'E46H runtime image must be pinned by a full SHA-256 digest.' +} +$imageDigest = $imageDigestMatch.Groups[1].Value +$streamSha = [string]$profile.source.stream_sha256 +$frameCount = [int]$profile.selection.frame_count +if ($frameCount -ne 4488 -or + [int]$profile.selection.first_source_frame_index -ne 0 -or + [int]$profile.selection.last_source_frame_index -ne 4487) { + throw 'E46H retained route contract changed.' +} +$parserPath = Join-Path $package "runtime\$([string]$profile.parser.library_file)" +Assert-Sha256 $parserPath ([string]$profile.parser.library_sha256) ` + 'official NVIDIA DeepStream TAO parser' + +if (-not (Get-Command ffmpeg -ErrorAction SilentlyContinue) -or + -not (Get-Command ffprobe -ErrorAction SilentlyContinue)) { + throw 'E46H requires the existing Worker ffmpeg/ffprobe installation.' +} + +New-Item -ItemType Directory -Force -Path $RuntimeRoot | Out-Null +$inputsRoot = Join-Path $RuntimeRoot 'inputs' +$runsRoot = Join-Path $RuntimeRoot 'runs' +$resultsRoot = Join-Path $RuntimeRoot 'full-rectified-front-results' +foreach ($path in @($inputsRoot, $runsRoot, $resultsRoot)) { + New-Item -ItemType Directory -Force -Path $path | Out-Null +} +if ($LogPath) { + $logParent = Split-Path -Parent $LogPath + if ($logParent) { New-Item -ItemType Directory -Force -Path $logParent | Out-Null } + Start-Transcript -LiteralPath $LogPath -Append | Out-Null +} + +try { + $jobPath = Join-Path $sourceJob 'job.json' + $job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json + if ($job.job_id -ne $profile.source.job_id -or + $job.input.archive_index_sha256 -ne $profile.source.archive_index_sha256 -or + $job.input.archive_summary_sha256 -ne $profile.source.archive_summary_sha256) { + throw 'Exact E46H source job binding changed.' + } + $inputPath = Join-Path $inputsRoot "right-$streamSha.mp4" + $e46gInput = "D:\NDC_MISSIONCORE\runtime\experiments\e46g\inputs\right-$streamSha.mp4" + $e46eInput = "D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-$streamSha.mp4" + if (Test-Path -LiteralPath $inputPath -PathType Leaf) { + Assert-Sha256 $inputPath $streamSha 'E46H controlled RIGHT stream' + } + elseif (Test-Path -LiteralPath $e46gInput -PathType Leaf) { + Assert-Sha256 $e46gInput $streamSha 'E46G controlled RIGHT stream' + Copy-Item -LiteralPath $e46gInput -Destination $inputPath + } + elseif (Test-Path -LiteralPath $e46eInput -PathType Leaf) { + Assert-Sha256 $e46eInput $streamSha 'E46E controlled RIGHT stream' + Copy-Item -LiteralPath $e46eInput -Destination $inputPath + } + else { + & (Join-Path $package 'runtime\Prepare-RectifiedCameraReplay.ps1') ` + -JobRoot $sourceJob -OutputPath $inputPath + } + Assert-Sha256 $inputPath $streamSha 'E46H reconstructed RIGHT stream' + + $modelRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e\models\trafficcamnet_transformer_lite\deployable_resnet50_v2.0' + $modelPath = Ensure-Model $profile.detector $modelRoot + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & docker image inspect $image *> $null + $imageCached = $LASTEXITCODE -eq 0 + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if (-not $imageCached) { + Invoke-Docker @('pull', $image) 'DeepStream image pull' + } + + $runId = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ') + $runRoot = Join-Path $runsRoot $runId + $rawRoot = Join-Path $runRoot 'raw' + $sourceInputMount = Join-Path $runRoot 'source-input' + $geometryRoot = Join-Path $rawRoot 'geometry' + $runOutput = Join-Path $rawRoot 'run' + $frontInputMount = Join-Path $runRoot 'front-input' + foreach ($path in @( + $rawRoot, + $sourceInputMount, + $geometryRoot, + $runOutput, + $frontInputMount, + (Join-Path $runOutput 'detections'), + (Join-Path $runOutput 'tracks') + )) { + New-Item -ItemType Directory -Force -Path $path | Out-Null + } + Copy-Item -LiteralPath $inputPath -Destination (Join-Path $sourceInputMount 'right.mp4') + $workerLog = Join-Path $rawRoot 'worker.log' + "E46H run $runId`nsource=$streamSha`nselection=0..4487`nview=front" | + Set-Content -LiteralPath $workerLog -Encoding UTF8 + $startedAt = (Get-Date).ToUniversalTime().ToString('o') + + $dewarperConfig = Join-Path $package "runtime\$([string]$profile.rectification.config_file)" + Assert-Sha256 $dewarperConfig ([string]$profile.rectification.config_sha256) ` + 'FRONT dewarper config' + $frontPath = Join-Path $geometryRoot 'front.mp4' + $dewarperLog = Join-Path $geometryRoot 'front.log' + $dewarperCommand = @" +set -euo pipefail +gst-launch-1.0 -e filesrc location=/workspace/input/right.mp4 ! qtdemux ! h264parse ! nvv4l2decoder ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=RGBA' ! nvdewarper config-file=/workspace/package/runtime/$([string]$profile.rectification.config_file) source-id=0 num-batch-buffers=1 ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=NV12' ! nvv4l2h264enc bitrate=6000000 ! h264parse ! qtmux ! filesink location=/workspace/output/front.mp4 +"@ + $dewarperArguments = @( + 'run', '--rm', '--name', "ndc-mission-core-e46h-dewarper-$runId", + '--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--shm-size', '4g', + '--label', 'com.nodedc.product=mission-core', + '--label', 'com.nodedc.stack=perception', + '--label', 'com.nodedc.role=nvdewarper-e46h-front', + '--mount', "type=bind,src=$sourceInputMount,dst=/workspace/input,readonly", + '--mount', "type=bind,src=$package,dst=/workspace/package,readonly", + '--mount', "type=bind,src=$geometryRoot,dst=/workspace/output", + '--entrypoint', '/bin/bash', $image, '-lc', $dewarperCommand + ) + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & docker @dewarperArguments 2>&1 | Tee-Object -LiteralPath $dewarperLog + $dewarperExit = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($dewarperExit -ne 0 -or -not (Test-Path -LiteralPath $frontPath -PathType Leaf)) { + throw "NVIDIA nvdewarper failed with exit code $dewarperExit" + } + if ((Get-VideoFrameCount $frontPath) -ne $frameCount) { + throw 'E46H FRONT frame coverage changed.' + } + $normalizedFront = Join-Path $geometryRoot 'front-normalized.mp4' + & ffmpeg -hide_banner -loglevel error -y -fflags +genpts -i $frontPath ` + -vf 'setpts=N/(10*TB)' -an -c:v libx264 -preset fast -crf 18 ` + -pix_fmt yuv420p -r 10 -movflags +faststart $normalizedFront + if ($LASTEXITCODE -ne 0 -or (Get-VideoFrameCount $normalizedFront) -ne $frameCount) { + throw 'E46H FRONT timestamp normalization failed.' + } + Move-Item -LiteralPath $normalizedFront -Destination $frontPath -Force + Copy-Item -LiteralPath $frontPath -Destination (Join-Path $frontInputMount 'view.mp4') + + $appConfig = Join-Path $package "runtime\$([string]$profile.detector.deepstream_app_config)" + $detectorConfig = Join-Path $package "runtime\$([string]$profile.detector.detector_config)" + Assert-Sha256 $appConfig ([string]$profile.detector.deepstream_app_config_sha256) ` + 'TrafficCamNet DeepStream app config' + Assert-Sha256 $detectorConfig ([string]$profile.detector.detector_config_sha256) ` + 'TrafficCamNet detector config' + $deepstreamLog = Join-Path $runOutput 'deepstream.log' + $deepstreamCommand = @" +set -euo pipefail +cp /opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml /workspace/output/tracker-config.yml +deepstream-app -c /workspace/package/runtime/$([string]$profile.detector.deepstream_app_config) +"@ + $deepstreamArguments = @( + 'run', '--rm', '--name', "ndc-mission-core-e46h-front-$runId", + '--gpus', 'all', '--network', 'none', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--shm-size', '4g', + '--label', 'com.nodedc.product=mission-core', + '--label', 'com.nodedc.stack=perception', + '--label', 'com.nodedc.role=e46h-front-trafficcamnet', + '--mount', "type=bind,src=$frontInputMount,dst=/workspace/input,readonly", + '--mount', "type=bind,src=$package,dst=/workspace/package,readonly", + '--mount', "type=bind,src=$modelRoot,dst=/workspace/model", + '--mount', "type=bind,src=$runOutput,dst=/workspace/output", + '--entrypoint', '/bin/bash', $image, '-lc', $deepstreamCommand + ) + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + & docker @deepstreamArguments 2>&1 | Tee-Object -LiteralPath $deepstreamLog + $deepstreamExit = $LASTEXITCODE + } + finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($deepstreamExit -ne 0) { + throw "DeepStream failed with exit code $deepstreamExit" + } + $overlayPath = Join-Path $runOutput 'overlay.mp4' + $trackerPath = Join-Path $runOutput 'tracker-config.yml' + $detections = @(Get-ChildItem -LiteralPath (Join-Path $runOutput 'detections') -File) + $tracks = @(Get-ChildItem -LiteralPath (Join-Path $runOutput 'tracks') -File) + if (-not (Test-Path -LiteralPath $overlayPath -PathType Leaf) -or + $detections.Count -ne $frameCount -or $tracks.Count -ne $frameCount) { + throw "DeepStream output coverage changed: detections=$($detections.Count), tracks=$($tracks.Count)" + } + $fastOverlay = Join-Path $runOutput 'overlay-faststart.mp4' + & ffmpeg -hide_banner -loglevel error -y -i $overlayPath -c copy -movflags +faststart $fastOverlay + if ($LASTEXITCODE -ne 0 -or (Get-VideoFrameCount $fastOverlay) -ne $frameCount) { + throw 'E46H fast-start overlay normalization failed.' + } + Move-Item -LiteralPath $fastOverlay -Destination $overlayPath -Force + $enginePath = "$modelPath`_b1_gpu0_fp16.engine" + if (-not (Test-Path -LiteralPath $enginePath -PathType Leaf)) { + throw 'TrafficCamNet TensorRT engine is missing.' + } + + $runtime = [ordered]@{ + schema_version = 'missioncore.e46h-full-rectified-front-runtime/v1' + status = 'completed' + worker_host = $env:COMPUTERNAME + gpu_name = ((& nvidia-smi --query-gpu=name --format=csv,noheader | Select-Object -First 1).Trim()) + started_at_utc = $startedAt + completed_at_utc = (Get-Date).ToUniversalTime().ToString('o') + container_image = $image + container_image_digest = $imageDigest + source_stream_sha256 = Get-Sha256 $inputPath + frame_count = $frameCount + retained_source_frame_index_range = @(0, 4487) + geometry = [ordered]@{ + video_path = 'geometry/front.mp4' + video_sha256 = Get-Sha256 $frontPath + log_path = 'geometry/front.log' + log_sha256 = Get-Sha256 $dewarperLog + config_sha256 = Get-Sha256 $dewarperConfig + frame_count = $frameCount + dewarper_exit_code = $dewarperExit + } + run = [ordered]@{ + overlay_path = 'run/overlay.mp4' + overlay_sha256 = Get-Sha256 $overlayPath + deepstream_log_path = 'run/deepstream.log' + deepstream_log_sha256 = Get-Sha256 $deepstreamLog + model_sha256 = Get-Sha256 $modelPath + model_engine_sha256 = Get-Sha256 $enginePath + parser_library_sha256 = Get-Sha256 $parserPath + deepstream_app_config_sha256 = Get-Sha256 $appConfig + detector_config_sha256 = Get-Sha256 $detectorConfig + tracker_config_sha256 = Get-Sha256 $trackerPath + frame_count = $frameCount + deepstream_exit_code = $deepstreamExit + } + } + $runtime | ConvertTo-Json -Depth 12 | + Set-Content -LiteralPath (Join-Path $rawRoot 'runtime.json') -Encoding UTF8 + Add-Content -LiteralPath $workerLog -Value "completed=$(Get-Date -Format o)" + + $consolidatorImage = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794' + $consolidatorArguments = @( + 'run', '--rm', '--name', "ndc-mission-core-e46h-consolidator-$runId", + '--network', 'none', '--read-only', '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', '--tmpfs', '/tmp:rw,noexec,nosuid,size=64m', + '--mount', "type=bind,src=$package,dst=/workspace/package,readonly", + '--mount', "type=bind,src=$sourceJob,dst=/workspace/source-job,readonly", + '--mount', "type=bind,src=$rawRoot,dst=/workspace/raw,readonly", + '--mount', "type=bind,src=$resultsRoot,dst=/workspace/results", + '-e', 'PYTHONPATH=/workspace/package/runtime', + '-e', 'PYTHONDONTWRITEBYTECODE=1', + $consolidatorImage, + 'python3', '/workspace/package/runtime/run_e46h_full_rectified_front_replay.py', + '--source-job', '/workspace/source-job', + '--raw-root', '/workspace/raw', + '--profile', '/workspace/package/profile.json', + '--output-root', '/workspace/results' + ) + Invoke-Docker $consolidatorArguments 'E46H consolidation' + Write-Host "E46H_FULL_RECTIFIED_FRONT_COMPLETED run=$runId results=$resultsRoot" +} +finally { + if ($LogPath) { Stop-Transcript | Out-Null } +} diff --git a/experiments/perception/worker/Invoke-E46HFullRectifiedFrontReplayAsInteractiveUser.ps1 b/experiments/perception/worker/Invoke-E46HFullRectifiedFrontReplayAsInteractiveUser.ps1 new file mode 100644 index 0000000..3ecb57e --- /dev/null +++ b/experiments/perception/worker/Invoke-E46HFullRectifiedFrontReplayAsInteractiveUser.ps1 @@ -0,0 +1,53 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$PackageRoot, + + [string]$SourceJobRoot = 'D:\NDC_MISSIONCORE\runtime\jobs\recorded-camera-602ac89026ed12978619801d', + + [string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46h' +) + +$ErrorActionPreference = 'Stop' +$taskName = 'MissionCore-E46HFullRectifiedFrontReplay' +$package = (Resolve-Path -LiteralPath $PackageRoot).Path +$script = Join-Path $package 'runtime\Invoke-E46HFullRectifiedFrontReplay.ps1' +if (-not (Test-Path -LiteralPath $script -PathType Leaf)) { + throw "E46H runner is missing: $script" +} +$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue +if ($existing -and $existing.State -eq 'Running') { + throw "$taskName is already running." +} +$logsRoot = Join-Path $RuntimeRoot 'logs' +New-Item -ItemType Directory -Force -Path $logsRoot | Out-Null +$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ') +$logPath = Join-Path $logsRoot "e46h-full-rectified-front-$stamp.log" +$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" +$arguments = @( + '-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', + '-File', "`"$script`"", + '-PackageRoot', "`"$package`"", + '-SourceJobRoot', "`"$SourceJobRoot`"", + '-RuntimeRoot', "`"$RuntimeRoot`"", + '-LogPath', "`"$logPath`"" +) -join ' ' +$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name +$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $package +$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited +$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30)) +$settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -ExecutionTimeLimit ([TimeSpan]::FromHours(6)) +Register-ScheduledTask ` + -TaskName $taskName ` + -Action $action ` + -Principal $principal ` + -Trigger $trigger ` + -Settings $settings ` + -Description 'One-shot E46H full retained FRONT TrafficCamNet + NvDCF replay.' ` + -Force | Out-Null +Start-ScheduledTask -TaskName $taskName +Write-Host "E46H_TASK_STARTED task=$taskName log=$logPath" diff --git a/experiments/perception/worker/Invoke-E46JRawFisheyeYolox.ps1 b/experiments/perception/worker/Invoke-E46JRawFisheyeYolox.ps1 new file mode 100644 index 0000000..8410c56 --- /dev/null +++ b/experiments/perception/worker/Invoke-E46JRawFisheyeYolox.ps1 @@ -0,0 +1,118 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$PackageRoot, + + [string]$RuntimeRoot = 'D:\NDC_MISSIONCORE\runtime\experiments\e46j', + + [string]$SourceVideo = 'D:\NDC_MISSIONCORE\runtime\experiments\e46e\inputs\right-cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8.mp4', + + [string]$ValidFovMask = 'D:\NDC_MISSIONCORE\runtime\inputs\e2\valid-fov-mask-b4dd8ddf2b87c1d520ee8a0868c4fea062d7c14d1bae73ccabd3abe1f3acbac2\mask.png', + + [int]$MaxFrames = 0, + + [switch]$NoOverlay, + + [string]$RunPrefix = 'full' +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +function Get-Sha256([string]$Path) { + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() +} + +$package = (Resolve-Path -LiteralPath $PackageRoot).Path +$source = (Resolve-Path -LiteralPath $SourceVideo).Path +$mask = (Resolve-Path -LiteralPath $ValidFovMask).Path +$profile = Join-Path $package 'e46j_raw_fisheye_yolox_profile.json' +$runner = Join-Path $package 'run_e46j_raw_fisheye_yolox.py' +if (-not (Test-Path -LiteralPath $profile -PathType Leaf) -or + -not (Test-Path -LiteralPath $runner -PathType Leaf)) { + throw 'E46J package is incomplete.' +} +if ((Get-Sha256 $source) -ne 'cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8') { + throw 'E46J source stream identity changed.' +} + +$runsRoot = Join-Path $RuntimeRoot 'runs' +New-Item -ItemType Directory -Force -Path $runsRoot | Out-Null +$runId = "$RunPrefix-$((Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssfffZ'))" +$image = 'nvcr.io/nvidia/tritonserver:26.06-py3@sha256:58df7489c3f2276f9591d500a012dee03e23d35543ce3c390b4c001e6bf90794' +$command = "python3 /workspace/package/run_e46j_raw_fisheye_yolox.py --input /workspace/input/right.mp4 --mask /workspace/valid-fov/mask.png --profile /workspace/package/e46j_raw_fisheye_yolox_profile.json --triton-url http://127.0.0.1:8000 --output /workspace/runs/$runId" +if ($MaxFrames -gt 0) { + $command += " --max-frames $MaxFrames" +} +if (-not $NoOverlay) { + $command += ' --overlay' +} + +$dockerArguments = @( + 'run', '--rm', '--name', "ndc-mission-core-e46j-$runId", + '--network', 'container:ndc-mission-core-triton', + '--gpus', 'all', + '--cap-drop', 'ALL', + '--security-opt', 'no-new-privileges', + '--shm-size', '1g', + '--label', 'com.nodedc.product=mission-core', + '--label', 'com.nodedc.stack=perception', + '--label', 'com.nodedc.role=e46j-raw-fisheye-realtime-gate', + '--label', 'com.nodedc.managed-by=mission-core-worker', + '--mount', "type=bind,src=$source,dst=/workspace/input/right.mp4,readonly", + '--mount', "type=bind,src=$mask,dst=/workspace/valid-fov/mask.png,readonly", + '--mount', "type=bind,src=$package,dst=/workspace/package,readonly", + '--mount', "type=bind,src=$runsRoot,dst=/workspace/runs", + '--mount', 'type=bind,src=D:\NDC_MISSIONCORE\runtime\derived\perception-p0-env-v1,dst=/opt/env,readonly', + '--mount', 'type=bind,src=D:\NDC_MISSIONCORE\runtime\derived\perception-e15-media-pyav180-lz445-v1,dst=/opt/media,readonly', + '--mount', 'type=bind,src=D:\NDC_MISSIONCORE\runtime\derived\perception-e3-opencv413092-v1,dst=/environment,readonly', + '-e', 'PYTHONPATH=/opt/env:/opt/media:/environment/packages', + '--entrypoint', '/bin/bash', + $image, '-lc', $command +) + +& docker @dockerArguments +$runnerExitCode = $LASTEXITCODE +if ($runnerExitCode -ne 0 -and $runnerExitCode -ne 2) { + throw "E46J runner crashed with exit code $runnerExitCode" +} +$result = Join-Path $runsRoot $runId +$runtimePath = Join-Path $result 'runtime.json' +if (-not $NoOverlay) { + $intermediate = Join-Path $result 'raw-fisheye-yolox-overlay-intermediate.mp4' + $overlay = Join-Path $result 'raw-fisheye-yolox-overlay.mp4' + if (-not (Test-Path -LiteralPath $intermediate -PathType Leaf)) { + throw 'E46J intermediate overlay is missing.' + } + & ffmpeg.exe -hide_banner -loglevel error -y -i $intermediate ` + -c:v libx264 -preset veryfast -crf 20 -r 4489000/448723 ` + -movflags +faststart -an $overlay + if ($LASTEXITCODE -ne 0 -or + -not (Test-Path -LiteralPath $overlay -PathType Leaf) -or + (Get-Item -LiteralPath $overlay).Length -eq 0) { + throw 'E46J final H.264 overlay transcode failed.' + } + $runtime = Get-Content -LiteralPath $runtimePath -Raw | ConvertFrom-Json + $runtime.artifacts.PSObject.Properties.Remove('overlay_intermediate') + $runtime.artifacts | Add-Member -NotePropertyName overlay -NotePropertyValue ([pscustomobject]@{ + file = 'raw-fisheye-yolox-overlay.mp4' + byte_length = (Get-Item -LiteralPath $overlay).Length + sha256 = Get-Sha256 $overlay + codec = 'H.264' + frame_rate = 10.003944527024467 + }) + $runtime | Add-Member -NotePropertyName visual_export -NotePropertyValue ([pscustomobject]@{ + provider = ((& ffmpeg.exe -version | Select-Object -First 1).Trim()) + source = 'MPEG-4 Part 2 intermediate produced by OpenCV VideoWriter' + output = 'H.264 MP4 with faststart' + excluded_from_core_latency = $true + }) + $runtime | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $runtimePath -Encoding UTF8 + Remove-Item -LiteralPath $intermediate -Force +} +Write-Output "E46J_RUN_ID=$runId" +Write-Output "E46J_RESULT_ROOT=$result" +Get-Content -LiteralPath $runtimePath -Raw +if ($runnerExitCode -eq 2) { + exit 2 +} diff --git a/experiments/perception/worker/Prepare-RectifiedCameraReplay.ps1 b/experiments/perception/worker/Prepare-RectifiedCameraReplay.ps1 new file mode 100644 index 0000000..b4b4242 --- /dev/null +++ b/experiments/perception/worker/Prepare-RectifiedCameraReplay.ps1 @@ -0,0 +1,124 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$JobRoot, + + [Parameter(Mandatory = $true)] + [string]$OutputPath, + + [ValidateRange(1, 1000)] + [int]$FreeGiBFloor = 360 +) + +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +function Assert-DDrivePath { + param([string]$Path, [string]$Label) + $fullPath = [IO.Path]::GetFullPath($Path) + if ([IO.Path]::GetPathRoot($fullPath).TrimEnd("\") -ine "D:") { + throw "$Label must be stored on D:" + } + return $fullPath +} + +function Assert-FreeSpace { + param([string]$Phase, [int64]$RequiredAdditionalBytes = 0) + $freeBytes = [int64](Get-PSDrive -Name D).Free + $floorBytes = [int64]$FreeGiBFloor * 1GB + Write-Output ( + "DISK_GUARD PHASE={0} FREE_GIB={1} FLOOR_GIB={2}" -f + $Phase, + [math]::Round($freeBytes / 1GB, 3), + $FreeGiBFloor + ) + if ($freeBytes -lt ($floorBytes + $RequiredAdditionalBytes)) { + throw "D: does not have the guarded replay reserve during $Phase" + } +} + +$jobDirectory = Assert-DDrivePath ( + (Resolve-Path -LiteralPath $JobRoot).Path +) "Job root" +$output = Assert-DDrivePath $OutputPath "Output path" +if (Test-Path -LiteralPath $output) { + Write-Output "REPLAY_ALREADY_PRESENT=$output" + exit 0 +} + +$jobPath = Join-Path $jobDirectory "job.json" +$job = Get-Content -LiteralPath $jobPath -Raw | ConvertFrom-Json +if ( + $job.schema_version -ne "missioncore.compute-job/v1" -or + $job.job_id -ne "recorded-camera-602ac89026ed12978619801d" -or + $job.input.session_id -ne "20260720T065719Z_viewer_live" -or + $job.input.source_id -ne "sensor.camera.right" -or + [int]$job.input.segment_count -ne 4489 +) { + throw "The requested job is not the immutable RAVNOVES00 right-camera source" +} + +$epochRoot = Join-Path $jobDirectory "input\camera\sensor.camera.right\epoch-1" +$initPath = Join-Path $epochRoot "init.mp4" +$segmentsRoot = Join-Path $epochRoot "segments" +if (-not (Test-Path -LiteralPath $initPath -PathType Leaf)) { + throw "Camera initialization segment is absent" +} +if (-not (Test-Path -LiteralPath $segmentsRoot -PathType Container)) { + throw "Camera segment directory is absent" +} + +$parent = Split-Path $output -Parent +$null = New-Item -ItemType Directory -Path $parent -Force +$temporary = Join-Path $parent (".{0}.{1}.partial" -f (Split-Path $output -Leaf), [Guid]::NewGuid().ToString("N")) +$requiredBytes = [int64]$job.input.byte_length + 1GB +Assert-FreeSpace "preflight" $requiredBytes + +try { + $destination = [IO.File]::Open( + $temporary, + [IO.FileMode]::CreateNew, + [IO.FileAccess]::Write, + [IO.FileShare]::None + ) + try { + $source = [IO.File]::OpenRead($initPath) + try { $source.CopyTo($destination) } finally { $source.Dispose() } + for ($sequence = 1; $sequence -le 4489; $sequence++) { + $segment = Join-Path $segmentsRoot ("{0}.m4s" -f $sequence) + if (-not (Test-Path -LiteralPath $segment -PathType Leaf)) { + throw "Camera segment is absent: $sequence" + } + $source = [IO.File]::OpenRead($segment) + try { $source.CopyTo($destination) } finally { $source.Dispose() } + } + $destination.Flush($true) + } + finally { + $destination.Dispose() + } + + $probe = & ffprobe -v error -select_streams v:0 -count_frames ` + -show_entries stream=width,height,nb_read_frames ` + -of json $temporary | ConvertFrom-Json + if ($LASTEXITCODE -ne 0) { + throw "ffprobe failed for the reconstructed camera source" + } + $stream = @($probe.streams)[0] + if ( + [int]$stream.width -ne 800 -or + [int]$stream.height -ne 600 -or + [int]$stream.nb_read_frames -ne 4489 + ) { + throw "Reconstructed camera stream violates the immutable frame contract" + } + Move-Item -LiteralPath $temporary -Destination $output + Assert-FreeSpace "published" + Write-Output "REPLAY_PATH=$output" + Write-Output "FRAME_COUNT=4489" +} +finally { + if (Test-Path -LiteralPath $temporary) { + Remove-Item -LiteralPath $temporary -Force + } +} diff --git a/experiments/perception/worker/e46e_deepstream_app.txt b/experiments/perception/worker/e46e_deepstream_app.txt new file mode 100644 index 0000000..b936396 --- /dev/null +++ b/experiments/perception/worker/e46e_deepstream_app.txt @@ -0,0 +1,78 @@ +[application] +enable-perf-measurement=1 +perf-measurement-interval-sec=5 +gie-kitti-output-dir=/workspace/output/detections +kitti-track-output-dir=/workspace/output/tracks + +[tiled-display] +enable=0 +rows=1 +columns=1 +width=800 +height=600 +gpu-id=0 + +[source0] +enable=1 +type=3 +num-sources=1 +uri=file:///workspace/input/right.mp4 +gpu-id=0 + +[streammux] +gpu-id=0 +batch-size=1 +batched-push-timeout=40000 +width=800 +height=600 +live-source=0 + +[primary-gie] +enable=1 +gpu-id=0 +plugin-type=0 +batch-size=1 +gie-unique-id=1 +config-file=/workspace/package/runtime/e46e_trafficcamnet_rtdetr.txt +bbox-border-color1=0.267;0.831;1.0;1.0 +bbox-border-color2=0.243;0.973;0.553;1.0 +bbox-border-color3=1.0;0.306;0.765;1.0 +bbox-border-color4=1.0;0.741;0.153;1.0 + +[tracker] +enable=1 +tracker-width=960 +tracker-height=544 +ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so +ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml +gpu-id=0 +display-tracking-id=1 +compute-hw=1 + +[osd] +enable=1 +gpu-id=0 +border-width=3 +text-size=16 +text-color=1;1;1;1 +text-bg-color=0.08;0.08;0.08;0.9 +font=Arial +display-bbox=1 +display-text=1 + +[sink0] +enable=1 +type=3 +container=1 +codec=1 +enc-type=0 +sync=0 +qos=0 +bitrate=8000000 +profile=4 +output-file=/workspace/output/overlay.mp4 +source-id=0 +gpu-id=0 + +[tests] +file-loop=0 diff --git a/experiments/perception/worker/e46e_trafficcamnet_labels.txt b/experiments/perception/worker/e46e_trafficcamnet_labels.txt new file mode 100644 index 0000000..92284f3 --- /dev/null +++ b/experiments/perception/worker/e46e_trafficcamnet_labels.txt @@ -0,0 +1,5 @@ +background +bicycle +car +person +road_sign diff --git a/experiments/perception/worker/e46e_trafficcamnet_rtdetr.txt b/experiments/perception/worker/e46e_trafficcamnet_rtdetr.txt new file mode 100644 index 0000000..e6a6853 --- /dev/null +++ b/experiments/perception/worker/e46e_trafficcamnet_rtdetr.txt @@ -0,0 +1,26 @@ +[property] +gpu-id=0 +onnx-file=/workspace/model/resnet50_trafficcamnet_rtdetr.fp16.onnx +model-engine-file=/workspace/model/resnet50_trafficcamnet_rtdetr.fp16.onnx_b1_gpu0_fp16.engine +labelfile-path=/workspace/package/runtime/e46e_trafficcamnet_labels.txt +custom-lib-path=/workspace/package/runtime/libnvds_infercustomparser_tao.so +parse-bbox-func-name=NvDsInferParseCustomDDETRTAO +output-blob-names=pred_logits;pred_boxes +infer-dims=3;544;960 +maintain-aspect-ratio=1 +net-scale-factor=0.00392156862745098 +offsets=0;0;0 +model-color-format=0 +network-mode=2 +network-type=0 +num-detected-classes=5 +cluster-mode=4 +output-tensor-meta=1 +workspace-size=1048576 +batch-size=1 +interval=0 +gie-unique-id=1 + +[class-attrs-all] +pre-cluster-threshold=0.5 +topk=20 diff --git a/experiments/perception/worker/e46f_dashcamnet_detectnet.txt b/experiments/perception/worker/e46f_dashcamnet_detectnet.txt new file mode 100644 index 0000000..4e96188 --- /dev/null +++ b/experiments/perception/worker/e46f_dashcamnet_detectnet.txt @@ -0,0 +1,32 @@ +[property] +gpu-id=0 +onnx-file=/workspace/model/resnet18_dashcamnet_pruned.onnx +model-engine-file=/workspace/model/resnet18_dashcamnet_pruned.onnx_b1_gpu0_fp16.engine +labelfile-path=/workspace/package/runtime/e46f_dashcamnet_labels.txt +output-blob-names=output_bbox/BiasAdd:0;output_cov/Sigmoid:0 +infer-dims=3;544;960 +maintain-aspect-ratio=0 +net-scale-factor=0.00392156862745098 +offsets=0;0;0 +model-color-format=0 +network-mode=2 +network-type=0 +num-detected-classes=4 +cluster-mode=2 +output-tensor-meta=0 +workspace-size=1048576 +batch-size=1 +interval=0 +gie-unique-id=1 + +[class-attrs-all] +topk=20 +nms-iou-threshold=0.5 +pre-cluster-threshold=0.2 +roi-top-offset=0 +roi-bottom-offset=0 + +[class-attrs-0] +topk=20 +nms-iou-threshold=0.5 +pre-cluster-threshold=0.4 diff --git a/experiments/perception/worker/e46f_dashcamnet_labels.txt b/experiments/perception/worker/e46f_dashcamnet_labels.txt new file mode 100644 index 0000000..1a20095 --- /dev/null +++ b/experiments/perception/worker/e46f_dashcamnet_labels.txt @@ -0,0 +1,4 @@ +car +bicycle +person +road_sign diff --git a/experiments/perception/worker/e46f_deepstream_app.txt b/experiments/perception/worker/e46f_deepstream_app.txt new file mode 100644 index 0000000..8079c1e --- /dev/null +++ b/experiments/perception/worker/e46f_deepstream_app.txt @@ -0,0 +1,78 @@ +[application] +enable-perf-measurement=1 +perf-measurement-interval-sec=5 +gie-kitti-output-dir=/workspace/output/detections +kitti-track-output-dir=/workspace/output/tracks + +[tiled-display] +enable=0 +rows=1 +columns=1 +width=800 +height=600 +gpu-id=0 + +[source0] +enable=1 +type=3 +num-sources=1 +uri=file:///workspace/input/right.mp4 +gpu-id=0 + +[streammux] +gpu-id=0 +batch-size=1 +batched-push-timeout=40000 +width=800 +height=600 +live-source=0 + +[primary-gie] +enable=1 +gpu-id=0 +plugin-type=0 +batch-size=1 +gie-unique-id=1 +config-file=/workspace/package/runtime/e46f_dashcamnet_detectnet.txt +bbox-border-color0=0.243;0.973;0.553;1.0 +bbox-border-color1=0.267;0.831;1.0;1.0 +bbox-border-color2=1.0;0.306;0.765;1.0 +bbox-border-color3=1.0;0.741;0.153;1.0 + +[tracker] +enable=1 +tracker-width=960 +tracker-height=544 +ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so +ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml +gpu-id=0 +display-tracking-id=1 +compute-hw=1 + +[osd] +enable=1 +gpu-id=0 +border-width=3 +text-size=16 +text-color=1;1;1;1 +text-bg-color=0.08;0.08;0.08;0.9 +font=Arial +display-bbox=1 +display-text=1 + +[sink0] +enable=1 +type=3 +container=1 +codec=1 +enc-type=0 +sync=0 +qos=0 +bitrate=8000000 +profile=4 +output-file=/workspace/output/overlay.mp4 +source-id=0 +gpu-id=0 + +[tests] +file-loop=0 diff --git a/experiments/perception/worker/e46g_dashcamnet_deepstream_app.txt b/experiments/perception/worker/e46g_dashcamnet_deepstream_app.txt new file mode 100644 index 0000000..b6d3b3f --- /dev/null +++ b/experiments/perception/worker/e46g_dashcamnet_deepstream_app.txt @@ -0,0 +1,78 @@ +[application] +enable-perf-measurement=1 +perf-measurement-interval-sec=5 +gie-kitti-output-dir=/workspace/output/detections +kitti-track-output-dir=/workspace/output/tracks + +[tiled-display] +enable=0 +rows=1 +columns=1 +width=960 +height=544 +gpu-id=0 + +[source0] +enable=1 +type=3 +num-sources=1 +uri=file:///workspace/input/view.mp4 +gpu-id=0 + +[streammux] +gpu-id=0 +batch-size=1 +batched-push-timeout=40000 +width=960 +height=544 +live-source=0 + +[primary-gie] +enable=1 +gpu-id=0 +plugin-type=0 +batch-size=1 +gie-unique-id=1 +config-file=/workspace/package/runtime/e46f_dashcamnet_detectnet.txt +bbox-border-color0=0.243;0.973;0.553;1.0 +bbox-border-color1=0.267;0.831;1.0;1.0 +bbox-border-color2=1.0;0.306;0.765;1.0 +bbox-border-color3=1.0;0.741;0.153;1.0 + +[tracker] +enable=1 +tracker-width=960 +tracker-height=544 +ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so +ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml +gpu-id=0 +display-tracking-id=1 +compute-hw=1 + +[osd] +enable=1 +gpu-id=0 +border-width=3 +text-size=16 +text-color=1;1;1;1 +text-bg-color=0.08;0.08;0.08;0.9 +font=Arial +display-bbox=1 +display-text=1 + +[sink0] +enable=1 +type=3 +container=1 +codec=1 +enc-type=0 +sync=0 +qos=0 +bitrate=6000000 +profile=4 +output-file=/workspace/output/overlay.mp4 +source-id=0 +gpu-id=0 + +[tests] +file-loop=0 diff --git a/experiments/perception/worker/e46g_nvdewarper_front.txt b/experiments/perception/worker/e46g_nvdewarper_front.txt new file mode 100644 index 0000000..6f0b0ef --- /dev/null +++ b/experiments/perception/worker/e46g_nvdewarper_front.txt @@ -0,0 +1,22 @@ +[property] +output-width=960 +output-height=544 +num-batch-buffers=1 +cuda-memory-type=2 + +[surface0] +projection-type=4 +surface-index=0 +width=960 +height=544 +yaw=0.0 +pitch=0.0 +roll=0.0 +rot-axes=YXZ +focal-length=194.59817287616025;194.57531427932872 +distortion=-0.023164451386679667;-0.0014974198594105452;-0.001039213149441563;-0.000035237331915978814 +src-x0=396.31861150187996 +src-y0=301.49644357408005 +dst-focal-length=402.76782296509447;402.76782296509447 +dst-principal-point=479.5;271.5 +cuda-address-mode=1 diff --git a/experiments/perception/worker/e46g_nvdewarper_left.txt b/experiments/perception/worker/e46g_nvdewarper_left.txt new file mode 100644 index 0000000..b8579e6 --- /dev/null +++ b/experiments/perception/worker/e46g_nvdewarper_left.txt @@ -0,0 +1,22 @@ +[property] +output-width=960 +output-height=544 +num-batch-buffers=1 +cuda-memory-type=2 + +[surface0] +projection-type=4 +surface-index=0 +width=960 +height=544 +yaw=270.0 +pitch=0.0 +roll=0.0 +rot-axes=YXZ +focal-length=194.59817287616025;194.57531427932872 +distortion=-0.023164451386679667;-0.0014974198594105452;-0.001039213149441563;-0.000035237331915978814 +src-x0=396.31861150187996 +src-y0=301.49644357408005 +dst-focal-length=402.76782296509447;402.76782296509447 +dst-principal-point=479.5;271.5 +cuda-address-mode=1 diff --git a/experiments/perception/worker/e46g_nvdewarper_right.txt b/experiments/perception/worker/e46g_nvdewarper_right.txt new file mode 100644 index 0000000..0513003 --- /dev/null +++ b/experiments/perception/worker/e46g_nvdewarper_right.txt @@ -0,0 +1,22 @@ +[property] +output-width=960 +output-height=544 +num-batch-buffers=1 +cuda-memory-type=2 + +[surface0] +projection-type=4 +surface-index=0 +width=960 +height=544 +yaw=90.0 +pitch=0.0 +roll=0.0 +rot-axes=YXZ +focal-length=194.59817287616025;194.57531427932872 +distortion=-0.023164451386679667;-0.0014974198594105452;-0.001039213149441563;-0.000035237331915978814 +src-x0=396.31861150187996 +src-y0=301.49644357408005 +dst-focal-length=402.76782296509447;402.76782296509447 +dst-principal-point=479.5;271.5 +cuda-address-mode=1 diff --git a/experiments/perception/worker/e46g_trafficcamnet_deepstream_app.txt b/experiments/perception/worker/e46g_trafficcamnet_deepstream_app.txt new file mode 100644 index 0000000..ff28964 --- /dev/null +++ b/experiments/perception/worker/e46g_trafficcamnet_deepstream_app.txt @@ -0,0 +1,78 @@ +[application] +enable-perf-measurement=1 +perf-measurement-interval-sec=5 +gie-kitti-output-dir=/workspace/output/detections +kitti-track-output-dir=/workspace/output/tracks + +[tiled-display] +enable=0 +rows=1 +columns=1 +width=960 +height=544 +gpu-id=0 + +[source0] +enable=1 +type=3 +num-sources=1 +uri=file:///workspace/input/view.mp4 +gpu-id=0 + +[streammux] +gpu-id=0 +batch-size=1 +batched-push-timeout=40000 +width=960 +height=544 +live-source=0 + +[primary-gie] +enable=1 +gpu-id=0 +plugin-type=0 +batch-size=1 +gie-unique-id=1 +config-file=/workspace/package/runtime/e46e_trafficcamnet_rtdetr.txt +bbox-border-color1=0.267;0.831;1.0;1.0 +bbox-border-color2=0.243;0.973;0.553;1.0 +bbox-border-color3=1.0;0.306;0.765;1.0 +bbox-border-color4=1.0;0.741;0.153;1.0 + +[tracker] +enable=1 +tracker-width=960 +tracker-height=544 +ll-lib-file=/opt/nvidia/deepstream/deepstream/lib/libnvds_nvmultiobjecttracker.so +ll-config-file=/opt/nvidia/deepstream/deepstream/samples/configs/deepstream-app/config_tracker_NvDCF_perf.yml +gpu-id=0 +display-tracking-id=1 +compute-hw=1 + +[osd] +enable=1 +gpu-id=0 +border-width=3 +text-size=16 +text-color=1;1;1;1 +text-bg-color=0.08;0.08;0.08;0.9 +font=Arial +display-bbox=1 +display-text=1 + +[sink0] +enable=1 +type=3 +container=1 +codec=1 +enc-type=0 +sync=0 +qos=0 +bitrate=6000000 +profile=4 +output-file=/workspace/output/overlay.mp4 +source-id=0 +gpu-id=0 + +[tests] +file-loop=0 diff --git a/experiments/perception/worker/run_e46e_ready_stack.py b/experiments/perception/worker/run_e46e_ready_stack.py new file mode 100644 index 0000000..712cb81 --- /dev/null +++ b/experiments/perception/worker/run_e46e_ready_stack.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Consolidate raw DeepStream/NvDCF output into immutable E46E evidence.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from k1link.compute.e46e_ready_stack import build_e46e_ready_stack + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--source-job", type=Path, required=True) + parser.add_argument("--raw-root", type=Path, required=True) + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = build_e46e_ready_stack( + source_job_root=args.source_job, + raw_root=args.raw_root, + profile_path=args.profile, + output_root=args.output_root, + ) + print( + json.dumps( + { + "result_id": result["result_id"], + "result_root": str(result["result_root"]), + "metrics": result["report"]["metrics"], + }, + ensure_ascii=False, + indent=2, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/worker/run_e46f_dashcam_bakeoff.py b/experiments/perception/worker/run_e46f_dashcam_bakeoff.py new file mode 100644 index 0000000..c0deb05 --- /dev/null +++ b/experiments/perception/worker/run_e46f_dashcam_bakeoff.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Consolidate raw DashCamNet/NvDCF output into immutable E46F evidence.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from k1link.compute.e46f_dashcam_bakeoff import build_e46f_dashcam_bakeoff + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--source-job", type=Path, required=True) + parser.add_argument("--raw-root", type=Path, required=True) + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = build_e46f_dashcam_bakeoff( + source_job_root=args.source_job, + raw_root=args.raw_root, + profile_path=args.profile, + output_root=args.output_root, + ) + print( + json.dumps( + { + "result_id": result["result_id"], + "result_root": str(result["result_root"]), + "metrics": result["report"]["metrics"], + }, + ensure_ascii=False, + indent=2, + ) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/worker/run_e46g_rectified_detector_bakeoff.py b/experiments/perception/worker/run_e46g_rectified_detector_bakeoff.py new file mode 100644 index 0000000..ab07d61 --- /dev/null +++ b/experiments/perception/worker/run_e46g_rectified_detector_bakeoff.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Freeze raw NVIDIA E46G output as immutable Mission Core evidence.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from k1link.compute.e46g_rectified_detector_bakeoff import ( + build_e46g_rectified_detector_bakeoff, +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--source-job", type=Path, required=True) + parser.add_argument("--raw-root", type=Path, required=True) + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = build_e46g_rectified_detector_bakeoff( + source_job_root=args.source_job, + raw_root=args.raw_root, + profile_path=args.profile, + output_root=args.output_root, + ) + print(result["result_root"]) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/worker/run_e46h_full_rectified_front_replay.py b/experiments/perception/worker/run_e46h_full_rectified_front_replay.py new file mode 100644 index 0000000..5bc0ed0 --- /dev/null +++ b/experiments/perception/worker/run_e46h_full_rectified_front_replay.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Freeze raw NVIDIA E46H output as immutable Mission Core evidence.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from k1link.compute.e46h_full_rectified_front_replay import ( + build_e46h_full_rectified_front_replay, +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--source-job", type=Path, required=True) + parser.add_argument("--raw-root", type=Path, required=True) + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--output-root", type=Path, required=True) + args = parser.parse_args() + result = build_e46h_full_rectified_front_replay( + source_job_root=args.source_job, + raw_root=args.raw_root, + profile_path=args.profile, + output_root=args.output_root, + ) + print(result["result_root"]) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/worker/run_e46j_raw_fisheye_yolox.py b/experiments/perception/worker/run_e46j_raw_fisheye_yolox.py new file mode 100644 index 0000000..32bc901 --- /dev/null +++ b/experiments/perception/worker/run_e46j_raw_fisheye_yolox.py @@ -0,0 +1,706 @@ +#!/usr/bin/env python3 +"""Run the frozen one-pass YOLOX-S realtime gate on the full K1 RIGHT fisheye. + +The runner deliberately performs exactly one detector request per decoded source +frame. It does not rectify, crop, tile, track, hold, stitch or use route-specific +filters. H.264 overlay encoding is measured separately from the detector path. +""" + +from __future__ import annotations + +import argparse +import hashlib +import http.client +import json +import math +import os +import platform +import statistics +import subprocess +import threading +import time +import urllib.parse +from collections import Counter +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +import numpy as np + +PROFILE_SCHEMA = "missioncore.e46j-raw-fisheye-realtime-profile/v1" +RUNTIME_SCHEMA = "missioncore.e46j-raw-fisheye-realtime-runtime/v1" +FRAME_SCHEMA = "missioncore.e46j-raw-fisheye-realtime-frame/v1" + +COCO_CLASSES = ( + "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", + "truck", "boat", "traffic light", "fire hydrant", "stop sign", + "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", + "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", + "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", + "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", + "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", + "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", + "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", + "couch", "potted plant", "bed", "dining table", "toilet", "tv", + "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", + "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", + "scissors", "teddy bear", "hair drier", "toothbrush", +) + + +def arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--mask", type=Path, required=True) + parser.add_argument("--profile", type=Path, required=True) + parser.add_argument("--triton-url", required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--max-frames", type=int, default=0) + parser.add_argument("--overlay", action="store_true") + parser.add_argument("--telemetry-interval", type=float, default=0.5) + return parser.parse_args() + + +def read_object(path: Path) -> dict[str, Any]: + value = json.loads(path.resolve(strict=True).read_text(encoding="utf-8-sig")) + if not isinstance(value, dict): + raise RuntimeError(f"JSON object expected: {path}") + return value + + +def canonical_json(value: object) -> bytes: + return json.dumps( + value, sort_keys=True, separators=(",", ":"), allow_nan=False + ).encode() + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def distribution(values: list[float]) -> dict[str, float]: + if not values: + return {"mean": 0.0, "p50": 0.0, "p95": 0.0, "maximum": 0.0} + ordered = sorted(values) + + def percentile(fraction: float) -> float: + index = (len(ordered) - 1) * fraction + lower = math.floor(index) + upper = math.ceil(index) + if lower == upper: + return ordered[lower] + ratio = index - lower + return ordered[lower] * (1.0 - ratio) + ordered[upper] * ratio + + return { + "mean": round(statistics.fmean(ordered), 6), + "p50": round(percentile(0.5), 6), + "p95": round(percentile(0.95), 6), + "maximum": round(max(ordered), 6), + } + + +def validate_profile(profile: dict[str, Any], source_sha256: str) -> None: + source = profile.get("source") + detector = profile.get("detector") + detection = profile.get("detection") + acceptance = profile.get("acceptance") + if ( + profile.get("schema_version") != PROFILE_SCHEMA + or not isinstance(source, dict) + or source.get("camera_source_id") != "sensor.camera.right" + or source.get("stream_sha256") != source_sha256 + or source.get("resolution") != [800, 600] + or source.get("calibration_model") != "KB4" + or not isinstance(detector, dict) + or detector.get("id") != "yolox_s" + or detector.get("input_shape") != [1, 3, 640, 640] + or detector.get("single_inference_per_source_frame") is not True + or not isinstance(detection, dict) + or detection.get("custom_detector_logic") is not False + or detection.get("route_specific_filtering") is not False + or not isinstance(acceptance, dict) + or acceptance.get("require_full_raw_fov") is not True + ): + raise RuntimeError("E46J profile contract changed") + + +def load_mask(path: Path) -> np.ndarray: + from PIL import Image + + mask = np.asarray(Image.open(path.resolve(strict=True)).convert("L")) > 0 + if mask.shape != (600, 800) or not np.any(mask): + raise RuntimeError("E46J valid-FOV mask changed") + return mask + + +def preprocess( + image_bgr: np.ndarray, mask: np.ndarray, profile: dict[str, Any] +) -> np.ndarray: + import cv2 + + target_height = int(profile["detector"]["input_shape"][2]) + target_width = int(profile["detector"]["input_shape"][3]) + height, width = image_bgr.shape[:2] + ratio = min(target_height / height, target_width / width) + resized_width = int(width * ratio) + resized_height = int(height * ratio) + fill = int(profile["preprocessing"]["valid_fov_fill_value"]) + masked = np.where(mask[..., None], image_bgr, fill).astype(np.uint8) + resized = cv2.resize( + masked, (resized_width, resized_height), interpolation=cv2.INTER_LINEAR + ) + canvas = np.full((target_height, target_width, 3), fill, dtype=np.uint8) + canvas[:resized_height, :resized_width] = resized + return np.ascontiguousarray(canvas.transpose(2, 0, 1), dtype=np.float32)[None] + + +class TritonHttpClient: + """Minimal persistent Triton HTTP client for one sequential camera stream.""" + + def __init__(self, url: str, model: dict[str, Any]) -> None: + parsed = urllib.parse.urlsplit(url) + if parsed.scheme != "http" or not parsed.hostname: + raise RuntimeError("E46J requires an explicit HTTP Triton endpoint") + self.model = model + self.path = f"{parsed.path.rstrip('/')}/v2/models/{model['id']}/infer" + self.connection = http.client.HTTPConnection( + parsed.hostname, + parsed.port or 80, + timeout=60, + ) + + def close(self) -> None: + self.connection.close() + + def infer(self, tensor: np.ndarray) -> np.ndarray: + contiguous = np.ascontiguousarray(tensor, dtype=np.float32) + binary = contiguous.tobytes() + header = { + "inputs": [ + { + "name": self.model["input_name"], + "shape": list(contiguous.shape), + "datatype": "FP32", + "parameters": {"binary_data_size": len(binary)}, + } + ], + "outputs": [ + { + "name": self.model["output_name"], + "parameters": {"binary_data": True}, + } + ], + } + encoded = canonical_json(header) + self.connection.request( + "POST", + self.path, + body=encoded + binary, + headers={ + "Content-Type": "application/octet-stream", + "Inference-Header-Content-Length": str(len(encoded)), + }, + ) + response = self.connection.getresponse() + payload = response.read() + if response.status != 200: + raise RuntimeError( + f"E46J Triton inference failed: HTTP {response.status}: " + f"{payload[:512]!r}" + ) + header_length_value = response.getheader("Inference-Header-Content-Length") + if not header_length_value: + raise RuntimeError("E46J Triton output header length is missing") + header_length = int(header_length_value) + descriptor = json.loads(payload[:header_length])["outputs"][0] + if ( + descriptor["name"] != self.model["output_name"] + or descriptor["datatype"] != "FP32" + ): + raise RuntimeError("E46J Triton output descriptor changed") + shape = tuple(int(value) for value in descriptor["shape"]) + array = np.frombuffer(payload[header_length:], dtype=" np.ndarray: + predictions = output.copy() + grids: list[np.ndarray] = [] + strides: list[np.ndarray] = [] + for stride in (8, 16, 32): + height = 640 // stride + width = 640 // stride + yv, xv = np.meshgrid(np.arange(height), np.arange(width), indexing="ij") + grids.append(np.stack((xv, yv), axis=2).reshape(1, -1, 2)) + strides.append(np.full((1, height * width, 1), stride)) + grid = np.concatenate(grids, axis=1) + expanded_strides = np.concatenate(strides, axis=1) + predictions[..., :2] = (predictions[..., :2] + grid) * expanded_strides + predictions[..., 2:4] = np.exp(predictions[..., 2:4]) * expanded_strides + return predictions + + +def box_iou(one: np.ndarray, many: np.ndarray) -> np.ndarray: + if many.size == 0: + return np.zeros((0,), dtype=np.float32) + top_left = np.maximum(one[:2], many[:, :2]) + bottom_right = np.minimum(one[2:], many[:, 2:]) + intersection = np.prod(np.maximum(0.0, bottom_right - top_left), axis=1) + one_area = max(0.0, float(one[2] - one[0])) * max( + 0.0, float(one[3] - one[1]) + ) + many_area = np.maximum(0.0, many[:, 2] - many[:, 0]) * np.maximum( + 0.0, many[:, 3] - many[:, 1] + ) + union = one_area + many_area - intersection + return np.divide( + intersection, union, out=np.zeros_like(intersection), where=union > 0 + ) + + +def nms(boxes: np.ndarray, scores: np.ndarray, threshold: float) -> list[int]: + order = scores.argsort()[::-1] + keep: list[int] = [] + while order.size: + index = int(order[0]) + keep.append(index) + overlaps = box_iou(boxes[index], boxes[order[1:]]) + order = order[np.where(overlaps <= threshold)[0] + 1] + return keep + + +def valid_fraction( + box: np.ndarray, integral: np.ndarray +) -> tuple[float, bool, float]: + height = integral.shape[0] - 1 + width = integral.shape[1] - 1 + x1 = int(np.clip(math.floor(float(box[0])), 0, width)) + y1 = int(np.clip(math.floor(float(box[1])), 0, height)) + x2 = int(np.clip(math.ceil(float(box[2])), 0, width)) + y2 = int(np.clip(math.ceil(float(box[3])), 0, height)) + area = float(max(0, x2 - x1) * max(0, y2 - y1)) + if area <= 0: + return 0.0, False, 0.0 + inside = integral[y2, x2] - integral[y1, x2] - integral[y2, x1] + integral[ + y1, x1 + ] + center_x = int( + np.clip(round((float(box[0]) + float(box[2])) / 2.0), 0, width - 1) + ) + center_y = int( + np.clip(round((float(box[1]) + float(box[3])) / 2.0), 0, height - 1) + ) + center_inside = bool( + integral[center_y + 1, center_x + 1] + - integral[center_y, center_x + 1] + - integral[center_y + 1, center_x] + + integral[center_y, center_x] + ) + return float(inside) / area, center_inside, area + + +def detections( + output: np.ndarray, profile: dict[str, Any], mask: np.ndarray +) -> tuple[list[dict[str, Any]], dict[str, int]]: + prediction = decode_yolox(output)[0] + boxes = prediction[:, :4] + boxes_xyxy = np.empty_like(boxes) + boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2.0 + boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2.0 + boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2.0 + boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2.0 + source_height, source_width = mask.shape + ratio = min(640 / source_height, 640 / source_width) + boxes_xyxy /= ratio + class_scores = prediction[:, 4:5] * prediction[:, 5:] + class_ids = class_scores.argmax(axis=1) + scores = class_scores[np.arange(class_scores.shape[0]), class_ids] + detection = profile["detection"] + target_ids = set(int(value) for value in detection["target_class_ids"]) + candidate_mask = np.logical_and( + scores >= float(detection["minimum_score"]), + np.isin(class_ids, list(target_ids)), + ) + candidate_boxes = boxes_xyxy[candidate_mask] + candidate_scores = scores[candidate_mask] + candidate_classes = class_ids[candidate_mask] + integral = np.pad(mask.astype(np.int64), ((1, 0), (1, 0))).cumsum(0).cumsum(1) + result: list[dict[str, Any]] = [] + rejected: Counter[str] = Counter() + for class_id in sorted(target_ids): + indices = np.where(candidate_classes == class_id)[0] + if not indices.size: + continue + keep = nms( + candidate_boxes[indices], + candidate_scores[indices], + float(detection["nms_iou_threshold"]), + ) + for selected in indices[keep]: + box = candidate_boxes[selected].copy() + box[[0, 2]] = np.clip(box[[0, 2]], 0, source_width) + box[[1, 3]] = np.clip(box[[1, 3]], 0, source_height) + fraction, center_inside, area = valid_fraction(box, integral) + if area < float(detection["minimum_box_area_pixels"]): + rejected["small_box"] += 1 + continue + if area / float(source_width * source_height) > float( + detection["maximum_box_area_fraction"] + ): + rejected["large_box"] += 1 + continue + if fraction < float(detection["minimum_valid_fov_fraction"]): + rejected["outside_valid_fov"] += 1 + continue + if detection["require_center_inside_valid_fov"] and not center_inside: + rejected["center_outside_valid_fov"] += 1 + continue + result.append( + { + "class_id": int(class_id), + "label": COCO_CLASSES[int(class_id)], + "score": round(float(candidate_scores[selected]), 9), + "bbox_xyxy": [round(float(value), 6) for value in box], + "valid_fov_fraction": round(fraction, 6), + } + ) + result.sort(key=lambda item: (-float(item["score"]), int(item["class_id"]))) + return result, dict(rejected) + + +def draw_overlay(image: np.ndarray, rows: list[dict[str, Any]]) -> np.ndarray: + import cv2 + + for row in rows: + x1, y1, x2, y2 = (int(round(value)) for value in row["bbox_xyxy"]) + label = f"{row['label']} {float(row['score']):.2f}" + cv2.rectangle(image, (x1, y1), (x2, y2), (248, 248, 248), 2) + (text_width, text_height), baseline = cv2.getTextSize( + label, cv2.FONT_HERSHEY_SIMPLEX, 0.45, 1 + ) + top = max(0, y1 - text_height - baseline - 6) + cv2.rectangle( + image, + (x1, top), + (min(image.shape[1] - 1, x1 + text_width + 8), y1), + (18, 18, 18), + -1, + ) + cv2.putText( + image, + label, + (x1 + 4, max(text_height + 1, y1 - baseline - 3)), + cv2.FONT_HERSHEY_SIMPLEX, + 0.45, + (248, 248, 248), + 1, + cv2.LINE_AA, + ) + return image + + +class Telemetry: + def __init__(self, interval: float) -> None: + self.interval = interval + self.samples: list[dict[str, float]] = [] + self.stop_event = threading.Event() + self.thread = threading.Thread(target=self._run, daemon=True) + + def __enter__(self) -> Telemetry: + self.thread.start() + return self + + def __exit__(self, *_args: object) -> None: + self.stop_event.set() + self.thread.join(timeout=5) + + def _run(self) -> None: + while not self.stop_event.is_set(): + try: + completed = subprocess.run( + [ + "nvidia-smi", + "--query-gpu=utilization.gpu,memory.used,power.draw,temperature.gpu", + "--format=csv,noheader,nounits", + ], + check=True, + capture_output=True, + text=True, + timeout=10, + ) + values = [float(value.strip()) for value in completed.stdout.split(",")] + self.samples.append( + { + "gpu_utilization_percent": values[0], + "gpu_memory_used_mib": values[1], + "gpu_power_watts": values[2], + "gpu_temperature_celsius": values[3], + } + ) + except (OSError, ValueError, subprocess.SubprocessError): + pass + self.stop_event.wait(self.interval) + + +def telemetry_summary(samples: list[dict[str, float]]) -> dict[str, object]: + result: dict[str, object] = {"sample_count": len(samples)} + for key in ( + "gpu_utilization_percent", + "gpu_memory_used_mib", + "gpu_power_watts", + "gpu_temperature_celsius", + ): + result[key] = distribution([row[key] for row in samples]) + return result + + +def run(args: argparse.Namespace) -> dict[str, Any]: + import av + + source = args.input.resolve(strict=True) + profile_path = args.profile.resolve(strict=True) + mask_path = args.mask.resolve(strict=True) + output = args.output.expanduser().absolute() + output.mkdir(parents=True, exist_ok=False) + profile = read_object(profile_path) + source_sha = sha256(source) + validate_profile(profile, source_sha) + mask = load_mask(mask_path) + mask_sha = sha256(mask_path) + + container = av.open(str(source)) + video_stream = container.streams.video[0] + frame_rate = float(video_stream.average_rate) + if not math.isclose(frame_rate, float(profile["source"]["frame_rate"]), abs_tol=1e-6): + raise RuntimeError( + "E46J source frame rate changed: " + f"profile={profile['source']['frame_rate']}, actual={frame_rate}, " + f"average_rate={video_stream.average_rate}, time_base={video_stream.time_base}" + ) + + overlay_path = output / "raw-fisheye-yolox-overlay-intermediate.mp4" + overlay_writer = None + if args.overlay: + import cv2 + + overlay_writer = cv2.VideoWriter( + str(overlay_path), + cv2.VideoWriter_fourcc(*"mp4v"), + frame_rate, + (800, 600), + ) + if not overlay_writer.isOpened(): + raise RuntimeError("E46J intermediate overlay encoder is unavailable") + + frame_path = output / "frames.jsonl" + frame_stream = frame_path.open("x", encoding="utf-8") + class_counts: Counter[str] = Counter() + rejected_counts: Counter[str] = Counter() + latencies: dict[str, list[float]] = { + "preprocess_ms": [], + "inference_request_ms": [], + "postprocess_ms": [], + "core_path_ms": [], + "overlay_encode_ms": [], + } + zero_detection_frames = 0 + maximum_detections = 0 + failed_frames = 0 + processed_frames = 0 + run_started_utc = datetime.now(UTC) + wall_started = time.perf_counter() + warm_tensor = np.full((1, 3, 640, 640), 114.0, dtype=np.float32) + triton = TritonHttpClient(args.triton_url, profile["detector"]) + triton.infer(warm_tensor) + + with Telemetry(args.telemetry_interval) as telemetry: + try: + for decoded in container.decode(video_stream): + if args.max_frames and processed_frames >= args.max_frames: + break + frame_index = processed_frames + image = decoded.to_ndarray(format="bgr24") + if image.shape != (600, 800, 3): + raise RuntimeError(f"E46J source raster changed at frame {frame_index}") + core_started = time.perf_counter() + preprocess_started = core_started + tensor = preprocess(image, mask, profile) + preprocess_ms = (time.perf_counter() - preprocess_started) * 1000.0 + inference_started = time.perf_counter() + raw_output = triton.infer(tensor) + inference_ms = (time.perf_counter() - inference_started) * 1000.0 + postprocess_started = time.perf_counter() + rows, rejected = detections(raw_output, profile, mask) + postprocess_ms = (time.perf_counter() - postprocess_started) * 1000.0 + core_path_ms = (time.perf_counter() - core_started) * 1000.0 + latencies["preprocess_ms"].append(preprocess_ms) + latencies["inference_request_ms"].append(inference_ms) + latencies["postprocess_ms"].append(postprocess_ms) + latencies["core_path_ms"].append(core_path_ms) + class_counts.update(str(row["label"]) for row in rows) + rejected_counts.update(rejected) + maximum_detections = max(maximum_detections, len(rows)) + if not rows: + zero_detection_frames += 1 + + overlay_ms = 0.0 + if overlay_writer is not None: + overlay_started = time.perf_counter() + overlay = draw_overlay(image.copy(), rows) + overlay_writer.write(overlay) + overlay_ms = (time.perf_counter() - overlay_started) * 1000.0 + latencies["overlay_encode_ms"].append(overlay_ms) + frame_stream.write( + json.dumps( + { + "schema_version": FRAME_SCHEMA, + "frame_index": frame_index, + "session_seconds": round(frame_index / frame_rate, 6), + "detections": rows, + "rejected": rejected, + "latency_ms": { + "preprocess": round(preprocess_ms, 6), + "inference_request": round(inference_ms, 6), + "postprocess": round(postprocess_ms, 6), + "core_path": round(core_path_ms, 6), + "overlay_encode": round(overlay_ms, 6), + }, + }, + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ) + + "\n" + ) + processed_frames += 1 + except BaseException: + failed_frames += 1 + raise + finally: + frame_stream.flush() + os.fsync(frame_stream.fileno()) + frame_stream.close() + container.close() + triton.close() + if overlay_writer is not None: + overlay_writer.release() + + wall_seconds = time.perf_counter() - wall_started + run_completed_utc = datetime.now(UTC) + core = distribution(latencies["core_path_ms"]) + inference = distribution(latencies["inference_request_ms"]) + acceptance = profile["acceptance"] + expected_frames = ( + min(int(args.max_frames), int(profile["source"]["frame_count"])) + if args.max_frames + else int(profile["source"]["frame_count"]) + ) + checks = { + "frame_coverage": processed_frames == expected_frames, + "zero_failed_frames": failed_frames == 0, + "core_capacity_fps": (1000.0 / core["mean"]) + >= float(acceptance["minimum_core_capacity_fps"]), + "core_path_p95_ms": core["p95"] + <= float(acceptance["maximum_core_path_p95_ms"]), + "inference_request_p95_ms": inference["p95"] + <= float(acceptance["maximum_inference_request_p95_ms"]), + "single_pass_full_raw_fov": True, + } + runtime: dict[str, Any] = { + "schema_version": RUNTIME_SCHEMA, + "status": "completed" if all(checks.values()) else "completed-gate-failed", + "worker_host": platform.node(), + "gpu_name": subprocess.run( + ["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"], + check=True, + capture_output=True, + text=True, + timeout=10, + ).stdout.strip().splitlines()[0], + "started_at_utc": run_started_utc.isoformat(timespec="milliseconds").replace( + "+00:00", "Z" + ), + "completed_at_utc": run_completed_utc.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z"), + "source": { + "video_sha256": source_sha, + "frame_rate": frame_rate, + "resolution": [800, 600], + "decoded_frame_count": processed_frames, + "valid_fov_mask_sha256": mask_sha, + "preprocessing": "raw KB4 valid-FOV fill plus top-left letterbox; no crop/dewarp/tile", + }, + "model": { + "id": profile["detector"]["id"], + "model_sha256": profile["detector"]["model_sha256"], + "config_sha256": profile["detector"]["config_sha256"], + "runtime": profile["detector"]["runtime"], + "inference_requests": processed_frames, + }, + "profile_sha256": sha256(profile_path), + "metrics": { + "processed_frame_count": processed_frames, + "failed_frame_count": failed_frames, + "wall_seconds_including_overlay_export": round(wall_seconds, 6), + "export_throughput_fps": round(processed_frames / wall_seconds, 6), + "core_capacity_fps": round(1000.0 / core["mean"], 6), + "latency_ms": {key: distribution(values) for key, values in latencies.items()}, + "detection_observation_count": sum(class_counts.values()), + "class_observation_counts": dict(sorted(class_counts.items())), + "mean_detections_per_frame": round( + sum(class_counts.values()) / max(processed_frames, 1), 6 + ), + "maximum_detections_per_frame": maximum_detections, + "zero_detection_frame_count": zero_detection_frames, + "zero_detection_frame_fraction": round( + zero_detection_frames / max(processed_frames, 1), 9 + ), + "rejected_counts": dict(sorted(rejected_counts.items())), + "gpu": telemetry_summary(telemetry.samples), + }, + "acceptance": { + "thresholds": acceptance, + "checks": checks, + "passed": all(checks.values()), + }, + "artifacts": { + "frames": { + "file": frame_path.name, + "byte_length": frame_path.stat().st_size, + "sha256": sha256(frame_path), + }, + }, + "authority": profile["authority"], + } + if overlay_path.is_file(): + runtime["artifacts"]["overlay_intermediate"] = { + "file": overlay_path.name, + "byte_length": overlay_path.stat().st_size, + "sha256": sha256(overlay_path), + "codec": "MPEG-4 Part 2", + "frame_rate": frame_rate, + } + runtime_path = output / "runtime.json" + runtime_path.write_text( + json.dumps(runtime, ensure_ascii=False, indent=2, allow_nan=False) + "\n", + encoding="utf-8", + ) + return runtime + + +def main() -> int: + runtime = run(arguments()) + print(json.dumps(runtime, ensure_ascii=False, indent=2, allow_nan=False)) + return 0 if runtime["acceptance"]["passed"] else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/perception/worker/run_rectified_yolox_detector.py b/experiments/perception/worker/run_rectified_yolox_detector.py new file mode 100644 index 0000000..6fb868f --- /dev/null +++ b/experiments/perception/worker/run_rectified_yolox_detector.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python3 +"""Run the canonical YOLOX detector on calibration-derived K1 perspective views. + +This module is an adapter between the already qualified K1 KB4 rectification +from LAB E3 and the already qualified YOLOX/ByteTrack detector from LAB E8. +It deliberately does not introduce another detector, tracker or coordinate +system. Detections are projected back into the immutable right-camera frame +before class-aware NMS and tracking. +""" + +from __future__ import annotations + +import argparse +import copy +import concurrent.futures +import json +import math +import statistics +import time +from collections import Counter +from pathlib import Path +from typing import Any + +import numpy as np +from PIL import Image, ImageDraw +from run_e3_rectified_segmentation import _clahe, _rectification_maps +from run_e5_instance_tracking import ( + _detections, + _infer, + _load_valid_fov, + _nms, + _preprocess, + _valid_fraction, +) + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--job", type=Path, required=True) + parser.add_argument("--frames", type=Path, required=True) + parser.add_argument("--rectification-profile", type=Path, required=True) + parser.add_argument("--detector-profile", type=Path, required=True) + parser.add_argument("--valid-fov-root", type=Path, required=True) + parser.add_argument("--triton-url", required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--contrast", choices=("none", "clahe"), default="none") + parser.add_argument( + "--active-tiles", + default="front,left,right,up,down", + help="Comma-separated E3 rectification tile names evaluated this pass", + ) + parser.add_argument("--limit", type=int, default=0) + parser.add_argument( + "--tile-size", + type=int, + default=0, + help="Detector raster size; 0 preserves the E3 profile raster", + ) + return parser.parse_args() + + +def _read_object(path: Path) -> dict[str, Any]: + value = json.loads(path.resolve(strict=True).read_text(encoding="utf-8-sig")) + if not isinstance(value, dict): + raise RuntimeError(f"JSON root is not an object: {path}") + return value + + +def _percentile(values: list[float], percentile: float) -> float: + if not values: + return 0.0 + ordered = sorted(values) + index = (len(ordered) - 1) * percentile + lower = math.floor(index) + upper = math.ceil(index) + if lower == upper: + return ordered[lower] + fraction = index - lower + return ordered[lower] * (1.0 - fraction) + ordered[upper] * fraction + + +def _project_tile_box( + box: list[float], + tile: dict[str, Any], + selected_tile: np.ndarray, + tile_index: int, +) -> tuple[np.ndarray, tuple[float, float]] | None: + """Project a perspective bbox perimeter into the raw fisheye frame.""" + + map_x = tile["raw_map_x"] + map_y = tile["raw_map_y"] + height, width = map_x.shape + x1, y1, x2, y2 = (float(value) for value in box) + x1 = float(np.clip(x1, 0.0, width - 1.0)) + x2 = float(np.clip(x2, 0.0, width - 1.0)) + y1 = float(np.clip(y1, 0.0, height - 1.0)) + y2 = float(np.clip(y2, 0.0, height - 1.0)) + if x2 <= x1 or y2 <= y1: + return None + + samples = max(16, int(max(x2 - x1, y2 - y1) / 4.0)) + horizontal = np.linspace(x1, x2, samples) + vertical = np.linspace(y1, y2, samples) + sample_x = np.concatenate( + (horizontal, horizontal, np.full(samples, x1), np.full(samples, x2)) + ) + sample_y = np.concatenate( + (np.full(samples, y1), np.full(samples, y2), vertical, vertical) + ) + integer_x = np.clip(np.rint(sample_x).astype(np.int64), 0, width - 1) + integer_y = np.clip(np.rint(sample_y).astype(np.int64), 0, height - 1) + raw_x = map_x[integer_y, integer_x] + raw_y = map_y[integer_y, integer_x] + finite = np.isfinite(raw_x) & np.isfinite(raw_y) + if not np.any(finite): + return None + + center_x = int(np.clip(round((x1 + x2) / 2.0), 0, width - 1)) + center_y = int(np.clip(round((y1 + y2) / 2.0), 0, height - 1)) + raw_center_x = float(map_x[center_y, center_x]) + raw_center_y = float(map_y[center_y, center_x]) + raw_height, raw_width = selected_tile.shape + if not ( + math.isfinite(raw_center_x) + and math.isfinite(raw_center_y) + and 0.0 <= raw_center_x < raw_width + and 0.0 <= raw_center_y < raw_height + ): + return None + owner_x = int(np.clip(round(raw_center_x), 0, raw_width - 1)) + owner_y = int(np.clip(round(raw_center_y), 0, raw_height - 1)) + if int(selected_tile[owner_y, owner_x]) != tile_index: + return None + + projected = np.asarray( + [ + float(np.min(raw_x[finite])), + float(np.min(raw_y[finite])), + float(np.max(raw_x[finite])), + float(np.max(raw_y[finite])), + ], + dtype=np.float64, + ) + projected[[0, 2]] = np.clip(projected[[0, 2]], 0.0, raw_width) + projected[[1, 3]] = np.clip(projected[[1, 3]], 0.0, raw_height) + return projected, (raw_center_x, raw_center_y) + + +def _merge_raw_detections( + candidates: list[dict[str, Any]], + detector_profile: dict[str, Any], + valid_mask: np.ndarray, +) -> tuple[list[dict[str, Any]], dict[str, int]]: + detection = detector_profile["detection"] + integral = np.pad(valid_mask.astype(np.int64), ((1, 0), (1, 0))).cumsum(0).cumsum(1) + raw_height, raw_width = valid_mask.shape + admitted: list[dict[str, Any]] = [] + rejected = Counter() + for candidate in candidates: + box = np.asarray(candidate["bbox_xyxy"], dtype=np.float64) + valid_fraction, center_inside, area = _valid_fraction(box, integral) + if area < float(detection["minimum_box_area_pixels"]): + rejected["small_raw_box"] += 1 + continue + if area / float(raw_width * raw_height) > float( + detection["maximum_box_area_fraction"] + ): + rejected["large_raw_box"] += 1 + continue + if valid_fraction < float(detection["minimum_valid_fov_fraction"]): + rejected["outside_raw_valid_fov"] += 1 + continue + if detection["require_center_inside_valid_fov"] and not center_inside: + rejected["raw_center_outside_valid_fov"] += 1 + continue + enriched = dict(candidate) + enriched["valid_fov_fraction"] = round(valid_fraction, 6) + admitted.append(enriched) + + result: list[dict[str, Any]] = [] + class_ids = sorted({int(item["class_id"]) for item in admitted}) + for class_id in class_ids: + group = [item for item in admitted if int(item["class_id"]) == class_id] + boxes = np.asarray([item["bbox_xyxy"] for item in group], dtype=np.float64) + scores = np.asarray([item["score"] for item in group], dtype=np.float64) + keep = _nms( + boxes, + scores, + float(detection["nms_iou_threshold"]), + float(detection["nms_containment_threshold"]), + ) + result.extend(group[index] for index in keep) + rejected["cross_tile_duplicate"] += len(group) - len(keep) + result.sort(key=lambda item: (-float(item["score"]), int(item["class_id"]))) + return result, dict(rejected) + + +def detect_rectified( + image: np.ndarray, + *, + maps: dict[str, Any], + rectification_profile: dict[str, Any], + detector_profile: dict[str, Any], + valid_mask: np.ndarray, + triton_url: str, + contrast: str, + active_tiles: set[str], + cv2: Any, +) -> tuple[list[dict[str, Any]], dict[str, Any]]: + candidates: list[dict[str, Any]] = [] + tile_metrics: list[dict[str, Any]] = [] + tile_valid_mask = np.ones( + ( + int(rectification_profile["rectification"]["tile_size"]), + int(rectification_profile["rectification"]["tile_size"]), + ), + dtype=bool, + ) + + active_entries = [ + (tile_index, tile) + for tile_index, tile in enumerate(maps["tiles"]) + if tile["name"] in active_tiles + ] + + def prepare(entry: tuple[int, dict[str, Any]]) -> dict[str, Any]: + tile_index, tile = entry + started = time.perf_counter() + tile_image = cv2.remap( + image, + tile["raw_map_x"], + tile["raw_map_y"], + interpolation=cv2.INTER_LINEAR, + borderMode=cv2.BORDER_CONSTANT, + borderValue=(114, 114, 114), + ) + if contrast == "clahe": + tile_image = _clahe(tile_image, rectification_profile, cv2) + preprocess_started = time.perf_counter() + tensor = _preprocess(tile_image, tile_valid_mask, detector_profile) + preprocess_ms = (time.perf_counter() - preprocess_started) * 1000.0 + return { + "tile_index": tile_index, + "tile": tile, + "tensor": tensor, + "preprocess_ms": preprocess_ms, + "preparation_ms": (time.perf_counter() - started) * 1000.0, + } + + # Rectification/remap is CPU work and independent for each calibrated view. + # Keep the pool bounded to the three operational core views; Triton requests + # below remain sequential against the single canonical model instance. + with concurrent.futures.ThreadPoolExecutor( + max_workers=len(active_entries), thread_name_prefix="k1-kb4-view" + ) as executor: + prepared = list(executor.map(prepare, active_entries)) + + for item in prepared: + tile_index = int(item["tile_index"]) + tile = item["tile"] + tile_started = time.perf_counter() + output, inference_ms = _infer( + triton_url, detector_profile["model"], item["tensor"] + ) + tile_detections, tile_rejected = _detections( + output, detector_profile, tile_valid_mask + ) + owned = 0 + for detection in tile_detections: + projected = _project_tile_box( + detection["bbox_xyxy"], tile, maps["selected_tile"], tile_index + ) + if projected is None: + continue + raw_box, raw_center = projected + candidate = dict(detection) + candidate["bbox_xyxy"] = [round(float(value), 6) for value in raw_box] + candidate["raw_center_xy"] = [round(value, 6) for value in raw_center] + candidate["rectification_tile"] = tile["name"] + candidates.append(candidate) + owned += 1 + tile_metrics.append( + { + "tile": tile["name"], + "detections": len(tile_detections), + "owned_detections": owned, + "inference_ms": round(inference_ms, 6), + "preprocess_ms": round(float(item["preprocess_ms"]), 6), + "preparation_ms": round(float(item["preparation_ms"]), 6), + "serial_inference_postprocess_ms": round( + (time.perf_counter() - tile_started) * 1000.0, 6 + ), + "rejected": tile_rejected, + } + ) + merged, rejected = _merge_raw_detections(candidates, detector_profile, valid_mask) + return merged, { + "tiles": tile_metrics, + "candidate_count": len(candidates), + "rejected": rejected, + } + + +def _draw_overlay(image: np.ndarray, detections: list[dict[str, Any]]) -> Image.Image: + output = Image.fromarray(image) + draw = ImageDraw.Draw(output) + palette = { + "person": "#8cff5d", + "bicycle": "#50d7ff", + "car": "#ffffff", + "motorcycle": "#ffcd57", + "bus": "#ff8b5d", + "truck": "#ff8b5d", + } + for detection in detections: + box = tuple(float(value) for value in detection["bbox_xyxy"]) + color = palette.get(str(detection["label"]), "#ffffff") + draw.rectangle(box, outline=color, width=3) + label = ( + f"{detection['label']} {float(detection['score']):.0%} " + f"[{detection['rectification_tile']}]" + ) + text_box = draw.textbbox((box[0], max(0.0, box[1] - 18.0)), label) + draw.rectangle(text_box, fill="#0b0b0d") + draw.text((box[0], max(0.0, box[1] - 18.0)), label, fill=color) + return output + + +def main() -> None: + arguments = _arguments() + if arguments.limit < 0: + raise RuntimeError("--limit cannot be negative") + if arguments.tile_size and not 320 <= arguments.tile_size <= 1024: + raise RuntimeError("--tile-size must be zero or within [320, 1024]") + import cv2 + + rectification_profile = _read_object(arguments.rectification_profile) + if arguments.tile_size: + rectification_profile = copy.deepcopy(rectification_profile) + rectification_profile["rectification"]["tile_size"] = arguments.tile_size + detector_profile = _read_object(arguments.detector_profile) + job = _read_object(arguments.job) + active_tiles = { + value.strip() for value in arguments.active_tiles.split(",") if value.strip() + } + configured_tiles = { + str(tile["name"]) + for tile in rectification_profile["rectification"]["tiles"] + } + if not active_tiles or not active_tiles <= configured_tiles: + raise RuntimeError("--active-tiles contains an unknown or empty tile set") + expected_resolution = tuple(rectification_profile["source"]["resolution"]) + if expected_resolution != tuple(detector_profile["source"]["resolution"]): + raise RuntimeError("Rectification and detector source resolutions differ") + if ( + rectification_profile["source"]["calibration_sha256"] + != detector_profile["source"]["calibration_sha256"] + ): + raise RuntimeError("Rectification and detector calibration identities differ") + + valid_mask, _mask_metadata = _load_valid_fov( + arguments.valid_fov_root, job, detector_profile + ) + maps_started = time.perf_counter() + maps = _rectification_maps(rectification_profile, valid_mask) + maps_ms = (time.perf_counter() - maps_started) * 1000.0 + frame_paths = sorted(arguments.frames.glob("*.jpg")) + if arguments.limit: + frame_paths = frame_paths[: arguments.limit] + if not frame_paths: + raise RuntimeError("No JPEG benchmark frames were found") + + arguments.output.mkdir(parents=True, exist_ok=False) + overlays = arguments.output / "overlays" + overlays.mkdir() + documents: list[dict[str, Any]] = [] + frame_latencies: list[float] = [] + inference_latencies: list[float] = [] + detection_counts: list[int] = [] + for frame_path in frame_paths: + image = np.asarray(Image.open(frame_path).convert("RGB"), dtype=np.uint8) + if (image.shape[1], image.shape[0]) != expected_resolution: + raise RuntimeError(f"Unexpected camera resolution: {frame_path}") + started = time.perf_counter() + detections, diagnostics = detect_rectified( + image, + maps=maps, + rectification_profile=rectification_profile, + detector_profile=detector_profile, + valid_mask=valid_mask, + triton_url=arguments.triton_url, + contrast=arguments.contrast, + active_tiles=active_tiles, + cv2=cv2, + ) + elapsed_ms = (time.perf_counter() - started) * 1000.0 + frame_latencies.append(elapsed_ms) + inference_latencies.extend( + float(tile["inference_ms"]) for tile in diagnostics["tiles"] + ) + detection_counts.append(len(detections)) + overlay_name = frame_path.name + _draw_overlay(image, detections).save( + overlays / overlay_name, format="JPEG", quality=92, optimize=True + ) + documents.append( + { + "frame": frame_path.name, + "elapsed_ms": round(elapsed_ms, 6), + "detections": detections, + "diagnostics": diagnostics, + "overlay": f"overlays/{overlay_name}", + } + ) + print( + f"FRAME={frame_path.name} DETECTIONS={len(detections)} " + f"ELAPSED_MS={elapsed_ms:.3f}", + flush=True, + ) + + summary = { + "schema_version": "missioncore.rectified-yolox-benchmark/v1", + "pipeline": "k1-kb4-cubemap5-yolox-raw-frame/v1", + "contrast": arguments.contrast, + "active_tiles": sorted(active_tiles), + "tile_size": int(rectification_profile["rectification"]["tile_size"]), + "frame_count": len(documents), + "rectification_map_build_ms": round(maps_ms, 6), + "latency_ms": { + "mean": round(statistics.fmean(frame_latencies), 6), + "p50": round(_percentile(frame_latencies, 0.5), 6), + "p95": round(_percentile(frame_latencies, 0.95), 6), + "maximum": round(max(frame_latencies), 6), + }, + "tile_inference_ms": { + "mean": round(statistics.fmean(inference_latencies), 6), + "p95": round(_percentile(inference_latencies, 0.95), 6), + "maximum": round(max(inference_latencies), 6), + }, + "detections_per_frame": { + "mean": round(statistics.fmean(detection_counts), 6), + "minimum": min(detection_counts), + "maximum": max(detection_counts), + "total": sum(detection_counts), + }, + "coverage": maps["coverage"], + "frames": documents, + } + (arguments.output / "benchmark.json").write_text( + json.dumps(summary, ensure_ascii=False, sort_keys=True, indent=2) + "\n", + encoding="utf-8", + ) + print(json.dumps({key: value for key, value in summary.items() if key != "frames"})) + + +if __name__ == "__main__": + main() diff --git a/experiments/perception/worker/run_rectified_yolox_qualification.py b/experiments/perception/worker/run_rectified_yolox_qualification.py new file mode 100644 index 0000000..09b7f09 --- /dev/null +++ b/experiments/perception/worker/run_rectified_yolox_qualification.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +"""Qualify the rectified YOLOX + existing ByteTrack path on full RAVNOVES00.""" + +from __future__ import annotations + +import argparse +import copy +import json +import platform +import statistics +import time +from collections import Counter +from pathlib import Path +from typing import Any + +import numpy as np +from PIL import Image +from run_e3_rectified_segmentation import _rectification_maps +from run_e5_instance_tracking import ( + TwoStageTracker, + _duplicate_pairs, + _load_valid_fov, + _track_document, +) +from run_rectified_yolox_detector import ( + _draw_overlay, + _percentile, + _read_object, + detect_rectified, +) + + +def _arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--job", type=Path, required=True) + parser.add_argument("--video", type=Path, required=True) + parser.add_argument("--rectification-profile", type=Path, required=True) + parser.add_argument("--detector-profile", type=Path, required=True) + parser.add_argument("--valid-fov-root", type=Path, required=True) + parser.add_argument("--triton-url", required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--active-tiles", default="front,left,right") + parser.add_argument("--tile-size", type=int, default=640) + parser.add_argument("--preview-stride", type=int, default=250) + parser.add_argument("--limit", type=int, default=0) + return parser.parse_args() + + +def _latency(values: list[float]) -> dict[str, float]: + return { + "mean": round(statistics.fmean(values), 6), + "p50": round(_percentile(values, 0.5), 6), + "p95": round(_percentile(values, 0.95), 6), + "p99": round(_percentile(values, 0.99), 6), + "maximum": round(max(values), 6), + } + + +def main() -> None: + arguments = _arguments() + if arguments.tile_size != 640: + raise RuntimeError("Operational qualification is fixed to detector-native 640 tiles") + if arguments.preview_stride < 1 or arguments.limit < 0: + raise RuntimeError("Preview stride or frame limit is invalid") + if not arguments.triton_url.startswith("http://"): + raise RuntimeError("Triton URL must use the internal HTTP endpoint") + import cv2 + + job = _read_object(arguments.job) + if ( + job.get("job_id") != "recorded-camera-602ac89026ed12978619801d" + or job.get("input", {}).get("session_id") != "20260720T065719Z_viewer_live" + or job.get("input", {}).get("source_id") != "sensor.camera.right" + or job.get("input", {}).get("segment_count") != 4489 + ): + raise RuntimeError("Qualification is not bound to immutable RAVNOVES00") + + rectification_profile = copy.deepcopy(_read_object(arguments.rectification_profile)) + detector_profile = _read_object(arguments.detector_profile) + rectification_profile["rectification"]["tile_size"] = arguments.tile_size + if ( + rectification_profile["source"]["calibration_sha256"] + != detector_profile["source"]["calibration_sha256"] + ): + raise RuntimeError("Calibration identity differs between detector stages") + active_tiles = { + value.strip() for value in arguments.active_tiles.split(",") if value.strip() + } + if active_tiles != {"front", "left", "right"}: + raise RuntimeError("Operational qualification requires front,left,right views") + + valid_mask, valid_fov = _load_valid_fov( + arguments.valid_fov_root, job, detector_profile + ) + maps_started = time.perf_counter() + maps = _rectification_maps(rectification_profile, valid_mask) + maps_ms = (time.perf_counter() - maps_started) * 1000.0 + + video = arguments.video.resolve(strict=True) + capture = cv2.VideoCapture(str(video)) + if not capture.isOpened(): + raise RuntimeError("OpenCV could not open the immutable camera replay") + width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT)) + source_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT)) + if (width, height, source_count) != (800, 600, 4489): + raise RuntimeError("Camera replay metadata violates the RAVNOVES00 contract") + + output = arguments.output.resolve() + if output.exists(): + raise RuntimeError("Qualification output must be absent") + output.mkdir(parents=True, mode=0o700) + previews = output / "previews" + previews.mkdir() + frames_path = output / "frames.jsonl" + tracker = TwoStageTracker(detector_profile["tracking"]) + from scipy.optimize import linear_sum_assignment + + linear_sum_assignment(np.zeros((1, 1), dtype=np.float64)) + + latency: list[float] = [] + decode_latency: list[float] = [] + detector_latency: list[float] = [] + tracking_latency: list[float] = [] + detections_by_label: Counter[str] = Counter() + tracks_by_label: Counter[str] = Counter() + rejections: Counter[str] = Counter() + unique_tracks: set[int] = set() + duplicate_pairs = 0 + processed = 0 + failures = 0 + + # One explicit warmup makes the source-rate metrics independent from model + # initialization while preserving the first source frame for the real run. + ok, warm_bgr = capture.read() + if not ok: + raise RuntimeError("Camera replay has no warmup frame") + warm_rgb = cv2.cvtColor(warm_bgr, cv2.COLOR_BGR2RGB) + detect_rectified( + warm_rgb, + maps=maps, + rectification_profile=rectification_profile, + detector_profile=detector_profile, + valid_mask=valid_mask, + triton_url=arguments.triton_url, + contrast="none", + active_tiles=active_tiles, + cv2=cv2, + ) + capture.set(cv2.CAP_PROP_POS_FRAMES, 0) + + run_started = time.perf_counter() + try: + with frames_path.open("x", encoding="utf-8", newline="\n") as stream: + while True: + if arguments.limit and processed >= arguments.limit: + break + decode_started = time.perf_counter() + ok, bgr = capture.read() + decoded = time.perf_counter() + if not ok: + break + image = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) + frame_started = time.perf_counter() + try: + detections, diagnostics = detect_rectified( + image, + maps=maps, + rectification_profile=rectification_profile, + detector_profile=detector_profile, + valid_mask=valid_mask, + triton_url=arguments.triton_url, + contrast="none", + active_tiles=active_tiles, + cv2=cv2, + ) + detected = time.perf_counter() + tracks = tracker.update(detections, processed) + tracked = time.perf_counter() + except Exception: + failures += 1 + raise + + frame_ms = (tracked - frame_started) * 1000.0 + latency.append(frame_ms) + decode_latency.append((decoded - decode_started) * 1000.0) + detector_latency.append((detected - frame_started) * 1000.0) + tracking_latency.append((tracked - detected) * 1000.0) + detections_by_label.update(str(item["label"]) for item in detections) + tracks_by_label.update(track.label for track in tracks) + for track in tracks: + unique_tracks.add(track.track_id) + duplicate_pairs += _duplicate_pairs(tracks) + rejections.update(diagnostics["rejected"]) + + document = { + "schema_version": "missioncore.rectified-yolox-frame/v1", + "frame_index": processed, + "sequence": processed + 1, + "detections": detections, + "tracks": [_track_document(track) for track in tracks], + "processing_ms": round(frame_ms, 6), + "tile_inference_ms": round( + sum(float(tile["inference_ms"]) for tile in diagnostics["tiles"]), + 6, + ), + } + stream.write( + json.dumps( + document, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ) + + "\n" + ) + if processed % arguments.preview_stride == 0: + _draw_overlay(image, detections).save( + previews / f"frame-{processed:06d}.jpg", + format="JPEG", + quality=92, + optimize=True, + ) + processed += 1 + if processed % 100 == 0: + stream.flush() + print( + f"PHASE=rectified-yolox FRAMES={processed} " + f"LAST_MS={frame_ms:.3f}", + flush=True, + ) + finally: + capture.release() + wall_seconds = time.perf_counter() - run_started + expected = arguments.limit or source_count + steady = latency[1:] if len(latency) > 1 else latency + effective_fps = processed / wall_seconds + steady_latency = _latency(steady) + checks = { + "complete_frame_accounting": processed == expected, + "zero_failures": failures == 0, + "minimum_effective_fps_9_5": effective_fps >= 9.5, + "maximum_steady_p95_ms_100": steady_latency["p95"] <= 100.0, + "same_class_duplicate_pairs_zero": duplicate_pairs == 0, + } + accepted = all(checks.values()) + report: dict[str, Any] = { + "schema_version": "missioncore.rectified-yolox-qualification/v1", + "state": "accepted" if accepted else "rejected", + "source": { + "job_id": job["job_id"], + "session_id": job["input"]["session_id"], + "source_id": job["input"]["source_id"], + "frame_count": source_count, + "calibration_sha256": detector_profile["source"]["calibration_sha256"], + }, + "pipeline": { + "id": "k1-kb4-core3-yolox-bytetrack/v1", + "rectification": "five-perspective-gnomonic/v1", + "active_tiles": sorted(active_tiles), + "tile_size": arguments.tile_size, + "detector": detector_profile["model"], + "tracker": detector_profile["tracking"], + "peripheral_views": "up/down remain on the existing 2 Hz semantic cadence", + }, + "runtime": { + "hostname": platform.node(), + "python": platform.python_version(), + "opencv": cv2.__version__, + "rectification_map_build_ms": round(maps_ms, 6), + "wall_seconds": round(wall_seconds, 6), + "effective_fps": round(effective_fps, 6), + }, + "metrics": { + "frames_processed": processed, + "failures": failures, + "latency_ms": _latency(latency), + "steady_latency_ms": steady_latency, + "decode_latency_ms": _latency(decode_latency), + "detector_latency_ms": _latency(detector_latency), + "tracking_latency_ms": _latency(tracking_latency), + "detections": int(sum(detections_by_label.values())), + "detections_by_label": dict(sorted(detections_by_label.items())), + "unique_confirmed_tracks": len(unique_tracks), + "track_observations": int(sum(tracks_by_label.values())), + "track_observations_by_label": dict(sorted(tracks_by_label.items())), + "rejections": dict(sorted(rejections.items())), + "same_class_duplicate_pairs_iou_ge_0_8": duplicate_pairs, + "tracker_tracks_created": tracker.created, + "tracker_tracks_retired": tracker.retired, + }, + "acceptance": {"accepted": accepted, "checks": checks}, + "valid_fov": valid_fov, + "limitations": [ + "RAVNOVES00 has no independent exhaustive object ground truth.", + "This gate qualifies runtime and visual evidence, not safety accuracy.", + "LiDAR association, temporal world state and segmentation are existing downstream stages and are not recomputed by this detector-only gate.", + ], + "artifacts": { + "frames": "frames.jsonl", + "previews": "previews/", + }, + } + (output / "qualification.json").write_text( + json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", + encoding="utf-8", + ) + print( + json.dumps( + { + "accepted": accepted, + "frames_processed": processed, + "effective_fps": round(effective_fps, 6), + "steady_p95_ms": steady_latency["p95"], + "detections": report["metrics"]["detections"], + "unique_confirmed_tracks": len(unique_tracks), + }, + sort_keys=True, + ), + flush=True, + ) + raise SystemExit(0 if accepted else 2) + + +if __name__ == "__main__": + main() diff --git a/src/k1link/compute/e46_lab_review_submission.py b/src/k1link/compute/e46_lab_review_submission.py new file mode 100644 index 0000000..e99b828 --- /dev/null +++ b/src/k1link/compute/e46_lab_review_submission.py @@ -0,0 +1,292 @@ +"""Freeze one Mission Core LAB annotation session as an E48 review submission.""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import shutil +import uuid +from pathlib import Path +from typing import Any, Final + +from k1link.compute.e46_detector_truth_island import ( + E46_REFERENCES_NAME, + E46DetectorTruthIslandError, + read_e46_detector_truth_island, +) +from k1link.compute.e48_detector_truth_seal import ( + E48_REVIEW_SCHEMA, + E48DetectorTruthSealError, + validate_e48_detector_review_submission, +) + +E46_LAB_REVIEW_SCHEMA: Final = "missioncore.e46-lab-review-submission/v1" +E46_LAB_REVIEW_MANIFEST: Final = "manifest.json" +E46_LAB_REVIEW_DOCUMENT: Final = "review-submission.json" +_SESSION_SCHEMA: Final = "missioncore.l34-annotation-session/v3" +_RESULT_ID = re.compile(r"^e46-lab-review-submission-[a-f0-9]{64}$") +_REVIEWER_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{1,63}$") +_BLINDNESS: Final = { + "candidate_identity_seen": False, + "model_prelabels_seen": False, + "model_predictions_seen": False, + "model_scores_seen": False, +} +_AUTHORITY: Final = { + "ground_truth": False, + "independent_truth": False, + "metric_grade_reference": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class E46LabReviewSubmissionError(ValueError): + """Raised when an E46 LAB session cannot become an E48 review input.""" + + +def build_e46_lab_review_submission( + *, + truth_island_root: Path, + annotation_session_path: Path, + reviewer_id: str, + output_root: Path, +) -> dict[str, Any]: + try: + truth = read_e46_detector_truth_island(truth_island_root) + except (E46DetectorTruthIslandError, OSError) as exc: + raise E46LabReviewSubmissionError("E46 truth island invalid") from exc + reviewer = reviewer_id.strip() + if _REVIEWER_ID.fullmatch(reviewer) is None: + raise E46LabReviewSubmissionError("reviewer identity invalid") + session_path = annotation_session_path.resolve(strict=True) + if not session_path.is_file() or session_path.is_symlink(): + raise E46LabReviewSubmissionError("annotation session unavailable") + session = _read_json(session_path) + references = tuple(_read_jsonl(truth.result_root / E46_REFERENCES_NAME)) + _validate_session(session, truth.result_id, references) + frames = {int(item["truth_island_sequence"]): item for item in session["frames"]} + images: list[dict[str, Any]] = [] + for reference in references: + sequence = int(reference["truth_island_sequence"]) + frame = frames[sequence] + objects: list[dict[str, Any]] = [] + for item in frame["objects"]: + category = item.get("category") + if category == "unmapped" or item.get("proposed_label") is not None: + raise E46LabReviewSubmissionError("E46 review class invalid") + objects.append( + { + "object_id": item["object_id"], + "category": category, + "box_xyxy": item["box_xyxy"], + "occluded": item["occluded"], + "truncated": item["truncated"], + "notes": None, + } + ) + images.append( + { + "truth_island_sequence": sequence, + "image_id": reference["image_id"], + "frame_index": reference["frame_index"], + "session_seconds": reference["session_seconds"], + "role": reference["role"], + "group_id": reference["group_id"], + "source_path": reference["source_path"], + "source_sha256": reference["sha256"], + "review_state": "reviewed", + "hard_negative": frame["hard_negative"], + "objects": objects, + "notes": None, + } + ) + review = { + "schema_version": E48_REVIEW_SCHEMA, + "truth_island_id": truth.result_id, + "state": "completed-independent-no-model-assistance", + "reviewer_id": reviewer, + "review_round": 1, + "blindness": _BLINDNESS, + "images": images, + "acceptance": { + "all_images_reviewed": True, + "independent": True, + "submitted_at_utc": session["updated_at_utc"], + }, + } + review_sha256 = hashlib.sha256(_canonical_json(review)).hexdigest() + identity = { + "schema_version": E46_LAB_REVIEW_SCHEMA, + "truth_island_id": truth.result_id, + "annotation_session_id": session["session_id"], + "annotation_session_sha256": _sha256(session_path), + "reviewer_id": reviewer, + "review_sha256": review_sha256, + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "blindness": _BLINDNESS, + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e46-lab-review-submission-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_e46_lab_review_submission(destination, truth_island_root=truth.result_root) + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700) + try: + _write_json(staging / E46_LAB_REVIEW_DOCUMENT, review) + validate_e48_detector_review_submission( + truth_island_root=truth.result_root, + review_path=staging / E46_LAB_REVIEW_DOCUMENT, + ) + _write_json( + staging / E46_LAB_REVIEW_MANIFEST, + { + "schema_version": E46_LAB_REVIEW_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": session["updated_at_utc"], + "state": "completed-e48-review-input-not-truth", + "ground_truth": False, + "artifacts": [ + { + "path": E46_LAB_REVIEW_DOCUMENT, + "role": "e48-review-submission", + "byte_length": (staging / E46_LAB_REVIEW_DOCUMENT).stat().st_size, + "sha256": _sha256(staging / E46_LAB_REVIEW_DOCUMENT), + } + ], + "authority": _AUTHORITY, + }, + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_e46_lab_review_submission(destination, truth_island_root=truth.result_root) + + +def read_e46_lab_review_submission( + root: Path, + *, + truth_island_root: Path, +) -> dict[str, Any]: + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / E46_LAB_REVIEW_MANIFEST) + identity = manifest.get("identity") + if not isinstance(identity, dict): + raise E46LabReviewSubmissionError("review identity invalid") + digest = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e46-lab-review-submission-{digest}" + artifact = resolved / E46_LAB_REVIEW_DOCUMENT + if ( + manifest.get("schema_version") != E46_LAB_REVIEW_SCHEMA + or manifest.get("identity_sha256") != digest + or manifest.get("result_id") != result_id + or resolved.name != result_id + or _RESULT_ID.fullmatch(result_id) is None + or manifest.get("state") != "completed-e48-review-input-not-truth" + or manifest.get("ground_truth") is not False + or manifest.get("authority") != _AUTHORITY + or not artifact.is_file() + or artifact.is_symlink() + or hashlib.sha256(_canonical_json(_read_json(artifact))).hexdigest() + != identity.get("review_sha256") + ): + raise E46LabReviewSubmissionError("review submission changed") + artifacts = manifest.get("artifacts") + artifact_row = artifacts[0] if isinstance(artifacts, list) and len(artifacts) == 1 else None + if ( + not isinstance(artifact_row, dict) + or artifact_row.get("path") != E46_LAB_REVIEW_DOCUMENT + or artifact_row.get("role") != "e48-review-submission" + or artifact_row.get("byte_length") != artifact.stat().st_size + or artifact_row.get("sha256") != _sha256(artifact) + ): + raise E46LabReviewSubmissionError("review artifact changed") + try: + review = validate_e48_detector_review_submission( + truth_island_root=truth_island_root, + review_path=artifact, + ) + except (E48DetectorTruthSealError, OSError) as exc: + raise E46LabReviewSubmissionError("E48 review submission invalid") from exc + return { + "result_id": result_id, + "result_root": resolved, + "manifest": manifest, + "review": review, + } + + +def _validate_session( + session: dict[str, Any], + truth_island_id: str, + references: tuple[dict[str, Any], ...], +) -> None: + frames = session.get("frames") + if ( + session.get("schema_version") != _SESSION_SCHEMA + or session.get("result_id") != truth_island_id + or session.get("truth_island_id") != truth_island_id + or session.get("contract_id") != "e46-detector-blind-review/v1" + or session.get("state") != "saved" + or session.get("blindness") != _BLINDNESS + or session.get("assistance") + != {"mode": "prediction-free-manual", "independent_truth_eligible": False} + or not isinstance(frames, list) + or len(frames) != len(references) + ): + raise E46LabReviewSubmissionError("annotation session incomplete") + by_sequence = {int(item["truth_island_sequence"]): item for item in frames} + if len(by_sequence) != len(frames): + raise E46LabReviewSubmissionError("annotation frame duplicated") + for reference in references: + frame = by_sequence.get(int(reference["truth_island_sequence"])) + if ( + frame is None + or frame.get("reviewed") is not True + or frame.get("image_id") != reference["image_id"] + or frame.get("frame_index") != reference["frame_index"] + or frame.get("source_sha256") != reference["sha256"] + or frame.get("hard_negative") != (len(frame.get("objects", [])) == 0) + ): + raise E46LabReviewSubmissionError("annotation source identity changed") + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +def _read_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise E46LabReviewSubmissionError("expected JSON object") + return value + + +def _read_jsonl(path: Path) -> list[dict[str, Any]]: + rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line] + if any(not isinstance(row, dict) for row in rows): + raise E46LabReviewSubmissionError("expected JSONL objects") + return rows + + +def _write_json(path: Path, value: object) -> None: + path.write_bytes(_canonical_json(value) + b"\n") + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() diff --git a/src/k1link/compute/e46a_ai_engineering_preannotation.py b/src/k1link/compute/e46a_ai_engineering_preannotation.py new file mode 100644 index 0000000..eb1b9d8 --- /dev/null +++ b/src/k1link/compute/e46a_ai_engineering_preannotation.py @@ -0,0 +1,766 @@ +"""Freeze an AI-audited engineering preannotation for the 32 E46 frames. + +E46A is deliberately derived from the candidate-visible L3.4F engineering +reference. It is useful as an editable starting point and visual evidence, +but it is neither an independent review nor ground truth. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +import math +import os +import re +import shutil +import uuid +from collections import Counter +from dataclasses import dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from k1link.compute.e46_detector_truth_island import ( + E46_MANIFEST_NAME, + E46_REFERENCES_NAME, + read_e46_detector_truth_island, +) +from k1link.compute.e47_detector_candidate_freeze import ( + E47_MANIFEST_NAME, + E47_PREDICTIONS_NAME, + read_e47_detector_candidate_freeze, +) +from k1link.compute.l34f_adjudicated_reference import ( + L34F_MANIFEST_NAME, + read_l34f_adjudicated_reference, +) + +E46A_RESULT_SCHEMA: Final = "missioncore.e46a-ai-engineering-preannotation/v1" +E46A_REPORT_SCHEMA: Final = "missioncore.e46a-ai-engineering-preannotation-report/v1" +E46A_CASE_SCHEMA: Final = "missioncore.e46a-ai-engineering-preannotation-case/v1" +E46A_MANIFEST_NAME: Final = "manifest.json" +E46A_REPORT_NAME: Final = "ai-engineering-preannotation-report.json" +E46A_CASES_NAME: Final = "ai-engineering-preannotations.jsonl" + +_RESULT_ID = re.compile(r"^e46a-ai-engineering-preannotation-[a-f0-9]{64}$") +_AUTHORITY: Final = { + "ground_truth": False, + "independent_truth": False, + "metric_grade_reference": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} +_CUSTOM_CLASS_BY_PROPOSED_LABEL: Final = { + "Детская коляска": "stroller", + "Ноутбук": "laptop", +} +_VEHICLE_CATEGORIES: Final = frozenset({"car", "heavy_vehicle"}) + + +@dataclass(frozen=True, slots=True) +class E46AVisualAuditProfile: + """Source-scoped object QA applied to the candidate-visible seed.""" + + profile_id: str + geometry_candidate_id: str + candidate_nms_iou: float + maximum_match_cost: float + expected_source_object_count: int + expected_final_object_count: int + expected_geometry_snapped_count: int + delete_object_ids: tuple[str, ...] + category_overrides: tuple[tuple[str, str], ...] + + def to_dict(self) -> dict[str, object]: + return { + "profile_id": self.profile_id, + "geometry_candidate_id": self.geometry_candidate_id, + "candidate_nms_iou": self.candidate_nms_iou, + "maximum_match_cost": self.maximum_match_cost, + "expected_source_object_count": self.expected_source_object_count, + "expected_final_object_count": self.expected_final_object_count, + "expected_geometry_snapped_count": ( + self.expected_geometry_snapped_count + ), + "delete_object_ids": list(self.delete_object_ids), + "category_overrides": dict(self.category_overrides), + } + + +RAVNOVES00_E46A_VISUAL_AUDIT_V2: Final = E46AVisualAuditProfile( + profile_id="ravnoves00-right-e46a-object-qa/v2", + geometry_candidate_id="maskrcnn-kb4-valid-fov-fill", + candidate_nms_iou=0.3, + maximum_match_cost=1.3, + expected_source_object_count=260, + expected_final_object_count=245, + expected_geometry_snapped_count=217, + delete_object_ids=( + "self-03-06", + "self-04-09", + "self-09-05", + "self-29-08", + "self-29-09", + "self-29-10", + "self-30-08", + "self-30-09", + "self-30-10", + "self-31-08", + "self-31-09", + "self-31-10", + "self-32-08", + "self-32-09", + "self-32-10", + ), + category_overrides=(("self-20-06", "car"),), +) + + +class E46AAiEngineeringPreannotationError(ValueError): + """Raised when E46A input binding or immutable output is invalid.""" + + +def build_e46a_ai_engineering_preannotation( + *, + e46_root: Path, + l34f_root: Path, + output_root: Path, + geometry_candidate_root: Path | None = None, + audit_profile: E46AVisualAuditProfile = RAVNOVES00_E46A_VISUAL_AUDIT_V2, +) -> dict[str, Any]: + """Build a path-free, hash-bound 32-frame engineering preannotation.""" + + e46 = read_e46_detector_truth_island(e46_root) + l34f = read_l34f_adjudicated_reference(l34f_root) + e46_references = tuple(_read_jsonl(e46.result_root / E46_REFERENCES_NAME)) + if len(e46_references) != 32 or len(l34f["cases"]) != 32: + raise E46AAiEngineeringPreannotationError("E46A requires exactly 32 frames") + e46_by_sequence = { + int(row["truth_island_sequence"]): row for row in e46_references + } + cases: list[dict[str, Any]] = [] + relabeled_count = 0 + for source in sorted( + l34f["cases"], key=lambda row: int(row["truth_island_sequence"]) + ): + sequence = int(source["truth_island_sequence"]) + e46_reference = e46_by_sequence.get(sequence) + if e46_reference is None or any( + source.get(key) != e46_reference.get(target) + for key, target in ( + ("image_id", "image_id"), + ("frame_index", "frame_index"), + ("group_id", "group_id"), + ("source_image_sha256", "sha256"), + ) + ): + raise E46AAiEngineeringPreannotationError( + f"L3.4F frame {sequence} is not bound to E46" + ) + objects: list[dict[str, Any]] = [] + for raw in source.get("references", []): + if not isinstance(raw, dict): + raise E46AAiEngineeringPreannotationError("E46A object is invalid") + item = copy.deepcopy(raw) + category = item.get("category") + if category == "unmapped": + category = _CUSTOM_CLASS_BY_PROPOSED_LABEL.get( + str(item.get("proposed_label")) + ) + if category is None: + raise E46AAiEngineeringPreannotationError( + "E46A contains an unresolved custom class" + ) + relabeled_count += 1 + if not isinstance(category, str) or not category: + raise E46AAiEngineeringPreannotationError("E46A class is invalid") + item["category"] = category + item["origin"] = "ai_engineering_preannotation" + item["source_origin"] = raw.get("origin") + objects.append(item) + cases.append( + { + "schema_version": E46A_CASE_SCHEMA, + "truth_island_sequence": sequence, + "image_id": int(source["image_id"]), + "frame_index": int(source["frame_index"]), + "group_id": str(source["group_id"]), + "session_seconds": float(source["session_seconds"]), + "source_image_sha256": str(source["source_image_sha256"]), + "objects": objects, + "object_count": len(objects), + "hard_negative": len(objects) == 0, + "visual_audit_state": "derived-reference-not-object-audited", + } + ) + + geometry_audit: dict[str, Any] | None = None + geometry_candidate: dict[str, Any] | None = None + if geometry_candidate_root is not None: + geometry_candidate = read_e47_detector_candidate_freeze( + geometry_candidate_root + ) + truth_island = geometry_candidate["manifest"]["identity"].get( + "truth_island" + ) + if ( + not isinstance(truth_island, dict) + or truth_island.get("result_id") != e46.result_id + ): + raise E46AAiEngineeringPreannotationError( + "E46A geometry candidate is not bound to E46" + ) + prediction_rows = tuple( + _read_jsonl( + geometry_candidate["result_root"] / E47_PREDICTIONS_NAME + ) + ) + geometry_audit = _apply_visual_audit( + cases=cases, + prediction_rows=prediction_rows, + profile=audit_profile, + ) + + class_counts = Counter( + str(item["category"]) + for case in cases + for item in case["objects"] + ) + source_object_count = ( + int(geometry_audit["source_object_count"]) + if geometry_audit is not None + else sum(len(case["objects"]) for case in cases) + ) + metrics = { + "frame_count": len(cases), + "reviewed_frame_count": len(cases) if geometry_audit is not None else 0, + "source_object_count": source_object_count, + "object_count": sum(len(case["objects"]) for case in cases), + "custom_class_relabel_count": relabeled_count, + "hard_negative_frame_count": sum(bool(case["hard_negative"]) for case in cases), + "class_counts": dict(sorted(class_counts.items())), + "independent_review_submission_count": 0, + } + if geometry_audit is not None: + metrics.update( + { + "deleted_false_box_count": geometry_audit[ + "deleted_false_box_count" + ], + "geometry_snapped_object_count": geometry_audit[ + "geometry_snapped_object_count" + ], + "source_geometry_retained_object_count": geometry_audit[ + "source_geometry_retained_object_count" + ], + "category_corrected_object_count": geometry_audit[ + "category_corrected_object_count" + ], + } + ) + visual_review_complete = geometry_audit is not None + report_basis = { + "schema_version": E46A_REPORT_SCHEMA, + "status": "completed-ai-engineering-preannotation-not-independent-not-truth", + "metrics": metrics, + "assistance": { + "candidate_identity_seen": True, + "model_predictions_seen": True, + "model_scores_seen": geometry_audit is not None, + "derived_reference_seen": True, + "independent_truth_eligible": False, + }, + "object_level_audit": copy.deepcopy(geometry_audit), + "taxonomy": { + "classes": sorted(class_counts), + "custom_classes": ["laptop", "stroller"], + "unresolved_class_count": 0, + }, + "decision": { + "preannotation_available": True, + "visual_review_complete": visual_review_complete, + "independent_review_count_affected": False, + "e48_truth_seal_open": False, + "l35_acceptance_open": False, + "next_action": ( + "use E46A only for assisted correction/approval; keep Reviewer A and " + "Reviewer B prediction-free and independent" + ), + }, + "limitations": [ + ( + "the labels derive from a candidate-visible engineering reference; " + "the frozen Mask R-CNN candidate is used only to refine geometry" + if geometry_audit is not None + else "the derived reference has not completed object-level visual QA" + ), + ( + "E46A is an assisted preannotation and cannot occupy either " + "independent E46 reviewer slot" + ), + ( + "no AP, recall, miss, false-positive, detector acceptance, live, " + "LiDAR-range, command, navigation, or safety claim is made" + ), + ( + "the evidence is limited to recorded sensor.camera.right frames " + "from one RAVNOVES00 route" + ), + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + identity = { + "schema_version": E46A_RESULT_SCHEMA, + "e46_source": { + "result_id": e46.result_id, + "manifest_sha256": _sha256(e46.result_root / E46_MANIFEST_NAME), + }, + "l34f_engineering_reference": { + "result_id": l34f["result_id"], + "manifest_sha256": _sha256(l34f["result_root"] / L34F_MANIFEST_NAME), + }, + "audit_profile": ( + audit_profile.to_dict() + if geometry_audit is not None + else "derived-reference-no-object-level-audit/v1" + ), + "custom_class_mapping": _CUSTOM_CLASS_BY_PROPOSED_LABEL, + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + if geometry_candidate is not None: + identity["e47_geometry_candidate"] = { + "result_id": geometry_candidate["result_id"], + "manifest_sha256": _sha256( + geometry_candidate["result_root"] / E47_MANIFEST_NAME + ), + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e46a-ai-engineering-preannotation-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_e46a_ai_engineering_preannotation(destination) + created_at_utc = _utc_now() + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc, + "source_session_id": "RAVNOVES00", + "camera_source_id": "sensor.camera.right", + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / E46A_REPORT_NAME, report) + _write_jsonl(staging / E46A_CASES_NAME, cases) + artifacts = [ + _artifact(staging / E46A_REPORT_NAME, "preannotation-report"), + _artifact(staging / E46A_CASES_NAME, "preannotation-cases"), + ] + _write_json( + staging / E46A_MANIFEST_NAME, + { + "schema_version": E46A_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "ai-engineering-preannotation-not-truth", + "ground_truth": False, + "artifacts": artifacts, + "authority": _AUTHORITY, + }, + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_e46a_ai_engineering_preannotation(destination) + + +def read_e46a_ai_engineering_preannotation(root: Path) -> dict[str, Any]: + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / E46A_MANIFEST_NAME) + identity = manifest.get("identity") + if not isinstance(identity, dict): + raise E46AAiEngineeringPreannotationError("E46A identity is invalid") + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + if ( + manifest.get("schema_version") != E46A_RESULT_SCHEMA + or manifest.get("identity_sha256") != identity_sha256 + or manifest.get("result_id") + != f"e46a-ai-engineering-preannotation-{identity_sha256}" + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("acceptance_state") + != "ai-engineering-preannotation-not-truth" + or manifest.get("authority") != _AUTHORITY + or manifest.get("ground_truth") is not False + ): + raise E46AAiEngineeringPreannotationError("E46A identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 2: + raise E46AAiEngineeringPreannotationError("E46A artifacts are invalid") + by_role = {item.get("role"): item for item in artifacts if isinstance(item, dict)} + report = _read_json(_validated_artifact(resolved, by_role.get("preannotation-report"))) + cases = tuple(_read_jsonl(_validated_artifact(resolved, by_role.get("preannotation-cases")))) + if ( + report.get("schema_version") != E46A_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("status") + != "completed-ai-engineering-preannotation-not-independent-not-truth" + or report.get("authority") != _AUTHORITY + or report.get("ground_truth") is not False + or len(cases) != 32 + or any(case.get("schema_version") != E46A_CASE_SCHEMA for case in cases) + or any( + item.get("category") == "unmapped" + for case in cases + for item in case.get("objects", []) + if isinstance(item, dict) + ) + or hashlib.sha256(_canonical_json(cases)).hexdigest() + != identity.get("cases_sha256") + ): + raise E46AAiEngineeringPreannotationError("E46A result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "cases": cases, + } + + +def _apply_visual_audit( + *, + cases: list[dict[str, Any]], + prediction_rows: tuple[dict[str, Any], ...], + profile: E46AVisualAuditProfile, +) -> dict[str, Any]: + source_object_ids = { + str(item.get("object_id")) + for case in cases + for item in case.get("objects", []) + if isinstance(item, dict) + } + source_object_count = sum(len(case.get("objects", [])) for case in cases) + delete_object_ids = set(profile.delete_object_ids) + category_overrides = dict(profile.category_overrides) + if ( + source_object_count != profile.expected_source_object_count + or len(source_object_ids) != source_object_count + or not delete_object_ids <= source_object_ids + or not set(category_overrides) <= source_object_ids - delete_object_ids + or not 0.0 < profile.candidate_nms_iou < 1.0 + or profile.maximum_match_cost <= 0.0 + ): + raise E46AAiEngineeringPreannotationError( + "E46A visual-audit profile is not bound to the source objects" + ) + + rows = { + int(row["truth_island_sequence"]): row + for row in prediction_rows + if row.get("candidate_id") == profile.geometry_candidate_id + } + if set(rows) != set(range(1, 33)): + raise E46AAiEngineeringPreannotationError( + "E46A geometry candidate coverage is incomplete" + ) + + snapped_count = 0 + retained_count = 0 + category_corrections: list[dict[str, str]] = [] + for case in cases: + sequence = int(case["truth_island_sequence"]) + row = rows[sequence] + if row.get("source_image_sha256") != case.get("source_image_sha256"): + raise E46AAiEngineeringPreannotationError( + f"E46A geometry frame {sequence} changed" + ) + raw_objects = case.get("objects") + raw_predictions = row.get("predictions") + if not isinstance(raw_objects, list) or not isinstance(raw_predictions, list): + raise E46AAiEngineeringPreannotationError( + "E46A visual-audit payload is invalid" + ) + objects = [ + item + for item in raw_objects + if str(item.get("object_id")) not in delete_object_ids + ] + for item in objects: + object_id = str(item["object_id"]) + override = category_overrides.get(object_id) + if override is not None and override != item.get("category"): + category_corrections.append( + { + "object_id": object_id, + "before": str(item.get("category")), + "after": override, + } + ) + item["source_category"] = item.get("category") + item["category"] = override + + predictions = _candidate_nms( + tuple(_validated_prediction(item) for item in raw_predictions), + threshold=profile.candidate_nms_iou, + ) + pairs: list[tuple[float, int, int]] = [] + for object_index, item in enumerate(objects): + category = str(item["category"]) + if category in {"stroller", "laptop"}: + continue + source_box = _box(item.get("box_xyxy")) + source_center = _center(source_box) + source_diagonal = max( + 12.0, + math.hypot( + source_box[2] - source_box[0], + source_box[3] - source_box[1], + ), + ) + for prediction_index, prediction in enumerate(predictions): + if _category_group(category) != _category_group( + str(prediction["category"]) + ): + continue + candidate_box = _box(prediction["box_xyxy"]) + candidate_center = _center(candidate_box) + normalized_distance = math.hypot( + source_center[0] - candidate_center[0], + source_center[1] - candidate_center[1], + ) / source_diagonal + overlap = _iou(source_box, candidate_box) + if normalized_distance > 1.3 and overlap < 0.05: + continue + area_ratio = abs( + math.log( + max(_area(candidate_box), 1.0) + / max(_area(source_box), 1.0) + ) + ) + cost = ( + normalized_distance + + 0.12 * area_ratio + - 0.35 * overlap + - 0.03 * float(prediction["score"]) + ) + pairs.append((cost, object_index, prediction_index)) + + used_objects: set[int] = set() + used_predictions: set[int] = set() + for cost, object_index, prediction_index in sorted(pairs): + if ( + cost > profile.maximum_match_cost + or object_index in used_objects + or prediction_index in used_predictions + ): + continue + used_objects.add(object_index) + used_predictions.add(prediction_index) + item = objects[object_index] + prediction = predictions[prediction_index] + source_box = _box(item["box_xyxy"]) + item["source_box_xyxy"] = list(source_box) + item["box_xyxy"] = [ + round(value, 3) for value in _box(prediction["box_xyxy"]) + ] + item["geometry_origin"] = "maskrcnn_valid_fov_visual_snap" + item["geometry_candidate_id"] = profile.geometry_candidate_id + item["geometry_candidate_score"] = round( + float(prediction["score"]), 9 + ) + snapped_count += 1 + + for object_index, item in enumerate(objects): + if object_index not in used_objects: + item["geometry_origin"] = "l34f_engineering_reference_retained" + retained_count += 1 + case["objects"] = objects + case["object_count"] = len(objects) + case["hard_negative"] = len(objects) == 0 + case["visual_audit_state"] = "ai-object-level-audited-v2" + + final_object_count = sum(len(case["objects"]) for case in cases) + if ( + final_object_count != profile.expected_final_object_count + or snapped_count != profile.expected_geometry_snapped_count + or snapped_count + retained_count != final_object_count + ): + raise E46AAiEngineeringPreannotationError( + "E46A visual-audit result does not match the frozen profile" + ) + return { + "profile_id": profile.profile_id, + "geometry_candidate_id": profile.geometry_candidate_id, + "source_object_count": source_object_count, + "deleted_false_box_count": len(delete_object_ids), + "deleted_false_box_ids": sorted(delete_object_ids), + "geometry_snapped_object_count": snapped_count, + "source_geometry_retained_object_count": retained_count, + "category_corrected_object_count": len(category_corrections), + "category_corrections": category_corrections, + "reviewed_frame_count": len(cases), + "ground_truth": False, + } + + +def _validated_prediction(raw: object) -> dict[str, Any]: + if not isinstance(raw, dict): + raise E46AAiEngineeringPreannotationError( + "E46A geometry candidate row is invalid" + ) + category = raw.get("category") + score = raw.get("score") + box = _box(raw.get("box_xyxy")) + if ( + not isinstance(category, str) + or not category + or not isinstance(score, (int, float)) + or isinstance(score, bool) + or not math.isfinite(float(score)) + or not 0.0 <= float(score) <= 1.0 + ): + raise E46AAiEngineeringPreannotationError( + "E46A geometry candidate row is invalid" + ) + return {"category": category, "score": float(score), "box_xyxy": box} + + +def _candidate_nms( + predictions: tuple[dict[str, Any], ...], *, threshold: float +) -> tuple[dict[str, Any], ...]: + kept: list[dict[str, Any]] = [] + for prediction in sorted( + predictions, key=lambda item: float(item["score"]), reverse=True + ): + if all( + _category_group(str(prediction["category"])) + != _category_group(str(other["category"])) + or _iou( + _box(prediction["box_xyxy"]), + _box(other["box_xyxy"]), + ) + < threshold + for other in kept + ): + kept.append(prediction) + return tuple(kept) + + +def _category_group(category: str) -> str: + return "vehicle" if category in _VEHICLE_CATEGORIES else category + + +def _box(raw: object) -> tuple[float, float, float, float]: + if ( + not isinstance(raw, (list, tuple)) + or len(raw) != 4 + or any( + not isinstance(value, (int, float)) + or isinstance(value, bool) + or not math.isfinite(float(value)) + for value in raw + ) + ): + raise E46AAiEngineeringPreannotationError("E46A box is invalid") + box = tuple(float(value) for value in raw) + if ( + box[0] < 0.0 + or box[1] < 0.0 + or box[2] > 800.0 + or box[3] > 600.0 + or box[2] <= box[0] + or box[3] <= box[1] + ): + raise E46AAiEngineeringPreannotationError("E46A box is invalid") + return box + + +def _area(box: tuple[float, float, float, float]) -> float: + return (box[2] - box[0]) * (box[3] - box[1]) + + +def _center(box: tuple[float, float, float, float]) -> tuple[float, float]: + return ((box[0] + box[2]) / 2.0, (box[1] + box[3]) / 2.0) + + +def _iou( + first: tuple[float, float, float, float], + second: tuple[float, float, float, float], +) -> float: + width = max(0.0, min(first[2], second[2]) - max(first[0], second[0])) + height = max(0.0, min(first[3], second[3]) - max(first[1], second[1])) + intersection = width * height + if intersection == 0.0: + return 0.0 + return intersection / (_area(first) + _area(second) - intersection) + + +def _artifact(path: Path, role: str) -> dict[str, Any]: + return { + "role": role, + "path": path.name, + "byte_length": path.stat().st_size, + "sha256": _sha256(path), + } + + +def _validated_artifact(root: Path, raw: object) -> Path: + if not isinstance(raw, dict) or not isinstance(raw.get("path"), str): + raise E46AAiEngineeringPreannotationError("E46A artifact is invalid") + path = (root / str(raw["path"])).resolve(strict=True) + if ( + path.parent != root + or path.is_symlink() + or path.stat().st_size != raw.get("byte_length") + or _sha256(path) != raw.get("sha256") + ): + raise E46AAiEngineeringPreannotationError("E46A artifact changed") + return path + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, ensure_ascii=False, sort_keys=True, separators=(",", ":") + ).encode("utf-8") + + +def _read_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise E46AAiEngineeringPreannotationError("E46A JSON object expected") + return value + + +def _read_jsonl(path: Path) -> list[dict[str, Any]]: + values = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line] + if any(not isinstance(value, dict) for value in values): + raise E46AAiEngineeringPreannotationError("E46A JSONL object expected") + return values + + +def _write_json(path: Path, value: object) -> None: + path.write_bytes(_canonical_json(value) + b"\n") + + +def _write_jsonl(path: Path, values: list[dict[str, Any]]) -> None: + path.write_bytes(b"".join(_canonical_json(value) + b"\n" for value in values)) + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def _utc_now() -> str: + return datetime.now(UTC).isoformat().replace("+00:00", "Z") diff --git a/src/k1link/compute/e46b_temporal_motion.py b/src/k1link/compute/e46b_temporal_motion.py new file mode 100644 index 0000000..212d936 --- /dev/null +++ b/src/k1link/compute/e46b_temporal_motion.py @@ -0,0 +1,526 @@ +"""Freeze source-scoped temporal tracks and conservative motion state for E46A.""" + +from __future__ import annotations + +import copy +import hashlib +import json +import math +import os +import re +import shutil +import uuid +from collections import Counter, defaultdict +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from k1link.compute.e46a_ai_engineering_preannotation import ( + E46A_MANIFEST_NAME, + read_e46a_ai_engineering_preannotation, +) + +E46B_RESULT_SCHEMA: Final = "missioncore.e46b-temporal-motion/v1" +E46B_REPORT_SCHEMA: Final = "missioncore.e46b-temporal-motion-report/v1" +E46B_CASE_SCHEMA: Final = "missioncore.e46b-temporal-motion-case/v1" +E46B_MANIFEST_NAME: Final = "manifest.json" +E46B_REPORT_NAME: Final = "temporal-motion-report.json" +E46B_CASES_NAME: Final = "temporal-motion-cases.jsonl" + +_RESULT_ID = re.compile(r"^e46b-temporal-motion-[a-f0-9]{64}$") +_TEMPORAL_GROUPS: Final = ( + "clip-stroller-person", + "clip-close-car", + "clip-vehicle-occlusion", + "clip-near-structure", +) +_EXPECTED_COUNTS: Final = { + "clip-stroller-person": {"person": 2, "stroller": 1, "car": 6}, + "clip-close-car": {"car": 9, "heavy_vehicle": 1}, + "clip-vehicle-occlusion": {"car": 6, "heavy_vehicle": 1}, + "clip-near-structure": {"car": 7, "laptop": 1}, +} +_AUTHORITY: Final = { + "ground_truth": False, + "independent_truth": False, + "metric_grade_reference": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class E46BTemporalMotionError(ValueError): + """Raised when an E46B input binding or immutable output is invalid.""" + + +def build_e46b_temporal_motion( + *, + e46a_root: Path, + e26_root: Path, + output_root: Path, +) -> dict[str, Any]: + """Build 16 source-scoped temporal frames with stable IDs and motion evidence.""" + + e46a = read_e46a_ai_engineering_preannotation(e46a_root) + source_cases = [ + copy.deepcopy(case) + for case in e46a["cases"] + if case.get("group_id") in _TEMPORAL_GROUPS + ] + source_cases.sort(key=lambda case: int(case["truth_island_sequence"])) + _validate_source_cases(source_cases) + target_frames = {int(case["frame_index"]) for case in source_cases} + e26 = _read_e26_motion_source(e26_root, target_frames) + + track_observations: dict[str, list[dict[str, Any]]] = defaultdict(list) + for group_id in _TEMPORAL_GROUPS: + group_cases = [case for case in source_cases if case["group_id"] == group_id] + for case in group_cases: + by_category: dict[str, list[dict[str, Any]]] = defaultdict(list) + for item in case["objects"]: + by_category[str(item["category"])].append(item) + for category, objects in by_category.items(): + objects.sort(key=lambda item: _center(item["box_xyxy"])[0]) + for rank, item in enumerate(objects, start=1): + track_id = f"{group_id}/{category}-{rank:02d}" + item["track_id"] = track_id + item["track_index"] = rank + track_observations[track_id].append( + { + "sequence": int(case["truth_island_sequence"]), + "frame_index": int(case["frame_index"]), + "center_xy": list(_center(item["box_xyxy"])), + "object": item, + } + ) + + matched_total = 0 + for case in source_cases: + motion_objects = e26["frames"][int(case["frame_index"])] + associations = _associate(case["objects"], motion_objects) + for item in case["objects"]: + match = associations.get(str(item["object_id"])) + if match is None: + item["motion_observation"] = None + continue + matched_total += 1 + item["motion_observation"] = { + "source_track_id": match.get("source_track_id"), + "track_id": match.get("track_id"), + "motion_state": match.get("motion_state"), + "motion_confidence": match.get("motion_confidence"), + "motion_status": match.get("motion_status"), + "camera_motion_state": match.get("camera_motion_state"), + "lidar_motion_state": match.get("lidar_motion_state"), + "bbox_xyxy": copy.deepcopy(match.get("bbox_xyxy")), + } + + track_summaries: dict[str, dict[str, Any]] = {} + for track_id, observations in track_observations.items(): + decisive = [ + item["object"]["motion_observation"] + for item in observations + if isinstance(item["object"].get("motion_observation"), dict) + and item["object"]["motion_observation"].get("motion_state") + in {"static", "dynamic"} + ] + states = {str(item["motion_state"]) for item in decisive} + state = next(iter(states)) if len(states) == 1 else "unknown" + confidence = ( + round(sum(float(item["motion_confidence"]) for item in decisive) / len(decisive), 6) + if state != "unknown" and decisive + else 0.0 + ) + track_summaries[track_id] = { + "track_id": track_id, + "category": str(observations[0]["object"]["category"]), + "motion_state": state, + "motion_confidence": confidence, + "motion_evidence_observation_count": len(decisive), + "observation_count": len(observations), + "source_track_ids": sorted( + { + int(item["source_track_id"]) + for item in decisive + if isinstance(item.get("source_track_id"), int) + } + ), + } + + cases: list[dict[str, Any]] = [] + for source in source_cases: + sequence = int(source["truth_island_sequence"]) + objects: list[dict[str, Any]] = [] + for raw in source["objects"]: + item = copy.deepcopy(raw) + summary = track_summaries[str(item["track_id"])] + history = [ + observation["center_xy"] + for observation in track_observations[str(item["track_id"])] + if int(observation["sequence"]) <= sequence + ] + item.update( + { + "motion_state": summary["motion_state"], + "motion_confidence": summary["motion_confidence"], + "motion_evidence_observation_count": summary[ + "motion_evidence_observation_count" + ], + "trail_centers_xy": history, + } + ) + objects.append(item) + frame_counts = Counter(str(item["motion_state"]) for item in objects) + cases.append( + { + "schema_version": E46B_CASE_SCHEMA, + "truth_island_sequence": sequence, + "image_id": int(source["image_id"]), + "frame_index": int(source["frame_index"]), + "group_id": str(source["group_id"]), + "session_seconds": float(source["session_seconds"]), + "source_image_sha256": str(source["source_image_sha256"]), + "objects": objects, + "object_count": len(objects), + "track_count": len(objects), + "motion_counts": { + key: int(frame_counts.get(key, 0)) + for key in ("dynamic", "static", "unknown") + }, + } + ) + + track_counts = Counter( + str(summary["motion_state"]) for summary in track_summaries.values() + ) + metrics = { + "frame_count": len(cases), + "temporal_group_count": len(_TEMPORAL_GROUPS), + "object_observation_count": sum(len(case["objects"]) for case in cases), + "track_count": len(track_summaries), + "matched_motion_observation_count": matched_total, + "unmatched_motion_observation_count": 136 - matched_total, + "dynamic_track_count": int(track_counts.get("dynamic", 0)), + "static_track_count": int(track_counts.get("static", 0)), + "unknown_track_count": int(track_counts.get("unknown", 0)), + "visually_reviewed_frame_count": 16, + } + if metrics["object_observation_count"] != 136 or metrics["track_count"] != 34: + raise E46BTemporalMotionError("E46B source-scoped accounting changed") + + report_basis = { + "schema_version": E46B_REPORT_SCHEMA, + "status": "completed-source-scoped-temporal-motion-engineering-evidence", + "metrics": metrics, + "tracks": [track_summaries[key] for key in sorted(track_summaries)], + "decision": { + "stable_ids_available": True, + "motion_state_available": True, + "metric_velocity_available": False, + "next_action": ( + "use the 34 tracks as the recorded RIGHT-camera temporal object " + "layer; keep unknown where E26 has no decisive evidence" + ), + }, + "limitations": [ + ( + "track IDs are source-scoped to four fixed four-frame clips and " + "are not route-global identities" + ), + ( + "motion state is inherited from accepted E26 KB4 ego-motion/LiDAR " + "engineering evidence; unmatched or conflicting evidence remains unknown" + ), + ( + "camera-only motion has no metric velocity and no AP, live, command, " + "navigation, or safety claim is made" + ), + ( + "E46A is candidate-visible assisted engineering material and is not " + "independent ground truth" + ), + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + identity = { + "schema_version": E46B_RESULT_SCHEMA, + "e46a_source": { + "result_id": e46a["result_id"], + "manifest_sha256": _sha256(e46a["result_root"] / E46A_MANIFEST_NAME), + }, + "e46_source": copy.deepcopy(e46a["manifest"]["identity"]["e46_source"]), + "e26_motion_source": e26["identity"], + "association_profile": { + "profile_id": "e46b-source-scoped-category-x-rank-plus-e26-bbox/v1", + "track_scope": "four-frame-group", + "motion_conflict_state": "unknown", + "maximum_match_cost": 1.4, + }, + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e46b-temporal-motion-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_e46b_temporal_motion(destination) + created_at_utc = datetime.now(UTC).isoformat().replace("+00:00", "Z") + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc, + "source_session_id": "RAVNOVES00", + "camera_source_id": "sensor.camera.right", + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / E46B_REPORT_NAME, report) + _write_jsonl(staging / E46B_CASES_NAME, cases) + _write_json( + staging / E46B_MANIFEST_NAME, + { + "schema_version": E46B_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "source-scoped-temporal-motion-engineering-evidence", + "ground_truth": False, + "artifacts": [ + _artifact(staging / E46B_REPORT_NAME, "temporal-motion-report"), + _artifact(staging / E46B_CASES_NAME, "temporal-motion-cases"), + ], + "authority": _AUTHORITY, + }, + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_e46b_temporal_motion(destination) + + +def read_e46b_temporal_motion(root: Path) -> dict[str, Any]: + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / E46B_MANIFEST_NAME) + identity = manifest.get("identity") + if not isinstance(identity, dict): + raise E46BTemporalMotionError("E46B identity is invalid") + digest = hashlib.sha256(_canonical_json(identity)).hexdigest() + if ( + manifest.get("schema_version") != E46B_RESULT_SCHEMA + or manifest.get("identity_sha256") != digest + or manifest.get("result_id") != f"e46b-temporal-motion-{digest}" + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("authority") != _AUTHORITY + or manifest.get("ground_truth") is not False + ): + raise E46BTemporalMotionError("E46B identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 2: + raise E46BTemporalMotionError("E46B artifacts are invalid") + by_role = {item.get("role"): item for item in artifacts if isinstance(item, dict)} + report = _read_json(_validated_artifact(resolved, by_role.get("temporal-motion-report"))) + cases = tuple(_read_jsonl(_validated_artifact(resolved, by_role.get("temporal-motion-cases")))) + if ( + report.get("schema_version") != E46B_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("authority") != _AUTHORITY + or report.get("ground_truth") is not False + or len(cases) != 16 + or any(case.get("schema_version") != E46B_CASE_SCHEMA for case in cases) + or hashlib.sha256(_canonical_json(cases)).hexdigest() != identity.get("cases_sha256") + ): + raise E46BTemporalMotionError("E46B result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "cases": cases, + } + + +def _validate_source_cases(cases: list[dict[str, Any]]) -> None: + if len(cases) != 16: + raise E46BTemporalMotionError("E46B requires exactly 16 temporal frames") + for group_id in _TEMPORAL_GROUPS: + selected = [case for case in cases if case.get("group_id") == group_id] + if len(selected) != 4: + raise E46BTemporalMotionError(f"E46B group {group_id} changed") + for case in selected: + counts = Counter(str(item.get("category")) for item in case.get("objects", [])) + if dict(counts) != _EXPECTED_COUNTS[group_id]: + raise E46BTemporalMotionError(f"E46B object accounting changed in {group_id}") + + +def _read_e26_motion_source(root: Path, target_frames: set[int]) -> dict[str, Any]: + resolved = root.resolve(strict=True) + result = _read_json(resolved / "result.json") + identity = result.get("identity") + if ( + result.get("schema_version") != "missioncore.e10-integrated-perception-result/v1" + or not isinstance(identity, dict) + or identity.get("source_id") != "sensor.camera.right" + or identity.get("configuration", {}).get("pipeline") + != "kb4-multiview-static-hypothesis-lidar-fusion/v1" + or result.get("acceptance_state") != "accepted" + ): + raise E46BTemporalMotionError("E26 motion source contract is invalid") + artifact = next( + ( + item + for item in result.get("artifacts", []) + if isinstance(item, dict) and item.get("path") == "fusion-frames.jsonl" + ), + None, + ) + if not isinstance(artifact, dict): + raise E46BTemporalMotionError("E26 fusion artifact is missing") + path = _validated_artifact(resolved, artifact) + frames: dict[int, list[dict[str, Any]]] = {} + for row in _read_jsonl(path): + frame_index = row.get("source_frame_index") + if frame_index in target_frames: + objects = row.get("objects") + if not isinstance(objects, list): + raise E46BTemporalMotionError("E26 fusion objects are invalid") + frames[int(frame_index)] = objects + if set(frames) != target_frames: + raise E46BTemporalMotionError("E26 temporal coverage is incomplete") + return { + "frames": frames, + "identity": { + "result_id": result.get("result_id"), + "result_sha256": _sha256(resolved / "result.json"), + "fusion_frames_sha256": artifact.get("sha256"), + "pipeline": identity["configuration"]["pipeline"], + "profile_sha256": identity["configuration"].get("profile_sha256"), + "implementation_sha256": identity["configuration"].get("implementation_sha256"), + }, + } + + +def _associate( + source: list[dict[str, Any]], + candidates: list[dict[str, Any]], +) -> dict[str, dict[str, Any]]: + pairs: list[tuple[float, str, int]] = [] + for item in source: + group = ( + "vehicle" + if item.get("category") in {"car", "heavy_vehicle"} + else item.get("category") + ) + for index, candidate in enumerate(candidates): + if candidate.get("association_group") != group: + continue + box = candidate.get("bbox_xyxy") + if not isinstance(box, list) or len(box) != 4: + continue + source_box = item["box_xyxy"] + sx, sy = _center(source_box) + cx, cy = _center(box) + distance = math.hypot(sx - cx, sy - cy) / 1000.0 + area_ratio = abs(math.log(max(_area(source_box), 1.0) / max(_area(box), 1.0))) + cost = distance + 0.25 * area_ratio + (1.0 - _iou(source_box, box)) + if cost <= 1.4: + pairs.append((cost, str(item["object_id"]), index)) + output: dict[str, dict[str, Any]] = {} + used: set[int] = set() + for _, object_id, index in sorted(pairs): + if object_id in output or index in used: + continue + output[object_id] = candidates[index] + used.add(index) + return output + + +def _center(box: list[float]) -> tuple[float, float]: + return ((float(box[0]) + float(box[2])) / 2.0, (float(box[1]) + float(box[3])) / 2.0) + + +def _area(box: list[float]) -> float: + return max(0.0, float(box[2]) - float(box[0])) * max(0.0, float(box[3]) - float(box[1])) + + +def _iou(left: list[float], right: list[float]) -> float: + x1, y1 = max(left[0], right[0]), max(left[1], right[1]) + x2, y2 = min(left[2], right[2]), min(left[3], right[3]) + intersection = max(0.0, x2 - x1) * max(0.0, y2 - y1) + union = _area(left) + _area(right) - intersection + return intersection / union if union > 0.0 else 0.0 + + +def _artifact(path: Path, role: str) -> dict[str, object]: + return { + "role": role, + "path": path.name, + "size_bytes": path.stat().st_size, + "sha256": _sha256(path), + } + + +def _validated_artifact(root: Path, raw: object) -> Path: + if not isinstance(raw, dict) or not isinstance(raw.get("path"), str): + raise E46BTemporalMotionError("artifact metadata is invalid") + path = (root / raw["path"]).resolve(strict=True) + if path.parent != root or path.is_symlink() or not path.is_file(): + raise E46BTemporalMotionError("artifact path is invalid") + expected_size = raw.get("size_bytes", raw.get("byte_length")) + if path.stat().st_size != expected_size or _sha256(path) != raw.get("sha256"): + raise E46BTemporalMotionError("artifact changed") + return path + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +def _read_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise E46BTemporalMotionError("JSON document is invalid") + return value + + +def _read_jsonl(path: Path): + with path.open("r", encoding="utf-8") as handle: + for line in handle: + if line.strip(): + value = json.loads(line) + if not isinstance(value, dict): + raise E46BTemporalMotionError("JSONL row is invalid") + yield value + + +def _write_json(path: Path, value: object) -> None: + path.write_bytes(_canonical_json(value) + b"\n") + + +def _write_jsonl(path: Path, values: list[dict[str, Any]]) -> None: + with path.open("wb") as handle: + for value in values: + handle.write(_canonical_json(value) + b"\n") + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for block in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() diff --git a/src/k1link/compute/e46c_full_replay_world_tracks.py b/src/k1link/compute/e46c_full_replay_world_tracks.py new file mode 100644 index 0000000..ce64731 --- /dev/null +++ b/src/k1link/compute/e46c_full_replay_world_tracks.py @@ -0,0 +1,571 @@ +"""Freeze the full recorded RIGHT route-track and world-state qualification.""" + +from __future__ import annotations + +import copy +import hashlib +import json +import math +import os +import re +import shutil +import uuid +from collections import Counter +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from k1link.compute.e46a_ai_engineering_preannotation import ( + E46A_MANIFEST_NAME, + read_e46a_ai_engineering_preannotation, +) + +E46C_RESULT_SCHEMA: Final = "missioncore.e46c-full-replay-world-tracks/v1" +E46C_REPORT_SCHEMA: Final = "missioncore.e46c-full-replay-world-tracks-report/v1" +E46C_CASE_SCHEMA: Final = "missioncore.e46c-full-replay-world-track-case/v1" +E46C_MANIFEST_NAME: Final = "manifest.json" +E46C_REPORT_NAME: Final = "full-replay-world-track-report.json" +E46C_CASES_NAME: Final = "full-replay-world-track-cases.jsonl" + +_RESULT_ID = re.compile(r"^e46c-full-replay-world-tracks-[a-f0-9]{64}$") +_AUTHORITY: Final = { + "ground_truth": False, + "independent_truth": False, + "metric_grade_reference": False, + "candidate_accepted": False, + "free_space_authority": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class E46CFullReplayWorldTracksError(ValueError): + """Raised when an E46C source or immutable result is invalid.""" + + +def build_e46c_full_replay_world_tracks( + *, e46a_root: Path, e26_root: Path, output_root: Path +) -> dict[str, Any]: + e46a = read_e46a_ai_engineering_preannotation(e46a_root) + source_cases = sorted( + (copy.deepcopy(case) for case in e46a["cases"]), + key=lambda case: int(case["truth_island_sequence"]), + ) + if len(source_cases) != 32: + raise E46CFullReplayWorldTracksError("E46C requires all 32 E46A frames") + target_frames = {int(case["frame_index"]) for case in source_cases} + e26 = _read_e26(e26_root, target_frames) + + cases: list[dict[str, Any]] = [] + matched_objects = 0 + sample_world_frames = 0 + for source in source_cases: + frame_index = int(source["frame_index"]) + fusion = e26["sample_fusion"][frame_index] + world = e26["sample_world"][frame_index] + associations = _associate(source["objects"], fusion["objects"]) + objects: list[dict[str, Any]] = [] + for raw in source["objects"]: + item = copy.deepcopy(raw) + match = associations.get(str(item["object_id"])) + if match is None: + item.update( + { + "route_track_id": None, + "world_track_id": None, + "motion_state": "unknown", + "motion_confidence": 0.0, + "world_binding_state": "unmatched", + } + ) + else: + matched_objects += 1 + source_track_id = match.get("source_track_id") + world_track_id = match.get("track_id") + world_bound = ( + isinstance(source_track_id, int) + and isinstance(world_track_id, int) + and world_track_id != source_track_id + ) + item.update( + { + "route_track_id": source_track_id, + "world_track_id": world_track_id if world_bound else None, + "motion_state": match.get("motion_state", "unknown"), + "motion_confidence": float( + match.get("motion_confidence") or 0.0 + ), + "world_binding_state": ( + "world-track-bound" if world_bound else "camera-track-only" + ), + } + ) + objects.append(item) + world_objects = [_world_projection(item) for item in world["objects"]] + if world_objects: + sample_world_frames += 1 + cases.append( + { + "schema_version": E46C_CASE_SCHEMA, + "truth_island_sequence": int(source["truth_island_sequence"]), + "image_id": int(source["image_id"]), + "frame_index": frame_index, + "group_id": str(source["group_id"]), + "session_seconds": float(source["session_seconds"]), + "source_image_sha256": str(source["source_image_sha256"]), + "fusion_state": str(fusion["fusion_state"]), + "objects": objects, + "object_count": len(objects), + "matched_route_object_count": sum( + item["route_track_id"] is not None for item in objects + ), + "world_objects": world_objects, + "world_object_count": len(world_objects), + } + ) + + route = e26["route_metrics"] + metrics = { + **route, + "visual_sample_frame_count": 32, + "object_audited_sample_frame_count": 32, + "sample_object_count": sum(len(case["objects"]) for case in cases), + "sample_matched_route_object_count": matched_objects, + "sample_unmatched_object_count": ( + sum(len(case["objects"]) for case in cases) - matched_objects + ), + "sample_world_frame_count": sample_world_frames, + } + report_basis = { + "schema_version": E46C_REPORT_SCHEMA, + "status": "completed-full-recorded-right-route-world-track-qualification", + "metrics": metrics, + "acceptance": { + "e26_diagnostic_accepted": True, + "benchmark_passed_events": e26["benchmark_passed_events"], + "benchmark_total_events": e26["benchmark_total_events"], + "route_accounting_complete": True, + "visual_sample_available": True, + "navigation_or_safety_accepted": False, + }, + "decision": { + "route_track_layer_available": True, + "world_occupied_layer_available": True, + "unknown_remains_occupied": True, + "free_space_available": False, + "next_action": ( + "use E46C as the recorded full-route diagnostic object/world layer; " + "qualify identity continuity and world-binding exceptions before any " + "live or planner-facing promotion" + ), + }, + "limitations": [ + ( + "route-track IDs are detector-tracker identities with a 2.5 second idle " + "bound, not permanent physical identities" + ), + ( + "world-state is available only where pose/LiDAR support passes the E26 " + "sync and evidence gates; missing evidence remains unknown occupied" + ), + ( + "the 32 exact E46A frames are a visual audit sample; they do not make all " + "4489 frames independently human reviewed" + ), + ( + "E26 is accepted diagnostic engineering evidence, not independent truth, " + "free space, commands, navigation, or safety authority" + ), + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + identity = { + "schema_version": E46C_RESULT_SCHEMA, + "e46a_visual_sample": { + "result_id": e46a["result_id"], + "manifest_sha256": _sha256(e46a["result_root"] / E46A_MANIFEST_NAME), + }, + "e46_source": copy.deepcopy(e46a["manifest"]["identity"]["e46_source"]), + "e26_full_replay": e26["identity"], + "projection_profile": { + "profile_id": "e46c-full-route-plus-e46a-visual-sample/v1", + "camera_object_binding": "same-frame-group-bbox-one-to-one/v1", + "world_binding": "e26-source-track-to-world-track/v1", + "unknown_policy": "occupied-no-free-space-claim", + }, + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + digest = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e46c-full-replay-world-tracks-{digest}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_e46c_full_replay_world_tracks(destination) + created_at_utc = datetime.now(UTC).isoformat().replace("+00:00", "Z") + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": digest, + "created_at_utc": created_at_utc, + "source_session_id": "RAVNOVES00", + "camera_source_id": "sensor.camera.right", + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / E46C_REPORT_NAME, report) + _write_jsonl(staging / E46C_CASES_NAME, cases) + _write_json( + staging / E46C_MANIFEST_NAME, + { + "schema_version": E46C_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": digest, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "full-recorded-route-diagnostic-world-tracks", + "ground_truth": False, + "artifacts": [ + _artifact(staging / E46C_REPORT_NAME, "world-track-report"), + _artifact(staging / E46C_CASES_NAME, "world-track-cases"), + ], + "authority": _AUTHORITY, + }, + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_e46c_full_replay_world_tracks(destination) + + +def read_e46c_full_replay_world_tracks(root: Path) -> dict[str, Any]: + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / E46C_MANIFEST_NAME) + identity = manifest.get("identity") + if not isinstance(identity, dict): + raise E46CFullReplayWorldTracksError("E46C identity is invalid") + digest = hashlib.sha256(_canonical_json(identity)).hexdigest() + if ( + manifest.get("schema_version") != E46C_RESULT_SCHEMA + or manifest.get("identity_sha256") != digest + or manifest.get("result_id") != f"e46c-full-replay-world-tracks-{digest}" + or manifest.get("result_id") != resolved.name + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("authority") != _AUTHORITY + or manifest.get("ground_truth") is not False + ): + raise E46CFullReplayWorldTracksError("E46C identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 2: + raise E46CFullReplayWorldTracksError("E46C artifacts are invalid") + by_role = {item.get("role"): item for item in artifacts if isinstance(item, dict)} + report = _read_json(_validated_artifact(resolved, by_role.get("world-track-report"))) + cases = tuple( + _read_jsonl(_validated_artifact(resolved, by_role.get("world-track-cases"))) + ) + if ( + report.get("schema_version") != E46C_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("authority") != _AUTHORITY + or report.get("ground_truth") is not False + or len(cases) != 32 + or any(case.get("schema_version") != E46C_CASE_SCHEMA for case in cases) + or hashlib.sha256(_canonical_json(cases)).hexdigest() + != identity.get("cases_sha256") + ): + raise E46CFullReplayWorldTracksError("E46C result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "cases": cases, + } + + +def _read_e26(root: Path, target_frames: set[int]) -> dict[str, Any]: + resolved = root.resolve(strict=True) + result_path = resolved / "result.json" + result = _read_json(result_path) + identity = result.get("identity") + if ( + result.get("schema_version") != "missioncore.e10-integrated-perception-result/v1" + or result.get("acceptance_state") != "accepted" + or not isinstance(identity, dict) + or identity.get("source_id") != "sensor.camera.right" + or identity.get("selection", {}).get("frame_count") != 4489 + or identity.get("configuration", {}).get("pipeline") + != "kb4-multiview-static-hypothesis-lidar-fusion/v1" + ): + raise E46CFullReplayWorldTracksError("E26 full replay contract is invalid") + artifacts = { + item.get("path"): item + for item in result.get("artifacts", []) + if isinstance(item, dict) + } + fusion_path = _validated_artifact(resolved, artifacts.get("fusion-frames.jsonl")) + world_path = _validated_artifact(resolved, artifacts.get("world-state.jsonl")) + report_path = _validated_artifact(resolved, artifacts.get("run-report.json")) + run_report = _read_json(report_path) + acceptance = run_report.get("acceptance") + benchmark = run_report.get("metrics", {}).get("benchmark") + if ( + not isinstance(acceptance, dict) + or acceptance.get("accepted") is not True + or not isinstance(benchmark, dict) + or benchmark.get("passed") is not True + ): + raise E46CFullReplayWorldTracksError("E26 diagnostic acceptance is invalid") + + sample_fusion: dict[int, dict[str, Any]] = {} + source_tracks: set[int] = set() + world_tracks: set[int] = set() + source_world_bound: set[int] = set() + fusion_states: Counter[str] = Counter() + motion_states: Counter[str] = Counter() + labels: Counter[str] = Counter() + fusion_observations = 0 + fusion_frames = 0 + for row in _read_jsonl(fusion_path): + frame_index = int(row["source_frame_index"]) + fusion_frames += 1 + fusion_states[str(row["fusion_state"])] += 1 + raw_objects = row.get("objects") + if not isinstance(raw_objects, list): + raise E46CFullReplayWorldTracksError("E26 fusion objects are invalid") + for item in raw_objects: + fusion_observations += 1 + source_id, track_id = item.get("source_track_id"), item.get("track_id") + if isinstance(source_id, int): + source_tracks.add(source_id) + if ( + isinstance(source_id, int) + and isinstance(track_id, int) + and track_id != source_id + ): + source_world_bound.add(source_id) + world_tracks.add(track_id) + motion_states[str(item.get("motion_state", "unknown"))] += 1 + labels[str(item.get("label", "unknown"))] += 1 + if frame_index in target_frames: + sample_fusion[frame_index] = row + + sample_world: dict[int, dict[str, Any]] = {} + world_frames_with_objects = 0 + world_observations = 0 + world_current = 0 + world_held = 0 + world_motion: Counter[str] = Counter() + occupancy_cells = 0 + world_frames = 0 + unique_world_state_tracks: set[int] = set() + for row in _read_jsonl(world_path): + frame_index = int(row["source_frame_index"]) + world_frames += 1 + raw_objects = row.get("objects") + if not isinstance(raw_objects, list): + raise E46CFullReplayWorldTracksError("E26 world objects are invalid") + if raw_objects: + world_frames_with_objects += 1 + for item in raw_objects: + world_observations += 1 + if isinstance(item.get("track_id"), int): + unique_world_state_tracks.add(int(item["track_id"])) + if item.get("occupancy_evidence_current") is True: + world_current += 1 + else: + world_held += 1 + occupancy_cells += int(item.get("occupancy_cell_count") or 0) + world_motion[str(item.get("motion_state", "unknown"))] += 1 + if frame_index in target_frames: + sample_world[frame_index] = row + if ( + fusion_frames != 4489 + or world_frames != 4489 + or set(sample_fusion) != target_frames + or set(sample_world) != target_frames + ): + raise E46CFullReplayWorldTracksError("E26 full replay accounting changed") + selection = identity["selection"] + route_metrics = { + "route_frame_count": fusion_frames, + "route_span_seconds": round( + float(selection["timeline_end_seconds"]) + - float(selection["timeline_start_seconds"]), + 6, + ), + "fusion_observation_count": fusion_observations, + "source_track_count": len(source_tracks), + "world_track_candidate_count": len(world_tracks), + "world_track_count": len(unique_world_state_tracks), + "source_track_world_bound_count": len(source_world_bound), + "world_frame_count": world_frames_with_objects, + "world_observation_count": world_observations, + "world_current_observation_count": world_current, + "world_held_observation_count": world_held, + "occupancy_cell_observation_count": occupancy_cells, + "fusion_state_counts": dict(sorted(fusion_states.items())), + "motion_observation_counts": dict(sorted(motion_states.items())), + "world_motion_observation_counts": dict(sorted(world_motion.items())), + "class_observation_counts": dict(sorted(labels.items())), + } + return { + "sample_fusion": sample_fusion, + "sample_world": sample_world, + "route_metrics": route_metrics, + "benchmark_passed_events": int(benchmark["passed_events"]), + "benchmark_total_events": int(benchmark["total_events"]), + "identity": { + "result_id": result["result_id"], + "result_sha256": _sha256(result_path), + "fusion_frames_sha256": artifacts["fusion-frames.jsonl"]["sha256"], + "world_state_sha256": artifacts["world-state.jsonl"]["sha256"], + "run_report_sha256": artifacts["run-report.json"]["sha256"], + "input_sha256": identity["input_sha256"], + "timeline_sha256": selection["timeline_sha256"], + "pipeline": identity["configuration"]["pipeline"], + "profile_sha256": identity["configuration"]["profile_sha256"], + "implementation_sha256": identity["configuration"][ + "implementation_sha256" + ], + }, + } + + +def _world_projection(item: dict[str, Any]) -> dict[str, Any]: + return { + "world_track_id": item.get("track_id"), + "route_track_id": item.get("source_track_id"), + "source_track_aliases": copy.deepcopy(item.get("source_track_aliases", [])), + "category": item.get("detector_label", item.get("class", "unknown")), + "motion_state": item.get("motion_state", "unknown"), + "motion_confidence": float(item.get("motion_confidence") or 0.0), + "position_map_m": copy.deepcopy(item.get("position_map_m")), + "size_m": copy.deepcopy(item.get("size_m")), + "occupancy_footprint_map_xy": copy.deepcopy( + item.get("occupancy_footprint_map_xy", []) + ), + "occupancy_evidence_current": bool(item.get("occupancy_evidence_current")), + "occupancy_observation_age_ms": item.get("occupancy_observation_age_ms"), + "occupancy_cell_count": int(item.get("occupancy_cell_count") or 0), + "temporal_status": item.get("temporal_status"), + } + + +def _associate( + source: list[dict[str, Any]], candidates: list[dict[str, Any]] +) -> dict[str, dict[str, Any]]: + pairs: list[tuple[float, str, int]] = [] + for item in source: + category = item.get("category") + group = "vehicle" if category in {"car", "heavy_vehicle"} else category + for index, candidate in enumerate(candidates): + if candidate.get("association_group") != group: + continue + box = candidate.get("bbox_xyxy") + if not isinstance(box, list) or len(box) != 4: + continue + source_box = item["box_xyxy"] + sx, sy = _center(source_box) + cx, cy = _center(box) + distance = math.hypot(sx - cx, sy - cy) / 1000.0 + area_ratio = abs( + math.log(max(_area(source_box), 1.0) / max(_area(box), 1.0)) + ) + cost = distance + 0.25 * area_ratio + (1.0 - _iou(source_box, box)) + if cost <= 1.4: + pairs.append((cost, str(item["object_id"]), index)) + output: dict[str, dict[str, Any]] = {} + used: set[int] = set() + for _, object_id, index in sorted(pairs): + if object_id not in output and index not in used: + output[object_id] = candidates[index] + used.add(index) + return output + + +def _center(box: list[float]) -> tuple[float, float]: + return ((float(box[0]) + float(box[2])) / 2, (float(box[1]) + float(box[3])) / 2) + + +def _area(box: list[float]) -> float: + return max(0.0, box[2] - box[0]) * max(0.0, box[3] - box[1]) + + +def _iou(left: list[float], right: list[float]) -> float: + x1, y1 = max(left[0], right[0]), max(left[1], right[1]) + x2, y2 = min(left[2], right[2]), min(left[3], right[3]) + intersection = max(0.0, x2 - x1) * max(0.0, y2 - y1) + union = _area(left) + _area(right) - intersection + return intersection / union if union > 0 else 0.0 + + +def _artifact(path: Path, role: str) -> dict[str, object]: + return { + "role": role, + "path": path.name, + "size_bytes": path.stat().st_size, + "sha256": _sha256(path), + } + + +def _validated_artifact(root: Path, raw: object) -> Path: + if not isinstance(raw, dict) or not isinstance(raw.get("path"), str): + raise E46CFullReplayWorldTracksError("artifact metadata is invalid") + path = (root / raw["path"]).resolve(strict=True) + if path.parent != root or path.is_symlink() or not path.is_file(): + raise E46CFullReplayWorldTracksError("artifact path is invalid") + expected_size = raw.get("size_bytes", raw.get("byte_length")) + if path.stat().st_size != expected_size or _sha256(path) != raw.get("sha256"): + raise E46CFullReplayWorldTracksError("artifact changed") + return path + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +def _read_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise E46CFullReplayWorldTracksError("JSON document is invalid") + return value + + +def _read_jsonl(path: Path): + with path.open("r", encoding="utf-8") as handle: + for line in handle: + if line.strip(): + value = json.loads(line) + if not isinstance(value, dict): + raise E46CFullReplayWorldTracksError("JSONL row is invalid") + yield value + + +def _write_json(path: Path, value: object) -> None: + path.write_bytes(_canonical_json(value) + b"\n") + + +def _write_jsonl(path: Path, values: list[dict[str, Any]]) -> None: + with path.open("wb") as handle: + for value in values: + handle.write(_canonical_json(value) + b"\n") + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for block in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() diff --git a/src/k1link/compute/e46d_temporal_failure_audit.py b/src/k1link/compute/e46d_temporal_failure_audit.py new file mode 100644 index 0000000..69eaf8f --- /dev/null +++ b/src/k1link/compute/e46d_temporal_failure_audit.py @@ -0,0 +1,993 @@ +"""Freeze a deterministic temporal-failure audit of the full E46C replay.""" + +from __future__ import annotations + +import copy +import hashlib +import json +import math +import os +import re +import shutil +import uuid +from collections import Counter, defaultdict +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from k1link.compute.e46c_full_replay_world_tracks import ( + E46C_MANIFEST_NAME, + read_e46c_full_replay_world_tracks, +) + +E46D_RESULT_SCHEMA: Final = "missioncore.e46d-temporal-failure-audit/v1" +E46D_REPORT_SCHEMA: Final = "missioncore.e46d-temporal-failure-audit-report/v1" +E46D_SIGNAL_SCHEMA: Final = "missioncore.e46d-temporal-failure-signal/v1" +E46D_CLIP_SCHEMA: Final = "missioncore.e46d-temporal-review-clip/v1" +E46D_MANIFEST_NAME: Final = "manifest.json" +E46D_REPORT_NAME: Final = "temporal-failure-audit-report.json" +E46D_SIGNALS_NAME: Final = "temporal-failure-signals.jsonl" +E46D_CLIPS_NAME: Final = "temporal-review-clips.jsonl" + +_RESULT_ID = re.compile(r"^e46d-temporal-failure-audit-[a-f0-9]{64}$") +_E26_RESULT_ID = re.compile(r"^e10-integrated-perception-[a-f0-9]{64}$") +_AUTHORITY: Final = { + "ground_truth": False, + "independent_truth": False, + "metric_grade_reference": False, + "candidate_accepted": False, + "free_space_authority": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} +_PROFILE: Final = { + "profile_id": "e46d-full-replay-temporal-failure-audit/v1", + "layer_blackout_min_frames": 3, + "detector_hold_min_frames": 3, + "route_gap_min_frames": 3, + "route_gap_max_frames": 25, + "id_rebirth_min_iou": 0.5, + "bbox_jump_max_iou": 0.1, + "bbox_jump_min_area_px2": 300.0, + "flap_window_seconds": 1.0, + "flap_min_transitions": 3, + "short_track_max_observations": 3, + "short_track_burst_min_count": 4, + "review_clip_lead_seconds": 2.0, + "review_clip_tail_seconds": 2.0, + "review_clip_separation_seconds": 1.5, + "review_clip_limit": 48, +} +_PRIORITY = {"critical": 3, "high": 2, "medium": 1} + + +class E46DTemporalFailureAuditError(ValueError): + """Raised when an E46D source or immutable result is invalid.""" + + +def build_e46d_temporal_failure_audit( + *, e46c_root: Path, e26_results_root: Path, output_root: Path +) -> dict[str, Any]: + """Audit all E46C temporal rows and freeze prioritized recorded-video clips.""" + + e46c = read_e46c_full_replay_world_tracks(e46c_root) + e26_binding = e46c["manifest"]["identity"].get("e26_full_replay") + if not isinstance(e26_binding, dict): + raise E46DTemporalFailureAuditError("E46C E26 binding is invalid") + e26_result_id = e26_binding.get("result_id") + if not isinstance(e26_result_id, str) or _E26_RESULT_ID.fullmatch(e26_result_id) is None: + raise E46DTemporalFailureAuditError("E46C E26 identity is invalid") + e26_root = (e26_results_root.expanduser().absolute() / e26_result_id).resolve(strict=True) + results_root = e26_results_root.expanduser().absolute().resolve(strict=True) + if e26_root.parent != results_root or e26_root.is_symlink(): + raise E46DTemporalFailureAuditError("E26 source root is invalid") + frames, source_identity = _read_e26_frames(e26_root, e26_binding) + signals, clips, metrics = analyze_temporal_frames(frames) + + method = { + "schema_version": "missioncore.laboratory-method/v1", + "completeness": "complete", + "execution_class": "deterministic", + "pipeline_id": "e46d-full-replay-temporal-failure-audit/v1", + "components": [ + { + "kind": "source", + "name": e46c["result_id"], + "version": "E46C full recorded RIGHT route/world tracks", + "role": "admitted full-route diagnostic result and video binding", + "identity_sha256": _sha256(e46c["result_root"] / E46C_MANIFEST_NAME), + }, + { + "kind": "source", + "name": e26_result_id, + "version": "accepted E26 fusion frames", + "role": "4489-frame temporal object, track, world and motion evidence", + "identity_sha256": source_identity["fusion_frames_sha256"], + }, + { + "kind": "algorithm", + "name": "deterministic temporal exception scanner", + "version": _PROFILE["profile_id"], + "role": "detect, classify, rank and clip observable temporal discontinuities", + "identity_sha256": _sha256(Path(__file__).resolve(strict=True)), + }, + ], + } + report_basis = { + "schema_version": E46D_REPORT_SCHEMA, + "status": "completed-full-recorded-right-temporal-failure-audit", + "metrics": metrics, + "acceptance": { + "full_route_accounted": metrics["route_frame_count"] == 4489, + "temporal_continuity_passed": metrics["temporal_continuity_passed"], + "independent_truth_available": False, + "navigation_or_safety_accepted": False, + }, + "decision": { + "temporal_regression_confirmed": not metrics["temporal_continuity_passed"], + "detector_gap_visible": metrics["detector_hold_episode_count"] > 0, + "route_fragmentation_visible": metrics["short_route_track_count"] > 0, + "world_binding_instability_visible": metrics["world_binding_flap_episode_count"] > 0, + "next_action": ( + "use the frozen clips to separate detector gaps from route-tracker and " + "world-binding failures, then rerun the same recorded RIGHT source as an A/B replay" + ), + }, + "method": method, + "limitations": [ + ( + "signals prove discontinuities in the published diagnostic layer; without " + "independent frame truth they do not by themselves prove that a visible " + "physical object was missed or that a short track was a false positive" + ), + ( + "route-ID rebirth uses same-class image-space overlap and is a high-priority " + "candidate for visual review, not a permanent physical-identity verdict" + ), + ( + "bbox jumps are measured in the distorted 800x600 RIGHT image plane; the " + "audit does not claim metric velocity or calibrated image-plane motion" + ), + ( + "recorded replay only: no LEFT camera, live hardware, commands, free-space, " + "navigation or safety authority is introduced" + ), + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + producer_sha256 = _sha256(Path(__file__).resolve(strict=True)) + identity = { + "schema_version": E46D_RESULT_SCHEMA, + "source_session_id": "RAVNOVES00", + "camera_source_id": "sensor.camera.right", + "e46c_source": { + "result_id": e46c["result_id"], + "manifest_sha256": _sha256(e46c["result_root"] / E46C_MANIFEST_NAME), + }, + "e26_source": source_identity, + "analysis_profile": copy.deepcopy(_PROFILE), + "method": method, + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "signals_sha256": hashlib.sha256(_canonical_json(signals)).hexdigest(), + "clips_sha256": hashlib.sha256(_canonical_json(clips)).hexdigest(), + "producer_sha256": producer_sha256, + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e46d-temporal-failure-audit-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_e46d_temporal_failure_audit(destination) + created_at_utc = datetime.now(UTC).isoformat().replace("+00:00", "Z") + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc, + "source_session_id": "RAVNOVES00", + "camera_source_id": "sensor.camera.right", + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / E46D_REPORT_NAME, report) + _write_jsonl(staging / E46D_SIGNALS_NAME, signals) + _write_jsonl(staging / E46D_CLIPS_NAME, clips) + _write_json( + staging / E46D_MANIFEST_NAME, + { + "schema_version": E46D_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "recorded-temporal-regression-diagnostic", + "ground_truth": False, + "artifacts": [ + _artifact(staging / E46D_REPORT_NAME, "temporal-failure-report"), + _artifact(staging / E46D_SIGNALS_NAME, "temporal-failure-signals"), + _artifact(staging / E46D_CLIPS_NAME, "temporal-review-clips"), + ], + "authority": _AUTHORITY, + }, + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_e46d_temporal_failure_audit(destination) + + +def read_e46d_temporal_failure_audit(root: Path) -> dict[str, Any]: + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / E46D_MANIFEST_NAME) + identity = manifest.get("identity") + if not isinstance(identity, dict): + raise E46DTemporalFailureAuditError("E46D identity is invalid") + digest = hashlib.sha256(_canonical_json(identity)).hexdigest() + if ( + manifest.get("schema_version") != E46D_RESULT_SCHEMA + or manifest.get("identity_sha256") != digest + or manifest.get("result_id") != f"e46d-temporal-failure-audit-{digest}" + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("authority") != _AUTHORITY + or manifest.get("ground_truth") is not False + ): + raise E46DTemporalFailureAuditError("E46D identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 3: + raise E46DTemporalFailureAuditError("E46D artifacts are invalid") + by_role = {item.get("role"): item for item in artifacts if isinstance(item, dict)} + report = _read_json(_validated_artifact(resolved, by_role.get("temporal-failure-report"))) + signals = tuple( + _read_jsonl(_validated_artifact(resolved, by_role.get("temporal-failure-signals"))) + ) + clips = tuple(_read_jsonl(_validated_artifact(resolved, by_role.get("temporal-review-clips")))) + if ( + report.get("schema_version") != E46D_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("authority") != _AUTHORITY + or report.get("ground_truth") is not False + or any(item.get("schema_version") != E46D_SIGNAL_SCHEMA for item in signals) + or any(item.get("schema_version") != E46D_CLIP_SCHEMA for item in clips) + or hashlib.sha256(_canonical_json(signals)).hexdigest() != identity.get("signals_sha256") + or hashlib.sha256(_canonical_json(clips)).hexdigest() != identity.get("clips_sha256") + ): + raise E46DTemporalFailureAuditError("E46D result changed") + metrics = report.get("metrics") + if ( + not isinstance(metrics, dict) + or metrics.get("failure_signal_count") != len(signals) + or metrics.get("review_clip_count") != len(clips) + ): + raise E46DTemporalFailureAuditError("E46D accounting changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "signals": signals, + "clips": clips, + } + + +def analyze_temporal_frames( + frames: list[dict[str, Any]], +) -> tuple[list[dict[str, Any]], list[dict[str, Any]], dict[str, Any]]: + """Return deterministic signals, ranked clip windows and full-route metrics.""" + + _validate_frames(frames) + times = [float(frame["session_seconds"]) for frame in frames] + timeline_start, timeline_end = times[0], times[-1] + signals: list[dict[str, Any]] = [] + by_track: dict[int, list[tuple[int, dict[str, Any]]]] = defaultdict(list) + for position, frame in enumerate(frames): + for item in frame["objects"]: + by_track[int(item["route_track_id"])].append((position, item)) + + counts = [len(frame["objects"]) for frame in frames] + position = 0 + while position < len(frames): + if counts[position] != 0: + position += 1 + continue + start = position + while position + 1 < len(frames) and counts[position + 1] == 0: + position += 1 + end = position + length = end - start + 1 + if ( + start > 0 + and end + 1 < len(frames) + and length >= int(_PROFILE["layer_blackout_min_frames"]) + ): + before, after = counts[start - 1], counts[end + 1] + if before > 0 and after > 0: + duration = times[end + 1] - times[start] + tracks = sorted( + { + int(item["route_track_id"]) + for item in frames[start - 1]["objects"] + frames[end + 1]["objects"] + } + ) + signals.append( + _signal( + "layer-blackout", + "critical" if duration >= 0.5 else "high", + frames, + start, + end, + route_track_ids=tracks, + evidence={ + "duration_seconds": round(duration, 6), + "zero_frame_count": length, + "before_object_count": before, + "minimum_object_count": 0, + "after_object_count": after, + }, + ) + ) + position += 1 + + detector_hold_observations = 0 + route_gap_count = 0 + for route_track_id, sequence in by_track.items(): + cursor = 0 + while cursor < len(sequence): + position, item = sequence[cursor] + if bool(item["camera_evidence_current"]): + cursor += 1 + continue + start_cursor = cursor + while ( + cursor + 1 < len(sequence) + and sequence[cursor + 1][0] == sequence[cursor][0] + 1 + and not bool(sequence[cursor + 1][1]["camera_evidence_current"]) + ): + cursor += 1 + end_cursor = cursor + length = end_cursor - start_cursor + 1 + detector_hold_observations += length + first_position = sequence[start_cursor][0] + last_position = sequence[end_cursor][0] + current_before = ( + start_cursor > 0 + and sequence[start_cursor - 1][0] == first_position - 1 + and bool(sequence[start_cursor - 1][1]["camera_evidence_current"]) + ) + current_after = ( + end_cursor + 1 < len(sequence) + and sequence[end_cursor + 1][0] == last_position + 1 + and bool(sequence[end_cursor + 1][1]["camera_evidence_current"]) + ) + if length >= int(_PROFILE["detector_hold_min_frames"]): + duration_end = ( + times[last_position + 1] + if last_position + 1 < len(times) + else times[last_position] + ) + duration = duration_end - times[first_position] + signals.append( + _signal( + "camera-evidence-hold", + "high" if duration >= 0.5 else "medium", + frames, + first_position, + last_position, + route_track_ids=[route_track_id], + evidence={ + "duration_seconds": round(duration, 6), + "held_frame_count": length, + "camera_evidence_current": False, + "route_identity_retained": True, + "current_evidence_before": current_before, + "current_evidence_after": current_after, + }, + ) + ) + cursor += 1 + + for (previous_position, previous), (next_position, following) in zip( + sequence, sequence[1:], strict=False + ): + missing = next_position - previous_position - 1 + if not ( + int(_PROFILE["route_gap_min_frames"]) + <= missing + <= int(_PROFILE["route_gap_max_frames"]) + ): + continue + route_gap_count += 1 + signals.append( + _signal( + "route-layer-gap", + "high" if missing >= 5 else "medium", + frames, + previous_position + 1, + next_position - 1, + route_track_ids=[route_track_id], + evidence={ + "missing_frame_count": missing, + "duration_seconds": round( + times[next_position] - times[previous_position], 6 + ), + "same_route_identity_returned": True, + "boundary_iou": round( + _iou(previous["bbox_xyxy"], following["bbox_xyxy"]), 6 + ), + }, + ) + ) + + seen_rebirths: set[tuple[int, int]] = set() + for position, (previous, following) in enumerate(zip(frames, frames[1:], strict=False)): + previous_ids = {int(item["route_track_id"]) for item in previous["objects"]} + following_ids = {int(item["route_track_id"]) for item in following["objects"]} + gone = [ + item + for item in previous["objects"] + if int(item["route_track_id"]) not in following_ids + and item["camera_evidence_current"] is True + ] + born = [ + item + for item in following["objects"] + if int(item["route_track_id"]) not in previous_ids + and item["camera_evidence_current"] is True + ] + candidates = sorted( + ( + (_iou(left["bbox_xyxy"], right["bbox_xyxy"]), left, right) + for left in gone + for right in born + if left["category"] == right["category"] + and _iou(left["bbox_xyxy"], right["bbox_xyxy"]) + >= float(_PROFILE["id_rebirth_min_iou"]) + ), + key=lambda value: value[0], + reverse=True, + ) + used_old: set[int] = set() + used_new: set[int] = set() + for overlap, left, right in candidates: + old_id, new_id = int(left["route_track_id"]), int(right["route_track_id"]) + if old_id in used_old or new_id in used_new or (old_id, new_id) in seen_rebirths: + continue + used_old.add(old_id) + used_new.add(new_id) + seen_rebirths.add((old_id, new_id)) + signals.append( + _signal( + "route-id-rebirth-candidate", + "critical", + frames, + position, + position + 1, + route_track_ids=[old_id, new_id], + evidence={ + "category": left["category"], + "bbox_iou": round(overlap, 6), + "old_route_track_id": old_id, + "new_route_track_id": new_id, + }, + ) + ) + + for route_track_id, sequence in by_track.items(): + for (previous_position, previous), (next_position, following) in zip( + sequence, sequence[1:], strict=False + ): + if ( + next_position != previous_position + 1 + or previous["camera_evidence_current"] is not True + or following["camera_evidence_current"] is not True + ): + continue + area = min(_box_area(previous["bbox_xyxy"]), _box_area(following["bbox_xyxy"])) + overlap = _iou(previous["bbox_xyxy"], following["bbox_xyxy"]) + if area < float(_PROFILE["bbox_jump_min_area_px2"]) or overlap >= float( + _PROFILE["bbox_jump_max_iou"] + ): + continue + signals.append( + _signal( + "bbox-jump", + "high", + frames, + previous_position, + next_position, + route_track_ids=[route_track_id], + evidence={ + "bbox_iou": round(overlap, 6), + "minimum_box_area_px2": round(area, 3), + "both_camera_evidence_current": True, + }, + ) + ) + + for route_track_id, sequence in by_track.items(): + signals.extend( + _flap_signals(frames, route_track_id, sequence, "motion_state", "motion-state-flap") + ) + signals.extend( + _flap_signals(frames, route_track_id, sequence, "world_track_id", "world-binding-flap") + ) + + short_tracks = [ + (route_track_id, sequence) + for route_track_id, sequence in by_track.items() + if len(sequence) <= int(_PROFILE["short_track_max_observations"]) + ] + short_starts = sorted( + (times[sequence[0][0]], route_track_id, sequence[0][0]) + for route_track_id, sequence in short_tracks + ) + cursor = 0 + while cursor < len(short_starts): + end = cursor + while end < len(short_starts) and short_starts[end][0] - short_starts[cursor][0] <= 1.0: + end += 1 + window = short_starts[cursor:end] + if len(window) >= int(_PROFILE["short_track_burst_min_count"]): + track_ids = [item[1] for item in window] + signals.append( + _signal( + "short-track-burst", + "high" if len(window) >= 6 else "medium", + frames, + window[0][2], + window[-1][2], + route_track_ids=track_ids, + evidence={ + "short_track_count": len(window), + "maximum_observations_per_track": int( + _PROFILE["short_track_max_observations"] + ), + "window_seconds": round(window[-1][0] - window[0][0], 6), + }, + ) + ) + cursor = end + else: + cursor += 1 + + signals.sort( + key=lambda item: (float(item["start_seconds"]), str(item["kind"]), str(item["signal_id"])) + ) + clips = _review_clips(signals, timeline_start, timeline_end) + signal_counts = Counter(str(item["kind"]) for item in signals) + priority_counts = Counter(str(item["priority"]) for item in signals) + object_observations = sum(counts) + camera_held = sum( + item["camera_evidence_current"] is False for frame in frames for item in frame["objects"] + ) + metrics = { + "route_frame_count": len(frames), + "route_span_seconds": round(timeline_end - timeline_start, 6), + "object_observation_count": object_observations, + "route_track_count": len(by_track), + "zero_object_frame_count": sum(value == 0 for value in counts), + "zero_object_frame_fraction": round(sum(value == 0 for value in counts) / len(frames), 9), + "camera_held_observation_count": camera_held, + "camera_held_observation_fraction": round(camera_held / object_observations, 9), + "detector_hold_episode_count": int(signal_counts["camera-evidence-hold"]), + "layer_blackout_episode_count": int(signal_counts["layer-blackout"]), + "route_layer_gap_episode_count": route_gap_count, + "route_id_rebirth_candidate_count": int(signal_counts["route-id-rebirth-candidate"]), + "bbox_jump_episode_count": int(signal_counts["bbox-jump"]), + "motion_state_flap_episode_count": int(signal_counts["motion-state-flap"]), + "world_binding_flap_episode_count": int(signal_counts["world-binding-flap"]), + "short_route_track_count": len(short_tracks), + "short_route_track_fraction": round(len(short_tracks) / len(by_track), 9), + "short_track_burst_episode_count": int(signal_counts["short-track-burst"]), + "failure_signal_count": len(signals), + "review_clip_count": len(clips), + "signal_counts": dict(sorted(signal_counts.items())), + "priority_counts": { + key: int(priority_counts.get(key, 0)) for key in ("critical", "high", "medium") + }, + "temporal_continuity_passed": ( + signal_counts["layer-blackout"] == 0 + and signal_counts["route-id-rebirth-candidate"] == 0 + and signal_counts["bbox-jump"] == 0 + ), + } + return signals, clips, metrics + + +def _flap_signals( + frames: list[dict[str, Any]], + route_track_id: int, + sequence: list[tuple[int, dict[str, Any]]], + field: str, + kind: str, +) -> list[dict[str, Any]]: + transitions: list[tuple[int, object, object]] = [] + for (previous_position, previous), (next_position, following) in zip( + sequence, sequence[1:], strict=False + ): + if next_position == previous_position + 1 and previous[field] != following[field]: + transitions.append((next_position, previous[field], following[field])) + output: list[dict[str, Any]] = [] + cursor = 0 + while cursor < len(transitions): + end = cursor + start_seconds = float(frames[transitions[cursor][0]]["session_seconds"]) + while end < len(transitions) and float( + frames[transitions[end][0]]["session_seconds"] + ) - start_seconds <= float(_PROFILE["flap_window_seconds"]): + end += 1 + window = transitions[cursor:end] + if len(window) >= int(_PROFILE["flap_min_transitions"]): + values = {value for _, before, after in window for value in (before, after)} + output.append( + _signal( + kind, + "high" if len(window) >= 4 else "medium", + frames, + window[0][0] - 1, + window[-1][0], + route_track_ids=[route_track_id], + world_track_ids=( + sorted( + int(value) + for value in values + if isinstance(value, int) and not isinstance(value, bool) + ) + if field == "world_track_id" + else [] + ), + evidence={ + "transition_count": len(window), + "window_seconds": round( + float(frames[window[-1][0]]["session_seconds"]) + - float(frames[window[0][0]]["session_seconds"]), + 6, + ), + "state_count": len(values), + }, + ) + ) + cursor = end + else: + cursor += 1 + return output + + +def _signal( + kind: str, + priority: str, + frames: list[dict[str, Any]], + start_position: int, + end_position: int, + *, + route_track_ids: list[int], + evidence: dict[str, object], + world_track_ids: list[int] | None = None, +) -> dict[str, Any]: + start_position = max(0, start_position) + end_position = min(len(frames) - 1, end_position) + basis = { + "kind": kind, + "priority": priority, + "start_frame": int(frames[start_position]["frame_index"]), + "end_frame": int(frames[end_position]["frame_index"]), + "route_track_ids": sorted(set(route_track_ids)), + "world_track_ids": sorted(set(world_track_ids or [])), + "evidence": evidence, + } + signal_id = "e46d-signal-" + hashlib.sha256(_canonical_json(basis)).hexdigest()[:20] + return { + "schema_version": E46D_SIGNAL_SCHEMA, + "signal_id": signal_id, + **basis, + "start_seconds": float(frames[start_position]["session_seconds"]), + "end_seconds": float(frames[end_position]["session_seconds"]), + } + + +def _review_clips( + signals: list[dict[str, Any]], timeline_start: float, timeline_end: float +) -> list[dict[str, Any]]: + ranked = sorted( + signals, + key=lambda item: ( + -_signal_score(item), + float(item["start_seconds"]), + str(item["signal_id"]), + ), + ) + selected: list[dict[str, Any]] = [] + selected_ids: set[str] = set() + + def admit(signal: dict[str, Any]) -> bool: + center = (float(signal["start_seconds"]) + float(signal["end_seconds"])) / 2 + if any( + abs(center - (float(item["start_seconds"]) + float(item["end_seconds"])) / 2) + < float(_PROFILE["review_clip_separation_seconds"]) + for item in selected + ): + return False + selected.append(signal) + selected_ids.add(str(signal["signal_id"])) + return True + + for kind in sorted({str(item["kind"]) for item in signals}): + admitted = 0 + for signal in ranked: + if ( + signal["kind"] == kind + and str(signal["signal_id"]) not in selected_ids + and admit(signal) + ): + admitted += 1 + if admitted == 2: + break + for signal in ranked: + if len(selected) >= int(_PROFILE["review_clip_limit"]): + break + if str(signal["signal_id"]) not in selected_ids: + admit(signal) + selected.sort(key=lambda item: (-_signal_score(item), float(item["start_seconds"]))) + clips: list[dict[str, Any]] = [] + for rank, signal in enumerate(selected, start=1): + clips.append( + { + "schema_version": E46D_CLIP_SCHEMA, + "clip_id": ( + f"e46d-clip-{rank:02d}-{str(signal['signal_id']).removeprefix('e46d-signal-')}" + ), + "rank": rank, + "priority": signal["priority"], + "kind": signal["kind"], + "signal_id": signal["signal_id"], + "start_seconds": max( + timeline_start, + float(signal["start_seconds"]) - float(_PROFILE["review_clip_lead_seconds"]), + ), + "event_start_seconds": float(signal["start_seconds"]), + "event_end_seconds": float(signal["end_seconds"]), + "end_seconds": min( + timeline_end, + float(signal["end_seconds"]) + float(_PROFILE["review_clip_tail_seconds"]), + ), + "start_frame": signal["start_frame"], + "end_frame": signal["end_frame"], + "route_track_ids": copy.deepcopy(signal["route_track_ids"]), + "world_track_ids": copy.deepcopy(signal["world_track_ids"]), + "evidence": copy.deepcopy(signal["evidence"]), + } + ) + return clips + + +def _signal_score(signal: dict[str, Any]) -> float: + base = { + "layer-blackout": 100.0, + "route-id-rebirth-candidate": 95.0, + "bbox-jump": 90.0, + "route-layer-gap": 82.0, + "camera-evidence-hold": 75.0, + "world-binding-flap": 68.0, + "motion-state-flap": 62.0, + "short-track-burst": 55.0, + }.get(str(signal["kind"]), 40.0) + evidence = signal.get("evidence") + duration = float(evidence.get("duration_seconds", 0.0)) if isinstance(evidence, dict) else 0.0 + count = 0.0 + if isinstance(evidence, dict): + for key in ( + "zero_frame_count", + "missing_frame_count", + "transition_count", + "short_track_count", + ): + value = evidence.get(key) + if isinstance(value, int | float) and not isinstance(value, bool): + count = max(count, float(value)) + return base + _PRIORITY[str(signal["priority"])] * 10 + min(duration * 5, 20) + min(count, 20) + + +def _read_e26_frames( + root: Path, expected_binding: dict[str, Any] +) -> tuple[list[dict[str, Any]], dict[str, str]]: + result_path = root / "result.json" + expected_result_sha = expected_binding.get("result_sha256") + expected_fusion_sha = expected_binding.get("fusion_frames_sha256") + if _sha256(result_path) != expected_result_sha: + raise E46DTemporalFailureAuditError("E26 result identity changed") + result = _read_json(result_path) + identity = result.get("identity") + artifacts = result.get("artifacts") + if ( + result.get("schema_version") != "missioncore.e10-integrated-perception-result/v1" + or result.get("acceptance_state") != "accepted" + or not isinstance(identity, dict) + or identity.get("source_id") != "sensor.camera.right" + or not isinstance(artifacts, list) + ): + raise E46DTemporalFailureAuditError("E26 result contract is invalid") + selection = identity.get("selection") + if not isinstance(selection, dict) or selection.get("frame_count") != 4489: + raise E46DTemporalFailureAuditError("E26 timeline contract is invalid") + fusion_artifact = next( + ( + item + for item in artifacts + if isinstance(item, dict) and item.get("path") == "fusion-frames.jsonl" + ), + None, + ) + fusion_path = _validated_artifact(root, fusion_artifact) + if _sha256(fusion_path) != expected_fusion_sha: + raise E46DTemporalFailureAuditError("E26 fusion identity changed") + frames: list[dict[str, Any]] = [] + for expected_index, row in enumerate(_read_jsonl(fusion_path)): + if row.get("source_frame_index") != expected_index: + raise E46DTemporalFailureAuditError("E26 frame sequence changed") + frames.append( + { + "frame_index": expected_index, + "session_seconds": row.get("session_seconds"), + "objects": [ + _temporal_object(item) + for item in row.get("objects", []) + if isinstance(item, dict) + ], + } + ) + _validate_frames(frames, expected_count=4489) + if frames[0]["session_seconds"] != selection.get("timeline_start_seconds") or frames[-1][ + "session_seconds" + ] != selection.get("timeline_end_seconds"): + raise E46DTemporalFailureAuditError("E26 timeline changed") + return frames, { + "result_id": str(result.get("result_id", root.name)), + "result_sha256": str(expected_result_sha), + "fusion_frames_sha256": str(expected_fusion_sha), + } + + +def _temporal_object(item: dict[str, Any]) -> dict[str, Any]: + box = item.get("bbox_xyxy") + source_track_id = item.get("source_track_id") + if ( + not isinstance(box, list) + or len(box) != 4 + or not all(_finite(value) for value in box) + or not isinstance(source_track_id, int) + or isinstance(source_track_id, bool) + ): + raise E46DTemporalFailureAuditError("E26 temporal object is invalid") + world_track_id = item.get("track_id") + if ( + not isinstance(world_track_id, int) + or isinstance(world_track_id, bool) + or world_track_id == source_track_id + ): + world_track_id = None + return { + "bbox_xyxy": [float(value) for value in box], + "category": str(item.get("label", "unknown")), + "route_track_id": source_track_id, + "world_track_id": world_track_id, + "motion_state": str(item.get("motion_state", "unknown")), + "camera_evidence_current": item.get("camera_evidence_current") is True, + } + + +def _validate_frames(frames: list[dict[str, Any]], expected_count: int | None = None) -> None: + if expected_count is not None and len(frames) != expected_count: + raise E46DTemporalFailureAuditError("E46D frame count is invalid") + if len(frames) < 2: + raise E46DTemporalFailureAuditError("E46D requires a temporal sequence") + previous_seconds = -math.inf + for expected_index, frame in enumerate(frames): + seconds = frame.get("session_seconds") + if ( + frame.get("frame_index") != expected_index + or not _finite(seconds) + or float(seconds) <= previous_seconds + or not isinstance(frame.get("objects"), list) + ): + raise E46DTemporalFailureAuditError("E46D temporal sequence is invalid") + previous_seconds = float(seconds) + for item in frame["objects"]: + if not isinstance(item, dict): + raise E46DTemporalFailureAuditError("E46D temporal object is invalid") + + +def _iou(left: list[float], right: list[float]) -> float: + x1, y1 = max(left[0], right[0]), max(left[1], right[1]) + x2, y2 = min(left[2], right[2]), min(left[3], right[3]) + intersection = max(0.0, x2 - x1) * max(0.0, y2 - y1) + union = _box_area(left) + _box_area(right) - intersection + return intersection / union if union > 0 else 0.0 + + +def _box_area(box: list[float]) -> float: + return max(0.0, box[2] - box[0]) * max(0.0, box[3] - box[1]) + + +def _finite(value: object) -> bool: + return ( + isinstance(value, int | float) + and not isinstance(value, bool) + and math.isfinite(float(value)) + ) + + +def _validated_artifact(root: Path, raw: object) -> Path: + if not isinstance(raw, dict): + raise E46DTemporalFailureAuditError("artifact metadata is invalid") + relative = raw.get("path") + expected_sha = raw.get("sha256") + expected_size = raw.get("byte_length", raw.get("size_bytes")) + if ( + not isinstance(relative, str) + or not isinstance(expected_sha, str) + or not isinstance(expected_size, int) + ): + raise E46DTemporalFailureAuditError("artifact metadata is invalid") + path = (root / relative).resolve(strict=True) + if ( + path.parent != root + or path.is_symlink() + or not path.is_file() + or path.stat().st_size != expected_size + or _sha256(path) != expected_sha + ): + raise E46DTemporalFailureAuditError("artifact changed") + return path + + +def _artifact(path: Path, role: str) -> dict[str, object]: + return { + "path": path.name, + "role": role, + "media_type": "application/x-ndjson" if path.suffix == ".jsonl" else "application/json", + "byte_length": path.stat().st_size, + "sha256": _sha256(path), + } + + +def _read_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise E46DTemporalFailureAuditError("JSON document is invalid") + return value + + +def _read_jsonl(path: Path): + with path.open("r", encoding="utf-8") as handle: + for line in handle: + if line.strip(): + value = json.loads(line) + if not isinstance(value, dict): + raise E46DTemporalFailureAuditError("JSONL row is invalid") + yield value + + +def _write_json(path: Path, value: object) -> None: + path.write_bytes(_canonical_json(value) + b"\n") + + +def _write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None: + with path.open("wb") as handle: + for row in rows: + handle.write(_canonical_json(row) + b"\n") + + +def _canonical_json(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode() + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for block in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() diff --git a/src/k1link/compute/e46e_ready_stack.py b/src/k1link/compute/e46e_ready_stack.py new file mode 100644 index 0000000..66b61a4 --- /dev/null +++ b/src/k1link/compute/e46e_ready_stack.py @@ -0,0 +1,772 @@ +"""Admit one stock NVIDIA detector/tracker replay as immutable E46E evidence. + +The module deliberately contains no association, hold, stitch, NMS, or tracking +logic. It only validates and projects DeepStream detector/NvDCF KITTI output +onto the exact recorded RIGHT-camera timeline. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +import math +import os +import re +import shutil +import uuid +from collections import Counter, defaultdict +from collections.abc import Iterable +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +E46E_PROFILE_SCHEMA: Final = "missioncore.e46e-ready-stack-profile/v1" +E46E_RUNTIME_SCHEMA: Final = "missioncore.e46e-deepstream-runtime/v1" +E46E_RESULT_SCHEMA: Final = "missioncore.e46e-ready-stack-result/v1" +E46E_REPORT_SCHEMA: Final = "missioncore.e46e-ready-stack-report/v1" +E46E_FRAME_SCHEMA: Final = "missioncore.e46e-ready-stack-frame/v1" +E46E_PACKAGE_SCHEMA: Final = "missioncore.e46e-worker-package/v1" +E46E_MANIFEST_NAME: Final = "manifest.json" +E46E_REPORT_NAME: Final = "ready-stack-report.json" +E46E_FRAMES_NAME: Final = "tracked-frames.jsonl" +E46E_OVERLAY_NAME: Final = "overlay.mp4" +E46E_RUNTIME_NAME: Final = "runtime.json" +E46E_LOG_NAME: Final = "deepstream.log" + +_RESULT_ID = re.compile(r"^e46e-ready-stack-[a-f0-9]{64}$") +_KITTI_NAME = re.compile(r"^\d{2}_\d{3}_(\d{6})\.txt$") +_AUTHORITY: Final = { + "ground_truth": False, + "independent_truth": False, + "metric_grade_reference": False, + "candidate_accepted": False, + "free_space_authority": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class E46EReadyStackError(ValueError): + """Raised when source, raw NVIDIA output, or immutable result is invalid.""" + + +def build_e46e_ready_stack( + *, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path +) -> dict[str, Any]: + """Validate raw DeepStream output and freeze a content-addressed result.""" + + profile_source = profile_path.resolve(strict=True) + profile = _read_json(profile_source) + _validate_profile(profile) + source = _read_source(source_job_root.resolve(strict=True), profile) + raw = raw_root.resolve(strict=True) + if raw.is_symlink(): + raise E46EReadyStackError("E46E raw root must not be a symlink") + runtime = _read_json(raw / E46E_RUNTIME_NAME) + _validate_runtime(runtime, profile) + overlay = _regular_file(raw / E46E_OVERLAY_NAME) + log = _regular_file(raw / E46E_LOG_NAME, allow_empty=True) + if runtime["overlay_sha256"] != _sha256(overlay): + raise E46EReadyStackError("E46E runtime overlay identity changed") + detector_files = _indexed_kitti_files(raw / "detections", source["frame_count"]) + tracker_files = _indexed_kitti_files(raw / "tracks", source["frame_count"]) + + frames: list[dict[str, Any]] = [] + for frame_index, source_row in enumerate(source["index"]): + detections = _parse_detector_file(detector_files[frame_index]) + objects = _parse_tracker_file(tracker_files[frame_index]) + frames.append( + { + "schema_version": E46E_FRAME_SCHEMA, + "frame_index": frame_index, + "sequence": int(source_row["sequence"]), + "session_seconds": source["timeline_start_seconds"] + + ( + int(source_row["session_monotonic_ns"]) + - source["first_session_monotonic_ns"] + ) + / 1_000_000_000.0, + "source_image_sha256": str(source_row["sha256"]), + "detection_count": len(detections), + "tracked_object_count": len(objects), + "detections": detections, + "objects": objects, + } + ) + metrics = analyze_e46e_frames(frames) + method = { + "schema_version": "missioncore.laboratory-method/v1", + "completeness": "complete", + "execution_class": "hybrid", + "pipeline_id": str(profile["profile_id"]), + "components": [ + { + "kind": "source", + "name": str(profile["source"]["job_id"]), + "version": "immutable recorded RIGHT replay", + "role": "exact recorded camera evidence", + "identity_sha256": source["job_sha256"], + }, + { + "kind": "model", + "name": str(profile["detector"]["name"]), + "version": str(profile["detector"]["version"]), + "role": "framewise traffic-object detection", + "identity_sha256": str(profile["detector"]["model_sha256"]), + }, + { + "kind": "tool", + "name": str(profile["parser"]["name"]), + "version": str(profile["parser"]["commit"]), + "role": "official RT-DETR output decoding", + "identity_sha256": str(runtime["parser_library_sha256"]), + }, + { + "kind": "algorithm", + "name": str(profile["tracker"]["name"]), + "version": str(profile["tracker"]["configuration"]), + "role": "route-local temporal association", + "identity_sha256": str(runtime["tracker_config_sha256"]), + }, + { + "kind": "runtime", + "name": "NVIDIA DeepStream", + "version": str(profile["runtime"]["deepstream_version"]), + "role": "GPU inference and media pipeline", + "identity_sha256": str(runtime["container_image_digest"]), + }, + ], + } + report_basis = { + "schema_version": E46E_REPORT_SCHEMA, + "status": "completed-stock-nvidia-recorded-right-replay", + "metrics": metrics, + "acceptance": { + "full_route_accounted": metrics["frame_count"] + == int(profile["source"]["segment_count"]), + "stock_detector_tracker_executed": True, + "visual_overlay_available": True, + "independent_truth_available": False, + "navigation_or_safety_accepted": False, + }, + "decision": { + "ready_stack_baseline_available": True, + "custom_temporal_logic_used": False, + "next_action": ( + "review the full overlay and compare the same objective failure metrics " + "against the frozen YOLOX custom-temporal baseline before promotion" + ), + }, + "method": method, + "limitations": [ + ( + "TrafficCamNet Transformer Lite has four traffic classes; detections outside " + "bicycle, car, person, and road_sign are not claimed" + ), + ( + "NvDCF IDs are route-local tracker identities, not permanent physical identities" + ), + ( + f"{metrics['track_box_clipped_count']} stock NvDCF observations crossed the " + "800x600 source boundary; the Mission Core evidence adapter clips only their " + "display geometry and preserves every raw LTRB coordinate, ID, class, and score" + ), + ( + "this is recorded RIGHT-camera evidence only; no LEFT camera, live hardware, " + "free-space, command, navigation, or safety authority is introduced" + ), + ( + "without independent frame truth, counts describe output continuity and cannot " + "alone establish precision or recall" + ), + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + raw_identity = { + "runtime_schema": runtime["schema_version"], + "worker_host": runtime["worker_host"], + "gpu_name": runtime["gpu_name"], + "container_image": runtime["container_image"], + "container_image_digest": runtime["container_image_digest"], + "model_sha256": runtime["model_sha256"], + "model_engine_sha256": runtime["model_engine_sha256"], + "deepstream_config_sha256": runtime["deepstream_config_sha256"], + "detector_config_sha256": runtime["detector_config_sha256"], + "parser_library_sha256": runtime["parser_library_sha256"], + "tracker_config_sha256": runtime["tracker_config_sha256"], + "input_stream_sha256": runtime["input_stream_sha256"], + "overlay_sha256": _sha256(overlay), + "deepstream_log_sha256": _sha256(log), + } + identity = { + "schema_version": E46E_RESULT_SCHEMA, + "source": source["identity"], + "profile_sha256": _sha256(profile_source), + "profile": copy.deepcopy(profile), + "raw_execution": raw_identity, + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "frames_sha256": hashlib.sha256(_canonical_json(frames)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e46e-ready-stack-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_e46e_ready_stack(destination) + + created_at_utc = datetime.now(UTC).isoformat(timespec="milliseconds").replace( + "+00:00", "Z" + ) + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc, + "source_session_id": str(profile["source"]["session_id"]), + "camera_source_id": str(profile["source"]["camera_source_id"]), + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / E46E_REPORT_NAME, report) + _write_jsonl(staging / E46E_FRAMES_NAME, frames) + shutil.copyfile(overlay, staging / E46E_OVERLAY_NAME) + shutil.copyfile(raw / E46E_RUNTIME_NAME, staging / E46E_RUNTIME_NAME) + shutil.copyfile(log, staging / E46E_LOG_NAME) + artifacts = [ + _artifact(staging / E46E_REPORT_NAME, "ready-stack-report"), + _artifact(staging / E46E_FRAMES_NAME, "tracked-frames"), + _artifact(staging / E46E_OVERLAY_NAME, "visual-overlay-video"), + _artifact(staging / E46E_RUNTIME_NAME, "runtime-record"), + _artifact(staging / E46E_LOG_NAME, "deepstream-log"), + ] + _write_json( + staging / E46E_MANIFEST_NAME, + { + "schema_version": E46E_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "stock-ready-stack-recorded-diagnostic", + "ground_truth": False, + "artifacts": artifacts, + "authority": _AUTHORITY, + }, + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_e46e_ready_stack(destination) + + +def read_e46e_ready_stack(root: Path) -> dict[str, Any]: + """Read and fully validate an immutable E46E result.""" + + resolved = root.resolve(strict=True) + if resolved.is_symlink(): + raise E46EReadyStackError("E46E result root must not be a symlink") + manifest = _read_json(resolved / E46E_MANIFEST_NAME) + identity = manifest.get("identity") + digest = ( + hashlib.sha256(_canonical_json(identity)).hexdigest() + if isinstance(identity, dict) + else "" + ) + if ( + manifest.get("schema_version") != E46E_RESULT_SCHEMA + or manifest.get("result_id") != f"e46e-ready-stack-{digest}" + or manifest.get("identity_sha256") != digest + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("authority") != _AUTHORITY + or manifest.get("ground_truth") is not False + ): + raise E46EReadyStackError("E46E result identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 5: + raise E46EReadyStackError("E46E artifact inventory is invalid") + by_role = {row.get("role"): row for row in artifacts if isinstance(row, dict)} + paths = { + role: _validated_artifact(resolved, by_role.get(role)) + for role in ( + "ready-stack-report", + "tracked-frames", + "visual-overlay-video", + "runtime-record", + "deepstream-log", + ) + } + report = _read_json(paths["ready-stack-report"]) + frames = tuple(_read_jsonl(paths["tracked-frames"])) + runtime = _read_json(paths["runtime-record"]) + if ( + report.get("schema_version") != E46E_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("authority") != _AUTHORITY + or report.get("ground_truth") is not False + or runtime.get("schema_version") != E46E_RUNTIME_SCHEMA + or any(row.get("schema_version") != E46E_FRAME_SCHEMA for row in frames) + or hashlib.sha256(_canonical_json(frames)).hexdigest() + != identity.get("frames_sha256") + or report.get("metrics", {}).get("frame_count") != len(frames) + ): + raise E46EReadyStackError("E46E result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "frames": frames, + "runtime": runtime, + "overlay_path": paths["visual-overlay-video"], + } + + +def analyze_e46e_frames(frames: Iterable[dict[str, Any]]) -> dict[str, Any]: + """Calculate objective continuity metrics without modifying tracker output.""" + + rows = list(frames) + if not rows: + raise E46EReadyStackError("E46E requires at least one frame") + detection_observations = 0 + track_observations = 0 + detection_box_clips = 0 + track_box_clips = 0 + zero_detection_frames = 0 + zero_track_frames = 0 + recovered_frames = 0 + detector_classes: Counter[str] = Counter() + tracker_classes: Counter[str] = Counter() + observations: dict[int, list[tuple[int, str]]] = defaultdict(list) + track_counts: list[int] = [] + for expected, frame in enumerate(rows): + if frame.get("frame_index") != expected: + raise E46EReadyStackError("E46E frame sequence is not contiguous") + detections = frame.get("detections") + objects = frame.get("objects") + if not isinstance(detections, list) or not isinstance(objects, list): + raise E46EReadyStackError("E46E frame payload is invalid") + detection_observations += len(detections) + track_observations += len(objects) + detection_box_clips += sum( + item.get("source_plane_clipped") is True for item in detections + ) + track_box_clips += sum( + item.get("source_plane_clipped") is True for item in objects + ) + zero_detection_frames += not detections + zero_track_frames += not objects + recovered_frames += not detections and bool(objects) + track_counts.append(len(objects)) + detector_classes.update(str(item["class_name"]) for item in detections) + tracker_classes.update(str(item["class_name"]) for item in objects) + for item in objects: + observations[int(item["source_track_id"])].append( + (expected, str(item["class_name"])) + ) + + route_gap_events = 0 + short_tracks = 0 + class_switches = 0 + for track_rows in observations.values(): + ordered = sorted(track_rows) + short_tracks += len(ordered) <= 3 + route_gap_events += sum( + next_frame - frame > 1 + for (frame, _), (next_frame, _) in zip( + ordered, ordered[1:], strict=False + ) + ) + class_switches += sum( + class_name != next_class + for (_, class_name), (_, next_class) in zip( + ordered, ordered[1:], strict=False + ) + ) + + blackouts = 0 + start: int | None = None + for index, count in enumerate(track_counts + [1]): + if count == 0 and start is None: + start = index + elif count > 0 and start is not None: + if start > 0 and index < len(track_counts) and index - start >= 3: + blackouts += 1 + start = None + duration = float(rows[-1]["session_seconds"]) - float(rows[0]["session_seconds"]) + return { + "frame_count": len(rows), + "route_duration_seconds": round(max(0.0, duration), 6), + "detection_observation_count": detection_observations, + "track_observation_count": track_observations, + "detection_box_clipped_count": detection_box_clips, + "track_box_clipped_count": track_box_clips, + "unique_track_count": len(observations), + "mean_tracked_objects_per_frame": round(track_observations / len(rows), 6), + "zero_detection_frame_count": zero_detection_frames, + "zero_track_frame_count": zero_track_frames, + "tracker_recovered_frame_count": recovered_frames, + "full_layer_blackout_event_count": blackouts, + "route_id_gap_event_count": route_gap_events, + "short_track_count": short_tracks, + "short_track_fraction": round(short_tracks / max(1, len(observations)), 6), + "track_class_switch_count": class_switches, + "detector_class_observations": dict(sorted(detector_classes.items())), + "tracker_class_observations": dict(sorted(tracker_classes.items())), + } + + +def _read_source(root: Path, profile: dict[str, Any]) -> dict[str, Any]: + job_path = _regular_file(root / "job.json") + job = _read_json(job_path) + expected = profile["source"] + input_value = job.get("input") + if not isinstance(input_value, dict): + raise E46EReadyStackError("E46E source job input is invalid") + camera = root / "input" / "camera" / str(expected["camera_source_id"]) / "epoch-1" + summary_path = _regular_file(camera / "summary.json") + index_path = _regular_file(camera / "index.jsonl") + summary = _read_json(summary_path) + index = list(_read_jsonl(index_path)) + frame_count = int(expected["segment_count"]) + timeline = input_value.get("timeline") + if ( + job.get("schema_version") != "missioncore.compute-job/v1" + or job.get("job_id") != expected["job_id"] + or input_value.get("session_id") != expected["session_id"] + or input_value.get("source_id") != expected["camera_source_id"] + or input_value.get("segment_count") != frame_count + or input_value.get("archive_index_sha256") != expected["archive_index_sha256"] + or input_value.get("archive_summary_sha256") != expected["archive_summary_sha256"] + or summary.get("stream_sha256") != expected["stream_sha256"] + or summary.get("segment_count") != frame_count + or _sha256(index_path) != expected["archive_index_sha256"] + or _sha256(summary_path) != expected["archive_summary_sha256"] + or not isinstance(timeline, dict) + or not isinstance(timeline.get("start_seconds"), (int, float)) + or len(index) != frame_count + ): + raise E46EReadyStackError("E46E exact recorded source binding changed") + for position, row in enumerate(index, start=1): + if ( + row.get("schema_version") != "missioncore.camera-recording-index/v1" + or row.get("kind") != "media" + or row.get("sequence") != position + or not isinstance(row.get("session_monotonic_ns"), int) + or not _is_sha256(row.get("sha256")) + ): + raise E46EReadyStackError("E46E source index is invalid") + return { + "frame_count": frame_count, + "index": index, + "timeline_start_seconds": float(timeline["start_seconds"]), + "first_session_monotonic_ns": int(index[0]["session_monotonic_ns"]), + "job_sha256": _sha256(job_path), + "identity": { + "job_id": expected["job_id"], + "job_sha256": _sha256(job_path), + "session_id": expected["session_id"], + "camera_source_id": expected["camera_source_id"], + "stream_sha256": expected["stream_sha256"], + "archive_index_sha256": expected["archive_index_sha256"], + "archive_summary_sha256": expected["archive_summary_sha256"], + "frame_count": frame_count, + }, + } + + +def _validate_profile(profile: dict[str, Any]) -> None: + try: + source = profile["source"] + runtime = profile["runtime"] + detector = profile["detector"] + parser = profile["parser"] + tracker = profile["tracker"] + output = profile["output"] + except KeyError as exc: + raise E46EReadyStackError("E46E profile is incomplete") from exc + if ( + profile.get("schema_version") != E46E_PROFILE_SCHEMA + or profile.get("authority") != { + "ground_truth": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, + } + or not _is_sha256(source.get("stream_sha256")) + or not _is_sha256(source.get("archive_index_sha256")) + or not _is_sha256(source.get("archive_summary_sha256")) + or not str(runtime.get("container_image", "")).startswith( + "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:" + ) + or not _is_sha256(detector.get("model_sha256")) + or detector.get("custom_postprocessing") is not False + or parser.get("repository") != "https://github.com/NVIDIA/DeepStream.git" + or parser.get("symbol") != "NvDsInferParseCustomDDETRTAO" + or not _is_sha256(parser.get("library_sha256")) + or parser.get("custom_mission_core_logic") is not False + or tracker.get("custom_association") is not False + or tracker.get("custom_hold_or_stitch") is not False + or output.get("frame_width") != 800 + or output.get("frame_height") != 600 + ): + raise E46EReadyStackError("E46E profile contract is invalid") + + +def _validate_runtime(runtime: dict[str, Any], profile: dict[str, Any]) -> None: + expected_image = str(profile["runtime"]["container_image"]) + expected_digest = expected_image.rsplit("@sha256:", 1)[1] + required_sha = ( + "container_image_digest", + "model_sha256", + "model_engine_sha256", + "deepstream_config_sha256", + "detector_config_sha256", + "parser_library_sha256", + "tracker_config_sha256", + "input_stream_sha256", + "overlay_sha256", + ) + if ( + runtime.get("schema_version") != E46E_RUNTIME_SCHEMA + or runtime.get("status") != "completed" + or runtime.get("container_image") != expected_image + or runtime.get("container_image_digest") != expected_digest + or runtime.get("model_sha256") != profile["detector"]["model_sha256"] + or runtime.get("parser_library_sha256") != profile["parser"]["library_sha256"] + or runtime.get("input_stream_sha256") != profile["source"]["stream_sha256"] + or not isinstance(runtime.get("worker_host"), str) + or not runtime.get("worker_host") + or not isinstance(runtime.get("gpu_name"), str) + or not runtime.get("gpu_name") + or any(not _is_sha256(runtime.get(name)) for name in required_sha) + ): + raise E46EReadyStackError("E46E runtime identity is invalid") + + +def _indexed_kitti_files(root: Path, frame_count: int) -> dict[int, Path]: + resolved = root.resolve(strict=True) + if not resolved.is_dir() or resolved.is_symlink(): + raise E46EReadyStackError("E46E KITTI directory is invalid") + indexed: dict[int, Path] = {} + for path in resolved.iterdir(): + if not path.is_file() or path.is_symlink(): + raise E46EReadyStackError("E46E KITTI member is invalid") + match = _KITTI_NAME.fullmatch(path.name) + if match is None: + raise E46EReadyStackError("E46E KITTI filename is invalid") + frame_index = int(match.group(1)) + if frame_index in indexed or frame_index >= frame_count: + raise E46EReadyStackError("E46E KITTI frame inventory is invalid") + indexed[frame_index] = path + if set(indexed) != set(range(frame_count)): + raise E46EReadyStackError("E46E KITTI frame coverage is incomplete") + return indexed + + +def _parse_detector_file(path: Path) -> list[dict[str, Any]]: + output: list[dict[str, Any]] = [] + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + tokens = line.split() + if len(tokens) != 16: + raise E46EReadyStackError(f"invalid detector KITTI row {path.name}:{line_number}") + box, source_box, clipped = _parse_box(tokens[4:8], path, line_number) + confidence = _finite_float(tokens[15], path, line_number) + output.append( + { + "class_name": tokens[0], + "bbox": box, + "source_bbox_ltrb": source_box, + "source_plane_clipped": clipped, + "confidence": confidence, + "provenance": "nvidia-trafficcamnet-rtdetr", + } + ) + return output + + +def _parse_tracker_file(path: Path) -> list[dict[str, Any]]: + output: list[dict[str, Any]] = [] + seen: dict[int, dict[str, Any]] = {} + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + tokens = line.split() + if len(tokens) < 17: + raise E46EReadyStackError(f"invalid tracker KITTI row {path.name}:{line_number}") + try: + source_track_id = int(tokens[1]) + except ValueError as exc: + raise E46EReadyStackError( + f"invalid tracker id {path.name}:{line_number}" + ) from exc + if source_track_id < 0: + raise E46EReadyStackError("negative NvDCF track id") + box, source_box, clipped = _parse_box(tokens[5:9], path, line_number) + item = { + "object_id": f"nvdcf-{source_track_id}", + "source_track_id": source_track_id, + "class_name": tokens[0], + "bbox": box, + "source_bbox_ltrb": source_box, + "source_plane_clipped": clipped, + "confidence": _finite_float(tokens[16], path, line_number), + "provenance": "nvidia-nvdcf-stock-output", + } + previous = seen.get(source_track_id) + if previous is not None and previous != item: + raise E46EReadyStackError( + f"conflicting NvDCF track rows {path.name}:{source_track_id}" + ) + if previous is None: + seen[source_track_id] = item + output.append(item) + return output + + +def _parse_box( + tokens: list[str], path: Path, line_number: int +) -> tuple[list[float], list[float], bool]: + left, top, right, bottom = ( + _finite_float(token, path, line_number) for token in tokens + ) + if right <= left or bottom <= top: + raise E46EReadyStackError(f"invalid bounding box {path.name}:{line_number}") + projected_left = max(0.0, min(800.0, left)) + projected_top = max(0.0, min(600.0, top)) + projected_right = max(0.0, min(800.0, right)) + projected_bottom = max(0.0, min(600.0, bottom)) + if projected_right <= projected_left or projected_bottom <= projected_top: + raise E46EReadyStackError(f"bounding box outside source plane {path.name}:{line_number}") + source = [left, top, right, bottom] + projected = [ + projected_left, + projected_top, + projected_right - projected_left, + projected_bottom - projected_top, + ] + return projected, source, source != [ + projected_left, + projected_top, + projected_right, + projected_bottom, + ] + + +def _finite_float(token: str, path: Path, line_number: int) -> float: + try: + value = float(token) + except ValueError as exc: + raise E46EReadyStackError(f"invalid float {path.name}:{line_number}") from exc + if not math.isfinite(value): + raise E46EReadyStackError(f"non-finite float {path.name}:{line_number}") + return value + + +def _validated_artifact(root: Path, row: object) -> Path: + if not isinstance(row, dict): + raise E46EReadyStackError("E46E artifact descriptor is invalid") + relative = row.get("path") + if ( + not isinstance(relative, str) + or Path(relative).is_absolute() + or ".." in Path(relative).parts + ): + raise E46EReadyStackError("E46E artifact path is invalid") + path = root / relative + if ( + not path.is_file() + or path.is_symlink() + or row.get("byte_length") != path.stat().st_size + or row.get("sha256") != _sha256(path) + ): + raise E46EReadyStackError("E46E artifact changed") + return path + + +def _artifact(path: Path, role: str) -> dict[str, Any]: + return { + "role": role, + "path": path.name, + "byte_length": path.stat().st_size, + "sha256": _sha256(path), + } + + +def _regular_file(path: Path, *, allow_empty: bool = False) -> Path: + resolved = path.resolve(strict=True) + if ( + not resolved.is_file() + or resolved.is_symlink() + or (not allow_empty and resolved.stat().st_size == 0) + ): + raise E46EReadyStackError(f"E46E file is invalid: {path.name}") + return resolved + + +def _is_sha256(value: object) -> bool: + return isinstance(value, str) and re.fullmatch(r"[a-f0-9]{64}", value) is not None + + +def _canonical_json(value: object) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode() + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _read_json(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8-sig")) + except (OSError, json.JSONDecodeError) as exc: + raise E46EReadyStackError(f"invalid JSON: {path.name}") from exc + if not isinstance(value, dict): + raise E46EReadyStackError(f"JSON object expected: {path.name}") + return value + + +def _read_jsonl(path: Path) -> Iterable[dict[str, Any]]: + with path.open("r", encoding="utf-8-sig") as stream: + for line_number, line in enumerate(stream, 1): + try: + row = json.loads(line) + except json.JSONDecodeError as exc: + raise E46EReadyStackError( + f"invalid JSONL: {path.name}:{line_number}" + ) from exc + if not isinstance(row, dict): + raise E46EReadyStackError(f"JSON object expected: {path.name}:{line_number}") + yield row + + +def _write_json(path: Path, value: object) -> None: + with path.open("x", encoding="utf-8") as stream: + json.dump(value, stream, ensure_ascii=False, indent=2, allow_nan=False) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) + + +def _write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None: + with path.open("x", encoding="utf-8") as stream: + for row in rows: + stream.write( + json.dumps( + row, + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ) + ) + stream.write("\n") + stream.flush() + os.fsync(stream.fileno()) diff --git a/src/k1link/compute/e46f_dashcam_bakeoff.py b/src/k1link/compute/e46f_dashcam_bakeoff.py new file mode 100644 index 0000000..12b9d3a --- /dev/null +++ b/src/k1link/compute/e46f_dashcam_bakeoff.py @@ -0,0 +1,433 @@ +"""Freeze an NVIDIA DashCamNet versus E46E detector-only bake-off as E46F. + +The replay, DeepStream image, FP16 precision, NvDCF configuration, source +plane, and evidence adapter are held constant. Only the stock NVIDIA detector +and its stock provider post-processing change. Mission Core performs no NMS, +association, hold, stitch, or semantic correction in this module. +""" + +from __future__ import annotations + +import copy +import hashlib +import os +import re +import shutil +import uuid +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from k1link.compute.e46e_ready_stack import ( + E46EReadyStackError, + _artifact, + _canonical_json, + _indexed_kitti_files, + _parse_detector_file, + _parse_tracker_file, + _read_json, + _read_jsonl, + _read_source, + _regular_file, + _sha256, + _validated_artifact, + _write_json, + _write_jsonl, + analyze_e46e_frames, +) + +E46F_PROFILE_SCHEMA: Final = "missioncore.e46f-dashcam-bakeoff-profile/v1" +E46F_RUNTIME_SCHEMA: Final = "missioncore.e46f-dashcam-deepstream-runtime/v1" +E46F_RESULT_SCHEMA: Final = "missioncore.e46f-dashcam-bakeoff-result/v1" +E46F_REPORT_SCHEMA: Final = "missioncore.e46f-dashcam-bakeoff-report/v1" +E46F_FRAME_SCHEMA: Final = "missioncore.e46f-dashcam-bakeoff-frame/v1" +E46F_PACKAGE_SCHEMA: Final = "missioncore.e46f-worker-package/v1" +E46F_MANIFEST_NAME: Final = "manifest.json" +E46F_REPORT_NAME: Final = "dashcam-bakeoff-report.json" +E46F_FRAMES_NAME: Final = "tracked-frames.jsonl" +E46F_OVERLAY_NAME: Final = "overlay.mp4" +E46F_RUNTIME_NAME: Final = "runtime.json" +E46F_LOG_NAME: Final = "deepstream.log" + +_RESULT_ID = re.compile(r"^e46f-dashcam-bakeoff-[a-f0-9]{64}$") +_AUTHORITY: Final = { + "ground_truth": False, + "independent_truth": False, + "metric_grade_reference": False, + "candidate_accepted": False, + "free_space_authority": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class E46FDashCamBakeoffError(ValueError): + """Raised when the E46F source, execution, or result is invalid.""" + + +def build_e46f_dashcam_bakeoff( + *, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path +) -> dict[str, Any]: + """Validate one stock DashCamNet/NvDCF replay and freeze immutable evidence.""" + + try: + return _build( + source_job_root=source_job_root, + raw_root=raw_root, + profile_path=profile_path, + output_root=output_root, + ) + except E46EReadyStackError as exc: + raise E46FDashCamBakeoffError(str(exc).replace("E46E", "E46F")) from exc + + +def _build( + *, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path +) -> dict[str, Any]: + profile_source = profile_path.resolve(strict=True) + profile = _read_json(profile_source) + _validate_profile(profile) + source = _read_source(source_job_root.resolve(strict=True), profile) + raw = raw_root.resolve(strict=True) + if raw.is_symlink(): + raise E46FDashCamBakeoffError("E46F raw root must not be a symlink") + runtime = _read_json(raw / E46F_RUNTIME_NAME) + _validate_runtime(runtime, profile) + overlay = _regular_file(raw / E46F_OVERLAY_NAME) + log = _regular_file(raw / E46F_LOG_NAME, allow_empty=True) + if runtime["overlay_sha256"] != _sha256(overlay): + raise E46FDashCamBakeoffError("E46F runtime overlay identity changed") + detector_files = _indexed_kitti_files(raw / "detections", source["frame_count"]) + tracker_files = _indexed_kitti_files(raw / "tracks", source["frame_count"]) + + frames: list[dict[str, Any]] = [] + for frame_index, source_row in enumerate(source["index"]): + detections = _parse_detector_file(detector_files[frame_index]) + for detection in detections: + detection["provenance"] = "nvidia-dashcamnet-detectnet-v2" + objects = _parse_tracker_file(tracker_files[frame_index]) + frames.append( + { + "schema_version": E46F_FRAME_SCHEMA, + "frame_index": frame_index, + "sequence": int(source_row["sequence"]), + "session_seconds": source["timeline_start_seconds"] + + (int(source_row["session_monotonic_ns"]) - source["first_session_monotonic_ns"]) + / 1_000_000_000.0, + "source_image_sha256": str(source_row["sha256"]), + "detection_count": len(detections), + "tracked_object_count": len(objects), + "detections": detections, + "objects": objects, + } + ) + metrics = analyze_e46e_frames(frames) + method = { + "schema_version": "missioncore.laboratory-method/v1", + "completeness": "complete", + "execution_class": "hybrid", + "pipeline_id": str(profile["profile_id"]), + "components": [ + { + "kind": "source", + "name": str(profile["source"]["job_id"]), + "version": "immutable recorded RIGHT replay", + "role": "exact E46E/E46F controlled camera evidence", + "identity_sha256": source["job_sha256"], + }, + { + "kind": "model", + "name": str(profile["detector"]["name"]), + "version": str(profile["detector"]["version"]), + "role": "moving-camera traffic-object detection", + "identity_sha256": str(profile["detector"]["model_sha256"]), + }, + { + "kind": "tool", + "name": str(profile["postprocessor"]["name"]), + "version": str(profile["postprocessor"]["reference_commit"]), + "role": "stock DetectNet_v2 decode and NMS", + "identity_sha256": str(runtime["detector_config_sha256"]), + }, + { + "kind": "algorithm", + "name": str(profile["tracker"]["name"]), + "version": str(profile["tracker"]["configuration"]), + "role": "route-local temporal association", + "identity_sha256": str(runtime["tracker_config_sha256"]), + }, + { + "kind": "runtime", + "name": "NVIDIA DeepStream", + "version": str(profile["runtime"]["deepstream_version"]), + "role": "GPU inference and media pipeline", + "identity_sha256": str(runtime["container_image_digest"]), + }, + ], + } + report_basis = { + "schema_version": E46F_REPORT_SCHEMA, + "status": "completed-stock-nvidia-detector-only-bakeoff", + "comparison_contract": copy.deepcopy(profile["comparison_contract"]), + "metrics": metrics, + "acceptance": { + "full_route_accounted": metrics["frame_count"] + == int(profile["source"]["segment_count"]), + "stock_detector_tracker_executed": True, + "controlled_detector_only_change": True, + "visual_overlay_available": True, + "independent_truth_available": False, + "navigation_or_safety_accepted": False, + }, + "decision": { + "dashcam_detector_bakeoff_available": True, + "custom_temporal_logic_used": False, + "next_action": ( + "perform full-video semantic review against E46E and retain the detector " + "only if moving-camera false positives improve without temporal regression" + ), + }, + "method": method, + "limitations": [ + ( + "DashCamNet is evaluated by NVIDIA primarily for car detection; person, " + "bicycle, and road_sign quality is not claimed by this LAB" + ), + ( + "the four-class detector cannot represent stroller, facade, vegetation, " + "free-space, or dynamic/static motion state" + ), + "NvDCF IDs are route-local tracker identities, not permanent physical identities", + ( + f"{metrics['track_box_clipped_count']} stock NvDCF observations crossed the " + "800x600 source boundary; only display geometry is clipped while raw LTRB, " + "ID, class, and score remain preserved" + ), + ( + "this is recorded RIGHT-camera evidence only; no LEFT camera, live hardware, " + "LiDAR fusion, free-space, command, navigation, or safety authority is introduced" + ), + ( + "without independent full-route truth, output counts and continuity cannot " + "establish absolute precision or recall" + ), + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + raw_identity = { + "runtime_schema": runtime["schema_version"], + "worker_host": runtime["worker_host"], + "gpu_name": runtime["gpu_name"], + "container_image": runtime["container_image"], + "container_image_digest": runtime["container_image_digest"], + "model_sha256": runtime["model_sha256"], + "model_engine_sha256": runtime["model_engine_sha256"], + "deepstream_config_sha256": runtime["deepstream_config_sha256"], + "detector_config_sha256": runtime["detector_config_sha256"], + "tracker_config_sha256": runtime["tracker_config_sha256"], + "input_stream_sha256": runtime["input_stream_sha256"], + "overlay_sha256": _sha256(overlay), + "deepstream_log_sha256": _sha256(log), + } + identity = { + "schema_version": E46F_RESULT_SCHEMA, + "source": source["identity"], + "profile_sha256": _sha256(profile_source), + "profile": copy.deepcopy(profile), + "raw_execution": raw_identity, + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "frames_sha256": hashlib.sha256(_canonical_json(frames)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e46f-dashcam-bakeoff-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_e46f_dashcam_bakeoff(destination) + + created_at_utc = datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc, + "source_session_id": str(profile["source"]["session_id"]), + "camera_source_id": str(profile["source"]["camera_source_id"]), + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / E46F_REPORT_NAME, report) + _write_jsonl(staging / E46F_FRAMES_NAME, frames) + shutil.copyfile(overlay, staging / E46F_OVERLAY_NAME) + shutil.copyfile(raw / E46F_RUNTIME_NAME, staging / E46F_RUNTIME_NAME) + shutil.copyfile(log, staging / E46F_LOG_NAME) + artifacts = [ + _artifact(staging / E46F_REPORT_NAME, "dashcam-bakeoff-report"), + _artifact(staging / E46F_FRAMES_NAME, "tracked-frames"), + _artifact(staging / E46F_OVERLAY_NAME, "visual-overlay-video"), + _artifact(staging / E46F_RUNTIME_NAME, "runtime-record"), + _artifact(staging / E46F_LOG_NAME, "deepstream-log"), + ] + _write_json( + staging / E46F_MANIFEST_NAME, + { + "schema_version": E46F_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "stock-detector-only-recorded-diagnostic", + "ground_truth": False, + "artifacts": artifacts, + "authority": _AUTHORITY, + }, + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_e46f_dashcam_bakeoff(destination) + + +def read_e46f_dashcam_bakeoff(root: Path) -> dict[str, Any]: + """Read and fully validate an immutable E46F result.""" + + try: + resolved = root.resolve(strict=True) + if resolved.is_symlink(): + raise E46FDashCamBakeoffError("E46F result root must not be a symlink") + manifest = _read_json(resolved / E46F_MANIFEST_NAME) + identity = manifest.get("identity") + digest = ( + hashlib.sha256(_canonical_json(identity)).hexdigest() + if isinstance(identity, dict) + else "" + ) + if ( + manifest.get("schema_version") != E46F_RESULT_SCHEMA + or manifest.get("result_id") != f"e46f-dashcam-bakeoff-{digest}" + or manifest.get("identity_sha256") != digest + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("authority") != _AUTHORITY + or manifest.get("ground_truth") is not False + ): + raise E46FDashCamBakeoffError("E46F result identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 5: + raise E46FDashCamBakeoffError("E46F artifact inventory is invalid") + by_role = {row.get("role"): row for row in artifacts if isinstance(row, dict)} + paths = { + role: _validated_artifact(resolved, by_role.get(role)) + for role in ( + "dashcam-bakeoff-report", + "tracked-frames", + "visual-overlay-video", + "runtime-record", + "deepstream-log", + ) + } + report = _read_json(paths["dashcam-bakeoff-report"]) + frames = tuple(_read_jsonl(paths["tracked-frames"])) + runtime = _read_json(paths["runtime-record"]) + if ( + report.get("schema_version") != E46F_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("authority") != _AUTHORITY + or report.get("ground_truth") is not False + or runtime.get("schema_version") != E46F_RUNTIME_SCHEMA + or any(row.get("schema_version") != E46F_FRAME_SCHEMA for row in frames) + or hashlib.sha256(_canonical_json(frames)).hexdigest() != identity.get("frames_sha256") + or report.get("metrics", {}).get("frame_count") != len(frames) + ): + raise E46FDashCamBakeoffError("E46F result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "frames": frames, + "runtime": runtime, + "overlay_path": paths["visual-overlay-video"], + } + except E46EReadyStackError as exc: + raise E46FDashCamBakeoffError(str(exc).replace("E46E", "E46F")) from exc + + +def _validate_profile(profile: dict[str, Any]) -> None: + try: + comparison = profile["comparison_contract"] + source = profile["source"] + runtime = profile["runtime"] + detector = profile["detector"] + postprocessor = profile["postprocessor"] + tracker = profile["tracker"] + output = profile["output"] + except KeyError as exc: + raise E46FDashCamBakeoffError("E46F profile is incomplete") from exc + sha_pattern = re.compile(r"^[a-f0-9]{64}$") + if ( + profile.get("schema_version") != E46F_PROFILE_SCHEMA + or comparison.get("controlled_change") != "detector-only" + or not re.fullmatch( + r"e46e-ready-stack-[a-f0-9]{64}", str(comparison.get("baseline_result_id", "")) + ) + or profile.get("authority") + != { + "ground_truth": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, + } + or any( + sha_pattern.fullmatch(str(source.get(key, ""))) is None + for key in ("stream_sha256", "archive_index_sha256", "archive_summary_sha256") + ) + or not str(runtime.get("container_image", "")).startswith( + "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:" + ) + or sha_pattern.fullmatch(str(detector.get("model_sha256", ""))) is None + or detector.get("custom_postprocessing") is not False + or postprocessor.get("cluster_mode") != "NMS" + or postprocessor.get("custom_mission_core_logic") is not False + or sha_pattern.fullmatch(str(postprocessor.get("reference_config_sha256", ""))) is None + or tracker.get("custom_association") is not False + or tracker.get("custom_hold_or_stitch") is not False + or output.get("frame_width") != 800 + or output.get("frame_height") != 600 + ): + raise E46FDashCamBakeoffError("E46F profile contract is invalid") + + +def _validate_runtime(runtime: dict[str, Any], profile: dict[str, Any]) -> None: + expected_image = str(profile["runtime"]["container_image"]) + expected_digest = expected_image.rsplit("@sha256:", 1)[1] + sha_pattern = re.compile(r"^[a-f0-9]{64}$") + required_sha = ( + "container_image_digest", + "model_sha256", + "model_engine_sha256", + "deepstream_config_sha256", + "detector_config_sha256", + "tracker_config_sha256", + "input_stream_sha256", + "overlay_sha256", + ) + if ( + runtime.get("schema_version") != E46F_RUNTIME_SCHEMA + or runtime.get("status") != "completed" + or runtime.get("container_image") != expected_image + or runtime.get("container_image_digest") != expected_digest + or runtime.get("model_sha256") != profile["detector"]["model_sha256"] + or runtime.get("input_stream_sha256") != profile["source"]["stream_sha256"] + or not isinstance(runtime.get("worker_host"), str) + or not runtime.get("worker_host") + or not isinstance(runtime.get("gpu_name"), str) + or not runtime.get("gpu_name") + or any(sha_pattern.fullmatch(str(runtime.get(name, ""))) is None for name in required_sha) + ): + raise E46FDashCamBakeoffError("E46F runtime identity is invalid") diff --git a/src/k1link/compute/e46g_rectified_detector_bakeoff.py b/src/k1link/compute/e46g_rectified_detector_bakeoff.py new file mode 100644 index 0000000..ed1e0d3 --- /dev/null +++ b/src/k1link/compute/e46g_rectified_detector_bakeoff.py @@ -0,0 +1,688 @@ +"""Freeze the calibrated E46G ready-detector bake-off. + +E46G changes only the camera geometry presented to the two already-qualified +NVIDIA detector providers. Factory KB4 calibration is consumed by NVIDIA +``nvdewarper`` and every detector run uses stock DeepStream decoding, NMS and +NvDCF. This module is an evidence adapter: it validates and publishes output, +but contains no detector, suppression, association, hold or stitch logic. +""" + +from __future__ import annotations + +import copy +import hashlib +import os +import re +import shutil +import uuid +from collections.abc import Iterable +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from k1link.compute.e46e_ready_stack import ( + E46EReadyStackError, + _artifact, + _canonical_json, + _finite_float, + _indexed_kitti_files, + _read_json, + _read_jsonl, + _read_source, + _regular_file, + _sha256, + _validated_artifact, + _write_json, + _write_jsonl, + analyze_e46e_frames, +) + +E46G_PROFILE_SCHEMA: Final = "missioncore.e46g-rectified-detector-bakeoff-profile/v1" +E46G_RUNTIME_SCHEMA: Final = "missioncore.e46g-rectified-detector-runtime/v1" +E46G_RESULT_SCHEMA: Final = "missioncore.e46g-rectified-detector-bakeoff-result/v1" +E46G_REPORT_SCHEMA: Final = "missioncore.e46g-rectified-detector-bakeoff-report/v1" +E46G_FRAME_SCHEMA: Final = "missioncore.e46g-rectified-detector-bakeoff-frame/v1" +E46G_PACKAGE_SCHEMA: Final = "missioncore.e46g-worker-package/v1" +E46G_MANIFEST_NAME: Final = "manifest.json" +E46G_REPORT_NAME: Final = "rectified-detector-bakeoff-report.json" +E46G_RUNTIME_NAME: Final = "runtime.json" +E46G_LOG_NAME: Final = "worker.log" + +_RESULT_ID = re.compile(r"^e46g-rectified-detector-bakeoff-[a-f0-9]{64}$") +_SHA256 = re.compile(r"^[a-f0-9]{64}$") +_CANDIDATES: Final = ("trafficcamnet", "dashcamnet") +_VIEWS: Final = ("left", "front", "right") +_AUTHORITY: Final = { + "ground_truth": False, + "independent_truth": False, + "metric_grade_reference": False, + "candidate_accepted": False, + "free_space_authority": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class E46GRectifiedDetectorBakeoffError(ValueError): + """Raised when the E46G source, execution, or result is invalid.""" + + +def build_e46g_rectified_detector_bakeoff( + *, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path +) -> dict[str, Any]: + """Validate one calibrated stock-detector A/B and freeze immutable evidence.""" + + try: + return _build_e46g_rectified_detector_bakeoff( + source_job_root=source_job_root, + raw_root=raw_root, + profile_path=profile_path, + output_root=output_root, + ) + except E46EReadyStackError as exc: + raise E46GRectifiedDetectorBakeoffError(str(exc).replace("E46E", "E46G")) from exc + + +def _build_e46g_rectified_detector_bakeoff( + *, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path +) -> dict[str, Any]: + profile_source = profile_path.resolve(strict=True) + profile = _read_json(profile_source) + _validate_profile(profile) + source = _read_source(source_job_root.resolve(strict=True), profile) + raw = raw_root.resolve(strict=True) + if raw.is_symlink(): + raise E46GRectifiedDetectorBakeoffError("E46G raw root must not be a symlink") + runtime = _read_json(raw / E46G_RUNTIME_NAME) + _validate_runtime(runtime, profile, raw) + worker_log = _regular_file(raw / E46G_LOG_NAME, allow_empty=True) + + first_source_frame = int(profile["selection"]["first_source_frame_index"]) + sample_frame_count = int(profile["selection"]["frame_count"]) + frames_by_run: dict[str, tuple[dict[str, Any], ...]] = {} + metrics_by_candidate: dict[str, Any] = {} + for candidate in _CANDIDATES: + per_view: dict[str, Any] = {} + for view in _VIEWS: + run_root = raw / "runs" / candidate / view + detector_files = _indexed_kitti_files(run_root / "detections", sample_frame_count) + tracker_files = _indexed_kitti_files(run_root / "tracks", sample_frame_count) + rows: list[dict[str, Any]] = [] + for sample_frame_index in range(sample_frame_count): + source_frame_index = first_source_frame + sample_frame_index + source_row = source["index"][source_frame_index] + detections = _parse_detector_file( + detector_files[sample_frame_index], profile, candidate + ) + objects = _parse_tracker_file( + tracker_files[sample_frame_index], profile, candidate, view + ) + rows.append( + { + "schema_version": E46G_FRAME_SCHEMA, + "candidate": candidate, + "view": view, + "frame_index": sample_frame_index, + "source_frame_index": source_frame_index, + "sequence": int(source_row["sequence"]), + "session_seconds": source["timeline_start_seconds"] + + ( + int(source_row["session_monotonic_ns"]) + - source["first_session_monotonic_ns"] + ) + / 1_000_000_000.0, + "source_image_sha256": str(source_row["sha256"]), + "detection_count": len(detections), + "tracked_object_count": len(objects), + "detections": detections, + "objects": objects, + } + ) + key = f"{candidate}-{view}" + frames_by_run[key] = tuple(rows) + per_view[view] = _metrics(rows, profile) + metrics_by_candidate[candidate] = { + "source_frame_count": sample_frame_count, + "view_frame_count": sample_frame_count * len(_VIEWS), + "views": per_view, + "detection_observation_count": sum( + item["detection_observation_count"] for item in per_view.values() + ), + "track_observation_count": sum( + item["track_observation_count"] for item in per_view.values() + ), + "unique_track_count": sum(item["unique_track_count"] for item in per_view.values()), + "large_track_observation_count": sum( + item["large_track_observation_count"] for item in per_view.values() + ), + "large_track_fraction": round( + sum(item["large_track_observation_count"] for item in per_view.values()) + / max( + 1, + sum(item["track_observation_count"] for item in per_view.values()), + ), + 6, + ), + } + + comparison_paths = { + candidate: _regular_file(raw / "comparison" / f"{candidate}.mp4") + for candidate in _CANDIDATES + } + method = _method(profile, runtime, source) + report_basis = { + "schema_version": E46G_REPORT_SCHEMA, + "status": "completed-awaiting-visual-semantic-adjudication", + "comparison_contract": copy.deepcopy(profile["comparison_contract"]), + "selection": copy.deepcopy(profile["selection"]), + "rectification": copy.deepcopy(profile["rectification"]), + "metrics": metrics_by_candidate, + "acceptance": { + "exact_recorded_right_source_bound": True, + "factory_calibration_bound": True, + "official_nvidia_dewarper_executed": True, + "stock_detector_tracker_executed": True, + "same_views_and_frames_for_both_candidates": True, + "visual_comparison_videos_available": True, + "independent_truth_available": False, + "candidate_accepted": False, + "navigation_or_safety_accepted": False, + }, + "decision": { + "automatic_winner_selected": False, + "custom_detector_or_tracker_logic_used": False, + "next_action": ( + "review both synchronized left/front/right videos for semantic false " + "positives, missed task objects and edge geometry; then run the selected " + "stock detector with NvDCF over the complete rectified route" + ), + }, + "method": method, + "limitations": [ + ( + "E46G is a controlled 60-second detector-selection gate, not the complete " + "route continuity result" + ), + ( + "left/front/right are three calibrated projections from one physical RIGHT " + "camera, not three cameras" + ), + ( + "DeepStream 9.1 decoding omits the terminal source frame at EOS; E46G admits " + "the timestamp-aligned 0..4487 prefix and the tested 1000..1599 gate is " + "unaffected" + ), + ("NvDCF identities are view-local; E46G does not invent cross-view identity stitching"), + ( + "the two ready detectors expose only their provider taxonomies and do not " + "establish free-space or dynamic/static state" + ), + ( + "without independent exhaustive truth, numerical counts cannot select a " + "winner without visual semantic review" + ), + ( + "recorded RIGHT evidence introduces no LEFT camera, live hardware, command, " + "navigation or safety authority" + ), + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + raw_identity = { + "runtime_schema": runtime["schema_version"], + "worker_host": runtime["worker_host"], + "gpu_name": runtime["gpu_name"], + "container_image": runtime["container_image"], + "container_image_digest": runtime["container_image_digest"], + "source_stream_sha256": runtime["source_stream_sha256"], + "geometry": copy.deepcopy(runtime["geometry"]), + "candidates": copy.deepcopy(runtime["candidates"]), + "comparison": copy.deepcopy(runtime["comparison"]), + "worker_log_sha256": _sha256(worker_log), + } + frames_identity = { + key: hashlib.sha256(_canonical_json(rows)).hexdigest() + for key, rows in sorted(frames_by_run.items()) + } + identity = { + "schema_version": E46G_RESULT_SCHEMA, + "source": source["identity"], + "profile_sha256": _sha256(profile_source), + "profile": copy.deepcopy(profile), + "raw_execution": raw_identity, + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "frames_sha256": frames_identity, + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e46g-rectified-detector-bakeoff-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_e46g_rectified_detector_bakeoff(destination) + + created_at_utc = datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc, + "source_session_id": str(profile["source"]["session_id"]), + "camera_source_id": str(profile["source"]["camera_source_id"]), + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / E46G_REPORT_NAME, report) + for key, rows in frames_by_run.items(): + _write_jsonl(staging / f"{key}.jsonl", rows) + for candidate, source_path in comparison_paths.items(): + shutil.copyfile(source_path, staging / f"{candidate}.mp4") + shutil.copyfile(raw / E46G_RUNTIME_NAME, staging / E46G_RUNTIME_NAME) + shutil.copyfile(worker_log, staging / E46G_LOG_NAME) + artifacts = [ + _artifact(staging / E46G_REPORT_NAME, "rectified-detector-bakeoff-report"), + *[ + _artifact(staging / f"{key}.jsonl", f"tracked-frames-{key}") + for key in sorted(frames_by_run) + ], + *[ + _artifact(staging / f"{candidate}.mp4", f"comparison-video-{candidate}") + for candidate in _CANDIDATES + ], + _artifact(staging / E46G_RUNTIME_NAME, "runtime-record"), + _artifact(staging / E46G_LOG_NAME, "worker-log"), + ] + _write_json( + staging / E46G_MANIFEST_NAME, + { + "schema_version": E46G_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "rectified-detector-bakeoff-awaiting-visual-review", + "ground_truth": False, + "artifacts": artifacts, + "authority": _AUTHORITY, + }, + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_e46g_rectified_detector_bakeoff(destination) + + +def read_e46g_rectified_detector_bakeoff(root: Path) -> dict[str, Any]: + """Read and fully validate an immutable E46G result.""" + + try: + return _read_e46g_rectified_detector_bakeoff(root) + except E46EReadyStackError as exc: + raise E46GRectifiedDetectorBakeoffError(str(exc).replace("E46E", "E46G")) from exc + + +def _read_e46g_rectified_detector_bakeoff(root: Path) -> dict[str, Any]: + resolved = root.resolve(strict=True) + if resolved.is_symlink(): + raise E46GRectifiedDetectorBakeoffError("E46G result root must not be a symlink") + manifest = _read_json(resolved / E46G_MANIFEST_NAME) + identity = manifest.get("identity") + digest = ( + hashlib.sha256(_canonical_json(identity)).hexdigest() if isinstance(identity, dict) else "" + ) + if ( + manifest.get("schema_version") != E46G_RESULT_SCHEMA + or manifest.get("result_id") != f"e46g-rectified-detector-bakeoff-{digest}" + or manifest.get("identity_sha256") != digest + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("authority") != _AUTHORITY + or manifest.get("ground_truth") is not False + ): + raise E46GRectifiedDetectorBakeoffError("E46G result identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 11: + raise E46GRectifiedDetectorBakeoffError("E46G artifact inventory is invalid") + by_role = {row.get("role"): row for row in artifacts if isinstance(row, dict)} + report_path = _validated_artifact(resolved, by_role.get("rectified-detector-bakeoff-report")) + runtime_path = _validated_artifact(resolved, by_role.get("runtime-record")) + _validated_artifact(resolved, by_role.get("worker-log")) + comparison_paths = { + candidate: _validated_artifact(resolved, by_role.get(f"comparison-video-{candidate}")) + for candidate in _CANDIDATES + } + frames: dict[str, tuple[dict[str, Any], ...]] = {} + for candidate in _CANDIDATES: + for view in _VIEWS: + key = f"{candidate}-{view}" + path = _validated_artifact(resolved, by_role.get(f"tracked-frames-{key}")) + rows = tuple(_read_jsonl(path)) + if hashlib.sha256(_canonical_json(rows)).hexdigest() != identity.get( + "frames_sha256", {} + ).get(key) or any(row.get("schema_version") != E46G_FRAME_SCHEMA for row in rows): + raise E46GRectifiedDetectorBakeoffError("E46G frames changed") + frames[key] = rows + report = _read_json(report_path) + runtime = _read_json(runtime_path) + if ( + report.get("schema_version") != E46G_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("authority") != _AUTHORITY + or report.get("ground_truth") is not False + or runtime.get("schema_version") != E46G_RUNTIME_SCHEMA + ): + raise E46GRectifiedDetectorBakeoffError("E46G result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "frames": frames, + "runtime": runtime, + "comparison_paths": comparison_paths, + } + + +def _validate_profile(profile: dict[str, Any]) -> None: + source = profile.get("source") + runtime = profile.get("runtime") + calibration = profile.get("calibration") + rectification = profile.get("rectification") + selection = profile.get("selection") + candidates = profile.get("candidates") + comparison = profile.get("comparison_contract") + authority = profile.get("authority") + image = runtime.get("container_image") if isinstance(runtime, dict) else None + if ( + profile.get("schema_version") != E46G_PROFILE_SCHEMA + or not isinstance(source, dict) + or source.get("camera_source_id") != "sensor.camera.right" + or source.get("segment_count") != 4489 + or not isinstance(image, str) + or "@sha256:" not in image + or not isinstance(calibration, dict) + or calibration.get("slot") != "camera_1" + or calibration.get("model") != "KB4" + or calibration.get("calibration_sha256") + != "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9" + or not isinstance(rectification, dict) + or rectification.get("provider") != "NVIDIA Gst-nvdewarper" + or rectification.get("projection_type") != 4 + or tuple(rectification.get("view_order", ())) != _VIEWS + or rectification.get("expected_full_frame_count") != 4488 + or rectification.get("retained_source_frame_index_range") != [0, 4487] + or rectification.get("excluded_source_tail_frame_count") != 1 + or not isinstance(selection, dict) + or not isinstance(selection.get("first_source_frame_index"), int) + or not isinstance(selection.get("frame_count"), int) + or selection["first_source_frame_index"] < 0 + or selection["frame_count"] < 1 + or selection["first_source_frame_index"] + selection["frame_count"] + > rectification["expected_full_frame_count"] + or not isinstance(candidates, dict) + or set(candidates) != set(_CANDIDATES) + or any(candidates[name].get("custom_postprocessing") is not False for name in candidates) + or not isinstance(comparison, dict) + or comparison.get("controlled_change") != "detector-provider-only" + or authority + != { + "ground_truth": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, + } + ): + raise E46GRectifiedDetectorBakeoffError("E46G profile is invalid") + + +def _validate_runtime(runtime: dict[str, Any], profile: dict[str, Any], raw: Path) -> None: + image = str(profile["runtime"]["container_image"]) + digest = image.rsplit("@sha256:", 1)[-1] + geometry = runtime.get("geometry") + candidates = runtime.get("candidates") + comparison = runtime.get("comparison") + if ( + runtime.get("schema_version") != E46G_RUNTIME_SCHEMA + or runtime.get("status") != "completed" + or runtime.get("container_image") != image + or runtime.get("container_image_digest") != digest + or runtime.get("source_stream_sha256") != profile["source"]["stream_sha256"] + or runtime.get("first_source_frame_index") + != profile["selection"]["first_source_frame_index"] + or runtime.get("sample_frame_count") != profile["selection"]["frame_count"] + or not runtime.get("worker_host") + or not runtime.get("gpu_name") + or not isinstance(geometry, dict) + or set(geometry) != set(_VIEWS) + or not isinstance(candidates, dict) + or set(candidates) != set(_CANDIDATES) + or not isinstance(comparison, dict) + or set(comparison) != set(_CANDIDATES) + ): + raise E46GRectifiedDetectorBakeoffError("E46G runtime identity is invalid") + for view in _VIEWS: + row = geometry[view] + _runtime_artifact(raw, row, "full_rectified_video", f"geometry/{view}.mp4") + _runtime_artifact(raw, row, "sample_video", f"samples/{view}.mp4") + if ( + row.get("dewarper_config_sha256") + != profile["rectification"]["views"][view]["config_sha256"] + or row.get("full_frame_count") != profile["rectification"]["expected_full_frame_count"] + or row.get("retained_source_frame_index_range") + != profile["rectification"]["retained_source_frame_index_range"] + or row.get("excluded_source_tail_frame_count") + != profile["rectification"]["excluded_source_tail_frame_count"] + or row.get("sample_frame_count") != profile["selection"]["frame_count"] + ): + raise E46GRectifiedDetectorBakeoffError("E46G dewarper config changed") + for candidate in _CANDIDATES: + candidate_row = candidates[candidate] + if candidate_row.get("model_sha256") != profile["candidates"][candidate][ + "model_sha256" + ] or set(candidate_row.get("runs", {})) != set(_VIEWS): + raise E46GRectifiedDetectorBakeoffError("E46G candidate identity changed") + for view in _VIEWS: + row = candidate_row["runs"][view] + prefix = f"runs/{candidate}/{view}" + _runtime_artifact(raw, row, "overlay", f"{prefix}/overlay.mp4") + _runtime_artifact(raw, row, "deepstream_log", f"{prefix}/deepstream.log") + if row.get("frame_count") != profile["selection"]["frame_count"]: + raise E46GRectifiedDetectorBakeoffError("E46G run coverage changed") + _runtime_artifact( + raw, + comparison[candidate], + "video", + f"comparison/{candidate}.mp4", + ) + + +def _runtime_artifact(raw: Path, row: object, key: str, expected_relative: str) -> None: + if not isinstance(row, dict): + raise E46GRectifiedDetectorBakeoffError("E46G runtime artifact is invalid") + relative = row.get(f"{key}_path") + digest = row.get(f"{key}_sha256") + path = raw / str(relative) + if ( + relative != expected_relative + or not isinstance(digest, str) + or _SHA256.fullmatch(digest) is None + or not path.is_file() + or path.is_symlink() + or _sha256(path) != digest + ): + raise E46GRectifiedDetectorBakeoffError("E46G runtime artifact changed") + + +def _parse_detector_file( + path: Path, profile: dict[str, Any], candidate: str +) -> list[dict[str, Any]]: + output: list[dict[str, Any]] = [] + width, height = profile["rectification"]["output_resolution"] + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + tokens = line.split() + if len(tokens) != 16: + raise E46GRectifiedDetectorBakeoffError( + f"invalid detector KITTI row {path.name}:{line_number}" + ) + box, source_box, clipped = _parse_box(tokens[4:8], path, line_number, width, height) + output.append( + { + "class_name": tokens[0], + "bbox": box, + "source_bbox_ltrb": source_box, + "source_plane_clipped": clipped, + "confidence": _finite_float(tokens[15], path, line_number), + "provenance": f"nvidia-{candidate}-stock-deepstream", + } + ) + return output + + +def _parse_tracker_file( + path: Path, profile: dict[str, Any], candidate: str, view: str +) -> list[dict[str, Any]]: + output: list[dict[str, Any]] = [] + seen: set[int] = set() + width, height = profile["rectification"]["output_resolution"] + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + tokens = line.split() + if len(tokens) < 17: + raise E46GRectifiedDetectorBakeoffError( + f"invalid tracker KITTI row {path.name}:{line_number}" + ) + try: + track_id = int(tokens[1]) + except ValueError as exc: + raise E46GRectifiedDetectorBakeoffError( + f"invalid tracker id {path.name}:{line_number}" + ) from exc + if track_id < 0 or track_id in seen: + raise E46GRectifiedDetectorBakeoffError("invalid NvDCF track identity") + seen.add(track_id) + box, source_box, clipped = _parse_box(tokens[5:9], path, line_number, width, height) + output.append( + { + "object_id": f"{candidate}-{view}-nvdcf-{track_id}", + "source_track_id": track_id, + "class_name": tokens[0], + "bbox": box, + "source_bbox_ltrb": source_box, + "source_plane_clipped": clipped, + "confidence": _finite_float(tokens[16], path, line_number), + "provenance": "nvidia-nvdcf-stock-view-local-output", + } + ) + return output + + +def _parse_box( + tokens: list[str], path: Path, line_number: int, width: int, height: int +) -> tuple[list[float], list[float], bool]: + left, top, right, bottom = (_finite_float(token, path, line_number) for token in tokens) + if right <= left or bottom <= top: + raise E46GRectifiedDetectorBakeoffError(f"invalid bounding box {path.name}:{line_number}") + projected = [ + max(0.0, min(float(width), left)), + max(0.0, min(float(height), top)), + max(0.0, min(float(width), right)), + max(0.0, min(float(height), bottom)), + ] + if projected[2] <= projected[0] or projected[3] <= projected[1]: + raise E46GRectifiedDetectorBakeoffError( + f"bounding box outside rectified plane {path.name}:{line_number}" + ) + source = [left, top, right, bottom] + return ( + [projected[0], projected[1], projected[2] - projected[0], projected[3] - projected[1]], + source, + source != projected, + ) + + +def _metrics(rows: Iterable[dict[str, Any]], profile: dict[str, Any]) -> dict[str, Any]: + values = list(rows) + base = analyze_e46e_frames(values) + width, height = profile["rectification"]["output_resolution"] + plane_area = float(width * height) + large = sum( + float(item["bbox"][2]) * float(item["bbox"][3]) / plane_area >= 0.2 + for row in values + for item in row["objects"] + ) + return { + **base, + "large_track_observation_count": large, + "large_track_fraction": round(large / max(1, base["track_observation_count"]), 6), + } + + +def _method( + profile: dict[str, Any], runtime: dict[str, Any], source: dict[str, Any] +) -> dict[str, Any]: + components: list[dict[str, Any]] = [ + { + "kind": "source", + "name": profile["source"]["job_id"], + "version": "immutable recorded RIGHT replay", + "role": "single physical camera source", + "identity_sha256": source["job_sha256"], + }, + { + "kind": "tool", + "name": "XGRIDS K1 factory camera_1 KB4", + "version": profile["calibration"]["model"], + "role": "fisheye source geometry", + "identity_sha256": profile["calibration"]["calibration_sha256"], + }, + { + "kind": "tool", + "name": "NVIDIA Gst-nvdewarper", + "version": profile["runtime"]["deepstream_version"], + "role": "calibrated fisheye-to-perspective adapter", + "identity_sha256": hashlib.sha256( + _canonical_json(profile["rectification"]) + ).hexdigest(), + }, + ] + for candidate in _CANDIDATES: + row = profile["candidates"][candidate] + components.append( + { + "kind": "model", + "name": row["name"], + "version": row["version"], + "role": "controlled ready detector candidate", + "identity_sha256": row["model_sha256"], + } + ) + components.extend( + [ + { + "kind": "algorithm", + "name": "NVIDIA NvDCF", + "version": profile["tracker"]["configuration"], + "role": "view-local temporal association", + "identity_sha256": runtime["candidates"]["trafficcamnet"]["runs"]["front"][ + "tracker_config_sha256" + ], + }, + { + "kind": "runtime", + "name": "NVIDIA DeepStream", + "version": profile["runtime"]["deepstream_version"], + "role": "GPU media, inference and tracking runtime", + "identity_sha256": runtime["container_image_digest"], + }, + ] + ) + return { + "schema_version": "missioncore.laboratory-method/v1", + "completeness": "complete", + "execution_class": "hybrid", + "pipeline_id": profile["profile_id"], + "components": components, + } diff --git a/src/k1link/compute/e46h_full_rectified_front_replay.py b/src/k1link/compute/e46h_full_rectified_front_replay.py new file mode 100644 index 0000000..ea34888 --- /dev/null +++ b/src/k1link/compute/e46h_full_rectified_front_replay.py @@ -0,0 +1,485 @@ +"""Freeze the selected stock NVIDIA provider over the retained FRONT route. + +E46H is deliberately narrow: one immutable recorded RIGHT source, factory KB4, +the official NVIDIA dewarper FRONT projection, TrafficCamNet and stock NvDCF. +Mission Core validates and publishes the evidence but implements none of the +detector, suppression, association, hold or stitch logic. +""" + +from __future__ import annotations + +import copy +import hashlib +import os +import re +import shutil +import uuid +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from k1link.compute.e46e_ready_stack import ( + E46EReadyStackError, + _artifact, + _canonical_json, + _indexed_kitti_files, + _read_json, + _read_jsonl, + _read_source, + _regular_file, + _sha256, + _validated_artifact, + _write_json, + _write_jsonl, +) +from k1link.compute.e46g_rectified_detector_bakeoff import ( + E46GRectifiedDetectorBakeoffError, + _metrics, + _parse_detector_file, + _parse_tracker_file, +) + +E46H_PROFILE_SCHEMA: Final = "missioncore.e46h-full-rectified-front-replay-profile/v1" +E46H_RUNTIME_SCHEMA: Final = "missioncore.e46h-full-rectified-front-runtime/v1" +E46H_RESULT_SCHEMA: Final = "missioncore.e46h-full-rectified-front-replay-result/v1" +E46H_REPORT_SCHEMA: Final = "missioncore.e46h-full-rectified-front-replay-report/v1" +E46H_FRAME_SCHEMA: Final = "missioncore.e46h-full-rectified-front-replay-frame/v1" +E46H_PACKAGE_SCHEMA: Final = "missioncore.e46h-worker-package/v1" +E46H_MANIFEST_NAME: Final = "manifest.json" +E46H_REPORT_NAME: Final = "full-rectified-front-report.json" +E46H_FRAMES_NAME: Final = "tracked-frames.jsonl" +E46H_OVERLAY_NAME: Final = "overlay.mp4" +E46H_RUNTIME_NAME: Final = "runtime.json" +E46H_LOG_NAME: Final = "worker.log" + +_RESULT_ID = re.compile(r"^e46h-full-rectified-front-replay-[a-f0-9]{64}$") +_SHA256 = re.compile(r"^[a-f0-9]{64}$") +_AUTHORITY: Final = { + "ground_truth": False, + "independent_truth": False, + "metric_grade_reference": False, + "candidate_accepted": False, + "free_space_authority": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class E46HFullRectifiedFrontReplayError(ValueError): + """Raised when the E46H source, execution, or result is invalid.""" + + +def build_e46h_full_rectified_front_replay( + *, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path +) -> dict[str, Any]: + """Validate the full retained FRONT replay and freeze immutable evidence.""" + + try: + return _build_e46h_full_rectified_front_replay( + source_job_root=source_job_root, + raw_root=raw_root, + profile_path=profile_path, + output_root=output_root, + ) + except (E46EReadyStackError, E46GRectifiedDetectorBakeoffError) as exc: + raise E46HFullRectifiedFrontReplayError(str(exc).replace("E46E", "E46H")) from exc + + +def _build_e46h_full_rectified_front_replay( + *, source_job_root: Path, raw_root: Path, profile_path: Path, output_root: Path +) -> dict[str, Any]: + profile_source = profile_path.resolve(strict=True) + profile = _read_json(profile_source) + _validate_profile(profile) + source = _read_source(source_job_root.resolve(strict=True), profile) + raw = raw_root.resolve(strict=True) + if raw.is_symlink(): + raise E46HFullRectifiedFrontReplayError("E46H raw root must not be a symlink") + runtime = _read_json(raw / E46H_RUNTIME_NAME) + _validate_runtime(runtime, profile, raw) + overlay = _regular_file(raw / "run" / E46H_OVERLAY_NAME) + worker_log = _regular_file(raw / E46H_LOG_NAME, allow_empty=True) + + frame_count = int(profile["selection"]["frame_count"]) + detector_files = _indexed_kitti_files(raw / "run" / "detections", frame_count) + tracker_files = _indexed_kitti_files(raw / "run" / "tracks", frame_count) + frames: list[dict[str, Any]] = [] + for frame_index in range(frame_count): + source_row = source["index"][frame_index] + detections = _parse_detector_file(detector_files[frame_index], profile, "trafficcamnet") + objects = _parse_tracker_file( + tracker_files[frame_index], profile, "trafficcamnet", "front" + ) + frames.append( + { + "schema_version": E46H_FRAME_SCHEMA, + "frame_index": frame_index, + "source_frame_index": frame_index, + "sequence": int(source_row["sequence"]), + "session_seconds": source["timeline_start_seconds"] + + ( + int(source_row["session_monotonic_ns"]) + - source["first_session_monotonic_ns"] + ) + / 1_000_000_000.0, + "source_image_sha256": str(source_row["sha256"]), + "detection_count": len(detections), + "tracked_object_count": len(objects), + "detections": detections, + "objects": objects, + } + ) + metrics = _metrics(frames, profile) + method = _method(profile, runtime, source) + report_basis = { + "schema_version": E46H_REPORT_SCHEMA, + "status": "completed-awaiting-full-route-visual-review", + "baseline_result_id": profile["baseline_result_id"], + "selection": copy.deepcopy(profile["selection"]), + "rectification": copy.deepcopy(profile["rectification"]), + "metrics": metrics, + "acceptance": { + "exact_recorded_right_source_bound": True, + "factory_calibration_bound": True, + "official_nvidia_dewarper_executed": True, + "selected_stock_detector_tracker_executed": True, + "retained_route_accounted": metrics["frame_count"] == frame_count, + "terminal_source_frame_excluded": True, + "full_visual_review_completed": False, + "independent_truth_available": False, + "candidate_accepted": False, + "navigation_or_safety_accepted": False, + }, + "decision": { + "selected_provider": "front-trafficcamnet-stock-nvdcf", + "custom_detector_or_tracker_logic_used": False, + "provider_promoted": False, + "next_action": ( + "review the complete seekable FRONT overlay for continuity, duplicates, stale " + "tracks, semantic false positives and long object-layer blackouts" + ), + }, + "method": method, + "limitations": [ + ( + "E46H covers the timestamp-aligned 0..4487 prefix; DeepStream 9.1 decoding " + "reproducibly omits the terminal source frame 4488 at EOS" + ), + ( + "TrafficCamNet and NvDCF remain ready providers; E46H contains no custom " + "detector, NMS, association, hold or stitch logic" + ), + ( + "FRONT is one calibrated projection of the physical RIGHT camera; no LEFT " + "camera or cross-view identity is introduced" + ), + ( + "without independent exhaustive truth, continuity counts cannot establish " + "precision, recall or physical identity correctness" + ), + ( + "class, temporal identity, LiDAR range and dynamic/static state remain separate " + "evidence layers; E46H introduces no command, navigation or safety authority" + ), + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + raw_identity = { + "runtime_schema": runtime["schema_version"], + "worker_host": runtime["worker_host"], + "gpu_name": runtime["gpu_name"], + "container_image": runtime["container_image"], + "container_image_digest": runtime["container_image_digest"], + "source_stream_sha256": runtime["source_stream_sha256"], + "geometry": copy.deepcopy(runtime["geometry"]), + "run": copy.deepcopy(runtime["run"]), + "worker_log_sha256": _sha256(worker_log), + } + identity = { + "schema_version": E46H_RESULT_SCHEMA, + "source": source["identity"], + "profile_sha256": _sha256(profile_source), + "profile": copy.deepcopy(profile), + "raw_execution": raw_identity, + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "frames_sha256": hashlib.sha256(_canonical_json(frames)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e46h-full-rectified-front-replay-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_e46h_full_rectified_front_replay(destination) + + created_at = datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at, + "source_session_id": profile["source"]["session_id"], + "camera_source_id": profile["source"]["camera_source_id"], + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / E46H_REPORT_NAME, report) + _write_jsonl(staging / E46H_FRAMES_NAME, frames) + shutil.copyfile(overlay, staging / E46H_OVERLAY_NAME) + shutil.copyfile(raw / E46H_RUNTIME_NAME, staging / E46H_RUNTIME_NAME) + shutil.copyfile(worker_log, staging / E46H_LOG_NAME) + artifacts = [ + _artifact(staging / E46H_REPORT_NAME, "full-rectified-front-report"), + _artifact(staging / E46H_FRAMES_NAME, "tracked-frames"), + _artifact(staging / E46H_OVERLAY_NAME, "visual-overlay-video"), + _artifact(staging / E46H_RUNTIME_NAME, "runtime-record"), + _artifact(staging / E46H_LOG_NAME, "worker-log"), + ] + _write_json( + staging / E46H_MANIFEST_NAME, + { + "schema_version": E46H_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at, + "acceptance_state": "full-rectified-front-awaiting-visual-review", + "ground_truth": False, + "artifacts": artifacts, + "authority": _AUTHORITY, + }, + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_e46h_full_rectified_front_replay(destination) + + +def read_e46h_full_rectified_front_replay(root: Path) -> dict[str, Any]: + """Read and fully validate one immutable E46H result.""" + + try: + return _read_e46h_full_rectified_front_replay(root) + except (E46EReadyStackError, E46GRectifiedDetectorBakeoffError) as exc: + raise E46HFullRectifiedFrontReplayError(str(exc).replace("E46E", "E46H")) from exc + + +def _read_e46h_full_rectified_front_replay(root: Path) -> dict[str, Any]: + resolved = root.resolve(strict=True) + if resolved.is_symlink(): + raise E46HFullRectifiedFrontReplayError("E46H result root must not be a symlink") + manifest = _read_json(resolved / E46H_MANIFEST_NAME) + identity = manifest.get("identity") + digest = ( + hashlib.sha256(_canonical_json(identity)).hexdigest() if isinstance(identity, dict) else "" + ) + if ( + manifest.get("schema_version") != E46H_RESULT_SCHEMA + or manifest.get("result_id") != f"e46h-full-rectified-front-replay-{digest}" + or manifest.get("identity_sha256") != digest + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("authority") != _AUTHORITY + or manifest.get("ground_truth") is not False + ): + raise E46HFullRectifiedFrontReplayError("E46H result identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 5: + raise E46HFullRectifiedFrontReplayError("E46H artifact inventory is invalid") + by_role = {row.get("role"): row for row in artifacts if isinstance(row, dict)} + report_path = _validated_artifact(resolved, by_role.get("full-rectified-front-report")) + frames_path = _validated_artifact(resolved, by_role.get("tracked-frames")) + overlay_path = _validated_artifact(resolved, by_role.get("visual-overlay-video")) + runtime_path = _validated_artifact(resolved, by_role.get("runtime-record")) + _validated_artifact(resolved, by_role.get("worker-log")) + report = _read_json(report_path) + frames = tuple(_read_jsonl(frames_path)) + runtime = _read_json(runtime_path) + if ( + report.get("schema_version") != E46H_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("authority") != _AUTHORITY + or report.get("ground_truth") is not False + or runtime.get("schema_version") != E46H_RUNTIME_SCHEMA + or any(row.get("schema_version") != E46H_FRAME_SCHEMA for row in frames) + or hashlib.sha256(_canonical_json(frames)).hexdigest() != identity.get("frames_sha256") + or report.get("metrics", {}).get("frame_count") != len(frames) + ): + raise E46HFullRectifiedFrontReplayError("E46H result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "frames": frames, + "runtime": runtime, + "overlay_path": overlay_path, + } + + +def _validate_profile(profile: dict[str, Any]) -> None: + source = profile.get("source") + selection = profile.get("selection") + calibration = profile.get("calibration") + rectification = profile.get("rectification") + runtime = profile.get("runtime") + detector = profile.get("detector") + parser = profile.get("parser") + tracker = profile.get("tracker") + authority = profile.get("authority") + image = runtime.get("container_image") if isinstance(runtime, dict) else None + if ( + profile.get("schema_version") != E46H_PROFILE_SCHEMA + or not isinstance(profile.get("baseline_result_id"), str) + or not profile["baseline_result_id"].startswith("e46g-rectified-detector-bakeoff-") + or not isinstance(source, dict) + or source.get("camera_source_id") != "sensor.camera.right" + or source.get("segment_count") != 4489 + or not isinstance(selection, dict) + or selection.get("first_source_frame_index") != 0 + or selection.get("last_source_frame_index") != 4487 + or selection.get("frame_count") != 4488 + or selection.get("excluded_source_tail_frame_count") != 1 + or not isinstance(calibration, dict) + or calibration.get("slot") != "camera_1" + or calibration.get("model") != "KB4" + or calibration.get("calibration_sha256") + != "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9" + or not isinstance(rectification, dict) + or rectification.get("provider") != "NVIDIA Gst-nvdewarper" + or rectification.get("projection_type") != 4 + or rectification.get("view") != "front" + or rectification.get("output_resolution") != [960, 544] + or not isinstance(image, str) + or "@sha256:" not in image + or not isinstance(detector, dict) + or detector.get("name") != "NVIDIA TrafficCamNet Transformer Lite" + or detector.get("custom_postprocessing") is not False + or not isinstance(parser, dict) + or parser.get("symbol") != "NvDsInferParseCustomDDETRTAO" + or parser.get("custom_mission_core_logic") is not False + or not isinstance(tracker, dict) + or tracker.get("custom_association") is not False + or tracker.get("custom_hold_or_stitch") is not False + or authority + != { + "ground_truth": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, + } + ): + raise E46HFullRectifiedFrontReplayError("E46H profile is invalid") + + +def _validate_runtime(runtime: dict[str, Any], profile: dict[str, Any], raw: Path) -> None: + image = profile["runtime"]["container_image"] + geometry = runtime.get("geometry") + run = runtime.get("run") + if ( + runtime.get("schema_version") != E46H_RUNTIME_SCHEMA + or runtime.get("status") != "completed" + or runtime.get("container_image") != image + or runtime.get("container_image_digest") != image.rsplit("@sha256:", 1)[-1] + or runtime.get("source_stream_sha256") != profile["source"]["stream_sha256"] + or runtime.get("frame_count") != profile["selection"]["frame_count"] + or runtime.get("retained_source_frame_index_range") != [0, 4487] + or not runtime.get("worker_host") + or not runtime.get("gpu_name") + or not isinstance(geometry, dict) + or not isinstance(run, dict) + ): + raise E46HFullRectifiedFrontReplayError("E46H runtime identity is invalid") + _runtime_artifact(raw, geometry, "video", "geometry/front.mp4") + _runtime_artifact(raw, geometry, "log", "geometry/front.log") + _runtime_artifact(raw, run, "overlay", "run/overlay.mp4") + _runtime_artifact(raw, run, "deepstream_log", "run/deepstream.log") + required = ( + geometry.get("config_sha256") == profile["rectification"]["config_sha256"], + geometry.get("frame_count") == profile["selection"]["frame_count"], + run.get("frame_count") == profile["selection"]["frame_count"], + run.get("model_sha256") == profile["detector"]["model_sha256"], + run.get("parser_library_sha256") == profile["parser"]["library_sha256"], + run.get("deepstream_app_config_sha256") + == profile["detector"]["deepstream_app_config_sha256"], + run.get("detector_config_sha256") + == profile["detector"]["detector_config_sha256"], + _SHA256.fullmatch(str(run.get("tracker_config_sha256"))) is not None, + _SHA256.fullmatch(str(run.get("model_engine_sha256"))) is not None, + ) + if not all(required): + raise E46HFullRectifiedFrontReplayError("E46H runtime component identity changed") + + +def _runtime_artifact(raw: Path, row: dict[str, Any], key: str, expected: str) -> None: + relative = row.get(f"{key}_path") + digest = row.get(f"{key}_sha256") + path = raw / str(relative) + if ( + relative != expected + or not isinstance(digest, str) + or _SHA256.fullmatch(digest) is None + or not path.is_file() + or path.is_symlink() + or _sha256(path) != digest + ): + raise E46HFullRectifiedFrontReplayError("E46H runtime artifact changed") + + +def _method( + profile: dict[str, Any], runtime: dict[str, Any], source: dict[str, Any] +) -> dict[str, Any]: + return { + "schema_version": "missioncore.laboratory-method/v1", + "completeness": "complete", + "execution_class": "hybrid", + "pipeline_id": profile["profile_id"], + "components": [ + { + "kind": "source", + "name": profile["source"]["job_id"], + "version": "immutable recorded RIGHT replay", + "role": "single physical camera source", + "identity_sha256": source["job_sha256"], + }, + { + "kind": "tool", + "name": "XGRIDS K1 factory camera_1 KB4", + "version": profile["calibration"]["model"], + "role": "fisheye source geometry", + "identity_sha256": profile["calibration"]["calibration_sha256"], + }, + { + "kind": "tool", + "name": "NVIDIA Gst-nvdewarper", + "version": profile["runtime"]["deepstream_version"], + "role": "FRONT fisheye-to-perspective adapter", + "identity_sha256": profile["rectification"]["config_sha256"], + }, + { + "kind": "model", + "name": profile["detector"]["name"], + "version": profile["detector"]["version"], + "role": "selected ready detector provider", + "identity_sha256": profile["detector"]["model_sha256"], + }, + { + "kind": "algorithm", + "name": profile["tracker"]["name"], + "version": profile["tracker"]["configuration"], + "role": "FRONT route-local temporal association", + "identity_sha256": runtime["run"]["tracker_config_sha256"], + }, + { + "kind": "runtime", + "name": "NVIDIA DeepStream", + "version": profile["runtime"]["deepstream_version"], + "role": "GPU media, inference and tracking runtime", + "identity_sha256": runtime["container_image_digest"], + }, + ], + } diff --git a/src/k1link/compute/e46i_grounding_dino_full_replay.py b/src/k1link/compute/e46i_grounding_dino_full_replay.py new file mode 100644 index 0000000..5892d6f --- /dev/null +++ b/src/k1link/compute/e46i_grounding_dino_full_replay.py @@ -0,0 +1,594 @@ +"""Freeze the ready NVIDIA Grounding DINO provider over the full FRONT replay. + +E46I keeps the E46H camera adapter and changes only the detector provider. The +result is diagnostic: detections and visual evidence are published, while +tracking, physical identity, motion state, LiDAR range and command authority +remain explicitly outside this experiment. +""" + +from __future__ import annotations + +import copy +import hashlib +import os +import re +import shutil +import uuid +from collections import Counter +from datetime import UTC, datetime +from pathlib import Path +from statistics import fmean +from typing import Any, Final + +from k1link.compute.e46e_ready_stack import ( + E46EReadyStackError, + _artifact, + _canonical_json, + _read_json, + _read_jsonl, + _sha256, + _validated_artifact, + _write_json, + _write_jsonl, +) + +E46I_PROFILE_SCHEMA: Final = "missioncore.e46i-grounding-dino-full-replay-profile/v1" +E46I_RUNTIME_SCHEMA: Final = "missioncore.e46i-grounding-dino-full-runtime/v1" +E46I_RESULT_SCHEMA: Final = "missioncore.e46i-grounding-dino-full-replay-result/v1" +E46I_REPORT_SCHEMA: Final = "missioncore.e46i-grounding-dino-full-replay-report/v1" +E46I_FRAME_SCHEMA: Final = "missioncore.e46i-grounding-dino-full-replay-frame/v1" +E46I_MANIFEST_NAME: Final = "manifest.json" +E46I_REPORT_NAME: Final = "grounding-dino-full-report.json" +E46I_FRAMES_NAME: Final = "detection-frames.jsonl" +E46I_OVERLAY_NAME: Final = "grounding-dino-full-overlay.mp4" +E46I_LABELS_NAME: Final = "e46i-full-labels.tar" +E46I_RUNTIME_NAME: Final = "runtime.json" +E46I_SHADOW_SHEET_NAME: Final = "shadow-gate-contact-sheet.png" +E46I_ROUTE_SHEET_NAME: Final = "full-route-10s-contact-sheet.png" +E46I_TARGETED_SHEET_NAME: Final = "targeted-windows-contact-sheet.png" + +_RESULT_ID = re.compile(r"^e46i-grounding-dino-full-replay-[a-f0-9]{64}$") +_SHA256 = re.compile(r"^[a-f0-9]{64}$") +_AUTHORITY: Final = { + "ground_truth": False, + "independent_truth": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + +_REVIEW_WINDOWS: Final = [ + { + "id": "legacy-false-wall", + "label": "6.0–10.9 с · прежний false car на стене", + "start_seconds": 6.0, + "end_seconds": 10.9, + "verdict": "legacy-background-false-positive-suppressed", + }, + { + "id": "legacy-false-shrub", + "label": "178.6–180.3 с · прежний false car на кусте", + "start_seconds": 178.6, + "end_seconds": 180.3, + "verdict": "legacy-background-false-positive-suppressed", + }, + { + "id": "legacy-false-ground", + "label": "250.7–265.6 с · прежний false car на полотне", + "start_seconds": 250.7, + "end_seconds": 265.6, + "verdict": "legacy-background-false-positive-suppressed", + }, + { + "id": "legacy-false-road", + "label": "392.2–400.6 с · прежний false car на дороге", + "start_seconds": 392.2, + "end_seconds": 400.6, + "verdict": "legacy-background-false-positive-suppressed", + }, + { + "id": "empty-scene", + "label": "419.4–426.9 с · пустая сцена", + "start_seconds": 419.4, + "end_seconds": 426.9, + "verdict": "empty-scene-mostly-preserved", + }, + { + "id": "legacy-false-terrace", + "label": "440.8–448.4 с · прежний false car на террасе", + "start_seconds": 440.8, + "end_seconds": 448.4, + "verdict": "legacy-background-false-positive-suppressed", + }, +] + + +class E46IGroundingDinoFullReplayError(ValueError): + """Raised when E46I evidence or identity is invalid.""" + + +def build_e46i_grounding_dino_full_replay( + *, raw_root: Path, profile_path: Path, output_root: Path +) -> dict[str, Any]: + """Validate raw NVIDIA output and freeze one immutable E46I result.""" + + try: + return _build_e46i_grounding_dino_full_replay( + raw_root=raw_root, profile_path=profile_path, output_root=output_root + ) + except E46EReadyStackError as exc: + raise E46IGroundingDinoFullReplayError(str(exc).replace("E46E", "E46I")) from exc + + +def _build_e46i_grounding_dino_full_replay( + *, raw_root: Path, profile_path: Path, output_root: Path +) -> dict[str, Any]: + raw = raw_root.resolve(strict=True) + if raw.is_symlink(): + raise E46IGroundingDinoFullReplayError("E46I raw root must not be a symlink") + profile_source = profile_path.resolve(strict=True) + profile = _read_json(profile_source) + _validate_profile(profile) + runtime_source = raw / E46I_RUNTIME_NAME + runtime = _read_json(runtime_source) + _validate_runtime(runtime, profile, raw) + + labels_root = raw / "labels" + if not labels_root.is_dir() or labels_root.is_symlink(): + raise E46IGroundingDinoFullReplayError("E46I labels root is invalid") + frame_count = int(profile["source"]["video_frame_count"]) + frames = _read_detection_frames(labels_root, profile, frame_count) + metrics = _metrics(frames, runtime) + method = _method(profile) + + report_basis = { + "schema_version": E46I_REPORT_SCHEMA, + "status": "semantic-regression-suppressed-awaiting-temporal-layer", + "baseline_result_id": profile["baseline_result_id"], + "source": copy.deepcopy(profile["source"]), + "provider": copy.deepcopy(profile["provider"]), + "inference": copy.deepcopy(profile["inference"]), + "metrics": metrics, + "shadow_gate": copy.deepcopy(profile["visual_shadow_gate"]), + "visual_review": { + "status": "full-playback-and-targeted-window-review-completed", + "complete_video_playback_completed": True, + "reviewed_video_range_seconds": [0.0, 448.8], + "playback_rate": 4.0, + "review_windows": copy.deepcopy(_REVIEW_WINDOWS), + "verdict": "material-semantic-progress-not-yet-complete-perception", + "finding": ( + "All five large E46H background false-car cases are absent in the new " + "provider output; real vehicles and people remain visible across the route." + ), + "known_error": ( + "The fixed four-caption ontology misses one partially cropped person and " + "labels a stroller as bicycle in the anchor review." + ), + }, + "acceptance": { + "exact_recorded_right_source_bound": True, + "same_calibrated_front_adapter_as_baseline": True, + "official_nvidia_model_executed": True, + "fixed_threshold_full_route_executed": True, + "retained_route_accounted": metrics["frame_count"] == frame_count, + "legacy_large_false_background_gate_passed": True, + "full_overlay_published": True, + "offline_reproducibility_completed": False, + "temporal_identity_available": False, + "motion_state_available": False, + "candidate_accepted": False, + "navigation_or_safety_accepted": False, + }, + "decision": { + "selected_provider": "nvidia-grounding-dino-swin-tiny-commercial-v1.0", + "provider_semantic_progress": True, + "provider_promoted": False, + "custom_detector_or_postprocessing_used": False, + "next_action": ( + "freeze this detector output, vendor the tokenizer for offline replay, then " + "attach a ready temporal tracker before evaluating dynamic/static state" + ), + }, + "method": method, + "limitations": [ + ( + "E46I has no independent exhaustive truth, so observation counts are not " + "precision or recall." + ), + ( + "Grounding DINO output is frame-local and contains no stable object identity " + "or motion state." + ), + "The fixed caption set contains car, person, bicycle and road sign only.", + ( + "TAO downloaded bert-base-uncased tokenizer data at startup; offline replay " + "is not sealed yet." + ), + ( + "LiDAR range, dynamic/static state, free space, commands, navigation and " + "safety remain unaccepted." + ), + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + identity = { + "schema_version": E46I_RESULT_SCHEMA, + "profile_sha256": _sha256(profile_source), + "profile": copy.deepcopy(profile), + "runtime_sha256": _sha256(runtime_source), + "runtime": copy.deepcopy(runtime), + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "frames_sha256": hashlib.sha256(_canonical_json(frames)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e46i-grounding-dino-full-replay-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_e46i_grounding_dino_full_replay(destination) + + created_at = datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at, + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / E46I_REPORT_NAME, report) + _write_jsonl(staging / E46I_FRAMES_NAME, frames) + for name in ( + E46I_OVERLAY_NAME, + E46I_LABELS_NAME, + E46I_RUNTIME_NAME, + E46I_SHADOW_SHEET_NAME, + E46I_ROUTE_SHEET_NAME, + E46I_TARGETED_SHEET_NAME, + ): + shutil.copyfile(raw / name, staging / name) + artifacts = [ + _artifact(staging / E46I_REPORT_NAME, "grounding-dino-full-report"), + _artifact(staging / E46I_FRAMES_NAME, "detection-frames"), + _artifact(staging / E46I_OVERLAY_NAME, "visual-overlay-video"), + _artifact(staging / E46I_LABELS_NAME, "raw-labels-archive"), + _artifact(staging / E46I_RUNTIME_NAME, "runtime-record"), + _artifact(staging / E46I_SHADOW_SHEET_NAME, "shadow-gate-contact-sheet"), + _artifact(staging / E46I_ROUTE_SHEET_NAME, "full-route-contact-sheet"), + _artifact(staging / E46I_TARGETED_SHEET_NAME, "targeted-windows-contact-sheet"), + ] + _write_json( + staging / E46I_MANIFEST_NAME, + { + "schema_version": E46I_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at, + "acceptance_state": "semantic-progress-awaiting-temporal-layer", + "ground_truth": False, + "artifacts": artifacts, + "authority": _AUTHORITY, + }, + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_e46i_grounding_dino_full_replay(destination) + + +def read_e46i_grounding_dino_full_replay(root: Path) -> dict[str, Any]: + """Read and fully validate one immutable E46I result.""" + + try: + return _read_e46i_grounding_dino_full_replay(root) + except E46EReadyStackError as exc: + raise E46IGroundingDinoFullReplayError(str(exc).replace("E46E", "E46I")) from exc + + +def _read_e46i_grounding_dino_full_replay(root: Path) -> dict[str, Any]: + resolved = root.resolve(strict=True) + if resolved.is_symlink(): + raise E46IGroundingDinoFullReplayError("E46I result root must not be a symlink") + manifest = _read_json(resolved / E46I_MANIFEST_NAME) + identity = manifest.get("identity") + digest = ( + hashlib.sha256(_canonical_json(identity)).hexdigest() if isinstance(identity, dict) else "" + ) + if ( + manifest.get("schema_version") != E46I_RESULT_SCHEMA + or manifest.get("result_id") != f"e46i-grounding-dino-full-replay-{digest}" + or manifest.get("identity_sha256") != digest + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("authority") != _AUTHORITY + or manifest.get("ground_truth") is not False + ): + raise E46IGroundingDinoFullReplayError("E46I result identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 8: + raise E46IGroundingDinoFullReplayError("E46I artifact inventory is invalid") + by_role = {row.get("role"): row for row in artifacts if isinstance(row, dict)} + report_path = _validated_artifact(resolved, by_role.get("grounding-dino-full-report")) + frames_path = _validated_artifact(resolved, by_role.get("detection-frames")) + overlay_path = _validated_artifact(resolved, by_role.get("visual-overlay-video")) + labels_path = _validated_artifact(resolved, by_role.get("raw-labels-archive")) + runtime_path = _validated_artifact(resolved, by_role.get("runtime-record")) + shadow_sheet_path = _validated_artifact( + resolved, by_role.get("shadow-gate-contact-sheet") + ) + route_sheet_path = _validated_artifact( + resolved, by_role.get("full-route-contact-sheet") + ) + targeted_sheet_path = _validated_artifact( + resolved, by_role.get("targeted-windows-contact-sheet") + ) + report = _read_json(report_path) + frames = tuple(_read_jsonl(frames_path)) + runtime = _read_json(runtime_path) + if ( + report.get("schema_version") != E46I_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("authority") != _AUTHORITY + or report.get("ground_truth") is not False + or runtime.get("schema_version") != E46I_RUNTIME_SCHEMA + or any(row.get("schema_version") != E46I_FRAME_SCHEMA for row in frames) + or hashlib.sha256(_canonical_json(frames)).hexdigest() != identity.get("frames_sha256") + or report.get("metrics", {}).get("frame_count") != len(frames) + ): + raise E46IGroundingDinoFullReplayError("E46I result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "frames": frames, + "runtime": runtime, + "overlay_path": overlay_path, + "labels_path": labels_path, + "shadow_sheet_path": shadow_sheet_path, + "route_sheet_path": route_sheet_path, + "targeted_sheet_path": targeted_sheet_path, + } + + +def _validate_profile(profile: dict[str, Any]) -> None: + source = profile.get("source") + provider = profile.get("provider") + inference = profile.get("inference") + shadow = profile.get("visual_shadow_gate") + if ( + profile.get("schema_version") != E46I_PROFILE_SCHEMA + or not str(profile.get("baseline_result_id", "")).startswith( + "e46h-full-rectified-front-replay-" + ) + or not isinstance(source, dict) + or source.get("camera_source_id") != "sensor.camera.right" + or source.get("view") != "front" + or source.get("projection_resolution") != [960, 544] + or source.get("video_frame_count") != 4488 + or source.get("video_frame_rate") != 10.0 + or source.get("video_duration_seconds") != 448.8 + or _SHA256.fullmatch(str(source.get("video_sha256"))) is None + or not isinstance(provider, dict) + or provider.get("name") != "NVIDIA TAO Grounding DINO Swin-Tiny Commercial" + or provider.get("custom_detector_or_postprocessing") is not False + or _SHA256.fullmatch(str(provider.get("model_sha256"))) is None + or _SHA256.fullmatch(str(provider.get("engine_sha256"))) is None + or not isinstance(inference, dict) + or inference.get("captions") != ["car", "person", "bicycle", "road sign"] + or inference.get("confidence_threshold") != 0.5 + or inference.get("processed_frame_count") != 4488 + or inference.get("input_resolution") != [960, 544] + or not isinstance(shadow, dict) + or shadow.get("threshold_changed_after_review") is not False + or shadow.get("legacy_large_false_background_cases_suppressed") != 5 + or profile.get("authority") != _AUTHORITY + ): + raise E46IGroundingDinoFullReplayError("E46I profile is invalid") + + +def _validate_runtime(runtime: dict[str, Any], profile: dict[str, Any], raw: Path) -> None: + overlay = runtime.get("overlay") + labels = runtime.get("labels_archive") + if ( + runtime.get("schema_version") != E46I_RUNTIME_SCHEMA + or runtime.get("run_status") != "SUCCESS" + or runtime.get("processed_frame_count") != 4488 + or runtime.get("annotated_frame_count") != 4488 + or runtime.get("label_file_count") != 4488 + or runtime.get("source_video_sha256") != profile["source"]["video_sha256"] + or runtime.get("model_onnx_sha256") != profile["provider"]["model_sha256"] + or runtime.get("tensorrt_engine_sha256") != profile["provider"]["engine_sha256"] + or runtime.get("spec_sha256") != profile["inference"]["spec_sha256"] + or not isinstance(overlay, dict) + or overlay.get("file") != E46I_OVERLAY_NAME + or overlay.get("frame_count") != 4488 + or overlay.get("duration_seconds") != 448.8 + or not isinstance(labels, dict) + or labels.get("file") != E46I_LABELS_NAME + ): + raise E46IGroundingDinoFullReplayError("E46I runtime identity is invalid") + for row in (overlay, labels): + path = raw / str(row["file"]) + if ( + not path.is_file() + or path.is_symlink() + or path.stat().st_size != row.get("byte_length") + or _sha256(path) != row.get("sha256") + ): + raise E46IGroundingDinoFullReplayError("E46I runtime artifact changed") + for name in ( + E46I_SHADOW_SHEET_NAME, + E46I_ROUTE_SHEET_NAME, + E46I_TARGETED_SHEET_NAME, + ): + path = raw / name + if not path.is_file() or path.is_symlink() or path.stat().st_size <= 0: + raise E46IGroundingDinoFullReplayError("E46I visual artifact is missing") + + +def _read_detection_frames( + labels_root: Path, profile: dict[str, Any], frame_count: int +) -> list[dict[str, Any]]: + allowed = set(profile["inference"]["captions"]) + threshold = float(profile["inference"]["confidence_threshold"]) + frames: list[dict[str, Any]] = [] + for frame_number in range(1, frame_count + 1): + path = labels_root / f"frame_{frame_number:06d}.txt" + if not path.is_file() or path.is_symlink(): + raise E46IGroundingDinoFullReplayError("E46I label sequence is incomplete") + detections: list[dict[str, Any]] = [] + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + if not line.strip(): + continue + tokens = line.split() + if len(tokens) < 16: + raise E46IGroundingDinoFullReplayError("E46I label row is invalid") + label = " ".join(tokens[:-15]) + try: + numeric = [float(value) for value in tokens[-15:]] + except ValueError as exc: + raise E46IGroundingDinoFullReplayError( + f"E46I label row {path.name}:{line_number} is invalid" + ) from exc + x1, y1, x2, y2 = numeric[3:7] + confidence = numeric[-1] + if ( + label not in allowed + or not threshold <= confidence <= 1.0 + or not 0.0 <= x1 < x2 <= 960.0 + or not 0.0 <= y1 < y2 <= 544.0 + ): + raise E46IGroundingDinoFullReplayError("E46I detection contract is invalid") + area_fraction = ((x2 - x1) * (y2 - y1)) / (960.0 * 544.0) + detections.append( + { + "class_name": label, + "confidence": round(confidence, 6), + "bbox_xyxy": [ + round(x1, 3), + round(y1, 3), + round(x2, 3), + round(y2, 3), + ], + "area_fraction": round(area_fraction, 9), + } + ) + frames.append( + { + "schema_version": E46I_FRAME_SCHEMA, + "frame_index": frame_number - 1, + "session_seconds": round((frame_number - 1) / 10.0, 1), + "label_sha256": _sha256(path), + "detection_count": len(detections), + "detections": detections, + } + ) + extras = [ + path + for path in labels_root.glob("frame_*.txt") + if path.name > f"frame_{frame_count:06d}.txt" + ] + if extras: + raise E46IGroundingDinoFullReplayError("E46I label sequence has extra frames") + return frames + + +def _metrics(frames: list[dict[str, Any]], runtime: dict[str, Any]) -> dict[str, Any]: + detections = [detection for frame in frames for detection in frame["detections"]] + counts = [int(frame["detection_count"]) for frame in frames] + confidences = [float(detection["confidence"]) for detection in detections] + areas = [float(detection["area_fraction"]) for detection in detections] + classes = Counter(str(detection["class_name"]) for detection in detections) + zero_runs: list[int] = [] + run = 0 + for count in counts: + if count == 0: + run += 1 + elif run: + zero_runs.append(run) + run = 0 + if run: + zero_runs.append(run) + return { + "frame_count": len(frames), + "route_duration_seconds": 448.8, + "detection_observation_count": len(detections), + "class_observation_counts": dict(sorted(classes.items())), + "mean_detections_per_frame": round(fmean(counts), 6), + "max_detections_per_frame": max(counts), + "zero_detection_frame_count": sum(count == 0 for count in counts), + "zero_detection_frame_fraction": round( + sum(count == 0 for count in counts) / len(frames), 9 + ), + "zero_detection_run_count": len(zero_runs), + "longest_zero_detection_run_frames": max(zero_runs, default=0), + "confidence_mean": round(fmean(confidences), 6), + "confidence_min": min(confidences), + "confidence_max": max(confidences), + "large_box_observation_count": sum(area >= 0.25 for area in areas), + "large_box_observation_fraction": round( + sum(area >= 0.25 for area in areas) / len(areas), 9 + ), + "largest_box_area_fraction": round(max(areas), 9), + "worker_elapsed_seconds": float(runtime["elapsed_seconds"]), + "worker_mean_frames_per_second": float(runtime["mean_frames_per_second"]), + } + + +def _method(profile: dict[str, Any]) -> dict[str, Any]: + source = profile["source"] + provider = profile["provider"] + inference = profile["inference"] + return { + "schema_version": "missioncore.laboratory-method/v1", + "completeness": "complete", + "execution_class": "hybrid", + "pipeline_id": profile["profile_id"], + "components": [ + { + "kind": "source", + "name": source["session_id"], + "version": "immutable recorded RIGHT FRONT replay", + "role": "single physical camera source", + "identity_sha256": source["video_sha256"], + }, + { + "kind": "tool", + "name": "NVIDIA Gst-nvdewarper", + "version": "DeepStream 9.1", + "role": "existing E46H calibrated FRONT adapter", + "identity_sha256": ( + "f861e31278550bbe3fc82f41df4381c8a7aaf113a98a10761d98fa085c6a56b4" + ), + }, + { + "kind": "model", + "name": provider["name"], + "version": provider["version"], + "role": "ready open-vocabulary detector provider", + "identity_sha256": provider["model_sha256"], + }, + { + "kind": "runtime", + "name": provider["deployment_toolkit"], + "version": "TensorRT FP16", + "role": "GPU inference runtime", + "identity_sha256": provider["engine_sha256"], + }, + { + "kind": "algorithm", + "name": "fixed open-vocabulary caption contract", + "version": ", ".join(inference["captions"]), + "role": "source-independent semantic query set", + "identity_sha256": inference["spec_sha256"], + }, + ], + } diff --git a/src/k1link/compute/e46j_raw_fisheye_realtime.py b/src/k1link/compute/e46j_raw_fisheye_realtime.py new file mode 100644 index 0000000..8c36459 --- /dev/null +++ b/src/k1link/compute/e46j_raw_fisheye_realtime.py @@ -0,0 +1,625 @@ +"""Freeze the one-pass YOLOX-S full-raw-fisheye realtime qualification. + +E46J deliberately evaluates the production-shaped detector path: one physical +K1 RIGHT frame produces one inference request. It does not dewarp, crop, tile, +track, hold or stitch detections. The result proves replay capacity and keeps +visual quality findings separate from ground-truth claims. +""" + +from __future__ import annotations + +import copy +import hashlib +import os +import re +import shutil +import uuid +from collections import Counter +from datetime import UTC, datetime +from pathlib import Path +from statistics import fmean +from typing import Any, Final + +from k1link.compute.e46e_ready_stack import ( + E46EReadyStackError, + _artifact, + _canonical_json, + _read_json, + _read_jsonl, + _sha256, + _validated_artifact, + _write_json, +) + +E46J_PROFILE_SCHEMA: Final = "missioncore.e46j-raw-fisheye-realtime-profile/v1" +E46J_RUNTIME_SCHEMA: Final = "missioncore.e46j-raw-fisheye-realtime-runtime/v1" +E46J_FRAME_SCHEMA: Final = "missioncore.e46j-raw-fisheye-realtime-frame/v1" +E46J_RESULT_SCHEMA: Final = "missioncore.e46j-raw-fisheye-realtime-result/v1" +E46J_REPORT_SCHEMA: Final = "missioncore.e46j-raw-fisheye-realtime-report/v1" + +E46J_MANIFEST_NAME: Final = "manifest.json" +E46J_REPORT_NAME: Final = "raw-fisheye-realtime-report.json" +E46J_FRAMES_NAME: Final = "frames.jsonl" +E46J_RUNTIME_NAME: Final = "runtime.json" +E46J_OVERLAY_NAME: Final = "raw-fisheye-yolox-overlay.mp4" +E46J_ROUTE_SHEET_NAME: Final = "full-route-10s-contact-sheet.png" +E46J_TARGETED_SHEET_NAME: Final = "targeted-windows-contact-sheet.png" +E46J_SHADOW_SHEET_NAME: Final = "operator-shadow-exception-contact-sheet.png" + +_RESULT_ID = re.compile(r"^e46j-raw-fisheye-realtime-[a-f0-9]{64}$") +_SHA256 = re.compile(r"^[a-f0-9]{64}$") +_CLASS_BY_ID: Final = { + 0: "person", + 1: "bicycle", + 2: "car", + 3: "motorcycle", + 5: "bus", + 7: "truck", +} +_AUTHORITY: Final = { + "ground_truth": False, + "provider_promoted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} +_REVIEW_WINDOWS: Final = [ + { + "id": "legacy-false-wall", + "label": "6.0–10.9 с · прежний false car на стене", + "start_seconds": 6.0, + "end_seconds": 10.9, + "verdict": "legacy-background-false-positive-suppressed", + }, + { + "id": "legacy-false-shrub", + "label": "178.6–180.3 с · прежний false car на кусте", + "start_seconds": 178.6, + "end_seconds": 180.3, + "verdict": "legacy-background-false-positive-suppressed", + }, + { + "id": "legacy-false-ground", + "label": "250.7–265.6 с · прежний false car на полотне", + "start_seconds": 250.7, + "end_seconds": 265.6, + "verdict": "legacy-background-false-positive-suppressed", + }, + { + "id": "legacy-false-road", + "label": "392.2–400.6 с · прежний false car на дороге", + "start_seconds": 392.2, + "end_seconds": 400.6, + "verdict": "legacy-background-false-positive-suppressed", + }, + { + "id": "operator-shadow", + "label": "419.4–426.9 с · тень оператора", + "start_seconds": 419.4, + "end_seconds": 426.9, + "verdict": "operator-shadow-person-false-positive-observed", + }, + { + "id": "legacy-false-terrace", + "label": "440.8–448.4 с · прежний false car на террасе", + "start_seconds": 440.8, + "end_seconds": 448.4, + "verdict": "legacy-background-false-positive-suppressed", + }, +] + + +class E46JRawFisheyeRealtimeError(ValueError): + """Raised when E46J evidence or immutable identity is invalid.""" + + +def build_e46j_raw_fisheye_realtime( + *, raw_root: Path, profile_path: Path, output_root: Path +) -> dict[str, Any]: + """Validate worker evidence and freeze one immutable E46J result.""" + + try: + return _build_e46j_raw_fisheye_realtime( + raw_root=raw_root, + profile_path=profile_path, + output_root=output_root, + ) + except E46EReadyStackError as exc: + raise E46JRawFisheyeRealtimeError(str(exc).replace("E46E", "E46J")) from exc + + +def _build_e46j_raw_fisheye_realtime( + *, raw_root: Path, profile_path: Path, output_root: Path +) -> dict[str, Any]: + raw = raw_root.resolve(strict=True) + if raw.is_symlink(): + raise E46JRawFisheyeRealtimeError("E46J raw root must not be a symlink") + profile_source = profile_path.resolve(strict=True) + profile = _read_json(profile_source) + _validate_profile(profile) + runtime_source = raw / E46J_RUNTIME_NAME + runtime = _read_json(runtime_source) + _validate_runtime(runtime, profile, profile_source, raw) + frames_source = raw / E46J_FRAMES_NAME + frames = _validated_frames(frames_source, profile, runtime) + metrics = _metrics(frames, runtime) + method = _method(profile) + + report_basis = { + "schema_version": E46J_REPORT_SCHEMA, + "status": "realtime-capacity-passed-awaiting-temporal-layer", + "source": copy.deepcopy(profile["source"]), + "detector": copy.deepcopy(profile["detector"]), + "preprocessing": copy.deepcopy(profile["preprocessing"]), + "detection": copy.deepcopy(profile["detection"]), + "metrics": metrics, + "visual_review": { + "status": "route-contact-sheet-and-targeted-window-review-completed", + "full_route_contact_sheet_review_completed": True, + "targeted_window_review_completed": True, + "reviewed_video_range_seconds": [0.0, 448.723], + "review_windows": copy.deepcopy(_REVIEW_WINDOWS), + "verdict": "realtime-detector-progress-with-known-shadow-exception", + "finding": ( + "Full raw KB4 fisheye coverage is retained. Vehicles remain visible on " + "the route and at the circular image edge; the five previously reviewed " + "large background car failures are absent in the targeted samples." + ), + "known_error": ( + "The operator shadow is classified as person in 35 of the 75 frames " + "inside the 419.4–426.9 second review window." + ), + }, + "acceptance": { + "exact_recorded_right_source_bound": True, + "full_raw_fisheye_retained": True, + "single_inference_per_source_frame": True, + "all_source_frames_processed": metrics["frame_count"] == 4489, + "zero_failed_frames": metrics["failed_frame_count"] == 0, + "ten_hz_capacity_gate_passed": metrics["core_capacity_fps"] >= 10.0, + "latency_gate_passed": ( + metrics["core_path_p95_ms"] <= 80.0 + and metrics["inference_request_p95_ms"] <= 60.0 + ), + "legacy_large_false_background_gate_passed": True, + "full_overlay_published": True, + "temporal_identity_available": False, + "motion_state_available": False, + "candidate_accepted": False, + "navigation_or_safety_accepted": False, + }, + "decision": { + "selected_provider": "megvii-yolox-s-0.1.1rc0", + "realtime_capacity_passed": True, + "ready_for_temporal_bakeoff": True, + "provider_promoted": False, + "custom_detector_logic_used": False, + "route_specific_filtering_used": False, + "next_action": ( + "Keep the single raw-fisheye detector pass unchanged and attach a ready " + "temporal tracker. Evaluate stable IDs, drop/recovery and dynamic/static " + "state on the same full video before any live-hardware claim." + ), + }, + "method": method, + "limitations": [ + ( + "E46J has no independent exhaustive truth; detection observation counts " + "are not precision or recall." + ), + ( + "The detector is frame-local and does not provide persistent object IDs, " + "track continuity or dynamic/static state." + ), + "A known person false positive occurs on the operator shadow.", + ( + "LiDAR range, free space, commands, navigation and safety remain outside " + "this result." + ), + ( + "The accepted capacity assumes the camera adapter and Triton share a " + "local GPU host path; routing full FP32 tensors through an external Windows " + "bridge is not an accepted realtime topology." + ), + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + identity = { + "schema_version": E46J_RESULT_SCHEMA, + "profile_sha256": _sha256(profile_source), + "profile": copy.deepcopy(profile), + "runtime_sha256": _sha256(runtime_source), + "runtime": copy.deepcopy(runtime), + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "frames_sha256": hashlib.sha256(_canonical_json(frames)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e46j-raw-fisheye-realtime-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_e46j_raw_fisheye_realtime(destination) + + created_at = datetime.now(UTC).isoformat(timespec="milliseconds").replace( + "+00:00", "Z" + ) + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at, + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / E46J_REPORT_NAME, report) + for name in ( + E46J_FRAMES_NAME, + E46J_RUNTIME_NAME, + E46J_OVERLAY_NAME, + E46J_ROUTE_SHEET_NAME, + E46J_TARGETED_SHEET_NAME, + E46J_SHADOW_SHEET_NAME, + ): + shutil.copyfile(raw / name, staging / name) + artifacts = [ + _artifact(staging / E46J_REPORT_NAME, "raw-fisheye-realtime-report"), + _artifact(staging / E46J_FRAMES_NAME, "detection-frames"), + _artifact(staging / E46J_RUNTIME_NAME, "runtime-record"), + _artifact(staging / E46J_OVERLAY_NAME, "visual-overlay-video"), + _artifact(staging / E46J_ROUTE_SHEET_NAME, "full-route-contact-sheet"), + _artifact(staging / E46J_TARGETED_SHEET_NAME, "targeted-windows-contact-sheet"), + _artifact(staging / E46J_SHADOW_SHEET_NAME, "operator-shadow-contact-sheet"), + ] + _write_json( + staging / E46J_MANIFEST_NAME, + { + "schema_version": E46J_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at, + "acceptance_state": "realtime-capacity-passed-awaiting-temporal-layer", + "ground_truth": False, + "artifacts": artifacts, + "authority": _AUTHORITY, + }, + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_e46j_raw_fisheye_realtime(destination) + + +def read_e46j_raw_fisheye_realtime(root: Path) -> dict[str, Any]: + """Read and fully validate one immutable E46J result.""" + + try: + return _read_e46j_raw_fisheye_realtime(root) + except E46EReadyStackError as exc: + raise E46JRawFisheyeRealtimeError(str(exc).replace("E46E", "E46J")) from exc + + +def _read_e46j_raw_fisheye_realtime(root: Path) -> dict[str, Any]: + resolved = root.resolve(strict=True) + if resolved.is_symlink(): + raise E46JRawFisheyeRealtimeError("E46J result root must not be a symlink") + manifest = _read_json(resolved / E46J_MANIFEST_NAME) + identity = manifest.get("identity") + digest = ( + hashlib.sha256(_canonical_json(identity)).hexdigest() + if isinstance(identity, dict) + else "" + ) + if ( + manifest.get("schema_version") != E46J_RESULT_SCHEMA + or manifest.get("result_id") != f"e46j-raw-fisheye-realtime-{digest}" + or manifest.get("identity_sha256") != digest + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("authority") != _AUTHORITY + or manifest.get("ground_truth") is not False + ): + raise E46JRawFisheyeRealtimeError("E46J result identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 7: + raise E46JRawFisheyeRealtimeError("E46J artifact inventory is invalid") + by_role = {row.get("role"): row for row in artifacts if isinstance(row, dict)} + paths = { + role: _validated_artifact(resolved, by_role.get(role)) + for role in ( + "raw-fisheye-realtime-report", + "detection-frames", + "runtime-record", + "visual-overlay-video", + "full-route-contact-sheet", + "targeted-windows-contact-sheet", + "operator-shadow-contact-sheet", + ) + } + report = _read_json(paths["raw-fisheye-realtime-report"]) + runtime = _read_json(paths["runtime-record"]) + frames = tuple(_read_jsonl(paths["detection-frames"])) + if ( + report.get("schema_version") != E46J_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("authority") != _AUTHORITY + or report.get("ground_truth") is not False + or runtime.get("schema_version") != E46J_RUNTIME_SCHEMA + or len(frames) != 4489 + or any(row.get("schema_version") != E46J_FRAME_SCHEMA for row in frames) + or hashlib.sha256(_canonical_json(frames)).hexdigest() + != identity.get("frames_sha256") + or report.get("metrics", {}).get("frame_count") != len(frames) + ): + raise E46JRawFisheyeRealtimeError("E46J result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "frames": frames, + "runtime": runtime, + "overlay_path": paths["visual-overlay-video"], + "route_sheet_path": paths["full-route-contact-sheet"], + "targeted_sheet_path": paths["targeted-windows-contact-sheet"], + "shadow_sheet_path": paths["operator-shadow-contact-sheet"], + } + + +def _validate_profile(profile: dict[str, Any]) -> None: + source = profile.get("source") + detector = profile.get("detector") + detection = profile.get("detection") + acceptance = profile.get("acceptance") + if ( + profile.get("schema_version") != E46J_PROFILE_SCHEMA + or profile.get("profile_id") != "e46j-k1-right-raw-kb4-yolox-s-one-pass/v1" + or not isinstance(source, dict) + or source.get("camera_source_id") != "sensor.camera.right" + or source.get("stream_sha256") + != "cadd1696ff000904eb78633a0a8418104b8024f178b91f3421789021ccb160e8" + or source.get("frame_count") != 4489 + or source.get("average_rate") != "4489000/448723" + or source.get("resolution") != [800, 600] + or source.get("calibration_model") != "KB4" + or not isinstance(detector, dict) + or detector.get("architecture") != "YOLOX-S" + or detector.get("license") != "Apache-2.0" + or detector.get("single_inference_per_source_frame") is not True + or _SHA256.fullmatch(str(detector.get("model_sha256"))) is None + or _SHA256.fullmatch(str(detector.get("config_sha256"))) is None + or not isinstance(detection, dict) + or detection.get("minimum_score") != 0.5 + or detection.get("nms_iou_threshold") != 0.45 + or detection.get("target_class_ids") != [0, 1, 2, 3, 5, 7] + or detection.get("custom_detector_logic") is not False + or detection.get("route_specific_filtering") is not False + or not isinstance(acceptance, dict) + or acceptance.get("minimum_core_capacity_fps") != 10.0 + or acceptance.get("require_full_raw_fov") is not True + or profile.get("authority") != _AUTHORITY + ): + raise E46JRawFisheyeRealtimeError("E46J profile is invalid") + + +def _validate_runtime( + runtime: dict[str, Any], + profile: dict[str, Any], + profile_path: Path, + raw: Path, +) -> None: + source = runtime.get("source") + model = runtime.get("model") + metrics = runtime.get("metrics") + acceptance = runtime.get("acceptance") + artifacts = runtime.get("artifacts") + if ( + runtime.get("schema_version") != E46J_RUNTIME_SCHEMA + or runtime.get("status") != "completed" + or runtime.get("profile_sha256") != _sha256(profile_path) + or not isinstance(source, dict) + or source.get("video_sha256") != profile["source"]["stream_sha256"] + or source.get("decoded_frame_count") != 4489 + or source.get("resolution") != [800, 600] + or "no crop/dewarp/tile" not in str(source.get("preprocessing")) + or not isinstance(model, dict) + or model.get("id") != profile["detector"]["id"] + or model.get("model_sha256") != profile["detector"]["model_sha256"] + or model.get("config_sha256") != profile["detector"]["config_sha256"] + or model.get("inference_requests") != 4489 + or not isinstance(metrics, dict) + or metrics.get("processed_frame_count") != 4489 + or metrics.get("failed_frame_count") != 0 + or float(metrics.get("core_capacity_fps", 0.0)) < 10.0 + or not isinstance(acceptance, dict) + or acceptance.get("passed") is not True + or not all(acceptance.get("checks", {}).values()) + or runtime.get("authority") != _AUTHORITY + or not isinstance(artifacts, dict) + ): + raise E46JRawFisheyeRealtimeError("E46J runtime identity is invalid") + expected = { + "frames": E46J_FRAMES_NAME, + "overlay": E46J_OVERLAY_NAME, + } + for role, name in expected.items(): + row = artifacts.get(role) + path = raw / name + if ( + not isinstance(row, dict) + or row.get("file") != name + or not path.is_file() + or path.is_symlink() + or path.stat().st_size != row.get("byte_length") + or _sha256(path) != row.get("sha256") + ): + raise E46JRawFisheyeRealtimeError("E46J runtime artifact changed") + for name in ( + E46J_ROUTE_SHEET_NAME, + E46J_TARGETED_SHEET_NAME, + E46J_SHADOW_SHEET_NAME, + ): + path = raw / name + if not path.is_file() or path.is_symlink() or path.stat().st_size <= 0: + raise E46JRawFisheyeRealtimeError("E46J visual artifact is missing") + + +def _validated_frames( + path: Path, profile: dict[str, Any], runtime: dict[str, Any] +) -> list[dict[str, Any]]: + if not path.is_file() or path.is_symlink(): + raise E46JRawFisheyeRealtimeError("E46J frame evidence is missing") + rows = list(_read_jsonl(path)) + if len(rows) != 4489: + raise E46JRawFisheyeRealtimeError("E46J frame sequence is incomplete") + artifact = runtime["artifacts"]["frames"] + if path.stat().st_size != artifact["byte_length"] or _sha256(path) != artifact["sha256"]: + raise E46JRawFisheyeRealtimeError("E46J frame evidence changed") + frame_rate = float(profile["source"]["frame_rate"]) + class_counts: Counter[str] = Counter() + for index, row in enumerate(rows): + detections = row.get("detections") + if ( + row.get("schema_version") != E46J_FRAME_SCHEMA + or row.get("frame_index") != index + or abs(float(row.get("session_seconds", -1.0)) - index / frame_rate) > 1e-5 + or not isinstance(detections, list) + or not isinstance(row.get("latency_ms"), dict) + ): + raise E46JRawFisheyeRealtimeError("E46J frame contract is invalid") + for detection in detections: + if not isinstance(detection, dict): + raise E46JRawFisheyeRealtimeError("E46J detection is invalid") + class_id = detection.get("class_id") + box = detection.get("bbox_xyxy") + score = detection.get("score") + if ( + not isinstance(class_id, int) + or detection.get("label") != _CLASS_BY_ID.get(class_id) + or not isinstance(score, (int, float)) + or not 0.5 <= float(score) <= 1.0 + or not isinstance(box, list) + or len(box) != 4 + or not 0.0 <= float(box[0]) < float(box[2]) <= 800.0 + or not 0.0 <= float(box[1]) < float(box[3]) <= 600.0 + ): + raise E46JRawFisheyeRealtimeError("E46J detection contract is invalid") + class_counts[str(detection["label"])] += 1 + if dict(sorted(class_counts.items())) != runtime["metrics"]["class_observation_counts"]: + raise E46JRawFisheyeRealtimeError("E46J class accounting changed") + return rows + + +def _metrics(frames: list[dict[str, Any]], runtime: dict[str, Any]) -> dict[str, Any]: + detections = [item for frame in frames for item in frame["detections"]] + counts = [len(frame["detections"]) for frame in frames] + confidences = [float(item["score"]) for item in detections] + zero_runs: list[int] = [] + run = 0 + for count in counts: + if count == 0: + run += 1 + elif run: + zero_runs.append(run) + run = 0 + if run: + zero_runs.append(run) + shadow_frames = [ + frame + for frame in frames + if 419.4 <= float(frame["session_seconds"]) <= 426.9 + ] + shadow_person_frames = sum( + any(item["label"] == "person" for item in frame["detections"]) + for frame in shadow_frames + ) + runtime_metrics = runtime["metrics"] + latency = runtime_metrics["latency_ms"] + return { + "frame_count": len(frames), + "route_duration_seconds": 448.723, + "failed_frame_count": int(runtime_metrics["failed_frame_count"]), + "detection_observation_count": len(detections), + "class_observation_counts": copy.deepcopy( + runtime_metrics["class_observation_counts"] + ), + "mean_detections_per_frame": round(fmean(counts), 6), + "max_detections_per_frame": max(counts), + "zero_detection_frame_count": sum(count == 0 for count in counts), + "zero_detection_frame_fraction": round( + sum(count == 0 for count in counts) / len(frames), 9 + ), + "zero_detection_run_count": len(zero_runs), + "longest_zero_detection_run_frames": max(zero_runs, default=0), + "confidence_mean": round(fmean(confidences), 6), + "confidence_min": min(confidences), + "confidence_max": max(confidences), + "core_capacity_fps": float(runtime_metrics["core_capacity_fps"]), + "core_path_mean_ms": float(latency["core_path_ms"]["mean"]), + "core_path_p95_ms": float(latency["core_path_ms"]["p95"]), + "inference_request_mean_ms": float( + latency["inference_request_ms"]["mean"] + ), + "inference_request_p95_ms": float(latency["inference_request_ms"]["p95"]), + "gpu_utilization_mean_percent": float( + runtime_metrics["gpu"]["gpu_utilization_percent"]["mean"] + ), + "operator_shadow_window_frame_count": len(shadow_frames), + "operator_shadow_person_frame_count": shadow_person_frames, + } + + +def _method(profile: dict[str, Any]) -> dict[str, Any]: + source = profile["source"] + detector = profile["detector"] + detection = profile["detection"] + return { + "schema_version": "missioncore.laboratory-method/v1", + "completeness": "complete", + "execution_class": "hybrid", + "pipeline_id": profile["profile_id"], + "components": [ + { + "kind": "source", + "name": source["session_id"], + "version": "immutable recorded K1 RIGHT raw KB4", + "role": "single physical camera source; full 800×600 raster", + "identity_sha256": source["stream_sha256"], + }, + { + "kind": "tool", + "name": "valid-FOV mask plus top-left letterbox", + "version": "raw KB4 adapter v1", + "role": "fill invalid pixels without crop, dewarp or virtual views", + "identity_sha256": source["calibration_sha256"], + }, + { + "kind": "model", + "name": detector["architecture"], + "version": detector["source"], + "role": "ready COCO-80 detector provider", + "identity_sha256": detector["model_sha256"], + }, + { + "kind": "runtime", + "name": detector["runtime"], + "version": "co-located GPU network path", + "role": "one synchronous inference request per source frame", + "identity_sha256": detector["config_sha256"], + }, + { + "kind": "algorithm", + "name": "standard YOLOX decode and class-wise NMS", + "version": ( + f"score {detection['minimum_score']} · IoU " + f"{detection['nms_iou_threshold']}" + ), + "role": "fixed source-independent detector output contract", + "identity_sha256": detector["config_sha256"], + }, + ], + } diff --git a/src/k1link/compute/e49_detector_truth_evaluation.py b/src/k1link/compute/e49_detector_truth_evaluation.py index 66b6fb9..9e11669 100644 --- a/src/k1link/compute/e49_detector_truth_evaluation.py +++ b/src/k1link/compute/e49_detector_truth_evaluation.py @@ -117,7 +117,7 @@ def build_e49_detector_truth_evaluation( "prediction freeze belongs to another truth island" ) - valid_fov = _read_valid_fov( + valid_fov = read_valid_fov_mask( valid_fov_root, calibration_sha256=str( truth_island.manifest["identity"]["source"]["calibration_sha256"] @@ -640,12 +640,14 @@ def _class_count(predictions: list[dict[str, Any]]) -> dict[str, int]: return dict(result) -def _read_valid_fov( +def read_valid_fov_mask( root: Path, *, calibration_sha256: str, calibration_slot: str, ) -> dict[str, Any]: + """Read an exact calibration-bound valid-FOV mask for detector metrics.""" + resolved = root.resolve(strict=True) manifest_path = resolved / E49_MANIFEST_NAME manifest = _read_json(manifest_path) diff --git a/src/k1link/compute/l32_pointpillars_camera_review.py b/src/k1link/compute/l32_pointpillars_camera_review.py new file mode 100644 index 0000000..f5cfe17 --- /dev/null +++ b/src/k1link/compute/l32_pointpillars_camera_review.py @@ -0,0 +1,435 @@ +"""Camera-bound visual evidence for the sealed RAVNOVES00 PointPillars run. + +L3.2 does not execute the detector again. It binds the immutable L3.1 visual +sample to exact right-camera frames, projects the same LiDAR sample and model +hypotheses through the admitted K1 calibration, and seals a new review result. +""" + +from __future__ import annotations + +import hashlib +import json +import math +import os +import shutil +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +import numpy as np + +from k1link.compute.e30_camera_evidence import ( + E30CameraEvidenceError, + materialize_e30_camera_frames, + open_e30_camera_evidence_source, +) +from k1link.compute.lidar_field_review import E10LidarFieldSource +from k1link.compute.semantic_geometry_fusion import projection_profile_from_source +from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import ( + Kb4ProjectionProfile, +) + +RESULT_SCHEMA: Final = "missioncore.l32-pointpillars-camera-review/v1" +CATALOG_SCHEMA: Final = "missioncore.l32-pointpillars-camera-review-catalog/v1" +FRAME_SCHEMA: Final = "missioncore.l32-pointpillars-camera-review-frame/v1" +MAX_CAMERA_BINDING_DELTA_MS: Final = 100.0 +BOX_EDGES: Final = ( + (0, 1), (1, 2), (2, 3), (3, 0), + (4, 5), (5, 6), (6, 7), (7, 4), + (0, 4), (1, 5), (2, 6), (3, 7), +) + + +class L32PointPillarsCameraReviewError(RuntimeError): + """The L3.2 evidence sources or generated result violate the contract.""" + + +def build_l32_pointpillars_camera_review( + *, + l31_result_root: Path, + e10_pack_root: Path, + camera_job_root: Path, + ffmpeg_path: Path, + output_root: Path, +) -> Path: + """Build one immutable camera-first review from sealed local sources.""" + + l31_root = l31_result_root.expanduser().resolve(strict=True) + if l31_root.is_symlink() or not l31_root.is_dir(): + raise L32PointPillarsCameraReviewError("L3.1 result root is invalid") + l31_manifest = _read_json(l31_root / "manifest.json") + l31_catalog = _read_json(l31_root / "catalog.json") + if ( + l31_manifest.get("schema_version") + != "missioncore.l31-pointpillars-ravnoves/v1" + or l31_manifest.get("result_id") != l31_root.name + or l31_catalog.get("schema_version") + != "missioncore.l31-pointpillars-ravnoves-catalog/v1" + or l31_catalog.get("result_id") != l31_root.name + ): + raise L32PointPillarsCameraReviewError("L3.1 source identity is invalid") + + source = E10LidarFieldSource(e10_pack_root) + try: + if ( + source.identity.get("session_id") + != l31_manifest["identity"].get("source_session_id") + or source.identity.get("source_id") != "sensor.camera.right" + ): + raise L32PointPillarsCameraReviewError( + "camera-aligned LiDAR pack does not match L3.1" + ) + projection = projection_profile_from_source(source) + try: + camera = open_e30_camera_evidence_source( + camera_job_root=camera_job_root, + ffmpeg_path=ffmpeg_path, + expected_session_id=str(source.identity["session_id"]), + expected_source_id=str(source.identity["source_id"]), + ) + except E30CameraEvidenceError as exc: + raise L32PointPillarsCameraReviewError( + "right-camera evidence source is invalid" + ) from exc + + bindings = _select_bindings(l31_root, l31_catalog, source) + if not bindings: + raise L32PointPillarsCameraReviewError( + "no L3.1 visual frames overlap the right camera" + ) + identity = { + "schema_version": RESULT_SCHEMA, + "source_session_id": source.identity["session_id"], + "source_l31_result_id": l31_root.name, + "source_l31_manifest_sha256": _sha256(l31_root / "manifest.json"), + "source_l31_catalog_sha256": _sha256(l31_root / "catalog.json"), + "source_e10_pack_id": source.pack_id, + "source_camera_job": camera.identity(), + "projection": { + "model": "kb4", + "source_id": projection.source_id, + "calibration_slot": projection.calibration_slot, + "width": projection.width, + "height": projection.height, + }, + "camera_binding": { + "clock": "session-monotonic", + "selection": "nearest-camera-frame", + "maximum_absolute_delta_ms": MAX_CAMERA_BINDING_DELTA_MS, + }, + "review_frame_indices": [item["frame_index"] for item in bindings], + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": { + "shadow_only": True, + "commands_enabled": False, + "navigation_or_safety_accepted": False, + "accuracy_accepted": False, + }, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"l32-pointpillars-camera-review-{identity_sha256}" + root = output_root.expanduser().resolve() + root.mkdir(mode=0o700, parents=True, exist_ok=True) + destination = root / result_id + if destination.exists(): + _validate_existing(destination, identity) + return destination + + staging = root / f".{result_id}.{os.getpid()}.incomplete" + staging.mkdir(mode=0o700, exist_ok=False) + try: + camera_artifacts = materialize_e30_camera_frames( + source=camera, + source_frame_indices=tuple( + int(item["camera_source_frame_index"]) for item in bindings + ), + destination_root=staging / "frames", + width=projection.width, + height=projection.height, + ) + catalog_frames: list[dict[str, object]] = [] + score_values: list[float] = [] + visible_boxes = 0 + projected_points = 0 + for binding in bindings: + old_payload = binding.pop("source_payload") + frame_id = str(binding["frame_id"]) + points = np.asarray( + old_payload["points"]["values"], dtype=np.float64 + ).reshape((-1, 4)) + projected = _project_sensor_points(points[:, :3], projection) + projected_box_items = [ + _project_box(box, projection) + for box in old_payload["prediction_boxes"] + ] + projected_box_items = [item for item in projected_box_items if item] + visible_boxes += len(projected_box_items) + projected_points += int(projected.shape[0]) + score_values.extend( + float(box["score"]) for box in old_payload["prediction_boxes"] + ) + camera_frame_index = int(binding["camera_source_frame_index"]) + frame_payload = { + "schema_version": FRAME_SCHEMA, + "frame_id": frame_id, + "summary": binding, + "camera": camera_artifacts[camera_frame_index], + "points": old_payload["points"], + "prediction_boxes": old_payload["prediction_boxes"], + "camera_projection": { + "point_layout": "flat-xy-depth-m", + "point_count": int(projected.shape[0]), + "point_values": projected.reshape(-1).tolist(), + "boxes": projected_box_items, + }, + "interpretation": { + "camera_is_semantic_reference": True, + "lidar_is_metric_overlay": True, + "boxes_are_model_hypotheses": True, + "ground_truth_available": False, + "accuracy_claim_allowed": False, + }, + } + frame_path = staging / f"frame-{frame_id}.json" + _write_json(frame_path, frame_payload) + catalog_frames.append( + { + **binding, + "detail_path": frame_path.name, + "detail_sha256": _sha256(frame_path), + "detail_byte_length": frame_path.stat().st_size, + "camera_path": camera_artifacts[camera_frame_index]["path"], + "camera_sha256": camera_artifacts[camera_frame_index]["sha256"], + } + ) + + catalog = { + "schema_version": CATALOG_SCHEMA, + "result_id": result_id, + "source_session_id": source.identity["session_id"], + "frame_count": len(catalog_frames), + "frames": catalog_frames, + } + catalog_path = staging / "catalog.json" + _write_json(catalog_path, catalog) + scores = np.asarray(score_values, dtype=np.float64) + deltas = np.asarray( + [abs(float(item["camera_delta_ms"])) for item in bindings], + dtype=np.float64, + ) + metrics = { + **l31_manifest["metrics"], + "review_frame_count": len(bindings), + "review_prediction_count": len(score_values), + "review_visible_projected_box_count": visible_boxes, + "review_projected_point_count": projected_points, + "score_below_0_25_fraction": float(np.mean(scores < 0.25)), + "score_below_0_50_fraction": float(np.mean(scores < 0.50)), + "camera_binding_absolute_delta_ms": { + "maximum": float(np.max(deltas)), + "p50": float(np.percentile(deltas, 50)), + "p95": float(np.percentile(deltas, 95)), + }, + } + limitations = [ + "The camera is a semantic reference, not labeled 3D ground truth.", + "PointPillars boxes remain cross-domain model hypotheses.", + ( + "LiDAR overlay uses the admitted factory KB4 calibration and " + "nearest camera frame within 100 ms." + ), + "The review cannot establish precision, recall or safety fitness.", + ] + manifest = { + "schema_version": RESULT_SCHEMA, + "result_id": result_id, + "identity": identity, + "identity_sha256": identity_sha256, + "created_at_utc": datetime.now(UTC).isoformat(timespec="milliseconds"), + "status": "camera-bound-review-rejects-current-candidate", + "metrics": metrics, + "catalog": { + "path": "catalog.json", + "sha256": _sha256(catalog_path), + "byte_length": catalog_path.stat().st_size, + }, + "limitations": limitations, + "authority": identity["authority"], + } + _write_json(staging / "manifest.json", manifest) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return destination + finally: + source.close() + + +def _select_bindings( + l31_root: Path, + catalog: dict[str, Any], + source: E10LidarFieldSource, +) -> list[dict[str, Any]]: + times = np.asarray(source.arrays["session_seconds"], dtype=np.float64) + selected: list[dict[str, Any]] = [] + for descriptor in catalog.get("frames", []): + if not isinstance(descriptor, dict): + raise L32PointPillarsCameraReviewError("L3.1 catalog frame is invalid") + target = float(descriptor["session_seconds"]) + row = int(np.argmin(np.abs(times - target))) + delta_ms = float((times[row] - target) * 1000.0) + if abs(delta_ms) > MAX_CAMERA_BINDING_DELTA_MS: + continue + frame_id = str(descriptor["frame_id"]) + payload_path = l31_root / str(descriptor["detail_path"]) + if ( + _sha256(payload_path) != descriptor.get("detail_sha256") + or payload_path.stat().st_size != descriptor.get("detail_byte_length") + ): + raise L32PointPillarsCameraReviewError("L3.1 visual frame changed") + payload = _read_json(payload_path) + selected.append( + { + "frame_id": frame_id, + "frame_index": int(descriptor["frame_index"]), + "session_seconds": target, + "source_point_count": int(descriptor["source_point_count"]), + "prediction_count": int(descriptor["prediction_count"]), + "class_counts": descriptor["class_counts"], + "inference_ms": float(descriptor["inference_ms"]), + "camera_source_frame_index": int( + source.arrays["source_frame_indices"][row] + ), + "camera_session_seconds": float(times[row]), + "camera_delta_ms": delta_ms, + "source_payload": payload, + } + ) + return selected + + +def _project_sensor_points( + points_lidar: np.ndarray, + profile: Kb4ProjectionProfile, +) -> np.ndarray: + pixels, depths, valid = _project_camera(points_lidar, profile) + if not np.any(valid): + return np.empty((0, 3), dtype=np.float64) + return np.column_stack((pixels[valid], depths[valid])) + + +def _project_box( + box: dict[str, Any], + profile: Kb4ProjectionProfile, +) -> dict[str, object] | None: + center = np.asarray([box["x_m"], box["y_m"], box["z_m"]], dtype=np.float64) + length = float(box["length_m"]) + width = float(box["width_m"]) + height = float(box["height_m"]) + yaw = float(box["yaw_rad"]) + cosine = math.cos(yaw) + sine = math.sin(yaw) + corners: list[list[float]] = [] + for z_offset in (-height / 2.0, height / 2.0): + for x_offset, y_offset in ( + (-length / 2.0, -width / 2.0), + (length / 2.0, -width / 2.0), + (length / 2.0, width / 2.0), + (-length / 2.0, width / 2.0), + ): + corners.append( + [ + center[0] + x_offset * cosine - y_offset * sine, + center[1] + x_offset * sine + y_offset * cosine, + center[2] + z_offset, + ] + ) + pixels, _, valid = _project_camera(np.asarray(corners), profile) + segments: list[float] = [] + for start, end in BOX_EDGES: + if valid[start] and valid[end]: + segments.extend( + [ + float(pixels[start, 0]), + float(pixels[start, 1]), + float(pixels[end, 0]), + float(pixels[end, 1]), + ] + ) + if not segments: + return None + return { + "model_class": box["model_class"], + "score": float(box["score"]), + "segments_xyxy": segments, + } + + +def _project_camera( + points_lidar: np.ndarray, + profile: Kb4ProjectionProfile, +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + points = np.asarray(points_lidar, dtype=np.float64) + transform = profile.t_camera_from_lidar + camera = points @ transform[:3, :3].T + transform[:3, 3] + x, y, z = camera.T + radial = np.hypot(x, y) + theta = np.arctan2(radial, z) + theta2 = theta * theta + k1, k2, k3, k4 = profile.distortion_kb4 + distorted = theta * ( + 1.0 + k1 * theta2 + k2 * theta2**2 + k3 * theta2**3 + k4 * theta2**4 + ) + scale = np.divide( + distorted, radial, out=np.zeros_like(distorted), where=radial > 1e-12 + ) + fx, fy, cx, cy = profile.intrinsic_fx_fy_cx_cy + pixels = np.column_stack((fx * x * scale + cx, fy * y * scale + cy)) + valid = ( + (z > 1e-6) + & np.isfinite(pixels).all(axis=1) + & (pixels[:, 0] >= 0.0) + & (pixels[:, 0] < profile.width) + & (pixels[:, 1] >= 0.0) + & (pixels[:, 1] < profile.height) + ) + return pixels, z, valid + + +def _validate_existing(root: Path, identity: dict[str, object]) -> None: + manifest = _read_json(root / "manifest.json") + if ( + manifest.get("schema_version") != RESULT_SCHEMA + or manifest.get("identity") != identity + or manifest.get("result_id") != root.name + ): + raise L32PointPillarsCameraReviewError("existing L3.2 result differs") + + +def _read_json(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise L32PointPillarsCameraReviewError(f"invalid JSON: {path.name}") from exc + if not isinstance(value, dict): + raise L32PointPillarsCameraReviewError(f"invalid object: {path.name}") + return value + + +def _write_json(path: Path, value: object) -> None: + path.write_bytes(_canonical_json(value) + b"\n") + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, ensure_ascii=False, sort_keys=True, separators=(",", ":") + ).encode("utf-8") + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() diff --git a/src/k1link/compute/l33_camera_first_admission.py b/src/k1link/compute/l33_camera_first_admission.py new file mode 100644 index 0000000..1d54739 --- /dev/null +++ b/src/k1link/compute/l33_camera_first_admission.py @@ -0,0 +1,40 @@ +"""Fail-closed source admission for the RAVNOVES00 L3.3 review.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Final + +RAVNOVES00_SESSION_ID: Final = "20260720T065719Z_viewer_live" +RAVNOVES00_CAMERA_SOURCE_ID: Final = "sensor.camera.right" +RAVNOVES00_ADMITTED_WORLD_STATE_RESULT_ID: Final = ( + "rectified-camera-world-state-04f0bb519af614ca16eae3d924ca930fb68869e0f5131916a56705e8b24d4bf7" +) + + +def is_admitted_world_state( + result_id: object, + identity: Mapping[str, object] | None = None, +) -> bool: + """Return whether one world-state result is the sealed RAVNOVES00 parent.""" + + if result_id != RAVNOVES00_ADMITTED_WORLD_STATE_RESULT_ID: + return False + if identity is None: + return True + source = identity.get("source") + return ( + isinstance(source, Mapping) + and source.get("session_id") == RAVNOVES00_SESSION_ID + and source.get("source_id") == RAVNOVES00_CAMERA_SOURCE_ID + ) + + +def is_admitted_l33_identity(identity: Mapping[str, object]) -> bool: + """Return whether an L3.3 identity is bound to the admitted replay source.""" + + return ( + identity.get("source_session_id") == RAVNOVES00_SESSION_ID + and identity.get("source_world_state_result_id") + == RAVNOVES00_ADMITTED_WORLD_STATE_RESULT_ID + ) diff --git a/src/k1link/compute/l33_camera_first_detector_review.py b/src/k1link/compute/l33_camera_first_detector_review.py new file mode 100644 index 0000000..122e881 --- /dev/null +++ b/src/k1link/compute/l33_camera_first_detector_review.py @@ -0,0 +1,1123 @@ +"""Seal camera-first detector evidence for RAVNOVES00. + +L3.3 repairs the evidence boundary exposed by L3.2: a missing synchronous +LiDAR sample must not erase a current camera track. The builder reuses the +sealed E26 camera track stream, E29 local-surface associations and the exact +camera/LiDAR materialization from L3.2. It performs no detector inference. +""" + +from __future__ import annotations + +import hashlib +import json +import math +import os +import shutil +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +import numpy as np + +from k1link.compute.l31_pointpillars_ravnoves import sensor_frame_xyzi +from k1link.compute.l33_camera_first_admission import is_admitted_world_state +from k1link.compute.lidar_field_review import E10LidarFieldSource +from k1link.compute.semantic_geometry_fusion import projection_profile_from_source +from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import ( + Kb4ProjectionProfile, + unproject_pixels_kb4, +) + +RESULT_SCHEMA: Final = "missioncore.l33-camera-first-detector-review/v1" +CATALOG_SCHEMA: Final = "missioncore.l33-camera-first-detector-review-catalog/v1" +FRAME_SCHEMA: Final = "missioncore.l33-camera-first-detector-review-frame/v1" +MINIMUM_DETECTOR_SCORE: Final = 0.50 +MAXIMUM_RECOVERY_SIDE_SECONDS: Final = 0.75 +MAXIMUM_RECOVERY_SPAN_SECONDS: Final = 1.25 +MAXIMUM_FRAMES: Final = 18 + + +class L33CameraFirstDetectorReviewError(RuntimeError): + """The sealed sources or generated camera-first result are invalid.""" + + +def build_l33_camera_first_detector_review( + *, + l32_result_root: Path, + e26_result_root: Path, + e29_result_root: Path, + e10_pack_root: Path, + output_root: Path, + rectified_world_state_root: Path | None = None, +) -> Path: + """Build one immutable camera-first review without running inference.""" + + if rectified_world_state_root is not None: + if not is_admitted_world_state(rectified_world_state_root.name): + raise L33CameraFirstDetectorReviewError( + "rectified world-state source is not admitted for RAVNOVES00" + ) + return _build_rectified_world_state_review( + l32_result_root=l32_result_root, + e26_result_root=e26_result_root, + e29_result_root=e29_result_root, + e10_pack_root=e10_pack_root, + rectified_world_state_root=rectified_world_state_root, + output_root=output_root, + ) + + l32_root = _strict_directory(l32_result_root, "l32-pointpillars-camera-review-") + e26_root = _strict_directory(e26_result_root, "e10-integrated-perception-") + e29_root = _strict_directory(e29_result_root, "e29-camera-geometry-") + l32_manifest = _read_json(l32_root / "manifest.json") + l32_catalog = _read_json(l32_root / "catalog.json") + e26_result = _read_json(e26_root / "result.json") + e29_report = _read_json(e29_root / "camera-geometry-report.json") + if ( + l32_manifest.get("schema_version") + != "missioncore.l32-pointpillars-camera-review/v1" + or l32_manifest.get("result_id") != l32_root.name + or l32_catalog.get("schema_version") + != "missioncore.l32-pointpillars-camera-review-catalog/v1" + or l32_catalog.get("result_id") != l32_root.name + or e26_result.get("result_id") != e26_root.name + or e26_result.get("schema_version") + != "missioncore.e10-integrated-perception-result/v1" + or e29_report.get("result_id") != e29_root.name + or e29_report.get("schema_version") + != "missioncore.e29-camera-geometry-report/v1" + ): + raise L33CameraFirstDetectorReviewError("source identity is invalid") + + catalog_frames = l32_catalog.get("frames") + if not isinstance(catalog_frames, list) or not 1 <= len(catalog_frames) <= MAXIMUM_FRAMES: + raise L33CameraFirstDetectorReviewError("L3.2 frame catalog is invalid") + target_indices = tuple(int(item["camera_source_frame_index"]) for item in catalog_frames) + e26_rows = _select_e26_rows(e26_root / "fusion-frames.jsonl", target_indices) + e29_rows = _select_rows( + e29_root / "camera-geometry-frames.jsonl", + set(target_indices), + ) + + source = E10LidarFieldSource(e10_pack_root) + try: + if ( + source.pack_id != e29_report.get("identity", {}).get("source_pack_id") + or source.identity.get("session_id") + != l32_manifest.get("identity", {}).get("source_session_id") + ): + raise L33CameraFirstDetectorReviewError("E10 source does not match E29/L3.2") + projection = projection_profile_from_source(source) + identity = { + "schema_version": RESULT_SCHEMA, + "source_session_id": source.identity["session_id"], + "source_l32_result_id": l32_root.name, + "source_l32_manifest_sha256": _sha256(l32_root / "manifest.json"), + "source_e26_result_id": e26_root.name, + "source_e26_result_sha256": _sha256(e26_root / "result.json"), + "source_e26_frames_sha256": _sha256(e26_root / "fusion-frames.jsonl"), + "source_e29_result_id": e29_root.name, + "source_e29_report_sha256": _sha256(e29_root / "camera-geometry-report.json"), + "source_e29_frames_sha256": _sha256(e29_root / "camera-geometry-frames.jsonl"), + "source_e10_pack_id": source.pack_id, + "detector": _detector_identity(e26_result), + "semantic_contract": { + "owner": "camera", + "minimum_detector_score": MINIMUM_DETECTOR_SCORE, + "lidar_unavailable_policy": "preserve-bounded-camera-track-state", + "recovery": "two-sided-linear-track-interpolation", + "maximum_recovery_side_seconds": MAXIMUM_RECOVERY_SIDE_SECONDS, + "maximum_recovery_span_seconds": MAXIMUM_RECOVERY_SPAN_SECONDS, + }, + "geometry_contract": { + "owner": "lidar", + "association": "e29-local-surface-connected-support", + "camera_only_representation": "calibrated-bearing-ray-without-range", + "ranged_representation": "e29-occupied-centroid-anchor", + "range_definition": "euclidean-camera-origin-to-e29-anchor", + "source_e29_depth_retained_as": "source_e29_optical_depth_m", + "absence_of_points_means_free": False, + "camera_only_range_allowed": False, + }, + "review_camera_frame_indices": list(target_indices), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": { + "shadow_only": True, + "commands_enabled": False, + "navigation_or_safety_accepted": False, + "accuracy_accepted": False, + }, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"l33-camera-first-detector-review-{identity_sha256}" + root = output_root.expanduser().resolve() + root.mkdir(mode=0o700, parents=True, exist_ok=True) + destination = root / result_id + if destination.exists(): + _validate_existing(destination, identity) + return destination + + staging = root / f".{result_id}.{os.getpid()}.incomplete" + staging.mkdir(mode=0o700, exist_ok=False) + try: + frames_root = staging / "frames" + frames_root.mkdir(mode=0o700) + output_frames: list[dict[str, object]] = [] + total_detections = 0 + ranged_detections = 0 + recovered_frames = 0 + exact_frames = 0 + class_counts: dict[str, int] = {} + for descriptor in catalog_frames: + frame_id = str(descriptor["frame_id"]) + camera_index = int(descriptor["camera_source_frame_index"]) + l32_payload = _read_json(l32_root / str(descriptor["detail_path"])) + exact, before, after = e26_rows[camera_index] + e29_row = e29_rows.get(camera_index) + detections, recovered = _camera_detections( + exact=exact, + before=before, + after=after, + e29_row=e29_row, + target_seconds=float(descriptor["camera_session_seconds"]), + source=source, + frame_index=camera_index, + projection=projection, + ) + total_detections += len(detections) + ranged_detections += sum(item["range_m"] is not None for item in detections) + recovered_frames += int(recovered) + exact_frames += int(not recovered) + for detection in detections: + label = str(detection["label"]) + class_counts[label] = class_counts.get(label, 0) + 1 + + source_camera_path = (l32_root / str(descriptor["camera_path"])).resolve() + camera_path = frames_root / source_camera_path.name + shutil.copyfile(source_camera_path, camera_path) + frame_summary = { + "frame_id": frame_id, + "frame_index": int(descriptor["frame_index"]), + "session_seconds": float(descriptor["session_seconds"]), + "camera_source_frame_index": camera_index, + "camera_session_seconds": float(descriptor["camera_session_seconds"]), + "camera_delta_ms": float(descriptor["camera_delta_ms"]), + "source_point_count": int(descriptor["source_point_count"]), + "detection_count": len(detections), + "ranged_detection_count": sum( + item["range_m"] is not None for item in detections + ), + "camera_only_detection_count": sum( + item["range_m"] is None for item in detections + ), + "semantic_provenance": ( + "bounded-track-interpolation" if recovered else "recorded-current" + ), + } + frame_payload = { + "schema_version": FRAME_SCHEMA, + "frame_id": frame_id, + "summary": frame_summary, + "camera": l32_payload["camera"], + "points": l32_payload["points"], + "camera_projection": { + "point_layout": l32_payload["camera_projection"]["point_layout"], + "point_count": l32_payload["camera_projection"]["point_count"], + "point_values": l32_payload["camera_projection"]["point_values"], + }, + "detections": detections, + "interpretation": { + "camera_owns_semantic_identity": True, + "lidar_owns_metric_range": True, + "missing_lidar_erases_camera_detection": False, + "ground_truth_available": False, + "accuracy_claim_allowed": False, + }, + } + detail_path = staging / f"frame-{frame_id}.json" + _write_json(detail_path, frame_payload) + output_frames.append( + { + **frame_summary, + "detail_path": detail_path.name, + "detail_sha256": _sha256(detail_path), + "detail_byte_length": detail_path.stat().st_size, + "camera_path": str(camera_path.relative_to(staging)), + "camera_sha256": _sha256(camera_path), + } + ) + + catalog = { + "schema_version": CATALOG_SCHEMA, + "result_id": result_id, + "source_session_id": source.identity["session_id"], + "frame_count": len(output_frames), + "frames": output_frames, + } + catalog_path = staging / "catalog.json" + _write_json(catalog_path, catalog) + metrics = { + "review_frame_count": len(output_frames), + "recorded_current_frame_count": exact_frames, + "recovered_camera_frame_count": recovered_frames, + "semantic_frame_coverage_fraction": float( + sum(item["detection_count"] > 0 for item in output_frames) + / len(output_frames) + ), + "detection_count": total_detections, + "ranged_detection_count": ranged_detections, + "camera_only_detection_count": total_detections - ranged_detections, + "range_coverage_fraction": ( + float(ranged_detections / total_detections) + if total_detections else 0.0 + ), + "class_counts": class_counts, + "minimum_detector_score": MINIMUM_DETECTOR_SCORE, + } + manifest = { + "schema_version": RESULT_SCHEMA, + "result_id": result_id, + "identity": identity, + "identity_sha256": identity_sha256, + "created_at_utc": datetime.now(UTC).isoformat(timespec="milliseconds"), + "status": "camera-first-boundary-restored-shadow-only", + "metrics": metrics, + "catalog": { + "path": "catalog.json", + "sha256": _sha256(catalog_path), + "byte_length": catalog_path.stat().st_size, + }, + "limitations": [ + "The selected frames are visual evidence, not labeled ground truth.", + ( + f"{recovered_frames} LiDAR-gap frames use bounded " + "interpolation of recorded camera tracks." + ), + "E29 range is published only for connected occupied LiDAR support.", + "No precision, recall, navigation or safety authority is claimed.", + ], + "authority": identity["authority"], + "ground_truth": False, + } + _write_json(staging / "manifest.json", manifest) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return destination + finally: + source.close() + + +def _build_rectified_world_state_review( + *, + l32_result_root: Path, + e26_result_root: Path, + e29_result_root: Path, + e10_pack_root: Path, + rectified_world_state_root: Path, + output_root: Path, +) -> Path: + """Seal selected visual frames from the accepted full-route composition.""" + + l32_root = _strict_directory(l32_result_root, "l32-pointpillars-camera-review-") + e26_root = _strict_directory(e26_result_root, "e10-integrated-perception-") + e29_root = _strict_directory(e29_result_root, "e29-camera-geometry-") + world_root = _strict_directory( + rectified_world_state_root, "rectified-camera-world-state-" + ) + l32_manifest = _read_json(l32_root / "manifest.json") + l32_catalog = _read_json(l32_root / "catalog.json") + e26_result = _read_json(e26_root / "result.json") + e29_report = _read_json(e29_root / "camera-geometry-report.json") + world_manifest = _read_json(world_root / "manifest.json") + world_report = _read_json(world_root / "run-report.json") + world_identity = world_report.get("identity") + if ( + l32_manifest.get("schema_version") + != "missioncore.l32-pointpillars-camera-review/v1" + or l32_manifest.get("result_id") != l32_root.name + or l32_catalog.get("schema_version") + != "missioncore.l32-pointpillars-camera-review-catalog/v1" + or l32_catalog.get("result_id") != l32_root.name + or e26_result.get("result_id") != e26_root.name + or e29_report.get("result_id") != e29_root.name + or world_report.get("schema_version") + != "missioncore.rectified-camera-world-state/v1" + or world_report.get("result_id") != world_root.name + or world_report.get("state") != "accepted" + or world_report.get("acceptance", {}).get("accepted") is not True + or world_manifest.get("result_id") != world_root.name + or world_manifest.get("identity") != world_identity + or not isinstance(world_identity, dict) + or not is_admitted_world_state(world_root.name, world_identity) + or world_identity.get("source", {}).get("session_id") + != l32_manifest.get("identity", {}).get("source_session_id") + or world_identity.get("source", {}).get("lidar_pack_id") + != l32_manifest.get("identity", {}).get("source_e10_pack_id") + ): + raise L33CameraFirstDetectorReviewError( + "rectified world-state source identity is invalid" + ) + artifacts = world_manifest.get("artifacts") + if not isinstance(artifacts, list): + raise L33CameraFirstDetectorReviewError("world-state manifest is invalid") + for artifact in artifacts: + if not isinstance(artifact, dict) or not isinstance(artifact.get("path"), str): + raise L33CameraFirstDetectorReviewError("world-state artifact is invalid") + path = world_root / artifact["path"] + if ( + path.is_symlink() + or not path.is_file() + or path.stat().st_size != artifact.get("byte_length") + or _sha256(path) != artifact.get("sha256") + ): + raise L33CameraFirstDetectorReviewError("world-state artifact changed") + + catalog_frames = l32_catalog.get("frames") + if not isinstance(catalog_frames, list) or not 1 <= len(catalog_frames) <= MAXIMUM_FRAMES: + raise L33CameraFirstDetectorReviewError("L3.2 frame catalog is invalid") + target_indices = tuple(int(item["camera_source_frame_index"]) for item in catalog_frames) + fusion_rows, route_metrics = _select_rectified_rows( + world_root / "fusion-frames.jsonl", target_indices + ) + world_rows = _select_exact_rows(world_root / "world-state.jsonl", target_indices) + detector = world_identity["composition"]["detector"]["detector"] + world_metrics = world_report["metrics"] + rss_growth_mib = world_metrics.get("rss_growth_mib") + rejection_counts = world_metrics.get("rejection_counts") + if ( + not isinstance(rss_growth_mib, int | float) + or isinstance(rss_growth_mib, bool) + or not math.isfinite(float(rss_growth_mib)) + or not 0.0 <= float(rss_growth_mib) <= 512.0 + or not isinstance(rejection_counts, dict) + ): + raise L33CameraFirstDetectorReviewError( + "rectified world-state metrics are not admitted" + ) + source = E10LidarFieldSource(e10_pack_root) + try: + if source.pack_id != world_identity["source"]["lidar_pack_id"]: + raise L33CameraFirstDetectorReviewError("world-state LiDAR pack changed") + projection = projection_profile_from_source(source) + identity = { + "schema_version": RESULT_SCHEMA, + "source_session_id": source.identity["session_id"], + "source_l32_result_id": l32_root.name, + "source_l32_manifest_sha256": _sha256(l32_root / "manifest.json"), + "source_e26_result_id": e26_root.name, + "source_e26_result_sha256": _sha256(e26_root / "result.json"), + "source_e29_result_id": e29_root.name, + "source_e29_report_sha256": _sha256(e29_root / "camera-geometry-report.json"), + "source_e10_pack_id": source.pack_id, + "source_world_state_result_id": world_root.name, + "source_world_state_manifest_sha256": _sha256(world_root / "manifest.json"), + "source_world_state_report_sha256": _sha256(world_root / "run-report.json"), + "source_world_state_frames_sha256": _sha256(world_root / "fusion-frames.jsonl"), + "detector": detector, + "semantic_contract": { + "owner": "camera", + "minimum_detector_score": 0.25, + "tracker": "ByteTrack-style two-stage IoU", + "lidar_unavailable_policy": "preserve-camera-track-without-invented-range", + "temporal_hold": "E23 bounded inline state", + }, + "geometry_contract": { + "owner": "lidar", + "association": "EoMT semantic support plus calibrated LiDAR projection", + "camera_only_representation": "calibrated-bearing-ray-without-range", + "ranged_representation": "stabilized-amodal-cuboid-and-center-anchor", + "absence_of_points_means_free": False, + "camera_only_range_allowed": False, + }, + "review_camera_frame_indices": list(target_indices), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": { + "shadow_only": True, + "commands_enabled": False, + "navigation_or_safety_accepted": False, + "accuracy_accepted": False, + }, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"l33-camera-first-detector-review-{identity_sha256}" + root = output_root.expanduser().resolve() + root.mkdir(mode=0o700, parents=True, exist_ok=True) + destination = root / result_id + if destination.exists(): + _validate_existing(destination, identity) + return destination + staging = root / f".{result_id}.{os.getpid()}.incomplete" + staging.mkdir(mode=0o700, exist_ok=False) + try: + frames_root = staging / "frames" + frames_root.mkdir(mode=0o700) + output_frames: list[dict[str, object]] = [] + selected_detections = 0 + selected_ranged = 0 + selected_cuboids = 0 + selected_current = 0 + selected_held = 0 + class_counts: dict[str, int] = {} + for descriptor in catalog_frames: + frame_id = str(descriptor["frame_id"]) + camera_index = int(descriptor["camera_source_frame_index"]) + l32_payload = _read_json(l32_root / str(descriptor["detail_path"])) + fusion_row = fusion_rows[camera_index] + world_row = world_rows[camera_index] + world_by_track = { + int(item["track_id"]): item + for item in world_row.get("objects", []) + if isinstance(item, dict) and isinstance(item.get("track_id"), int) + } + detections: list[dict[str, object]] = [] + held_count = 0 + for item in fusion_row.get("objects", []): + if ( + not isinstance(item, dict) + or float(item.get("score", 0.0)) < 0.25 + or not _number_list(item.get("bbox_xyxy"), 4) + or not isinstance(item.get("track_id"), int) + or not isinstance(item.get("label"), str) + ): + continue + track_id = int(item["track_id"]) + geometry = world_by_track.get(track_id) + temporal_status = str(item.get("temporal_status") or "") + temporal_2d_status = str(item.get("temporal_2d_status") or "") + held = "held" in temporal_status or temporal_2d_status == "held" + held_count += int(held) + anchor = ( + [float(value) for value in geometry["position_lidar_m"]] + if geometry is not None + and _number_list(geometry.get("position_lidar_m"), 3) + else None + ) + range_m = ( + float(geometry["range_m"]) + if geometry is not None + and isinstance(geometry.get("range_m"), (int, float)) + else None + ) + bbox = [float(value) for value in item["bbox_xyxy"]] + detections.append( + { + "source_track_id": track_id, + "label": item["label"], + "association_group": item.get( + "association_group", item["label"] + ), + "score": float(item["score"]), + "bbox_xyxy": bbox, + "semantic_provenance": ( + "e23-temporal-hold" + if held else "rectified-detector-current" + ), + "geometry_status": ( + str(geometry.get("temporal_status")) + if geometry is not None else "camera-only" + ), + "geometry_reason": ( + str(geometry.get("geometry")) + if geometry is not None + else "qualified-lidar-range-unavailable" + ), + "range_m": range_m, + "source_e29_optical_depth_m": None, + "support": { + "projected_points": int( + item.get("candidate_projected_points", 0) + ), + "occupied_points": int( + item.get("semantic_compatible_points", 0) + ), + "connected_occupied_points": int( + item.get("clustered_points", 0) + ), + }, + "geometry_anchor_xyz_m": anchor, + "camera_ray_lidar": _camera_ray_lidar(bbox, projection), + } + ) + label = str(item["label"]) + class_counts[label] = class_counts.get(label, 0) + 1 + detections.sort( + key=lambda item: (-float(item["score"]), int(item["source_track_id"])) + ) + prediction_boxes = _world_prediction_boxes( + world_by_track.values(), source=source, frame_index=camera_index + ) + selected_detections += len(detections) + selected_ranged += sum(item["range_m"] is not None for item in detections) + selected_cuboids += len(prediction_boxes) + selected_held += held_count + selected_current += len(detections) - held_count + source_camera_path = (l32_root / str(descriptor["camera_path"])).resolve() + camera_path = frames_root / source_camera_path.name + shutil.copyfile(source_camera_path, camera_path) + frame_provenance = ( + "e23-temporal-hold" + if held_count else "rectified-detector-current" + ) + frame_summary = { + "frame_id": frame_id, + "frame_index": int(descriptor["frame_index"]), + "session_seconds": float(descriptor["session_seconds"]), + "camera_source_frame_index": camera_index, + "camera_session_seconds": float(descriptor["camera_session_seconds"]), + "camera_delta_ms": float(descriptor["camera_delta_ms"]), + "source_point_count": int(descriptor["source_point_count"]), + "detection_count": len(detections), + "ranged_detection_count": sum( + item["range_m"] is not None for item in detections + ), + "camera_only_detection_count": sum( + item["range_m"] is None for item in detections + ), + "cuboid_count": len(prediction_boxes), + "semantic_provenance": frame_provenance, + } + frame_payload = { + "schema_version": FRAME_SCHEMA, + "frame_id": frame_id, + "summary": frame_summary, + "camera": l32_payload["camera"], + "points": l32_payload["points"], + "camera_projection": l32_payload["camera_projection"], + "detections": detections, + "prediction_boxes": prediction_boxes, + "interpretation": { + "camera_owns_semantic_identity": True, + "lidar_owns_metric_range": True, + "missing_lidar_erases_camera_detection": False, + "ground_truth_available": False, + "accuracy_claim_allowed": False, + }, + } + detail_path = staging / f"frame-{frame_id}.json" + _write_json(detail_path, frame_payload) + output_frames.append( + { + **frame_summary, + "detail_path": detail_path.name, + "detail_sha256": _sha256(detail_path), + "detail_byte_length": detail_path.stat().st_size, + "camera_path": str(camera_path.relative_to(staging)), + "camera_sha256": _sha256(camera_path), + } + ) + catalog = { + "schema_version": CATALOG_SCHEMA, + "result_id": result_id, + "source_session_id": source.identity["session_id"], + "frame_count": len(output_frames), + "frames": output_frames, + } + catalog_path = staging / "catalog.json" + _write_json(catalog_path, catalog) + metrics = { + "review_frame_count": len(output_frames), + "recorded_current_frame_count": selected_current, + "recovered_camera_frame_count": selected_held, + "semantic_frame_coverage_fraction": float( + sum(item["detection_count"] > 0 for item in output_frames) + / len(output_frames) + ), + "detection_count": selected_detections, + "ranged_detection_count": selected_ranged, + "camera_only_detection_count": selected_detections - selected_ranged, + "range_coverage_fraction": ( + float(selected_ranged / selected_detections) + if selected_detections else 0.0 + ), + "cuboid_count": selected_cuboids, + "class_counts": class_counts, + "minimum_detector_score": 0.25, + "route_detection_count": route_metrics["route_detection_count"], + "route_detection_frame_count": route_metrics[ + "route_detection_frame_count" + ], + "frames_processed": int(world_metrics["frames_processed"]), + "effective_composed_fps": float( + world_metrics["effective_composed_fps"] + ), + "composed_p95_ms": float( + world_metrics["latency_ms"]["composed_compute"]["p95"] + ), + "semantic_effective_fps": float( + world_metrics["semantic_effective_fps"] + ), + "lidar_fused_frames": int(world_metrics["lidar_fused_frames"]), + "raw_accepted_cuboids": int(world_metrics["raw_accepted_cuboids"]), + "duplicate_support_rejections": int( + rejection_counts.get("rejected-duplicate-lidar-support", 0) + ), + "temporal_accepted_cuboids": int( + world_metrics["temporal_accepted_cuboids"] + ), + "world_object_observations": int( + world_metrics["world_object_observations"] + ), + } + manifest = { + "schema_version": RESULT_SCHEMA, + "result_id": result_id, + "identity": identity, + "identity_sha256": identity_sha256, + "created_at_utc": datetime.now(UTC).isoformat(timespec="milliseconds"), + "status": "camera-first-boundary-restored-shadow-only", + "metrics": metrics, + "catalog": { + "path": "catalog.json", + "sha256": _sha256(catalog_path), + "byte_length": catalog_path.stat().st_size, + }, + "limitations": list(world_report["limitations"]), + "authority": identity["authority"], + "ground_truth": False, + } + _write_json(staging / "manifest.json", manifest) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return destination + finally: + source.close() + + +def _select_rectified_rows( + path: Path, target_indices: tuple[int, ...] +) -> tuple[dict[int, dict[str, Any]], dict[str, int]]: + targets = set(target_indices) + selected: dict[int, dict[str, Any]] = {} + route_detection_count = 0 + route_detection_frames = 0 + with path.open("r", encoding="utf-8") as stream: + for expected, line in enumerate(stream): + row = json.loads(line) + if row.get("frame_index") != expected: + raise L33CameraFirstDetectorReviewError( + "rectified fusion stream is not contiguous" + ) + visible = [ + item for item in row.get("objects", []) + if isinstance(item, dict) and float(item.get("score", 0.0)) >= 0.25 + ] + route_detection_count += len(visible) + route_detection_frames += int(bool(visible)) + if expected in targets: + selected[expected] = row + if selected.keys() != targets: + raise L33CameraFirstDetectorReviewError("rectified review frames are missing") + return selected, { + "route_detection_count": route_detection_count, + "route_detection_frame_count": route_detection_frames, + } + + +def _select_exact_rows( + path: Path, target_indices: tuple[int, ...] +) -> dict[int, dict[str, Any]]: + targets = set(target_indices) + selected: dict[int, dict[str, Any]] = {} + with path.open("r", encoding="utf-8") as stream: + for expected, line in enumerate(stream): + row = json.loads(line) + if row.get("frame_index") != expected: + raise L33CameraFirstDetectorReviewError( + "rectified world-state stream is not contiguous" + ) + if expected in targets: + selected[expected] = row + if selected.keys() != targets: + raise L33CameraFirstDetectorReviewError("world-state review frames are missing") + return selected + + +def _world_prediction_boxes( + objects: Any, + *, + source: E10LidarFieldSource, + frame_index: int, +) -> list[dict[str, object]]: + objects = list(objects) + if not objects: + return [] + sensor_quaternion = source.arrays["pose_quaternions_map_from_lidar"][frame_index] + if not np.isfinite(sensor_quaternion).all(): + # A temporal world object can survive a source frame without a current + # LiDAR pose. Publishing it in the current sensor frame would invent + # a transform, so the camera track remains visible but the cube does not. + return [] + sensor_rotation = _quaternion_rotation( + sensor_quaternion + ) + boxes: list[dict[str, object]] = [] + for item in objects: + if ( + not isinstance(item, dict) + or not _number_list(item.get("position_lidar_m"), 3) + or not _number_list(item.get("size_m"), 3) + or not _number_list(item.get("orientation_map_xyzw"), 4) + ): + continue + object_rotation = _quaternion_rotation(item["orientation_map_xyzw"]) + local_forward = sensor_rotation.T @ object_rotation[:, 0] + yaw = math.atan2(float(local_forward[1]), float(local_forward[0])) + boxes.append( + { + "benchmark_class": str(item.get("detector_label", item.get("class"))), + "center_xyz_m": [float(value) for value in item["position_lidar_m"]], + "size_lwh_m": [float(value) for value in item["size_m"]], + "yaw_rad": yaw, + "status": "model-prediction", + "score": float(item.get("confidence", 0.0)), + "track_id": int(item["track_id"]), + "temporal_status": str(item.get("temporal_status", "unknown")), + } + ) + boxes.sort(key=lambda item: int(item["track_id"])) + return boxes + + +def _quaternion_rotation(value: Any) -> np.ndarray: + quaternion = np.asarray(value, dtype=np.float64) + if quaternion.shape != (4,) or not np.isfinite(quaternion).all(): + raise L33CameraFirstDetectorReviewError("invalid visualization quaternion") + norm = float(np.linalg.norm(quaternion)) + if norm <= 1e-9: + raise L33CameraFirstDetectorReviewError("zero visualization quaternion") + x, y, z, w = quaternion / norm + return np.asarray( + [ + [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)], + [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)], + [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)], + ], + dtype=np.float64, + ) + + +def _camera_detections( + *, + exact: dict[str, Any], + before: dict[str, Any] | None, + after: dict[str, Any] | None, + e29_row: dict[str, Any] | None, + target_seconds: float, + source: E10LidarFieldSource, + frame_index: int, + projection: Kb4ProjectionProfile, +) -> tuple[list[dict[str, object]], bool]: + exact_objects = exact.get("objects") + if isinstance(exact_objects, list) and exact_objects: + candidates = [dict(item) for item in exact_objects if isinstance(item, dict)] + recovered = False + else: + candidates = _interpolate_tracks(before, after, target_seconds) + recovered = True + e29_by_track = { + int(item.get("source_track_id", item.get("track_id"))): item + for item in (e29_row or {}).get("semantic_observations", []) + if isinstance(item, dict) + and isinstance(item.get("source_track_id", item.get("track_id")), int) + } + detections: list[dict[str, object]] = [] + for item in candidates: + score = float(item.get("score", 0.0)) + bbox = item.get("bbox_xyxy") + source_track_id = item.get("source_track_id", item.get("track_id")) + if ( + score < MINIMUM_DETECTOR_SCORE + or not isinstance(source_track_id, int) + or not _number_list(bbox, 4) + or not isinstance(item.get("label"), str) + ): + continue + geometry = e29_by_track.get(source_track_id) if not recovered else None + geometry_status = ( + str(geometry.get("geometry_status")) if geometry else "lidar-unavailable" + ) + source_optical_depth_m = geometry.get("range_m") if geometry else None + support = geometry.get("support") if geometry else {} + geometry_anchor = ( + _local_geometry_anchor(geometry, source, frame_index) + if geometry else None + ) + camera_ray = _camera_ray_lidar(bbox, projection) + range_m = ( + float( + np.linalg.norm( + np.asarray(geometry_anchor, dtype=np.float64) + - np.asarray(camera_ray["origin_xyz_m"], dtype=np.float64) + ) + ) + if geometry_anchor is not None else None + ) + detections.append( + { + "source_track_id": source_track_id, + "label": item["label"], + "association_group": item.get("association_group", item["label"]), + "score": score, + "bbox_xyxy": [float(value) for value in bbox], + "semantic_provenance": ( + "bounded-track-interpolation" if recovered else "recorded-current" + ), + "geometry_status": geometry_status, + "geometry_reason": ( + geometry.get("geometry_reason") if geometry else "synchronous-lidar-unavailable" + ), + "range_m": range_m, + "source_e29_optical_depth_m": ( + float(source_optical_depth_m) + if isinstance(source_optical_depth_m, (int, float)) else None + ), + "support": { + "projected_points": int(support.get("projected_points_in_bbox", 0)), + "occupied_points": int(support.get("occupied_points_in_bbox", 0)), + "connected_occupied_points": int( + support.get("connected_occupied_points", 0) + ), + }, + "geometry_anchor_xyz_m": geometry_anchor, + "camera_ray_lidar": camera_ray, + } + ) + detections.sort(key=lambda item: (-float(item["score"]), int(item["source_track_id"]))) + return detections, recovered + + +def _interpolate_tracks( + before: dict[str, Any] | None, + after: dict[str, Any] | None, + target_seconds: float, +) -> list[dict[str, object]]: + if before is None or after is None: + raise L33CameraFirstDetectorReviewError("camera track recovery is unbounded") + before_seconds = float(before["session_seconds"]) + after_seconds = float(after["session_seconds"]) + span = after_seconds - before_seconds + if ( + span <= 0 + or target_seconds - before_seconds > MAXIMUM_RECOVERY_SIDE_SECONDS + or after_seconds - target_seconds > MAXIMUM_RECOVERY_SIDE_SECONDS + or span > MAXIMUM_RECOVERY_SPAN_SECONDS + ): + raise L33CameraFirstDetectorReviewError("camera track recovery exceeds bounds") + before_tracks = _tracks_by_source_id(before) + after_tracks = _tracks_by_source_id(after) + ratio = (target_seconds - before_seconds) / span + result: list[dict[str, object]] = [] + for track_id in sorted(before_tracks.keys() & after_tracks.keys()): + left = before_tracks[track_id] + right = after_tracks[track_id] + if left.get("label") != right.get("label"): + continue + left_bbox = left.get("bbox_xyxy") + right_bbox = right.get("bbox_xyxy") + if not _number_list(left_bbox, 4) or not _number_list(right_bbox, 4): + continue + result.append( + { + "source_track_id": track_id, + "track_id": track_id, + "label": left["label"], + "association_group": left.get("association_group", left["label"]), + "score": (1.0 - ratio) * float(left.get("score", 0.0)) + + ratio * float(right.get("score", 0.0)), + "bbox_xyxy": [ + (1.0 - ratio) * float(a) + ratio * float(b) + for a, b in zip(left_bbox, right_bbox, strict=True) + ], + } + ) + if not result: + raise L33CameraFirstDetectorReviewError("camera track recovery found no stable tracks") + return result + + +def _tracks_by_source_id(row: dict[str, Any]) -> dict[int, dict[str, Any]]: + result: dict[int, dict[str, Any]] = {} + for item in row.get("objects", []): + if not isinstance(item, dict): + continue + track_id = item.get("source_track_id", item.get("track_id")) + if isinstance(track_id, int): + result[track_id] = item + return result + + +def _local_geometry_anchor( + geometry: dict[str, Any], + source: E10LidarFieldSource, + frame_index: int, +) -> list[float] | None: + center = geometry.get("occupied_centroid_map_xyz_m") + if ( + not _number_list(center, 3) + or not bool(source.arrays["sample_available"][frame_index]) + ): + return None + local = sensor_frame_xyzi( + np.asarray([center], dtype=np.float64), + np.asarray([255], dtype=np.uint8), + position_map_xyz=np.asarray( + source.arrays["pose_positions_map"][frame_index], dtype=np.float64 + ), + orientation_map_from_lidar_xyzw=np.asarray( + source.arrays["pose_quaternions_map_from_lidar"][frame_index], + dtype=np.float64, + ), + )[0, :3] + return [float(value) for value in local] + + +def _camera_ray_lidar( + bbox: list[object], + projection: Kb4ProjectionProfile, +) -> dict[str, list[float]]: + pixel = np.asarray( + [[ + (float(bbox[0]) + float(bbox[2])) / 2.0, + (float(bbox[1]) + float(bbox[3])) / 2.0, + ]], + dtype=np.float64, + ) + camera_direction = unproject_pixels_kb4(pixel, profile=projection)[0] + transform = np.asarray(projection.t_camera_from_lidar, dtype=np.float64) + rotation_camera_from_lidar = transform[:3, :3] + translation_camera_from_lidar = transform[:3, 3] + origin = -rotation_camera_from_lidar.T @ translation_camera_from_lidar + direction = rotation_camera_from_lidar.T @ camera_direction + direction /= np.linalg.norm(direction) + return { + "origin_xyz_m": [float(value) for value in origin], + "direction_xyz": [float(value) for value in direction], + } + + +def _select_e26_rows( + path: Path, + target_indices: tuple[int, ...], +) -> dict[int, tuple[dict[str, Any], dict[str, Any] | None, dict[str, Any] | None]]: + targets = set(target_indices) + exact: dict[int, dict[str, Any]] = {} + before: dict[int, dict[str, Any] | None] = {} + after: dict[int, dict[str, Any] | None] = {} + previous_nonempty: dict[str, Any] | None = None + pending: list[int] = [] + for row in _jsonl(path): + frame_index = int(row.get("frame_index", -1)) + objects = row.get("objects") + nonempty = isinstance(objects, list) and bool(objects) + if frame_index in targets: + exact[frame_index] = row + before[frame_index] = previous_nonempty + if not nonempty: + pending.append(frame_index) + if nonempty: + for target in tuple(pending): + if target < frame_index: + after[target] = row + pending.remove(target) + previous_nonempty = row + if set(exact) != targets: + raise L33CameraFirstDetectorReviewError("E26 target frames are incomplete") + return { + target: (exact[target], before.get(target), after.get(target)) + for target in target_indices + } + + +def _select_rows(path: Path, target_indices: set[int]) -> dict[int, dict[str, Any]]: + result: dict[int, dict[str, Any]] = {} + for row in _jsonl(path): + frame_index = int(row.get("frame_index", -1)) + if frame_index in target_indices: + result[frame_index] = row + if set(result) != target_indices: + raise L33CameraFirstDetectorReviewError("E29 target frames are incomplete") + return result + + +def _detector_identity(result: dict[str, Any]) -> dict[str, object]: + detector = result.get("identity", {}).get("models", {}).get("detector") + if not isinstance(detector, dict): + raise L33CameraFirstDetectorReviewError("E26 detector identity is missing") + return { + "id": detector.get("id"), + "architecture": detector.get("architecture"), + "classes": detector.get("classes"), + "model_sha256": detector.get("model_sha256"), + "input_shape": detector.get("input_shape"), + } + + +def _jsonl(path: Path): + if path.is_symlink() or not path.is_file(): + raise L33CameraFirstDetectorReviewError("JSONL source is invalid") + with path.open("r", encoding="utf-8") as stream: + for line in stream: + value = json.loads(line) + if not isinstance(value, dict): + raise L33CameraFirstDetectorReviewError("JSONL row is invalid") + yield value + + +def _strict_directory(path: Path, prefix: str) -> Path: + candidate = path.expanduser().resolve(strict=True) + if candidate.is_symlink() or not candidate.is_dir() or not candidate.name.startswith(prefix): + raise L33CameraFirstDetectorReviewError("source directory is invalid") + return candidate + + +def _validate_existing(destination: Path, identity: dict[str, object]) -> None: + manifest = _read_json(destination / "manifest.json") + expected = hashlib.sha256(_canonical_json(identity)).hexdigest() + if ( + manifest.get("identity") != identity + or manifest.get("identity_sha256") != expected + or manifest.get("result_id") != destination.name + ): + raise L33CameraFirstDetectorReviewError("existing L3.3 result changed") + + +def _number_list(value: object, length: int) -> bool: + return ( + isinstance(value, list) + and len(value) == length + and all(isinstance(item, (int, float)) and math.isfinite(float(item)) for item in value) + ) + + +def _read_json(path: Path) -> dict[str, Any]: + if path.is_symlink() or not path.is_file(): + raise L33CameraFirstDetectorReviewError("JSON source is invalid") + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise L33CameraFirstDetectorReviewError("JSON source is invalid") + return value + + +def _write_json(path: Path, value: object) -> None: + path.write_text( + json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")), + encoding="utf-8", + ) + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, ensure_ascii=False, sort_keys=True, separators=(",", ":") + ).encode("utf-8") + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() diff --git a/src/k1link/compute/l34_right_yolox_truth_island_freeze.py b/src/k1link/compute/l34_right_yolox_truth_island_freeze.py new file mode 100644 index 0000000..2f8c045 --- /dev/null +++ b/src/k1link/compute/l34_right_yolox_truth_island_freeze.py @@ -0,0 +1,579 @@ +"""Freeze the first YOLOX candidate for RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1.""" + +from __future__ import annotations + +import hashlib +import json +import math +import os +import re +import shutil +import uuid +from collections import Counter +from dataclasses import dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from .e46_detector_truth_island import ( + E46_CONTRACT_NAME, + E46_MANIFEST_NAME, + E46_REFERENCES_NAME, + E46DetectorTruthIslandError, + read_e46_detector_truth_island, +) + +L34_RESULT_SCHEMA: Final = "missioncore.l34-right-yolox-truth-island-freeze/v1" +L34_REPORT_SCHEMA: Final = "missioncore.l34-right-yolox-truth-island-report/v1" +L34_PREDICTION_SCHEMA: Final = ( + "missioncore.l34-right-yolox-truth-island-prediction/v1" +) +L34_PROFILE_SCHEMA: Final = "missioncore.l34-right-yolox-truth-island-profile/v1" +L34_MANIFEST_NAME: Final = "manifest.json" +L34_REPORT_NAME: Final = "benchmark-report.json" +L34_PREDICTIONS_NAME: Final = "candidate-predictions.jsonl" + +_L33_SCHEMA: Final = "missioncore.l33-camera-first-detector-review/v1" +_DETECTOR_FRAME_SCHEMA: Final = "missioncore.rectified-yolox-frame/v1" +_RESULT_ID: Final = re.compile(r"^l34-right-yolox-truth-island-freeze-[a-f0-9]{64}$") +_SHA256: Final = re.compile(r"^[a-f0-9]{64}$") +_CANDIDATE_ID: Final = "yolox-s-kb4-core3" +_TARGET_CLASSES: Final = ( + "person", + "bicycle", + "motorcycle", + "car", + "heavy_vehicle", + "static_obstacle", + "animal", +) +_DETECTOR_LABELS: Final = frozenset( + {"person", "bicycle", "motorcycle", "car", "truck", "bus"} +) +_AUTHORITY: Final = { + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class L34RightYoloxTruthIslandError(RuntimeError): + """The benchmark preregistration or one of its immutable inputs is invalid.""" + + +@dataclass(frozen=True, slots=True) +class L34RightYoloxTruthIsland: + result_id: str + result_root: Path + manifest: dict[str, Any] + report: dict[str, Any] + predictions: tuple[dict[str, Any], ...] + + +def build_l34_right_yolox_truth_island_freeze( + *, + profile_path: Path, + truth_island_root: Path, + detector_qualification_root: Path, + l33_result_root: Path, + output_root: Path, +) -> L34RightYoloxTruthIsland: + """Freeze right-camera YOLOX predictions without reading human labels.""" + + profile = _read_profile(profile_path) + try: + truth_island = read_e46_detector_truth_island(truth_island_root) + except E46DetectorTruthIslandError as reason: + raise L34RightYoloxTruthIslandError("E46 truth island is invalid") from reason + + contract = _read_json(truth_island.result_root / E46_CONTRACT_NAME) + annotation = _object(contract.get("annotation"), "E46 annotation") + truth_source = _object( + _object(truth_island.manifest.get("identity"), "E46 identity").get( + "source" + ), + "E46 source", + ) + if ( + contract.get("truth_state") != "labels-unavailable" + or annotation.get("classes") != list(_TARGET_CLASSES) + or truth_source.get("source_id") != "sensor.camera.right" + or truth_source.get("session_id") != "20260720T065719Z_viewer_live" + ): + raise L34RightYoloxTruthIslandError( + "truth island does not preserve the blind right-camera contract" + ) + + qualification_root = _directory(detector_qualification_root) + qualification_path = qualification_root / "qualification.json" + frames_path = qualification_root / "frames.jsonl" + qualification = _read_json(qualification_path) + if ( + qualification.get("schema_version") + != "missioncore.rectified-yolox-qualification/v1" + or _object(qualification.get("metrics"), "qualification metrics").get( + "frames_processed" + ) + != 4489 + ): + raise L34RightYoloxTruthIslandError("YOLOX qualification is invalid") + + l33_root = _directory(l33_result_root) + l33_manifest_path = l33_root / L34_MANIFEST_NAME + l33_manifest = _read_json(l33_manifest_path) + l33_identity = _object(l33_manifest.get("identity"), "L3.3 identity") + detector = _object(l33_identity.get("detector"), "L3.3 detector") + semantic = _object( + l33_identity.get("semantic_contract"), + "L3.3 semantic contract", + ) + if ( + l33_manifest.get("schema_version") != _L33_SCHEMA + or l33_manifest.get("result_id") != l33_root.name + or l33_identity.get("source_session_id") + != truth_source.get("session_id") + or detector.get("architecture") != "YOLOX-S" + or detector.get("model_sha256") != profile["candidate"]["model_sha256"] + or semantic.get("minimum_detector_score") + != profile["candidate"]["minimum_score"] + ): + raise L34RightYoloxTruthIslandError("L3.3 candidate identity is invalid") + + references = tuple( + _read_jsonl(truth_island.result_root / E46_REFERENCES_NAME) + ) + target_indices = { + _integer(reference.get("frame_index"), "truth frame index") + for reference in references + } + frames = _selected_detector_frames(frames_path, target_indices) + predictions = freeze_l34_candidate_predictions( + references=references, + detector_frames=frames, + minimum_score=float(profile["candidate"]["minimum_score"]), + ) + predictions_bytes = b"".join( + _canonical_json(row) + b"\n" for row in predictions + ) + predictions_sha256 = hashlib.sha256(predictions_bytes).hexdigest() + class_counts = Counter( + prediction["label"] + for row in predictions + for prediction in row["predictions"] + ) + identity = { + "schema_version": L34_RESULT_SCHEMA, + "profile": profile, + "source": { + "session_id": truth_source["session_id"], + "source_id": truth_source["source_id"], + "mode": "recorded-replay-only", + }, + "truth_island": { + "result_id": truth_island.result_id, + "manifest_sha256": _sha256( + truth_island.result_root / E46_MANIFEST_NAME + ), + "truth_state": "labels-unavailable", + }, + "candidate": { + "l33_result_id": l33_root.name, + "l33_manifest_sha256": _sha256(l33_manifest_path), + "qualification_sha256": _sha256(qualification_path), + "detector_frames_sha256": _sha256(frames_path), + "prediction_rows_sha256": predictions_sha256, + }, + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"l34-right-yolox-truth-island-freeze-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_l34_right_yolox_truth_island_freeze(destination) + + report = { + "schema_version": L34_REPORT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "status": "predictions-frozen-awaiting-independent-truth", + "profile_id": profile["profile_id"], + "pipeline_id": profile["pipeline_id"], + "source_session_id": truth_source["session_id"], + "camera_source_id": truth_source["source_id"], + "metrics": { + "frame_count": len(predictions), + "temporal_group_count": len( + {str(row["group_id"]) for row in predictions} + ), + "prediction_count": sum( + len(row["predictions"]) for row in predictions + ), + "frames_with_predictions": sum( + bool(row["predictions"]) for row in predictions + ), + "class_counts": { + label: class_counts.get(label, 0) for label in _TARGET_CLASSES + }, + "accuracy_metrics_available": False, + }, + "decision": { + "candidate_predictions_frozen": True, + "truth_labels_read": False, + "candidate_accepted": False, + "model_retraining_authorized": False, + "next_gate": ( + "complete two independent blind reviews, seal adjudicated " + "truth, then evaluate these exact predictions" + ), + }, + "limitations": [ + "RAVNOVES00 source-scoped benchmark; no cross-route claim", + "accuracy remains unavailable until independent truth is sealed", + "recorded replay only; live transport and hardware are out of scope", + "only sensor.camera.right is admitted", + ], + "authority": _AUTHORITY, + "access": "read-only", + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + (staging / L34_PREDICTIONS_NAME).write_bytes(predictions_bytes) + _write_json(staging / L34_REPORT_NAME, report) + manifest = { + "schema_version": L34_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": _utc_now(), + "ground_truth": False, + "acceptance_state": "prepared-awaiting-independent-human-truth", + "artifacts": [ + _artifact(staging / L34_REPORT_NAME, "benchmark-report"), + _artifact( + staging / L34_PREDICTIONS_NAME, + "frozen-candidate-predictions", + ), + ], + "authority": _AUTHORITY, + } + _write_json(staging / L34_MANIFEST_NAME, manifest) + os.replace(staging, destination) + except Exception: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_l34_right_yolox_truth_island_freeze(destination) + + +def read_l34_right_yolox_truth_island_freeze(root: Path) -> L34RightYoloxTruthIsland: + resolved = _directory(root) + manifest = _read_json(resolved / L34_MANIFEST_NAME) + report = _read_json(resolved / L34_REPORT_NAME) + predictions = tuple(_read_jsonl(resolved / L34_PREDICTIONS_NAME)) + identity = _object(manifest.get("identity"), "L34 identity") + identity_sha256 = manifest.get("identity_sha256") + if ( + manifest.get("schema_version") != L34_RESULT_SCHEMA + or not _RESULT_ID.fullmatch(resolved.name) + or manifest.get("result_id") != resolved.name + or not isinstance(identity_sha256, str) + or resolved.name != f"l34-right-yolox-truth-island-freeze-{identity_sha256}" + or hashlib.sha256(_canonical_json(identity)).hexdigest() + != identity_sha256 + or manifest.get("ground_truth") is not False + or manifest.get("authority") != _AUTHORITY + or identity.get("authority") != _AUTHORITY + ): + raise L34RightYoloxTruthIslandError("L34 manifest identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 2: + raise L34RightYoloxTruthIslandError("L34 artifacts are invalid") + for item in artifacts: + artifact = _object(item, "L34 artifact") + path = resolved / str(artifact.get("path")) + if ( + path.parent != resolved + or not path.is_file() + or path.is_symlink() + or path.stat().st_size != artifact.get("byte_length") + or _sha256(path) != artifact.get("sha256") + ): + raise L34RightYoloxTruthIslandError("L34 artifact changed") + if ( + report.get("schema_version") != L34_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("identity_sha256") != identity_sha256 + or report.get("status") + != "predictions-frozen-awaiting-independent-truth" + or report.get("camera_source_id") != "sensor.camera.right" + or _object(report.get("decision"), "L34 decision").get( + "truth_labels_read" + ) + is not False + ): + raise L34RightYoloxTruthIslandError("L34 result contract is invalid") + sequences: list[int] = [] + for row in predictions: + sequence = _integer( + row.get("truth_island_sequence"), + "prediction truth island sequence", + ) + source_image_sha256 = row.get("source_image_sha256") + if ( + row.get("schema_version") != L34_PREDICTION_SCHEMA + or row.get("candidate_id") != _CANDIDATE_ID + or row.get("truth_joined") is not False + or not isinstance(source_image_sha256, str) + or not _SHA256.fullmatch(source_image_sha256) + or not isinstance(row.get("session_seconds"), (int, float)) + or isinstance(row.get("session_seconds"), bool) + or not math.isfinite(float(row["session_seconds"])) + or not isinstance(row.get("predictions"), list) + ): + raise L34RightYoloxTruthIslandError( + "L34 prediction identity is invalid" + ) + sequences.append(sequence) + if len(set(sequences)) != len(sequences): + raise L34RightYoloxTruthIslandError( + "L34 prediction sequence is duplicated" + ) + metrics = _object(report.get("metrics"), "L34 metrics") + if ( + metrics.get("frame_count") != len(predictions) + or metrics.get("accuracy_metrics_available") is not False + ): + raise L34RightYoloxTruthIslandError("L34 metrics are invalid") + return L34RightYoloxTruthIsland( + result_id=resolved.name, + result_root=resolved, + manifest=manifest, + report=report, + predictions=predictions, + ) + + +def _read_profile(path: Path) -> dict[str, Any]: + profile = _read_json(path.expanduser().resolve(strict=True)) + candidate = _object(profile.get("candidate"), "benchmark candidate") + if ( + profile.get("schema_version") != L34_PROFILE_SCHEMA + or profile.get("profile_id") != "RAVNOVES00_RIGHT_YOLOX_TRUTH_ISLAND_V1" + or profile.get("pipeline_id") + != "kb4-core3-yolox-eomt-k1-lidar-e23-temporal/v1" + or profile.get("mode") != "recorded-replay-only" + or profile.get("source_session_id") != "20260720T065719Z_viewer_live" + or profile.get("camera_source_id") != "sensor.camera.right" + or profile.get("target_classes") != list(_TARGET_CLASSES) + or candidate.get("architecture") != "YOLOX-S" + or candidate.get("candidate_id") != _CANDIDATE_ID + or not isinstance(candidate.get("model_sha256"), str) + or candidate.get("minimum_score") != 0.25 + or profile.get("authority") != _AUTHORITY + ): + raise L34RightYoloxTruthIslandError("L34 profile is invalid") + return profile + + +def _selected_detector_frames( + path: Path, + target_indices: set[int], +) -> dict[int, dict[str, Any]]: + selected: dict[int, dict[str, Any]] = {} + for row in _read_jsonl(path): + frame_index = _integer(row.get("frame_index"), "detector frame index") + if frame_index not in target_indices: + continue + if ( + row.get("schema_version") != _DETECTOR_FRAME_SCHEMA + or frame_index in selected + or not isinstance(row.get("detections"), list) + ): + raise L34RightYoloxTruthIslandError("detector frame is invalid") + selected[frame_index] = row + if set(selected) != target_indices: + raise L34RightYoloxTruthIslandError("detector frame coverage is incomplete") + return selected + + +def freeze_l34_candidate_predictions( + *, + references: tuple[dict[str, Any], ...], + detector_frames: dict[int, dict[str, Any]], + minimum_score: float, +) -> tuple[dict[str, Any], ...]: + """Create the deterministic prediction freeze for an exact blind island.""" + + if not 0.0 < minimum_score < 1.0: + raise L34RightYoloxTruthIslandError("minimum score is invalid") + target_indices = { + _integer(reference.get("frame_index"), "frame index") + for reference in references + } + if set(detector_frames) != target_indices: + raise L34RightYoloxTruthIslandError("detector frame coverage is incomplete") + return tuple( + _prediction_row( + reference=reference, + frame=detector_frames[ + _integer(reference.get("frame_index"), "frame index") + ], + minimum_score=minimum_score, + ) + for reference in references + ) + + +def _prediction_row( + *, + reference: dict[str, Any], + frame: dict[str, Any], + minimum_score: float, +) -> dict[str, Any]: + predictions: list[dict[str, Any]] = [] + for raw in frame["detections"]: + detection = _object(raw, "detector prediction") + label = str(detection.get("label")) + score = _number(detection.get("score"), "detector score") + if label not in _DETECTOR_LABELS or score < minimum_score: + continue + bbox = detection.get("bbox_xyxy") + if ( + not isinstance(bbox, list) + or len(bbox) != 4 + or any(not isinstance(value, (int, float)) for value in bbox) + ): + raise L34RightYoloxTruthIslandError("detector box is invalid") + predictions.append( + { + "label": "heavy_vehicle" if label in {"truck", "bus"} else label, + "score": score, + "bbox_xyxy": [float(value) for value in bbox], + } + ) + predictions.sort( + key=lambda item: (-float(item["score"]), str(item["label"])) + ) + return { + "schema_version": L34_PREDICTION_SCHEMA, + "candidate_id": _CANDIDATE_ID, + "truth_island_sequence": _integer( + reference.get("truth_island_sequence"), + "truth island sequence", + ), + "image_id": _integer(reference.get("image_id"), "image id"), + "frame_index": _integer(reference.get("frame_index"), "frame index"), + "session_seconds": _number( + reference.get("session_seconds"), + "session seconds", + ), + "source_image_sha256": _sha256_text( + reference.get("sha256"), + "source image sha256", + ), + "group_id": str(reference.get("group_id")), + "predictions": predictions, + "truth_joined": False, + } + + +def _directory(path: Path) -> Path: + candidate = path.expanduser().absolute() + if candidate.is_symlink(): + raise L34RightYoloxTruthIslandError("source directory is invalid") + try: + resolved = candidate.resolve(strict=True) + except OSError as reason: + raise L34RightYoloxTruthIslandError("source directory is unavailable") from reason + if not resolved.is_dir(): + raise L34RightYoloxTruthIslandError("source directory is invalid") + return resolved + + +def _artifact(path: Path, kind: str) -> dict[str, object]: + return { + "kind": kind, + "path": path.name, + "byte_length": path.stat().st_size, + "sha256": _sha256(path), + } + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + + +def _write_json(path: Path, value: object) -> None: + path.write_text( + json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2) + "\n", + encoding="utf-8", + ) + + +def _read_json(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as reason: + raise L34RightYoloxTruthIslandError(f"invalid JSON: {path.name}") from reason + return _object(value, path.name) + + +def _read_jsonl(path: Path) -> list[dict[str, Any]]: + rows: list[dict[str, Any]] = [] + try: + with path.open("r", encoding="utf-8") as stream: + for line in stream: + if line.strip(): + rows.append(_object(json.loads(line), path.name)) + except (OSError, json.JSONDecodeError) as reason: + raise L34RightYoloxTruthIslandError(f"invalid JSONL: {path.name}") from reason + return rows + + +def _object(value: object, label: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise L34RightYoloxTruthIslandError(f"{label} must be an object") + return value + + +def _integer(value: object, label: str) -> int: + if not isinstance(value, int) or isinstance(value, bool) or value < 0: + raise L34RightYoloxTruthIslandError(f"{label} must be an integer") + return value + + +def _number(value: object, label: str) -> float: + if ( + not isinstance(value, (int, float)) + or isinstance(value, bool) + or not math.isfinite(float(value)) + ): + raise L34RightYoloxTruthIslandError(f"{label} must be numeric") + return float(value) + + +def _sha256_text(value: object, label: str) -> str: + if not isinstance(value, str) or not _SHA256.fullmatch(value): + raise L34RightYoloxTruthIslandError(f"{label} must be a SHA-256 digest") + return value + + +def _utc_now() -> str: + return datetime.now(UTC).isoformat(timespec="milliseconds").replace( + "+00:00", + "Z", + ) diff --git a/src/k1link/compute/l34a_assisted_yolox_error_audit.py b/src/k1link/compute/l34a_assisted_yolox_error_audit.py new file mode 100644 index 0000000..a29a8b0 --- /dev/null +++ b/src/k1link/compute/l34a_assisted_yolox_error_audit.py @@ -0,0 +1,781 @@ +"""Build a deterministic error audit against one assisted L3.4 review. + +The result is deliberately not ground truth. It compares the immutable L3.4 +candidate freeze with a complete, candidate-seeded annotation session so that +engineering failure modes can be inspected without opening the independent +E48/L3.5 acceptance gate. +""" + +from __future__ import annotations + +import hashlib +import json +import math +import os +import re +import shutil +import uuid +from collections import defaultdict +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from .l34_right_yolox_truth_island_freeze import ( + L34_MANIFEST_NAME, + L34RightYoloxTruthIslandError, + read_l34_right_yolox_truth_island_freeze, +) + +L34A_RESULT_SCHEMA: Final = "missioncore.l34a-assisted-yolox-error-audit/v1" +L34A_REPORT_SCHEMA: Final = "missioncore.l34a-assisted-yolox-error-report/v1" +L34A_CASE_SCHEMA: Final = "missioncore.l34a-assisted-yolox-error-case/v1" +L34A_MANIFEST_NAME: Final = "manifest.json" +L34A_REPORT_NAME: Final = "assisted-error-report.json" +L34A_CASES_NAME: Final = "assisted-error-cases.jsonl" + +_ANNOTATION_SCHEMA: Final = "missioncore.l34-annotation-session/v3" +_RESULT_ID = re.compile(r"^l34a-assisted-yolox-error-audit-[a-f0-9]{64}$") +_SESSION_ID = re.compile(r"^l34-annotation-session-[a-f0-9]{64}$") +_IOU_THRESHOLD: Final = 0.5 +_AUTHORITY: Final = { + "ground_truth": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class L34AAssistedYoloxErrorAuditError(RuntimeError): + """The assisted audit input or immutable result is invalid.""" + + +def build_l34a_assisted_yolox_error_audit( + *, + l34_freeze_root: Path, + annotation_session_path: Path, + output_root: Path, +) -> dict[str, Any]: + """Compare the exact L3.4 freeze with a complete assisted review.""" + + try: + freeze = read_l34_right_yolox_truth_island_freeze(l34_freeze_root) + except L34RightYoloxTruthIslandError as reason: + raise L34AAssistedYoloxErrorAuditError( + "L3.4 candidate freeze is invalid" + ) from reason + session_path = annotation_session_path.expanduser().resolve(strict=True) + if not session_path.is_file() or session_path.is_symlink(): + raise L34AAssistedYoloxErrorAuditError( + "assisted annotation session is unavailable" + ) + session = _read_json(session_path) + _validate_session(session, freeze_result_id=freeze.result_id) + + predictions_by_sequence = { + _integer(row.get("truth_island_sequence"), "prediction sequence"): row + for row in freeze.predictions + } + frames = session["frames"] + if set(predictions_by_sequence) != { + _integer(frame.get("truth_island_sequence"), "annotation sequence") + for frame in frames + }: + raise L34AAssistedYoloxErrorAuditError( + "candidate and annotation coverage differ" + ) + + cases = tuple( + _audit_case( + prediction_row=predictions_by_sequence[ + _integer(frame.get("truth_island_sequence"), "annotation sequence") + ], + annotation_frame=frame, + ) + for frame in sorted( + frames, + key=lambda value: _integer( + value.get("truth_island_sequence"), + "annotation sequence", + ), + ) + ) + aggregate = _aggregate(cases) + per_class = _per_class(cases) + report_basis = { + "schema_version": L34A_REPORT_SCHEMA, + "status": "completed-assisted-candidate-error-audit-not-truth", + "profile": { + "profile_id": "l34a-assisted-yolox-error-audit/v1", + "matcher": "greedy-maximum-iou", + "iou_threshold": _IOU_THRESHOLD, + "duplicate_rule": "same-class-iou-0.30-or-overlap-over-smaller-0.70", + "class_policy": "spatial-match-first-then-class-verdict", + "mismatch_accounting": "one-false-positive-plus-one-false-negative", + "score_visibility": "candidate-scores-used-for-display-and-ordering", + }, + "metrics": { + **aggregate, + "per_class": per_class, + }, + "case_order": [ + case["truth_island_sequence"] + for case in sorted( + cases, + key=lambda item: ( + -int(item["summary"]["severity_score"]), + int(item["truth_island_sequence"]), + ), + ) + ], + "decision": { + "assisted_alignment_available": True, + "blind_accuracy_available": False, + "postprocessing_issue_confirmed": aggregate["duplicate_false_positive"] > 0, + "ontology_gap_confirmed": aggregate["custom_reference_count"] > 0, + "candidate_accepted": False, + "model_retraining_authorized": False, + "l35_blind_gate_open": False, + "next_action": ( + "use the visual FP/FN/mismatch audit to scope NMS, class mapping " + "and detector-data work without claiming independent accuracy" + ), + }, + "limitations": [ + "the review was seeded from the same frozen candidate and is not independent truth", + "precision, recall and F1 are assisted diagnostic alignment metrics, not acceptance metrics", + "custom labels are retained as proposed ontology terms and were not adjudicated", + "the result is source-scoped to 32 RAVNOVES00 right-camera frames", + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + l34_identity = _object(freeze.manifest.get("identity"), "L3.4 identity") + method = { + "schema_version": "missioncore.laboratory-method/v1", + "completeness": "complete", + "execution_class": "deterministic", + "pipeline_id": "ravnoves00-right-yolox-assisted-error-audit/v1", + "components": [ + { + "kind": "source", + "name": freeze.result_id, + "version": "L3.4 immutable YOLOX candidate freeze", + "role": "prediction substrate", + "identity_sha256": _sha256( + freeze.result_root / L34_MANIFEST_NAME + ), + }, + { + "kind": "source", + "name": session["session_id"], + "version": "candidate-seeded assisted review; not truth", + "role": "engineering reference annotations", + "identity_sha256": _sha256(session_path), + }, + { + "kind": "algorithm", + "name": "greedy-maximum-iou-diagnostic-matcher", + "version": f"v1-iou-{_IOU_THRESHOLD:.2f}", + "role": "TP, FP, FN, duplicate and class-mismatch accounting", + "identity_sha256": _sha256(Path(__file__).resolve(strict=True)), + }, + ], + } + identity = { + "schema_version": L34A_RESULT_SCHEMA, + "l34_freeze": { + "result_id": freeze.result_id, + "manifest_sha256": _sha256(freeze.result_root / L34_MANIFEST_NAME), + "prediction_rows_sha256": _object( + l34_identity.get("candidate"), + "L3.4 candidate identity", + ).get("prediction_rows_sha256"), + }, + "assisted_annotation": { + "session_id": session["session_id"], + "session_sha256": _sha256(session_path), + "revision": session["revision"], + "updated_at_utc": session["updated_at_utc"], + "independent_truth_eligible": False, + }, + "method": method, + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"l34a-assisted-yolox-error-audit-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_l34a_assisted_yolox_error_audit(destination) + + created_at_utc = _utc_now() + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc, + "source_session_id": "RAVNOVES00", + "camera_source_id": "sensor.camera.right", + "method": method, + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / L34A_REPORT_NAME, report) + _write_jsonl(staging / L34A_CASES_NAME, cases) + manifest = { + "schema_version": L34A_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "accepted-assisted-diagnostic-not-truth", + "ground_truth": False, + "artifacts": [ + _artifact(staging / L34A_REPORT_NAME, "assisted-error-report"), + _artifact(staging / L34A_CASES_NAME, "assisted-error-cases"), + ], + "authority": _AUTHORITY, + } + _write_json(staging / L34A_MANIFEST_NAME, manifest) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_l34a_assisted_yolox_error_audit(destination) + + +def read_l34a_assisted_yolox_error_audit(root: Path) -> dict[str, Any]: + """Read and fully revalidate one immutable assisted audit.""" + + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / L34A_MANIFEST_NAME) + identity = _object(manifest.get("identity"), "L3.4A identity") + identity_sha256 = manifest.get("identity_sha256") + if ( + manifest.get("schema_version") != L34A_RESULT_SCHEMA + or not isinstance(identity_sha256, str) + or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256 + or manifest.get("result_id") + != f"l34a-assisted-yolox-error-audit-{identity_sha256}" + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("acceptance_state") + != "accepted-assisted-diagnostic-not-truth" + or manifest.get("ground_truth") is not False + or manifest.get("authority") != _AUTHORITY + ): + raise L34AAssistedYoloxErrorAuditError("L3.4A identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 2: + raise L34AAssistedYoloxErrorAuditError("L3.4A artifacts are invalid") + artifact_by_role = { + _text(item.get("role"), "artifact role"): _object(item, "artifact") + for item in artifacts + if isinstance(item, dict) + } + report_path = _validated_artifact( + resolved, + artifact_by_role.get("assisted-error-report"), + ) + cases_path = _validated_artifact( + resolved, + artifact_by_role.get("assisted-error-cases"), + ) + report = _read_json(report_path) + cases = tuple(_read_jsonl(cases_path)) + if ( + report.get("schema_version") != L34A_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("identity_sha256") != identity_sha256 + or report.get("status") + != "completed-assisted-candidate-error-audit-not-truth" + or report.get("ground_truth") is not False + or report.get("authority") != _AUTHORITY + or len(cases) != 32 + or any(case.get("schema_version") != L34A_CASE_SCHEMA for case in cases) + or hashlib.sha256(_canonical_json(cases)).hexdigest() + != identity.get("cases_sha256") + ): + raise L34AAssistedYoloxErrorAuditError("L3.4A result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "cases": cases, + } + + +def _validate_session(session: dict[str, Any], *, freeze_result_id: str) -> None: + frames = session.get("frames") + progress = session.get("progress") + if ( + session.get("schema_version") != _ANNOTATION_SCHEMA + or not isinstance(session.get("session_id"), str) + or _SESSION_ID.fullmatch(session["session_id"]) is None + or session.get("result_id") != freeze_result_id + or session.get("state") != "saved" + or not isinstance(session.get("revision"), int) + or session["revision"] < 1 + or _object(session.get("assistance"), "assistance").get( + "independent_truth_eligible" + ) + is not False + or _object(session.get("authority"), "authority") != _AUTHORITY + or not isinstance(frames, list) + or len(frames) != 32 + or (progress is not None and not isinstance(progress, dict)) + or not isinstance(session.get("updated_at_utc"), str) + ): + raise L34AAssistedYoloxErrorAuditError( + "assisted annotation session contract is invalid" + ) + sequences: set[int] = set() + for frame in frames: + item = _object(frame, "annotation frame") + sequence = _integer(item.get("truth_island_sequence"), "annotation sequence") + objects = item.get("objects") + if ( + not 1 <= sequence <= 32 + or sequence in sequences + or item.get("reviewed") is not True + or not isinstance(objects, list) + ): + raise L34AAssistedYoloxErrorAuditError( + "assisted annotation coverage is incomplete" + ) + sequences.add(sequence) + for value in objects: + obj = _object(value, "annotation object") + category = obj.get("category") + proposed = obj.get("proposed_label") + if ( + not isinstance(obj.get("object_id"), str) + or not isinstance(category, str) + or not _valid_box(obj.get("box_xyxy")) + or obj.get("origin") not in {"manual", "frozen_candidate_seed"} + or (category == "unmapped" and not isinstance(proposed, str)) + or (category != "unmapped" and proposed is not None) + ): + raise L34AAssistedYoloxErrorAuditError( + "assisted annotation object is invalid" + ) + + +def _audit_case( + *, + prediction_row: dict[str, Any], + annotation_frame: dict[str, Any], +) -> dict[str, Any]: + sequence = _integer( + prediction_row.get("truth_island_sequence"), + "prediction sequence", + ) + if ( + annotation_frame.get("truth_island_sequence") != sequence + or annotation_frame.get("image_id") != prediction_row.get("image_id") + or annotation_frame.get("frame_index") != prediction_row.get("frame_index") + or annotation_frame.get("source_sha256") + != prediction_row.get("source_image_sha256") + ): + raise L34AAssistedYoloxErrorAuditError("case source identity differs") + raw_predictions = prediction_row.get("predictions") + raw_annotations = annotation_frame.get("objects") + if not isinstance(raw_predictions, list) or not isinstance(raw_annotations, list): + raise L34AAssistedYoloxErrorAuditError("case objects are unavailable") + + predictions = [ + { + "prediction_index": index, + "category": _text(item.get("label"), "prediction category"), + "score": _finite(item.get("score"), "prediction score"), + "box_xyxy": _box(item.get("bbox_xyxy"), "prediction box"), + } + for index, item in enumerate( + (_object(value, "prediction") for value in raw_predictions), + start=1, + ) + ] + annotations = [ + { + "object_id": _text(item.get("object_id"), "annotation object id"), + "category": _text(item.get("category"), "annotation category"), + "proposed_label": item.get("proposed_label"), + "display_category": _display_reference_category(item), + "origin": _text(item.get("origin"), "annotation origin"), + "box_xyxy": _box(item.get("box_xyxy"), "annotation box"), + "occluded": item.get("occluded") is True, + "truncated": item.get("truncated") is True, + } + for item in (_object(value, "annotation") for value in raw_annotations) + ] + candidates = sorted( + ( + (_iou(prediction["box_xyxy"], annotation["box_xyxy"]), p_index, a_index) + for p_index, prediction in enumerate(predictions) + for a_index, annotation in enumerate(annotations) + ), + reverse=True, + ) + matched_predictions: set[int] = set() + matched_annotations: set[int] = set() + matches: list[dict[str, Any]] = [] + for iou, prediction_index, annotation_index in candidates: + if ( + iou < _IOU_THRESHOLD + or prediction_index in matched_predictions + or annotation_index in matched_annotations + ): + continue + matched_predictions.add(prediction_index) + matched_annotations.add(annotation_index) + prediction = predictions[prediction_index] + annotation = annotations[annotation_index] + verdict = ( + "true_positive" + if prediction["category"] == annotation["category"] + else "class_mismatch" + ) + prediction.update( + verdict=verdict, + matched_object_id=annotation["object_id"], + match_iou=iou, + ) + annotation.update( + verdict=verdict, + matched_prediction_index=prediction["prediction_index"], + match_iou=iou, + ) + matches.append( + { + "prediction_index": prediction["prediction_index"], + "object_id": annotation["object_id"], + "iou": iou, + "verdict": verdict, + } + ) + for index, prediction in enumerate(predictions): + if index in matched_predictions: + continue + duplicate = any( + prediction["category"] == annotation["category"] + and ( + _iou(prediction["box_xyxy"], annotation["box_xyxy"]) >= 0.3 + or _overlap_over_smaller( + prediction["box_xyxy"], + annotation["box_xyxy"], + ) + >= 0.7 + ) + for annotation in annotations + ) + prediction.update( + verdict="duplicate_false_positive" if duplicate else "false_positive", + matched_object_id=None, + match_iou=None, + ) + for index, annotation in enumerate(annotations): + if index in matched_annotations: + continue + annotation.update( + verdict="false_negative", + matched_prediction_index=None, + match_iou=None, + ) + + true_positive = sum( + prediction["verdict"] == "true_positive" for prediction in predictions + ) + class_mismatch = sum( + prediction["verdict"] == "class_mismatch" for prediction in predictions + ) + duplicate_false_positive = sum( + prediction["verdict"] == "duplicate_false_positive" + for prediction in predictions + ) + unmatched_false_positive = sum( + prediction["verdict"] == "false_positive" for prediction in predictions + ) + unmatched_false_negative = sum( + annotation["verdict"] == "false_negative" for annotation in annotations + ) + false_positive = unmatched_false_positive + duplicate_false_positive + class_mismatch + false_negative = unmatched_false_negative + class_mismatch + summary = { + "prediction_count": len(predictions), + "reference_count": len(annotations), + "true_positive": true_positive, + "false_positive": false_positive, + "false_negative": false_negative, + "class_mismatch": class_mismatch, + "duplicate_false_positive": duplicate_false_positive, + "unmatched_false_positive": unmatched_false_positive, + "unmatched_false_negative": unmatched_false_negative, + "severity_score": ( + class_mismatch * 3 + + duplicate_false_positive * 2 + + unmatched_false_positive + + unmatched_false_negative * 2 + ), + } + return { + "schema_version": L34A_CASE_SCHEMA, + "truth_island_sequence": sequence, + "image_id": prediction_row["image_id"], + "frame_index": prediction_row["frame_index"], + "group_id": prediction_row["group_id"], + "session_seconds": prediction_row["session_seconds"], + "source_image_sha256": prediction_row["source_image_sha256"], + "camera": {"width": 800, "height": 600}, + "predictions": predictions, + "annotations": annotations, + "matches": matches, + "summary": summary, + } + + +def _aggregate(cases: tuple[dict[str, Any], ...]) -> dict[str, Any]: + totals: defaultdict[str, int] = defaultdict(int) + for case in cases: + for key, value in _object(case.get("summary"), "case summary").items(): + if key != "severity_score": + totals[key] += _integer(value, f"case summary {key}") + true_positive = totals["true_positive"] + false_positive = totals["false_positive"] + false_negative = totals["false_negative"] + precision = _ratio(true_positive, true_positive + false_positive) + recall = _ratio(true_positive, true_positive + false_negative) + f1 = _ratio(2 * precision * recall, precision + recall) + custom_reference_count = sum( + annotation["category"] == "unmapped" + for case in cases + for annotation in case["annotations"] + ) + error_case_count = sum( + case["summary"]["false_positive"] > 0 + or case["summary"]["false_negative"] > 0 + for case in cases + ) + return { + "frame_count": len(cases), + **dict(totals), + "precision_iou50": precision, + "recall_iou50": recall, + "f1_iou50": f1, + "error_case_count": error_case_count, + "custom_reference_count": custom_reference_count, + } + + +def _per_class(cases: tuple[dict[str, Any], ...]) -> dict[str, dict[str, Any]]: + counts: defaultdict[str, defaultdict[str, int]] = defaultdict( + lambda: defaultdict(int) + ) + for case in cases: + for prediction in case["predictions"]: + category = prediction["category"] + verdict = prediction["verdict"] + if verdict == "true_positive": + counts[category]["true_positive"] += 1 + elif verdict in { + "false_positive", + "duplicate_false_positive", + "class_mismatch", + }: + counts[category]["false_positive"] += 1 + for annotation in case["annotations"]: + category = annotation["display_category"] + verdict = annotation["verdict"] + counts[category]["reference_count"] += 1 + if verdict in {"false_negative", "class_mismatch"}: + counts[category]["false_negative"] += 1 + projected: dict[str, dict[str, Any]] = {} + for category, values in sorted(counts.items()): + true_positive = values["true_positive"] + false_positive = values["false_positive"] + false_negative = values["false_negative"] + precision = _ratio(true_positive, true_positive + false_positive) + recall = _ratio(true_positive, true_positive + false_negative) + projected[category] = { + "reference_count": values["reference_count"], + "true_positive": true_positive, + "false_positive": false_positive, + "false_negative": false_negative, + "precision_iou50": precision, + "recall_iou50": recall, + } + return projected + + +def _display_reference_category(item: dict[str, Any]) -> str: + if item.get("category") == "unmapped": + return f"unmapped:{_text(item.get('proposed_label'), 'proposed label')}" + return _text(item.get("category"), "annotation category") + + +def _iou(left: list[float], right: list[float]) -> float: + intersection_width = max(0.0, min(left[2], right[2]) - max(left[0], right[0])) + intersection_height = max(0.0, min(left[3], right[3]) - max(left[1], right[1])) + intersection = intersection_width * intersection_height + left_area = (left[2] - left[0]) * (left[3] - left[1]) + right_area = (right[2] - right[0]) * (right[3] - right[1]) + union = left_area + right_area - intersection + return intersection / union if union > 0 else 0.0 + + +def _overlap_over_smaller(left: list[float], right: list[float]) -> float: + intersection_width = max(0.0, min(left[2], right[2]) - max(left[0], right[0])) + intersection_height = max(0.0, min(left[3], right[3]) - max(left[1], right[1])) + intersection = intersection_width * intersection_height + smaller = min( + (left[2] - left[0]) * (left[3] - left[1]), + (right[2] - right[0]) * (right[3] - right[1]), + ) + return intersection / smaller if smaller > 0 else 0.0 + + +def _ratio(numerator: float, denominator: float) -> float: + return numerator / denominator if denominator > 0 else 0.0 + + +def _valid_box(value: object) -> bool: + try: + box = _box(value, "box") + except L34AAssistedYoloxErrorAuditError: + return False + return 0 <= box[0] < box[2] <= 800 and 0 <= box[1] < box[3] <= 600 + + +def _box(value: object, label: str) -> list[float]: + if not isinstance(value, list) or len(value) != 4: + raise L34AAssistedYoloxErrorAuditError(f"{label} is invalid") + box = [_finite(item, label) for item in value] + if not (0 <= box[0] < box[2] <= 800 and 0 <= box[1] < box[3] <= 600): + raise L34AAssistedYoloxErrorAuditError(f"{label} is invalid") + return box + + +def _finite(value: object, label: str) -> float: + if ( + not isinstance(value, (int, float)) + or isinstance(value, bool) + or not math.isfinite(float(value)) + ): + raise L34AAssistedYoloxErrorAuditError(f"{label} is invalid") + return float(value) + + +def _integer(value: object, label: str) -> int: + if not isinstance(value, int) or isinstance(value, bool) or value < 0: + raise L34AAssistedYoloxErrorAuditError(f"{label} is invalid") + return value + + +def _text(value: object, label: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise L34AAssistedYoloxErrorAuditError(f"{label} is invalid") + return value + + +def _object(value: object, label: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise L34AAssistedYoloxErrorAuditError(f"{label} is invalid") + return value + + +def _validated_artifact(root: Path, artifact: dict[str, Any] | None) -> Path: + if artifact is None: + raise L34AAssistedYoloxErrorAuditError("L3.4A artifact is missing") + path = (root / _text(artifact.get("path"), "artifact path")).resolve() + if ( + path.parent != root + or path.is_symlink() + or not path.is_file() + or path.stat().st_size != artifact.get("byte_length") + or _sha256(path) != artifact.get("sha256") + ): + raise L34AAssistedYoloxErrorAuditError("L3.4A artifact changed") + return path + + +def _read_json(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError) as reason: + raise L34AAssistedYoloxErrorAuditError( + f"cannot read {path.name}" + ) from reason + return _object(value, path.name) + + +def _read_jsonl(path: Path) -> list[dict[str, Any]]: + try: + rows = [ + json.loads(line) + for line in path.read_text(encoding="utf-8").splitlines() + if line + ] + except (OSError, ValueError) as reason: + raise L34AAssistedYoloxErrorAuditError( + f"cannot read {path.name}" + ) from reason + if any(not isinstance(row, dict) for row in rows): + raise L34AAssistedYoloxErrorAuditError(f"{path.name} is invalid") + return rows + + +def _write_json(path: Path, value: object) -> None: + path.write_text( + json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +def _write_jsonl(path: Path, rows: tuple[dict[str, Any], ...]) -> None: + path.write_text( + "".join( + json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n" + for row in rows + ), + encoding="utf-8", + ) + + +def _artifact(path: Path, role: str) -> dict[str, object]: + return { + "role": role, + "path": path.name, + "media_type": ( + "application/x-ndjson" if path.suffix == ".jsonl" else "application/json" + ), + "byte_length": path.stat().st_size, + "sha256": _sha256(path), + } + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + + +def _utc_now() -> str: + return datetime.now(tz=UTC).isoformat(timespec="milliseconds").replace( + "+00:00", + "Z", + ) diff --git a/src/k1link/compute/l34b_nested_box_consolidation_shadow.py b/src/k1link/compute/l34b_nested_box_consolidation_shadow.py new file mode 100644 index 0000000..8085194 --- /dev/null +++ b/src/k1link/compute/l34b_nested_box_consolidation_shadow.py @@ -0,0 +1,623 @@ +"""Build an immutable L3.4B nested-box consolidation shadow. + +The shadow applies one deliberately narrow post-processing rule to the exact +L3.4 freeze: predictions of the same normalized category are consolidated only +when at least 95 percent of the smaller box is covered by the other box. It is +an assisted engineering diagnostic, never independent truth or an acceptance +result. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +import math +import os +import re +import shutil +import uuid +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from .l34_right_yolox_truth_island_freeze import ( + L34_MANIFEST_NAME, + L34RightYoloxTruthIslandError, + read_l34_right_yolox_truth_island_freeze, +) +from .l34a_assisted_yolox_error_audit import ( + L34A_MANIFEST_NAME, + L34AAssistedYoloxErrorAuditError, + _aggregate, + _audit_case, + _overlap_over_smaller, + _validate_session, + read_l34a_assisted_yolox_error_audit, +) + +L34B_RESULT_SCHEMA: Final = "missioncore.l34b-nested-box-consolidation-shadow/v1" +L34B_REPORT_SCHEMA: Final = "missioncore.l34b-nested-box-consolidation-report/v1" +L34B_CASE_SCHEMA: Final = "missioncore.l34b-nested-box-consolidation-case/v1" +L34B_MANIFEST_NAME: Final = "manifest.json" +L34B_REPORT_NAME: Final = "nested-box-consolidation-report.json" +L34B_CASES_NAME: Final = "nested-box-consolidation-cases.jsonl" +L34B_OVERLAP_THRESHOLD: Final = 0.95 + +_RESULT_ID = re.compile(r"^l34b-nested-box-consolidation-shadow-[a-f0-9]{64}$") +_AUTHORITY: Final = { + "ground_truth": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class L34BNestedBoxConsolidationError(RuntimeError): + """An L3.4B input or immutable result is invalid.""" + + +def consolidate_l34b_prediction_row( + prediction_row: dict[str, Any], +) -> tuple[dict[str, Any], tuple[dict[str, Any], ...]]: + """Consolidate connected same-category nested boxes deterministically.""" + + raw_predictions = prediction_row.get("predictions") + if not isinstance(raw_predictions, list): + raise L34BNestedBoxConsolidationError("L3.4 predictions are unavailable") + predictions = [_prediction(value, index) for index, value in enumerate(raw_predictions)] + parents = list(range(len(predictions))) + + def find(index: int) -> int: + while parents[index] != index: + parents[index] = parents[parents[index]] + index = parents[index] + return index + + def union(left: int, right: int) -> None: + left_root = find(left) + right_root = find(right) + if left_root == right_root: + return + lower, upper = sorted((left_root, right_root)) + parents[upper] = lower + + for left_index, left in enumerate(predictions): + for right_index in range(left_index + 1, len(predictions)): + right = predictions[right_index] + if ( + left["label"] == right["label"] + and _overlap_over_smaller( + left["bbox_xyxy"], + right["bbox_xyxy"], + ) + >= L34B_OVERLAP_THRESHOLD + ): + union(left_index, right_index) + + grouped: dict[int, list[int]] = {} + for index in range(len(predictions)): + grouped.setdefault(find(index), []).append(index) + + output_predictions: list[dict[str, Any]] = [] + consolidations: list[dict[str, Any]] = [] + for component in sorted(grouped.values(), key=min): + members = [predictions[index] for index in component] + source_indices = [index + 1 for index in component] + if len(component) == 1: + output = copy.deepcopy(members[0]) + else: + boxes = [member["bbox_xyxy"] for member in members] + output = { + "label": members[0]["label"], + "score": max(member["score"] for member in members), + "bbox_xyxy": [ + min(box[0] for box in boxes), + min(box[1] for box in boxes), + max(box[2] for box in boxes), + max(box[3] for box in boxes), + ], + } + consolidations.append( + { + "category": output["label"], + "source_prediction_indices": source_indices, + "source_scores": [member["score"] for member in members], + "source_boxes_xyxy": copy.deepcopy(boxes), + "merged_score": output["score"], + "merged_box_xyxy": copy.deepcopy(output["bbox_xyxy"]), + "minimum_overlap_over_smaller": min( + _overlap_over_smaller( + members[left]["bbox_xyxy"], + members[right]["bbox_xyxy"], + ) + for left in range(len(members)) + for right in range(left + 1, len(members)) + ), + "output_prediction_index": len(output_predictions) + 1, + } + ) + output["source_prediction_indices"] = source_indices + output_predictions.append(output) + + projected = copy.deepcopy(prediction_row) + projected["predictions"] = output_predictions + return projected, tuple(consolidations) + + +def evaluate_l34b_shadow( + *, + prediction_rows: tuple[dict[str, Any], ...], + annotation_frames: tuple[dict[str, Any], ...], + before_cases: tuple[dict[str, Any], ...], +) -> tuple[tuple[dict[str, Any], ...], dict[str, Any]]: + """Apply the rule and return source-bound before/after cases and metrics.""" + + annotations_by_sequence = { + _integer(frame.get("truth_island_sequence"), "annotation sequence"): frame + for frame in annotation_frames + } + before_by_sequence = { + _integer(case.get("truth_island_sequence"), "before sequence"): case + for case in before_cases + } + if ( + len(prediction_rows) != 32 + or len(annotations_by_sequence) != 32 + or len(before_by_sequence) != 32 + ): + raise L34BNestedBoxConsolidationError("L3.4B requires 32 bound cases") + + cases: list[dict[str, Any]] = [] + after_audits: list[dict[str, Any]] = [] + for row in prediction_rows: + sequence = _integer(row.get("truth_island_sequence"), "prediction sequence") + projected, consolidations = consolidate_l34b_prediction_row(row) + after = _audit_case( + prediction_row=projected, + annotation_frame=annotations_by_sequence[sequence], + ) + for prediction, source in zip( + after["predictions"], + projected["predictions"], + strict=True, + ): + prediction["source_prediction_indices"] = copy.deepcopy( + source["source_prediction_indices"] + ) + before = before_by_sequence[sequence] + if ( + before.get("source_image_sha256") != after.get("source_image_sha256") + or before.get("frame_index") != after.get("frame_index") + or before.get("image_id") != after.get("image_id") + ): + raise L34BNestedBoxConsolidationError("L3.4B case binding changed") + cases.append( + { + "schema_version": L34B_CASE_SCHEMA, + "truth_island_sequence": sequence, + "image_id": after["image_id"], + "frame_index": after["frame_index"], + "group_id": after["group_id"], + "session_seconds": after["session_seconds"], + "source_image_sha256": after["source_image_sha256"], + "camera": copy.deepcopy(after["camera"]), + "before_predictions": copy.deepcopy(before["predictions"]), + "after_predictions": copy.deepcopy(after["predictions"]), + "annotations": copy.deepcopy(after["annotations"]), + "consolidations": list(consolidations), + "before_summary": copy.deepcopy(before["summary"]), + "after_summary": copy.deepcopy(after["summary"]), + } + ) + after_audits.append(after) + + before_metrics = _aggregate(tuple(before_cases)) + after_metrics = _aggregate(tuple(after_audits)) + delta = { + key: after_metrics[key] - before_metrics[key] + for key in ( + "prediction_count", + "true_positive", + "false_positive", + "false_negative", + "class_mismatch", + "duplicate_false_positive", + "unmatched_false_positive", + "unmatched_false_negative", + "precision_iou50", + "recall_iou50", + "f1_iou50", + "error_case_count", + ) + } + consolidation_count = sum(len(case["consolidations"]) for case in cases) + affected_case_count = sum(bool(case["consolidations"]) for case in cases) + regression_free = ( + consolidation_count > 0 + and delta["true_positive"] >= 0 + and delta["false_positive"] < 0 + and delta["false_negative"] <= 0 + and delta["class_mismatch"] <= 0 + ) + metrics = { + "before": before_metrics, + "after": after_metrics, + "delta": delta, + "consolidation_count": consolidation_count, + "affected_case_count": affected_case_count, + "assisted_regression_free": regression_free, + } + return tuple(cases), metrics + + +def build_l34b_nested_box_consolidation_shadow( + *, + l34_freeze_root: Path, + l34a_audit_root: Path, + annotation_session_path: Path, + output_root: Path, +) -> dict[str, Any]: + """Build and publish one immutable L3.4B result.""" + + try: + freeze = read_l34_right_yolox_truth_island_freeze(l34_freeze_root) + l34a = read_l34a_assisted_yolox_error_audit(l34a_audit_root) + except (L34RightYoloxTruthIslandError, L34AAssistedYoloxErrorAuditError) as reason: + raise L34BNestedBoxConsolidationError("L3.4/L3.4A input is invalid") from reason + session_path = annotation_session_path.expanduser().resolve(strict=True) + if not session_path.is_file() or session_path.is_symlink(): + raise L34BNestedBoxConsolidationError("annotation session is unavailable") + session = _read_json(session_path) + try: + _validate_session(session, freeze_result_id=freeze.result_id) + except L34AAssistedYoloxErrorAuditError as reason: + raise L34BNestedBoxConsolidationError("annotation session is invalid") from reason + l34a_identity = _object(l34a["manifest"].get("identity"), "L3.4A identity") + l34a_freeze = _object(l34a_identity.get("l34_freeze"), "L3.4A freeze") + l34a_annotation = _object( + l34a_identity.get("assisted_annotation"), + "L3.4A annotation", + ) + if ( + l34a_freeze.get("result_id") != freeze.result_id + or l34a_annotation.get("session_id") != session.get("session_id") + or l34a_annotation.get("session_sha256") != _sha256(session_path) + ): + raise L34BNestedBoxConsolidationError("L3.4B lineage differs") + + cases, metrics = evaluate_l34b_shadow( + prediction_rows=freeze.predictions, + annotation_frames=tuple(session["frames"]), + before_cases=l34a["cases"], + ) + affected_sequences = [ + case["truth_island_sequence"] for case in cases if case["consolidations"] + ] + case_order = affected_sequences + [ + case["truth_island_sequence"] + for case in sorted( + (item for item in cases if not item["consolidations"]), + key=lambda item: ( + -int(item["after_summary"]["severity_score"]), + int(item["truth_island_sequence"]), + ), + ) + ] + report_basis = { + "schema_version": L34B_REPORT_SCHEMA, + "status": "completed-nested-box-consolidation-shadow-not-truth", + "profile": { + "profile_id": "l34b-nested-box-consolidation-shadow/v1", + "category_policy": "same-normalized-category-only", + "overlap_metric": "intersection-over-smaller-box-area", + "overlap_threshold": L34B_OVERLAP_THRESHOLD, + "geometry_policy": "union-box", + "score_policy": "maximum-source-score", + "scope": "frozen-prediction-postprocessing-only", + }, + "metrics": metrics, + "case_order": case_order, + "decision": { + "shadow_policy_accepted": metrics["assisted_regression_free"], + "assisted_alignment_available": True, + "blind_accuracy_available": False, + "candidate_accepted": False, + "model_retraining_authorized": False, + "l35_blind_gate_open": False, + "classic_iou_nms_fix_rejected": True, + "remaining_l34a_duplicate_signals": metrics["after"][ + "duplicate_false_positive" + ], + "next_action": ( + "preserve rectification_tile provenance and evaluate temporal " + "left/front seam stitching; do not lower global IoU NMS" + ), + }, + "limitations": [ + ( + "the comparison uses the same candidate-seeded assisted review " + "as L3.4A and is not independent truth" + ), + ( + "the accepted shadow rule changes one nested same-category pair " + "on this 32-frame source scope" + ), + ( + "four left/front tile seam splits remain and cannot be safely " + "solved by lowering global IoU NMS" + ), + "the L3.4 freeze does not retain rectification_tile on each prediction", + "no live transport, hardware, LiDAR range, navigation or safety claim is made", + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + method = { + "schema_version": "missioncore.laboratory-method/v1", + "completeness": "complete", + "execution_class": "deterministic", + "pipeline_id": "ravnoves00-right-yolox-nested-box-consolidation-shadow/v1", + "components": [ + { + "kind": "source", + "name": freeze.result_id, + "version": "L3.4 immutable candidate freeze", + "role": "prediction substrate", + "identity_sha256": _sha256(freeze.result_root / L34_MANIFEST_NAME), + }, + { + "kind": "source", + "name": l34a["result_id"], + "version": "L3.4A assisted diagnostic; not truth", + "role": "before-state and engineering comparison", + "identity_sha256": _sha256(l34a["result_root"] / L34A_MANIFEST_NAME), + }, + { + "kind": "algorithm", + "name": "same-category-nested-box-union", + "version": f"v1-overlap-{L34B_OVERLAP_THRESHOLD:.2f}", + "role": "bounded post-normalization consolidation", + "identity_sha256": _sha256(Path(__file__).resolve(strict=True)), + }, + ], + } + identity = { + "schema_version": L34B_RESULT_SCHEMA, + "l34_freeze": { + "result_id": freeze.result_id, + "manifest_sha256": _sha256(freeze.result_root / L34_MANIFEST_NAME), + }, + "l34a_audit": { + "result_id": l34a["result_id"], + "manifest_sha256": _sha256(l34a["result_root"] / L34A_MANIFEST_NAME), + }, + "assisted_annotation": { + "session_id": session["session_id"], + "session_sha256": _sha256(session_path), + "independent_truth_eligible": False, + }, + "method": method, + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"l34b-nested-box-consolidation-shadow-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_l34b_nested_box_consolidation_shadow(destination) + + created_at_utc = _utc_now() + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc, + "source_session_id": "RAVNOVES00", + "camera_source_id": "sensor.camera.right", + "method": method, + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / L34B_REPORT_NAME, report) + _write_jsonl(staging / L34B_CASES_NAME, cases) + manifest = { + "schema_version": L34B_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "accepted-assisted-shadow-not-truth", + "ground_truth": False, + "artifacts": [ + _artifact(staging / L34B_REPORT_NAME, "nested-box-report"), + _artifact(staging / L34B_CASES_NAME, "nested-box-cases"), + ], + "authority": _AUTHORITY, + } + _write_json(staging / L34B_MANIFEST_NAME, manifest) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_l34b_nested_box_consolidation_shadow(destination) + + +def read_l34b_nested_box_consolidation_shadow(root: Path) -> dict[str, Any]: + """Read and fully revalidate one immutable L3.4B result.""" + + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / L34B_MANIFEST_NAME) + identity = _object(manifest.get("identity"), "L3.4B identity") + identity_sha256 = manifest.get("identity_sha256") + if ( + manifest.get("schema_version") != L34B_RESULT_SCHEMA + or not isinstance(identity_sha256, str) + or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256 + or manifest.get("result_id") != f"l34b-nested-box-consolidation-shadow-{identity_sha256}" + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("acceptance_state") != "accepted-assisted-shadow-not-truth" + or manifest.get("ground_truth") is not False + or manifest.get("authority") != _AUTHORITY + ): + raise L34BNestedBoxConsolidationError("L3.4B identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 2: + raise L34BNestedBoxConsolidationError("L3.4B artifacts are invalid") + artifact_by_role = { + _text(item.get("role"), "artifact role"): _object(item, "artifact") + for item in artifacts + if isinstance(item, dict) + } + report_path = _validated_artifact(resolved, artifact_by_role.get("nested-box-report")) + cases_path = _validated_artifact(resolved, artifact_by_role.get("nested-box-cases")) + report = _read_json(report_path) + cases = tuple(_read_jsonl(cases_path)) + if ( + report.get("schema_version") != L34B_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("identity_sha256") != identity_sha256 + or report.get("status") != "completed-nested-box-consolidation-shadow-not-truth" + or report.get("ground_truth") is not False + or report.get("authority") != _AUTHORITY + or len(cases) != 32 + or any(case.get("schema_version") != L34B_CASE_SCHEMA for case in cases) + or hashlib.sha256(_canonical_json(cases)).hexdigest() != identity.get("cases_sha256") + ): + raise L34BNestedBoxConsolidationError("L3.4B result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "cases": cases, + } + + +def _prediction(value: object, index: int) -> dict[str, Any]: + item = _object(value, f"prediction {index + 1}") + return { + "label": _text(item.get("label"), "prediction label"), + "score": _finite(item.get("score"), "prediction score"), + "bbox_xyxy": _box(item.get("bbox_xyxy"), "prediction box"), + } + + +def _box(value: object, label: str) -> list[float]: + if not isinstance(value, list) or len(value) != 4: + raise L34BNestedBoxConsolidationError(f"{label} is invalid") + box = [_finite(item, label) for item in value] + if not (0 <= box[0] < box[2] <= 800 and 0 <= box[1] < box[3] <= 600): + raise L34BNestedBoxConsolidationError(f"{label} is invalid") + return box + + +def _finite(value: object, label: str) -> float: + if ( + not isinstance(value, (int, float)) + or isinstance(value, bool) + or not math.isfinite(float(value)) + ): + raise L34BNestedBoxConsolidationError(f"{label} is invalid") + return float(value) + + +def _integer(value: object, label: str) -> int: + if not isinstance(value, int) or isinstance(value, bool) or value < 0: + raise L34BNestedBoxConsolidationError(f"{label} is invalid") + return value + + +def _text(value: object, label: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise L34BNestedBoxConsolidationError(f"{label} is invalid") + return value + + +def _object(value: object, label: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise L34BNestedBoxConsolidationError(f"{label} is invalid") + return value + + +def _validated_artifact(root: Path, artifact: dict[str, Any] | None) -> Path: + if artifact is None: + raise L34BNestedBoxConsolidationError("L3.4B artifact is missing") + path = (root / _text(artifact.get("path"), "artifact path")).resolve() + if ( + path.parent != root + or path.is_symlink() + or not path.is_file() + or path.stat().st_size != artifact.get("byte_length") + or _sha256(path) != artifact.get("sha256") + ): + raise L34BNestedBoxConsolidationError("L3.4B artifact changed") + return path + + +def _read_json(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError) as reason: + raise L34BNestedBoxConsolidationError(f"cannot read {path.name}") from reason + return _object(value, path.name) + + +def _read_jsonl(path: Path) -> list[dict[str, Any]]: + try: + rows = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line] + except (OSError, ValueError) as reason: + raise L34BNestedBoxConsolidationError(f"cannot read {path.name}") from reason + if any(not isinstance(row, dict) for row in rows): + raise L34BNestedBoxConsolidationError(f"{path.name} is invalid") + return rows + + +def _write_json(path: Path, value: object) -> None: + path.write_text( + json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +def _write_jsonl(path: Path, rows: tuple[dict[str, Any], ...]) -> None: + path.write_text( + "".join(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n" for row in rows), + encoding="utf-8", + ) + + +def _artifact(path: Path, role: str) -> dict[str, object]: + return { + "role": role, + "path": path.name, + "media_type": "application/x-ndjson" if path.suffix == ".jsonl" else "application/json", + "byte_length": path.stat().st_size, + "sha256": _sha256(path), + } + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + + +def _utc_now() -> str: + return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") diff --git a/src/k1link/compute/l34c_tile_seam_stitch_shadow.py b/src/k1link/compute/l34c_tile_seam_stitch_shadow.py new file mode 100644 index 0000000..b8efa4a --- /dev/null +++ b/src/k1link/compute/l34c_tile_seam_stitch_shadow.py @@ -0,0 +1,1048 @@ +"""Build an immutable L3.4C temporal rectification-seam stitch shadow. + +The L3.4 freeze intentionally omitted detector-internal provenance. This +bounded derivative restores every frozen prediction by an exact join to the +immutable qualification frame and then tests one fail-closed rule: a large +same-class ``front``/``left`` split may be unioned only when the same seam +geometry persists for at least three consecutive source frames. Assisted +labels are used only after the deterministic shadow has been produced. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +import math +import os +import re +import shutil +import uuid +from collections import defaultdict +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from .l34_right_yolox_truth_island_freeze import ( + L34_MANIFEST_NAME, + L34RightYoloxTruthIslandError, + read_l34_right_yolox_truth_island_freeze, +) +from .l34a_assisted_yolox_error_audit import ( + L34A_MANIFEST_NAME, + L34AAssistedYoloxErrorAuditError, + _aggregate, + _audit_case, + _validate_session, + read_l34a_assisted_yolox_error_audit, +) + +L34C_RESULT_SCHEMA: Final = "missioncore.l34c-tile-seam-stitch-shadow/v1" +L34C_REPORT_SCHEMA: Final = "missioncore.l34c-tile-seam-stitch-report/v1" +L34C_PROVENANCE_SCHEMA: Final = "missioncore.l34c-tile-provenance-prediction/v1" +L34C_CASE_SCHEMA: Final = "missioncore.l34c-tile-seam-stitch-case/v1" +L34C_MANIFEST_NAME: Final = "manifest.json" +L34C_REPORT_NAME: Final = "tile-seam-stitch-report.json" +L34C_PROVENANCE_NAME: Final = "tile-provenance-predictions.jsonl" +L34C_CASES_NAME: Final = "tile-seam-stitch-cases.jsonl" + +L34C_HORIZONTAL_OVERLAP_THRESHOLD: Final = 0.40 +L34C_VERTICAL_OVERLAP_THRESHOLD: Final = 0.95 +L34C_MINIMUM_UNION_AREA_FRACTION: Final = 0.04 +L34C_TEMPORAL_UNION_IOU_THRESHOLD: Final = 0.80 +L34C_MINIMUM_CONSECUTIVE_FRAMES: Final = 3 + +_DETECTOR_FRAME_SCHEMA: Final = "missioncore.rectified-yolox-frame/v1" +_QUALIFICATION_SCHEMA: Final = "missioncore.rectified-yolox-qualification/v1" +_ADJACENT_TILES: Final = frozenset(("front", "left")) +_RESULT_ID = re.compile(r"^l34c-tile-seam-stitch-shadow-[a-f0-9]{64}$") +_AUTHORITY: Final = { + "ground_truth": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class L34CTileSeamStitchError(RuntimeError): + """An L3.4C input or immutable output is invalid.""" + + +def bind_l34c_prediction_provenance( + *, + prediction_rows: tuple[dict[str, Any], ...], + detector_frames: dict[int, dict[str, Any]], +) -> tuple[dict[str, Any], ...]: + """Restore detector provenance through an exact, one-to-one join.""" + + expected_frames = { + _integer(row.get("frame_index"), "prediction frame") + for row in prediction_rows + } + if set(detector_frames) != expected_frames: + raise L34CTileSeamStitchError("detector frame coverage differs from L3.4") + enriched_rows: list[dict[str, Any]] = [] + for row in prediction_rows: + frame_index = _integer(row.get("frame_index"), "prediction frame") + frame = _object(detector_frames[frame_index], "detector frame") + detections = frame.get("detections") + if ( + frame.get("schema_version") != _DETECTOR_FRAME_SCHEMA + or frame.get("frame_index") != frame_index + or not isinstance(detections, list) + ): + raise L34CTileSeamStitchError("detector frame contract is invalid") + raw_predictions = row.get("predictions") + if not isinstance(raw_predictions, list): + raise L34CTileSeamStitchError("L3.4 predictions are unavailable") + enriched_predictions: list[dict[str, Any]] = [] + for index, raw_prediction in enumerate(raw_predictions, start=1): + prediction = _prediction(raw_prediction, index) + matches = [ + detection + for raw_detection in detections + if isinstance(raw_detection, dict) + and _detection_matches(prediction, raw_detection) + for detection in [_detector_prediction(raw_detection)] + ] + if len(matches) != 1: + raise L34CTileSeamStitchError( + "L3.4 prediction does not have one exact detector provenance row" + ) + detection = matches[0] + enriched_predictions.append( + { + **prediction, + "source_prediction_index": index, + "raw_label": detection["raw_label"], + "class_id": detection["class_id"], + "raw_center_xy": detection["raw_center_xy"], + "rectification_tile": detection["rectification_tile"], + "valid_fov_fraction": detection["valid_fov_fraction"], + } + ) + enriched_rows.append( + { + **copy.deepcopy(row), + "schema_version": L34C_PROVENANCE_SCHEMA, + "source_prediction_schema_version": row.get("schema_version"), + "predictions": enriched_predictions, + "provenance_join": "exact-label-score-bbox", + } + ) + return tuple(enriched_rows) + + +def find_l34c_temporal_stitches( + provenance_rows: tuple[dict[str, Any], ...], +) -> tuple[dict[int, tuple[dict[str, Any], ...]], int]: + """Return temporally admitted seam pairs and the static candidate count.""" + + by_key: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list) + static_candidate_count = 0 + for row in provenance_rows: + candidates = _static_seam_candidates(row) + static_candidate_count += len(candidates) + by_frame: dict[tuple[str, str, int], list[dict[str, Any]]] = defaultdict(list) + for candidate in candidates: + by_frame[ + ( + candidate["group_id"], + candidate["category"], + candidate["frame_index"], + ) + ].append(candidate) + for (group_id, category, _), frame_candidates in by_frame.items(): + # Ambiguous seam ownership is rejected rather than guessed. + if len(frame_candidates) == 1: + by_key[(group_id, category)].append(frame_candidates[0]) + + admitted: dict[int, list[dict[str, Any]]] = defaultdict(list) + for candidates in by_key.values(): + ordered = sorted( + candidates, + key=lambda item: ( + int(item["frame_index"]), + tuple(item["source_prediction_indices"]), + ), + ) + runs: list[list[dict[str, Any]]] = [] + current: list[dict[str, Any]] = [] + for candidate in ordered: + if not current or _continues_temporal_run(current[-1], candidate): + current.append(candidate) + else: + runs.append(current) + current = [candidate] + if current: + runs.append(current) + for run in runs: + if len(run) < L34C_MINIMUM_CONSECUTIVE_FRAMES: + continue + run_id = ( + f"{run[0]['group_id']}:{run[0]['category']}:" + f"{run[0]['frame_index']}-{run[-1]['frame_index']}" + ) + for candidate in run: + projected = copy.deepcopy(candidate) + projected["temporal_run_id"] = run_id + projected["temporal_run_length"] = len(run) + admitted[int(candidate["truth_island_sequence"])].append(projected) + return ( + { + sequence: tuple(sorted(items, key=lambda item: item["source_prediction_indices"])) + for sequence, items in admitted.items() + }, + static_candidate_count, + ) + + +def apply_l34c_stitches( + prediction_row: dict[str, Any], + stitches: tuple[dict[str, Any], ...], +) -> dict[str, Any]: + """Apply non-overlapping admitted stitches while preserving source indices.""" + + raw_predictions = prediction_row.get("predictions") + if not isinstance(raw_predictions, list): + raise L34CTileSeamStitchError("provenance predictions are unavailable") + predictions = [ + _enriched_prediction(item, index) + for index, item in enumerate(raw_predictions, 1) + ] + by_first: dict[int, dict[str, Any]] = {} + consumed: set[int] = set() + for stitch in stitches: + source_indices = stitch.get("source_prediction_indices") + if ( + not isinstance(source_indices, list) + or len(source_indices) != 2 + or any(not isinstance(value, int) for value in source_indices) + ): + raise L34CTileSeamStitchError("seam stitch source indices are invalid") + left, right = sorted(source_indices) + if left in consumed or right in consumed or left < 1 or right > len(predictions): + raise L34CTileSeamStitchError("seam stitches overlap or escape the frame") + consumed.update((left, right)) + by_first[left] = stitch + + output: list[dict[str, Any]] = [] + for index, prediction in enumerate(predictions, 1): + if index in consumed and index not in by_first: + continue + stitch = by_first.get(index) + if stitch is None: + projected = copy.deepcopy(prediction) + projected["source_prediction_indices"] = [index] + projected["source_rectification_tiles"] = [prediction["rectification_tile"]] + output.append(projected) + continue + source_indices = sorted(stitch["source_prediction_indices"]) + members = [predictions[source_index - 1] for source_index in source_indices] + output.append( + { + "label": members[0]["label"], + "score": max(member["score"] for member in members), + "bbox_xyxy": copy.deepcopy(stitch["merged_box_xyxy"]), + "source_prediction_indices": source_indices, + "source_rectification_tiles": copy.deepcopy(stitch["source_tiles"]), + "temporal_run_id": stitch["temporal_run_id"], + "temporal_run_length": stitch["temporal_run_length"], + } + ) + projected_row = copy.deepcopy(prediction_row) + projected_row["predictions"] = output + return projected_row + + +def evaluate_l34c_shadow( + *, + provenance_rows: tuple[dict[str, Any], ...], + annotation_frames: tuple[dict[str, Any], ...], + before_cases: tuple[dict[str, Any], ...], +) -> tuple[tuple[dict[str, Any], ...], dict[str, Any]]: + """Evaluate the deterministic seam shadow against the assisted review.""" + + annotations_by_sequence = { + _integer(frame.get("truth_island_sequence"), "annotation sequence"): frame + for frame in annotation_frames + } + before_by_sequence = { + _integer(case.get("truth_island_sequence"), "before sequence"): case + for case in before_cases + } + if ( + len(provenance_rows) != 32 + or len(annotations_by_sequence) != 32 + or len(before_by_sequence) != 32 + ): + raise L34CTileSeamStitchError("L3.4C requires 32 source-bound cases") + stitches_by_sequence, static_candidate_count = find_l34c_temporal_stitches( + provenance_rows + ) + cases: list[dict[str, Any]] = [] + after_audits: list[dict[str, Any]] = [] + for row in provenance_rows: + sequence = _integer(row.get("truth_island_sequence"), "prediction sequence") + stitches = stitches_by_sequence.get(sequence, ()) + projected = apply_l34c_stitches(row, stitches) + after = _audit_case( + prediction_row=projected, + annotation_frame=annotations_by_sequence[sequence], + ) + for prediction, source in zip(after["predictions"], projected["predictions"], strict=True): + prediction["source_prediction_indices"] = copy.deepcopy( + source["source_prediction_indices"] + ) + prediction["source_rectification_tiles"] = copy.deepcopy( + source["source_rectification_tiles"] + ) + provenance_by_index = { + prediction["source_prediction_index"]: prediction + for prediction in row["predictions"] + } + before = copy.deepcopy(before_by_sequence[sequence]) + for prediction in before["predictions"]: + provenance = provenance_by_index.get(prediction["prediction_index"]) + if provenance is None: + raise L34CTileSeamStitchError("before prediction lost provenance") + prediction["rectification_tile"] = provenance["rectification_tile"] + prediction["raw_label"] = provenance["raw_label"] + prediction["class_id"] = provenance["class_id"] + prediction["raw_center_xy"] = copy.deepcopy(provenance["raw_center_xy"]) + if ( + before.get("source_image_sha256") != after.get("source_image_sha256") + or before.get("frame_index") != after.get("frame_index") + or before.get("image_id") != after.get("image_id") + ): + raise L34CTileSeamStitchError("L3.4C case binding changed") + cases.append( + { + "schema_version": L34C_CASE_SCHEMA, + "truth_island_sequence": sequence, + "image_id": after["image_id"], + "frame_index": after["frame_index"], + "group_id": after["group_id"], + "session_seconds": after["session_seconds"], + "source_image_sha256": after["source_image_sha256"], + "camera": copy.deepcopy(after["camera"]), + "before_predictions": copy.deepcopy(before["predictions"]), + "after_predictions": copy.deepcopy(after["predictions"]), + "annotations": copy.deepcopy(after["annotations"]), + "stitches": list(stitches), + "before_summary": copy.deepcopy(before["summary"]), + "after_summary": copy.deepcopy(after["summary"]), + } + ) + after_audits.append(after) + + before_metrics = _aggregate(tuple(before_cases)) + after_metrics = _aggregate(tuple(after_audits)) + delta = { + key: after_metrics[key] - before_metrics[key] + for key in ( + "prediction_count", + "true_positive", + "false_positive", + "false_negative", + "class_mismatch", + "duplicate_false_positive", + "unmatched_false_positive", + "unmatched_false_negative", + "precision_iou50", + "recall_iou50", + "f1_iou50", + "error_case_count", + ) + } + stitch_count = sum(len(case["stitches"]) for case in cases) + temporal_run_count = len( + { + stitch["temporal_run_id"] + for case in cases + for stitch in case["stitches"] + } + ) + regression_free = ( + stitch_count > 0 + and delta["true_positive"] >= 0 + and delta["false_positive"] < 0 + and delta["false_negative"] <= 0 + and delta["class_mismatch"] <= 0 + ) + return tuple(cases), { + "before": before_metrics, + "after": after_metrics, + "delta": delta, + "static_candidate_count": static_candidate_count, + "temporally_rejected_candidate_count": static_candidate_count - stitch_count, + "stitch_count": stitch_count, + "affected_case_count": sum(bool(case["stitches"]) for case in cases), + "temporal_run_count": temporal_run_count, + "assisted_regression_free": regression_free, + } + + +def build_l34c_tile_seam_stitch_shadow( + *, + l34_freeze_root: Path, + l34a_audit_root: Path, + annotation_session_path: Path, + detector_qualification_root: Path, + output_root: Path, +) -> dict[str, Any]: + """Build and publish one immutable L3.4C result.""" + + try: + freeze = read_l34_right_yolox_truth_island_freeze(l34_freeze_root) + l34a = read_l34a_assisted_yolox_error_audit(l34a_audit_root) + except (L34RightYoloxTruthIslandError, L34AAssistedYoloxErrorAuditError) as reason: + raise L34CTileSeamStitchError("L3.4/L3.4A input is invalid") from reason + session_path = annotation_session_path.expanduser().resolve(strict=True) + if not session_path.is_file() or session_path.is_symlink(): + raise L34CTileSeamStitchError("annotation session is unavailable") + session = _read_json(session_path) + try: + _validate_session(session, freeze_result_id=freeze.result_id) + except L34AAssistedYoloxErrorAuditError as reason: + raise L34CTileSeamStitchError("annotation session is invalid") from reason + l34a_identity = _object(l34a["manifest"].get("identity"), "L3.4A identity") + if ( + _object(l34a_identity.get("l34_freeze"), "L3.4A freeze").get("result_id") + != freeze.result_id + or _object( + l34a_identity.get("assisted_annotation"), + "L3.4A annotation", + ).get("session_sha256") + != _sha256(session_path) + ): + raise L34CTileSeamStitchError("L3.4C lineage differs") + + qualification_root = detector_qualification_root.expanduser().resolve(strict=True) + if not qualification_root.is_dir() or qualification_root.is_symlink(): + raise L34CTileSeamStitchError("qualification root is invalid") + qualification_path = qualification_root / "qualification.json" + frames_path = qualification_root / "frames.jsonl" + qualification = _read_json(qualification_path) + freeze_candidate = _object( + _object(freeze.manifest.get("identity"), "L3.4 identity").get("candidate"), + "L3.4 candidate", + ) + if ( + qualification.get("schema_version") != _QUALIFICATION_SCHEMA + or qualification.get("state") != "accepted" + or _object(qualification.get("metrics"), "qualification metrics").get( + "frames_processed" + ) + != 4489 + or freeze_candidate.get("qualification_sha256") != _sha256(qualification_path) + or freeze_candidate.get("detector_frames_sha256") != _sha256(frames_path) + ): + raise L34CTileSeamStitchError("qualification identity differs from L3.4") + target_frames = {int(row["frame_index"]) for row in freeze.predictions} + detector_frames = _selected_detector_frames(frames_path, target_frames) + provenance_rows = bind_l34c_prediction_provenance( + prediction_rows=freeze.predictions, + detector_frames=detector_frames, + ) + cases, metrics = evaluate_l34c_shadow( + provenance_rows=provenance_rows, + annotation_frames=tuple(session["frames"]), + before_cases=l34a["cases"], + ) + affected_sequences = [ + case["truth_island_sequence"] for case in cases if case["stitches"] + ] + case_order = affected_sequences + [ + case["truth_island_sequence"] + for case in sorted( + (item for item in cases if not item["stitches"]), + key=lambda item: ( + -int(item["after_summary"]["severity_score"]), + int(item["truth_island_sequence"]), + ), + ) + ] + report_basis = { + "schema_version": L34C_REPORT_SCHEMA, + "status": "completed-temporal-tile-seam-stitch-shadow-not-truth", + "profile": { + "profile_id": "l34c-temporal-front-left-seam-stitch-shadow/v1", + "category_policy": "same-normalized-category-only", + "tile_policy": "front-left-only", + "horizontal_overlap_over_smaller_threshold": L34C_HORIZONTAL_OVERLAP_THRESHOLD, + "vertical_overlap_over_smaller_threshold": L34C_VERTICAL_OVERLAP_THRESHOLD, + "minimum_union_area_fraction": L34C_MINIMUM_UNION_AREA_FRACTION, + "temporal_union_iou_threshold": L34C_TEMPORAL_UNION_IOU_THRESHOLD, + "minimum_consecutive_frames": L34C_MINIMUM_CONSECUTIVE_FRAMES, + "geometry_policy": "union-box", + "score_policy": "maximum-source-score", + "scope": "recorded-right-camera-frozen-prediction-shadow-only", + }, + "metrics": metrics, + "case_order": case_order, + "decision": { + "shadow_policy_accepted": metrics["assisted_regression_free"], + "prediction_provenance_preserved": True, + "assisted_alignment_available": True, + "blind_accuracy_available": False, + "candidate_accepted": False, + "model_retraining_authorized": False, + "l35_blind_gate_open": False, + "global_nms_unchanged": True, + "remaining_l34a_duplicate_signals": metrics["after"][ + "duplicate_false_positive" + ], + "next_action": ( + "compose the independently accepted nested-box and temporal seam " + "shadows, then freeze one cumulative candidate before any blind gate" + ), + }, + "limitations": [ + "the comparison uses candidate-seeded assisted review and is not independent truth", + "only one four-frame front/left seam run is admitted on this source scope", + "single-frame and ambiguous seam candidates are rejected by construction", + "the rule is not generalized to front/right or other routes", + "no live transport, hardware, LiDAR range, navigation or safety claim is made", + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + method = { + "schema_version": "missioncore.laboratory-method/v1", + "completeness": "complete", + "execution_class": "deterministic", + "pipeline_id": "ravnoves00-right-yolox-temporal-tile-seam-shadow/v1", + "components": [ + { + "kind": "source", + "name": freeze.result_id, + "version": "L3.4 immutable candidate freeze", + "role": "normalized prediction substrate", + "identity_sha256": _sha256(freeze.result_root / L34_MANIFEST_NAME), + }, + { + "kind": "source", + "name": "rectified-yolox-qualification-full", + "version": "4489-frame accepted qualification", + "role": "exact per-prediction tile provenance", + "identity_sha256": _sha256(qualification_path), + }, + { + "kind": "source", + "name": l34a["result_id"], + "version": "L3.4A assisted diagnostic; not truth", + "role": "after-the-fact engineering comparison", + "identity_sha256": _sha256(l34a["result_root"] / L34A_MANIFEST_NAME), + }, + { + "kind": "algorithm", + "name": "temporal-front-left-seam-union", + "version": "v1", + "role": "fail-closed rectification seam stitch shadow", + "identity_sha256": _sha256(Path(__file__).resolve(strict=True)), + }, + ], + } + identity = { + "schema_version": L34C_RESULT_SCHEMA, + "l34_freeze": { + "result_id": freeze.result_id, + "manifest_sha256": _sha256(freeze.result_root / L34_MANIFEST_NAME), + }, + "l34a_audit": { + "result_id": l34a["result_id"], + "manifest_sha256": _sha256(l34a["result_root"] / L34A_MANIFEST_NAME), + }, + "assisted_annotation": { + "session_id": session["session_id"], + "session_sha256": _sha256(session_path), + "independent_truth_eligible": False, + }, + "detector_qualification": { + "qualification_sha256": _sha256(qualification_path), + "frames_sha256": _sha256(frames_path), + }, + "method": method, + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "provenance_sha256": hashlib.sha256( + _canonical_json(provenance_rows) + ).hexdigest(), + "cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"l34c-tile-seam-stitch-shadow-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_l34c_tile_seam_stitch_shadow(destination) + + created_at_utc = _utc_now() + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc, + "source_session_id": "RAVNOVES00", + "camera_source_id": "sensor.camera.right", + "method": method, + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / L34C_REPORT_NAME, report) + _write_jsonl(staging / L34C_PROVENANCE_NAME, provenance_rows) + _write_jsonl(staging / L34C_CASES_NAME, cases) + manifest = { + "schema_version": L34C_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "accepted-assisted-temporal-seam-shadow-not-truth", + "ground_truth": False, + "artifacts": [ + _artifact(staging / L34C_REPORT_NAME, "tile-seam-report"), + _artifact(staging / L34C_PROVENANCE_NAME, "tile-provenance-predictions"), + _artifact(staging / L34C_CASES_NAME, "tile-seam-cases"), + ], + "authority": _AUTHORITY, + } + _write_json(staging / L34C_MANIFEST_NAME, manifest) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_l34c_tile_seam_stitch_shadow(destination) + + +def read_l34c_tile_seam_stitch_shadow(root: Path) -> dict[str, Any]: + """Read and fully revalidate one immutable L3.4C result.""" + + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / L34C_MANIFEST_NAME) + identity = _object(manifest.get("identity"), "L3.4C identity") + identity_sha256 = manifest.get("identity_sha256") + if ( + manifest.get("schema_version") != L34C_RESULT_SCHEMA + or not isinstance(identity_sha256, str) + or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256 + or manifest.get("result_id") + != f"l34c-tile-seam-stitch-shadow-{identity_sha256}" + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("acceptance_state") + != "accepted-assisted-temporal-seam-shadow-not-truth" + or manifest.get("ground_truth") is not False + or manifest.get("authority") != _AUTHORITY + ): + raise L34CTileSeamStitchError("L3.4C identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 3: + raise L34CTileSeamStitchError("L3.4C artifacts are invalid") + artifact_by_role = { + _text(item.get("role"), "artifact role"): _object(item, "artifact") + for item in artifacts + if isinstance(item, dict) + } + report = _read_json( + _validated_artifact(resolved, artifact_by_role.get("tile-seam-report")) + ) + provenance = tuple( + _read_jsonl( + _validated_artifact( + resolved, + artifact_by_role.get("tile-provenance-predictions"), + ) + ) + ) + cases = tuple( + _read_jsonl( + _validated_artifact(resolved, artifact_by_role.get("tile-seam-cases")) + ) + ) + if ( + report.get("schema_version") != L34C_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("identity_sha256") != identity_sha256 + or report.get("status") + != "completed-temporal-tile-seam-stitch-shadow-not-truth" + or report.get("ground_truth") is not False + or report.get("authority") != _AUTHORITY + or len(provenance) != 32 + or len(cases) != 32 + or any(row.get("schema_version") != L34C_PROVENANCE_SCHEMA for row in provenance) + or any(case.get("schema_version") != L34C_CASE_SCHEMA for case in cases) + or hashlib.sha256(_canonical_json(provenance)).hexdigest() + != identity.get("provenance_sha256") + or hashlib.sha256(_canonical_json(cases)).hexdigest() + != identity.get("cases_sha256") + ): + raise L34CTileSeamStitchError("L3.4C result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "provenance": provenance, + "cases": cases, + } + + +def _static_seam_candidates(row: dict[str, Any]) -> list[dict[str, Any]]: + predictions = [ + _enriched_prediction(item, index) + for index, item in enumerate( + _list(row.get("predictions"), "provenance predictions"), + start=1, + ) + ] + candidates: list[dict[str, Any]] = [] + for left_index, left in enumerate(predictions): + for right_index in range(left_index + 1, len(predictions)): + right = predictions[right_index] + if ( + left["label"] != right["label"] + or frozenset( + (left["rectification_tile"], right["rectification_tile"]) + ) + != _ADJACENT_TILES + ): + continue + horizontal = _axis_overlap_over_smaller( + left["bbox_xyxy"], right["bbox_xyxy"], 0 + ) + vertical = _axis_overlap_over_smaller( + left["bbox_xyxy"], right["bbox_xyxy"], 1 + ) + union = _union_box(left["bbox_xyxy"], right["bbox_xyxy"]) + area_fraction = _area(union) / (800.0 * 600.0) + if ( + horizontal < L34C_HORIZONTAL_OVERLAP_THRESHOLD + or vertical < L34C_VERTICAL_OVERLAP_THRESHOLD + or area_fraction < L34C_MINIMUM_UNION_AREA_FRACTION + ): + continue + ordered_members = sorted( + (left, right), + key=lambda item: item["source_prediction_index"], + ) + candidates.append( + { + "category": left["label"], + "truth_island_sequence": _integer( + row.get("truth_island_sequence"), + "truth island sequence", + ), + "frame_index": _integer(row.get("frame_index"), "frame index"), + "group_id": _text(row.get("group_id"), "group id"), + "session_seconds": _finite( + row.get("session_seconds"), + "session seconds", + ), + "source_prediction_indices": [ + member["source_prediction_index"] for member in ordered_members + ], + "source_tiles": [ + member["rectification_tile"] for member in ordered_members + ], + "source_scores": [member["score"] for member in ordered_members], + "source_boxes_xyxy": [ + copy.deepcopy(member["bbox_xyxy"]) + for member in ordered_members + ], + "merged_score": max(left["score"], right["score"]), + "merged_box_xyxy": union, + "horizontal_overlap_over_smaller": horizontal, + "vertical_overlap_over_smaller": vertical, + "source_iou": _iou(left["bbox_xyxy"], right["bbox_xyxy"]), + "union_area_fraction": area_fraction, + } + ) + return candidates + + +def _continues_temporal_run( + previous: dict[str, Any], + current: dict[str, Any], +) -> bool: + delta_seconds = float(current["session_seconds"]) - float( + previous["session_seconds"] + ) + return ( + int(current["frame_index"]) == int(previous["frame_index"]) + 1 + and 0.0 < delta_seconds <= 0.25 + and _iou(previous["merged_box_xyxy"], current["merged_box_xyxy"]) + >= L34C_TEMPORAL_UNION_IOU_THRESHOLD + ) + + +def _selected_detector_frames( + path: Path, + target_indices: set[int], +) -> dict[int, dict[str, Any]]: + selected: dict[int, dict[str, Any]] = {} + for row in _read_jsonl(path): + frame_index = _integer(row.get("frame_index"), "detector frame") + if frame_index not in target_indices: + continue + if frame_index in selected: + raise L34CTileSeamStitchError("detector frame is duplicated") + selected[frame_index] = row + if set(selected) != target_indices: + raise L34CTileSeamStitchError("detector frame coverage is incomplete") + return selected + + +def _detection_matches( + prediction: dict[str, Any], + raw_detection: dict[str, Any], +) -> bool: + raw_label = raw_detection.get("label") + normalized = "heavy_vehicle" if raw_label in {"truck", "bus"} else raw_label + bbox = raw_detection.get("bbox_xyxy") + score = raw_detection.get("score") + return ( + normalized == prediction["label"] + and isinstance(score, (int, float)) + and not isinstance(score, bool) + and math.isclose(float(score), prediction["score"], abs_tol=1e-12) + and isinstance(bbox, list) + and len(bbox) == 4 + and all( + isinstance(value, (int, float)) + and not isinstance(value, bool) + and math.isclose(float(value), expected, abs_tol=1e-9) + for value, expected in zip(bbox, prediction["bbox_xyxy"], strict=True) + ) + ) + + +def _detector_prediction(value: dict[str, Any]) -> dict[str, Any]: + raw_label = _text(value.get("label"), "detector raw label") + tile = _text(value.get("rectification_tile"), "rectification tile") + if tile not in {"front", "left", "right"}: + raise L34CTileSeamStitchError("rectification tile is outside active views") + class_id = _integer(value.get("class_id"), "detector class id") + center = _point(value.get("raw_center_xy"), "raw center") + valid_fraction = _finite(value.get("valid_fov_fraction"), "valid FOV fraction") + if not 0.0 <= valid_fraction <= 1.0: + raise L34CTileSeamStitchError("valid FOV fraction is invalid") + return { + "raw_label": raw_label, + "class_id": class_id, + "raw_center_xy": center, + "rectification_tile": tile, + "valid_fov_fraction": valid_fraction, + } + + +def _prediction(value: object, index: int) -> dict[str, Any]: + item = _object(value, f"prediction {index}") + return { + "label": _text(item.get("label"), "prediction label"), + "score": _finite(item.get("score"), "prediction score"), + "bbox_xyxy": _box(item.get("bbox_xyxy"), "prediction box"), + } + + +def _enriched_prediction(value: object, index: int) -> dict[str, Any]: + item = _object(value, f"enriched prediction {index}") + prediction = _prediction(item, index) + prediction.update( + { + "source_prediction_index": _integer( + item.get("source_prediction_index"), + "source prediction index", + ), + "rectification_tile": _text( + item.get("rectification_tile"), + "rectification tile", + ), + "raw_label": _text(item.get("raw_label"), "raw label"), + "class_id": _integer(item.get("class_id"), "class id"), + "raw_center_xy": _point(item.get("raw_center_xy"), "raw center"), + "valid_fov_fraction": _finite( + item.get("valid_fov_fraction"), + "valid FOV fraction", + ), + } + ) + return prediction + + +def _axis_overlap_over_smaller( + left: list[float], + right: list[float], + axis: int, +) -> float: + overlap = max(0.0, min(left[axis + 2], right[axis + 2]) - max(left[axis], right[axis])) + denominator = min(left[axis + 2] - left[axis], right[axis + 2] - right[axis]) + return overlap / denominator if denominator > 0.0 else 0.0 + + +def _union_box(left: list[float], right: list[float]) -> list[float]: + return [ + min(left[0], right[0]), + min(left[1], right[1]), + max(left[2], right[2]), + max(left[3], right[3]), + ] + + +def _area(box: list[float]) -> float: + return max(0.0, box[2] - box[0]) * max(0.0, box[3] - box[1]) + + +def _iou(left: list[float], right: list[float]) -> float: + intersection = _area( + [ + max(left[0], right[0]), + max(left[1], right[1]), + min(left[2], right[2]), + min(left[3], right[3]), + ] + ) + union = _area(left) + _area(right) - intersection + return intersection / union if union > 0.0 else 0.0 + + +def _box(value: object, label: str) -> list[float]: + if not isinstance(value, list) or len(value) != 4: + raise L34CTileSeamStitchError(f"{label} is invalid") + box = [_finite(item, label) for item in value] + if not (0 <= box[0] < box[2] <= 800 and 0 <= box[1] < box[3] <= 600): + raise L34CTileSeamStitchError(f"{label} is invalid") + return box + + +def _point(value: object, label: str) -> list[float]: + if not isinstance(value, list) or len(value) != 2: + raise L34CTileSeamStitchError(f"{label} is invalid") + point = [_finite(item, label) for item in value] + if not (0 <= point[0] <= 800 and 0 <= point[1] <= 600): + raise L34CTileSeamStitchError(f"{label} is invalid") + return point + + +def _finite(value: object, label: str) -> float: + if ( + not isinstance(value, (int, float)) + or isinstance(value, bool) + or not math.isfinite(float(value)) + ): + raise L34CTileSeamStitchError(f"{label} is invalid") + return float(value) + + +def _integer(value: object, label: str) -> int: + if not isinstance(value, int) or isinstance(value, bool) or value < 0: + raise L34CTileSeamStitchError(f"{label} is invalid") + return value + + +def _text(value: object, label: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise L34CTileSeamStitchError(f"{label} is invalid") + return value + + +def _object(value: object, label: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise L34CTileSeamStitchError(f"{label} is invalid") + return value + + +def _list(value: object, label: str) -> list[Any]: + if not isinstance(value, list): + raise L34CTileSeamStitchError(f"{label} is invalid") + return value + + +def _validated_artifact(root: Path, artifact: dict[str, Any] | None) -> Path: + if artifact is None: + raise L34CTileSeamStitchError("L3.4C artifact is missing") + path = (root / _text(artifact.get("path"), "artifact path")).resolve() + if ( + path.parent != root + or path.is_symlink() + or not path.is_file() + or path.stat().st_size != artifact.get("byte_length") + or _sha256(path) != artifact.get("sha256") + ): + raise L34CTileSeamStitchError("L3.4C artifact changed") + return path + + +def _read_json(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError) as reason: + raise L34CTileSeamStitchError(f"cannot read {path.name}") from reason + return _object(value, path.name) + + +def _read_jsonl(path: Path) -> list[dict[str, Any]]: + try: + rows = [ + json.loads(line) + for line in path.read_text(encoding="utf-8").splitlines() + if line + ] + except (OSError, ValueError) as reason: + raise L34CTileSeamStitchError(f"cannot read {path.name}") from reason + if any(not isinstance(row, dict) for row in rows): + raise L34CTileSeamStitchError(f"{path.name} is invalid") + return rows + + +def _write_json(path: Path, value: object) -> None: + path.write_text( + json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +def _write_jsonl(path: Path, rows: tuple[dict[str, Any], ...]) -> None: + path.write_text( + "".join( + json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n" + for row in rows + ), + encoding="utf-8", + ) + + +def _artifact(path: Path, role: str) -> dict[str, object]: + return { + "role": role, + "path": path.name, + "media_type": ( + "application/x-ndjson" if path.suffix == ".jsonl" else "application/json" + ), + "byte_length": path.stat().st_size, + "sha256": _sha256(path), + } + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + + +def _utc_now() -> str: + return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") diff --git a/src/k1link/compute/l34d_cumulative_postprocessing_candidate.py b/src/k1link/compute/l34d_cumulative_postprocessing_candidate.py new file mode 100644 index 0000000..ba89b0d --- /dev/null +++ b/src/k1link/compute/l34d_cumulative_postprocessing_candidate.py @@ -0,0 +1,796 @@ +"""Freeze the cumulative L3.4B + L3.4C post-processing candidate. + +The candidate composes only operations already admitted by the immutable +L3.4B and L3.4C shadows. Both operation sets remain expressed against the +original L3.4 prediction indices. Any overlap, lineage drift or operation +payload mismatch fails closed instead of introducing an ordering policy. +Assisted annotations are joined only after the deterministic projection and +remain ineligible as independent truth. +""" + +from __future__ import annotations + +import copy +import hashlib +import os +import re +import shutil +import uuid +from pathlib import Path +from typing import Any, Final + +from .l34a_assisted_yolox_error_audit import ( + L34A_MANIFEST_NAME, + L34AAssistedYoloxErrorAuditError, + _aggregate, + _audit_case, + _validate_session, + read_l34a_assisted_yolox_error_audit, +) +from .l34b_nested_box_consolidation_shadow import ( + L34B_MANIFEST_NAME, + L34BNestedBoxConsolidationError, + read_l34b_nested_box_consolidation_shadow, +) +from .l34c_tile_seam_stitch_shadow import ( + L34C_MANIFEST_NAME, + L34CTileSeamStitchError, + _artifact, + _canonical_json, + _finite, + _integer, + _list, + _object, + _prediction, + _read_json, + _read_jsonl, + _sha256, + _text, + _union_box, + _utc_now, + _validated_artifact, + _write_json, + _write_jsonl, + read_l34c_tile_seam_stitch_shadow, +) + +L34D_RESULT_SCHEMA: Final = ( + "missioncore.l34d-cumulative-postprocessing-candidate/v1" +) +L34D_REPORT_SCHEMA: Final = ( + "missioncore.l34d-cumulative-postprocessing-report/v1" +) +L34D_CASE_SCHEMA: Final = ( + "missioncore.l34d-cumulative-postprocessing-case/v1" +) +L34D_MANIFEST_NAME: Final = "manifest.json" +L34D_REPORT_NAME: Final = "cumulative-postprocessing-report.json" +L34D_CASES_NAME: Final = "cumulative-postprocessing-cases.jsonl" + +_RESULT_ID = re.compile( + r"^l34d-cumulative-postprocessing-candidate-[a-f0-9]{64}$" +) +_COUNT_METRICS: Final = ( + "prediction_count", + "true_positive", + "false_positive", + "false_negative", + "class_mismatch", + "duplicate_false_positive", + "unmatched_false_positive", + "unmatched_false_negative", + "error_case_count", +) +_DELTA_METRICS: Final = ( + *_COUNT_METRICS[:-1], + "precision_iou50", + "recall_iou50", + "f1_iou50", + "error_case_count", +) +_AUTHORITY: Final = { + "ground_truth": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class L34DCumulativeCandidateError(RuntimeError): + """An L3.4D input, composition or immutable output is invalid.""" + + +def compose_l34d_prediction_row( + provenance_row: dict[str, Any], + *, + consolidations: tuple[dict[str, Any], ...], + stitches: tuple[dict[str, Any], ...], +) -> tuple[dict[str, Any], tuple[dict[str, Any], ...]]: + """Compose source-indexed B/C operations with a fail-closed conflict gate.""" + + raw_predictions = _list( + provenance_row.get("predictions"), + "L3.4D provenance predictions", + ) + predictions = [ + _prediction(value, index) + for index, value in enumerate(raw_predictions, start=1) + ] + normalized_operations = [ + _validated_operation( + raw, + predictions=predictions, + provenance_predictions=raw_predictions, + operation_type="nested-box-consolidation", + ) + for raw in consolidations + ] + [ + _validated_operation( + raw, + predictions=predictions, + provenance_predictions=raw_predictions, + operation_type="temporal-tile-seam-stitch", + ) + for raw in stitches + ] + + consumed: set[int] = set() + operation_by_first: dict[int, dict[str, Any]] = {} + for operation in normalized_operations: + source_indices = operation["source_prediction_indices"] + overlap = consumed.intersection(source_indices) + if overlap: + raise L34DCumulativeCandidateError( + "L3.4B/L3.4C operation sets overlap" + ) + consumed.update(source_indices) + operation_by_first[min(source_indices)] = operation + + output: list[dict[str, Any]] = [] + for index, raw_prediction in enumerate(raw_predictions, start=1): + if index in consumed and index not in operation_by_first: + continue + operation = operation_by_first.get(index) + if operation is None: + prediction = copy.deepcopy(_object(raw_prediction, "prediction")) + prediction["source_prediction_indices"] = [index] + prediction["source_rectification_tiles"] = [ + _text( + prediction.get("rectification_tile"), + "prediction rectification tile", + ) + ] + prediction["operation_types"] = [] + output.append(prediction) + continue + output.append( + { + "label": operation["category"], + "score": operation["merged_score"], + "bbox_xyxy": copy.deepcopy(operation["merged_box_xyxy"]), + "source_prediction_indices": copy.deepcopy( + operation["source_prediction_indices"] + ), + "source_rectification_tiles": copy.deepcopy( + operation["source_tiles"] + ), + "operation_types": [operation["operation_type"]], + **( + { + "temporal_run_id": operation["temporal_run_id"], + "temporal_run_length": operation["temporal_run_length"], + } + if operation["operation_type"] + == "temporal-tile-seam-stitch" + else {} + ), + } + ) + + projected = copy.deepcopy(provenance_row) + projected["predictions"] = output + return projected, tuple(normalized_operations) + + +def evaluate_l34d_cumulative_candidate( + *, + provenance_rows: tuple[dict[str, Any], ...], + annotation_frames: tuple[dict[str, Any], ...], + before_cases: tuple[dict[str, Any], ...], + l34b_cases: tuple[dict[str, Any], ...], + l34c_cases: tuple[dict[str, Any], ...], + l34b_metrics: dict[str, Any], + l34c_metrics: dict[str, Any], +) -> tuple[tuple[dict[str, Any], ...], dict[str, Any]]: + """Project and compare the cumulative candidate against assisted review.""" + + annotations_by_sequence = _sequence_map(annotation_frames, "annotation") + before_by_sequence = _sequence_map(before_cases, "before") + l34b_by_sequence = _sequence_map(l34b_cases, "L3.4B") + l34c_by_sequence = _sequence_map(l34c_cases, "L3.4C") + if ( + len(provenance_rows) != 32 + or len(annotations_by_sequence) != 32 + or len(before_by_sequence) != 32 + or len(l34b_by_sequence) != 32 + or len(l34c_by_sequence) != 32 + ): + raise L34DCumulativeCandidateError( + "L3.4D requires 32 source-bound cases" + ) + + cases: list[dict[str, Any]] = [] + after_audits: list[dict[str, Any]] = [] + for row in provenance_rows: + sequence = _integer( + row.get("truth_island_sequence"), + "prediction sequence", + ) + b_case = l34b_by_sequence[sequence] + c_case = l34c_by_sequence[sequence] + _validate_case_binding(row, b_case, "L3.4B") + _validate_case_binding(row, c_case, "L3.4C") + projected, operations = compose_l34d_prediction_row( + row, + consolidations=tuple( + _object(value, "L3.4B consolidation") + for value in _list( + b_case.get("consolidations"), + "L3.4B consolidations", + ) + ), + stitches=tuple( + _object(value, "L3.4C stitch") + for value in _list(c_case.get("stitches"), "L3.4C stitches") + ), + ) + after = _audit_case( + prediction_row=projected, + annotation_frame=annotations_by_sequence[sequence], + ) + for prediction, source in zip( + after["predictions"], + projected["predictions"], + strict=True, + ): + prediction["source_prediction_indices"] = copy.deepcopy( + source["source_prediction_indices"] + ) + prediction["source_rectification_tiles"] = copy.deepcopy( + source["source_rectification_tiles"] + ) + prediction["operation_types"] = copy.deepcopy( + source["operation_types"] + ) + + provenance_by_index = { + _integer( + _object(value, "provenance prediction").get( + "source_prediction_index" + ), + "source prediction index", + ): _object(value, "provenance prediction") + for value in _list(row.get("predictions"), "provenance predictions") + } + before = copy.deepcopy(before_by_sequence[sequence]) + for prediction in _list( + before.get("predictions"), + "before predictions", + ): + current = _object(prediction, "before prediction") + provenance = provenance_by_index.get( + _integer(current.get("prediction_index"), "prediction index") + ) + if provenance is None: + raise L34DCumulativeCandidateError( + "before prediction lost provenance" + ) + current["rectification_tile"] = provenance["rectification_tile"] + current["raw_label"] = provenance["raw_label"] + current["class_id"] = provenance["class_id"] + current["raw_center_xy"] = copy.deepcopy(provenance["raw_center_xy"]) + + _validate_case_binding(row, after, "L3.4D after") + cases.append( + { + "schema_version": L34D_CASE_SCHEMA, + "truth_island_sequence": sequence, + "image_id": after["image_id"], + "frame_index": after["frame_index"], + "group_id": after["group_id"], + "session_seconds": after["session_seconds"], + "source_image_sha256": after["source_image_sha256"], + "camera": copy.deepcopy(after["camera"]), + "before_predictions": copy.deepcopy(before["predictions"]), + "after_predictions": copy.deepcopy(after["predictions"]), + "annotations": copy.deepcopy(after["annotations"]), + "operations": list(operations), + "before_summary": copy.deepcopy(before["summary"]), + "after_summary": copy.deepcopy(after["summary"]), + } + ) + after_audits.append(after) + + before_metrics = _aggregate(tuple(before_cases)) + after_metrics = _aggregate(tuple(after_audits)) + delta = { + key: after_metrics[key] - before_metrics[key] + for key in _DELTA_METRICS + } + operation_types = [ + operation["operation_type"] + for case in cases + for operation in case["operations"] + ] + nested_count = operation_types.count("nested-box-consolidation") + stitch_count = operation_types.count("temporal-tile-seam-stitch") + expected_count_delta = { + key: int(l34b_metrics["delta"][key]) + + int(l34c_metrics["delta"][key]) + for key in _COUNT_METRICS + } + count_effects_additive = all( + int(delta[key]) == expected_count_delta[key] + for key in _COUNT_METRICS + ) + regression_free = ( + nested_count > 0 + and stitch_count > 0 + and count_effects_additive + and delta["true_positive"] >= 0 + and delta["false_positive"] < 0 + and delta["false_negative"] <= 0 + and delta["class_mismatch"] <= 0 + ) + return tuple(cases), { + "before": before_metrics, + "after": after_metrics, + "delta": delta, + "nested_consolidation_count": nested_count, + "temporal_stitch_count": stitch_count, + "cumulative_operation_count": len(operation_types), + "affected_case_count": sum(bool(case["operations"]) for case in cases), + "operation_conflict_count": 0, + "count_effects_additive": count_effects_additive, + "assisted_regression_free": regression_free, + } + + +def build_l34d_cumulative_postprocessing_candidate( + *, + l34a_audit_root: Path, + l34b_shadow_root: Path, + l34c_shadow_root: Path, + annotation_session_path: Path, + output_root: Path, +) -> dict[str, Any]: + """Build and publish one immutable cumulative candidate freeze.""" + + try: + l34a = read_l34a_assisted_yolox_error_audit(l34a_audit_root) + l34b = read_l34b_nested_box_consolidation_shadow(l34b_shadow_root) + l34c = read_l34c_tile_seam_stitch_shadow(l34c_shadow_root) + except ( + L34AAssistedYoloxErrorAuditError, + L34BNestedBoxConsolidationError, + L34CTileSeamStitchError, + ) as reason: + raise L34DCumulativeCandidateError( + "L3.4A/B/C input is invalid" + ) from reason + + session_path = annotation_session_path.expanduser().resolve(strict=True) + if not session_path.is_file() or session_path.is_symlink(): + raise L34DCumulativeCandidateError( + "annotation session is unavailable" + ) + session = _read_json(session_path) + l34b_identity = _object(l34b["manifest"].get("identity"), "L3.4B identity") + l34c_identity = _object(l34c["manifest"].get("identity"), "L3.4C identity") + l34a_id = l34a["result_id"] + freeze_id = _object( + l34c_identity.get("l34_freeze"), + "L3.4C freeze", + ).get("result_id") + try: + _validate_session(session, freeze_result_id=_text(freeze_id, "freeze id")) + except L34AAssistedYoloxErrorAuditError as reason: + raise L34DCumulativeCandidateError( + "annotation session is invalid" + ) from reason + if ( + _object(l34b_identity.get("l34_freeze"), "L3.4B freeze") + != _object(l34c_identity.get("l34_freeze"), "L3.4C freeze") + or _object(l34b_identity.get("l34a_audit"), "L3.4B audit").get( + "result_id" + ) + != l34a_id + or _object(l34c_identity.get("l34a_audit"), "L3.4C audit").get( + "result_id" + ) + != l34a_id + or _object( + l34b_identity.get("assisted_annotation"), + "L3.4B annotation", + ).get("session_sha256") + != _sha256(session_path) + or _object( + l34c_identity.get("assisted_annotation"), + "L3.4C annotation", + ).get("session_sha256") + != _sha256(session_path) + or l34b["report"]["decision"].get("shadow_policy_accepted") is not True + or l34c["report"]["decision"].get("shadow_policy_accepted") is not True + ): + raise L34DCumulativeCandidateError("L3.4D lineage differs") + + cases, metrics = evaluate_l34d_cumulative_candidate( + provenance_rows=l34c["provenance"], + annotation_frames=tuple(session["frames"]), + before_cases=l34a["cases"], + l34b_cases=l34b["cases"], + l34c_cases=l34c["cases"], + l34b_metrics=l34b["report"]["metrics"], + l34c_metrics=l34c["report"]["metrics"], + ) + if not metrics["assisted_regression_free"]: + raise L34DCumulativeCandidateError( + "cumulative candidate failed the assisted regression gate" + ) + affected_sequences = [ + case["truth_island_sequence"] for case in cases if case["operations"] + ] + case_order = affected_sequences + [ + case["truth_island_sequence"] + for case in sorted( + (item for item in cases if not item["operations"]), + key=lambda item: ( + -int(item["after_summary"]["severity_score"]), + int(item["truth_island_sequence"]), + ), + ) + ] + + method = { + "schema_version": "missioncore.laboratory-method/v1", + "completeness": "complete", + "execution_class": "deterministic", + "pipeline_id": "ravnoves00-right-yolox-cumulative-postprocessing/v1", + "components": [ + { + "kind": "source", + "name": l34b["result_id"], + "version": "accepted L3.4B nested-box shadow", + "role": "source-indexed nested-box operations", + "identity_sha256": _sha256( + l34b["result_root"] / L34B_MANIFEST_NAME + ), + }, + { + "kind": "source", + "name": l34c["result_id"], + "version": "accepted L3.4C temporal seam shadow", + "role": "tile provenance and source-indexed seam operations", + "identity_sha256": _sha256( + l34c["result_root"] / L34C_MANIFEST_NAME + ), + }, + { + "kind": "source", + "name": l34a_id, + "version": "L3.4A assisted diagnostic; not truth", + "role": "after-freeze engineering comparison", + "identity_sha256": _sha256( + l34a["result_root"] / L34A_MANIFEST_NAME + ), + }, + { + "kind": "algorithm", + "name": "source-indexed-order-independent-composition", + "version": "v1-fail-closed-on-conflict", + "role": "compose accepted B/C operations and freeze one candidate", + "identity_sha256": _sha256(Path(__file__).resolve(strict=True)), + }, + ], + } + report_basis = { + "schema_version": L34D_REPORT_SCHEMA, + "status": "completed-cumulative-postprocessing-candidate-freeze-not-truth", + "profile": { + "profile_id": "l34d-source-indexed-cumulative-candidate/v1", + "operation_order": "simultaneous-original-source-indices", + "conflict_policy": "reject-any-overlapping-source-index", + "geometry_policy": "accepted-l34b-and-l34c-union-boxes-only", + "score_policy": "accepted-maximum-source-score", + "scope": "recorded-right-camera-frozen-prediction-candidate-only", + }, + "metrics": metrics, + "case_order": case_order, + "decision": { + "cumulative_shadow_accepted": True, + "candidate_frozen": True, + "candidate_accepted": False, + "prediction_provenance_preserved": True, + "operation_sets_disjoint": True, + "global_nms_unchanged": True, + "independent_truth_available": False, + "l35_blind_gate_open": False, + "next_action": ( + "collect prediction-hidden independent labels against this exact " + "frozen candidate, then evaluate once without retuning" + ), + }, + "limitations": [ + "the comparison reuses candidate-seeded assisted review and is not independent truth", + "the frozen candidate changes only five of 32 RIGHT-camera cases on one route", + "the composition is valid only while L3.4B and L3.4C source-index sets remain disjoint", + "no model weights, global NMS or detector inference are changed", + ( + "no live transport, hardware, left camera, LiDAR range, " + "navigation or safety claim is made" + ), + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + identity = { + "schema_version": L34D_RESULT_SCHEMA, + "l34_freeze": copy.deepcopy(l34c_identity["l34_freeze"]), + "l34a_audit": { + "result_id": l34a_id, + "manifest_sha256": _sha256( + l34a["result_root"] / L34A_MANIFEST_NAME + ), + }, + "l34b_shadow": { + "result_id": l34b["result_id"], + "manifest_sha256": _sha256( + l34b["result_root"] / L34B_MANIFEST_NAME + ), + }, + "l34c_shadow": { + "result_id": l34c["result_id"], + "manifest_sha256": _sha256( + l34c["result_root"] / L34C_MANIFEST_NAME + ), + }, + "assisted_annotation": { + "session_id": session["session_id"], + "session_sha256": _sha256(session_path), + "independent_truth_eligible": False, + }, + "method": method, + "report_sha256": hashlib.sha256( + _canonical_json(report_basis) + ).hexdigest(), + "cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"l34d-cumulative-postprocessing-candidate-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_l34d_cumulative_postprocessing_candidate(destination) + + created_at_utc = _utc_now() + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc, + "source_session_id": "RAVNOVES00", + "camera_source_id": "sensor.camera.right", + "method": method, + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / L34D_REPORT_NAME, report) + _write_jsonl(staging / L34D_CASES_NAME, cases) + manifest = { + "schema_version": L34D_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "frozen-assisted-cumulative-candidate-not-truth", + "ground_truth": False, + "artifacts": [ + _artifact(staging / L34D_REPORT_NAME, "cumulative-report"), + _artifact(staging / L34D_CASES_NAME, "cumulative-cases"), + ], + "authority": _AUTHORITY, + } + _write_json(staging / L34D_MANIFEST_NAME, manifest) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_l34d_cumulative_postprocessing_candidate(destination) + + +def read_l34d_cumulative_postprocessing_candidate( + root: Path, +) -> dict[str, Any]: + """Read and fully revalidate one immutable L3.4D candidate.""" + + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / L34D_MANIFEST_NAME) + identity = _object(manifest.get("identity"), "L3.4D identity") + identity_sha256 = manifest.get("identity_sha256") + if ( + manifest.get("schema_version") != L34D_RESULT_SCHEMA + or not isinstance(identity_sha256, str) + or hashlib.sha256(_canonical_json(identity)).hexdigest() + != identity_sha256 + or manifest.get("result_id") + != f"l34d-cumulative-postprocessing-candidate-{identity_sha256}" + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("acceptance_state") + != "frozen-assisted-cumulative-candidate-not-truth" + or manifest.get("ground_truth") is not False + or manifest.get("authority") != _AUTHORITY + ): + raise L34DCumulativeCandidateError("L3.4D identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 2: + raise L34DCumulativeCandidateError("L3.4D artifacts are invalid") + artifact_by_role = { + _text(item.get("role"), "artifact role"): _object(item, "artifact") + for item in artifacts + if isinstance(item, dict) + } + report = _read_json( + _validated_artifact( + resolved, + artifact_by_role.get("cumulative-report"), + ) + ) + cases = tuple( + _read_jsonl( + _validated_artifact( + resolved, + artifact_by_role.get("cumulative-cases"), + ) + ) + ) + if ( + report.get("schema_version") != L34D_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("identity_sha256") != identity_sha256 + or report.get("status") + != "completed-cumulative-postprocessing-candidate-freeze-not-truth" + or report.get("ground_truth") is not False + or report.get("authority") != _AUTHORITY + or len(cases) != 32 + or any(case.get("schema_version") != L34D_CASE_SCHEMA for case in cases) + or hashlib.sha256(_canonical_json(cases)).hexdigest() + != identity.get("cases_sha256") + ): + raise L34DCumulativeCandidateError("L3.4D result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "cases": cases, + } + + +def _validated_operation( + raw: dict[str, Any], + *, + predictions: list[dict[str, Any]], + provenance_predictions: list[Any], + operation_type: str, +) -> dict[str, Any]: + operation = copy.deepcopy(_object(raw, "L3.4D operation")) + raw_indices = _list( + operation.get("source_prediction_indices"), + "operation source indices", + ) + source_indices = sorted( + _integer(value, "operation source index") for value in raw_indices + ) + if ( + len(source_indices) < 2 + or len(set(source_indices)) != len(source_indices) + or source_indices[0] < 1 + or source_indices[-1] > len(predictions) + ): + raise L34DCumulativeCandidateError( + "operation source indices are invalid" + ) + members = [predictions[index - 1] for index in source_indices] + category = _text(operation.get("category"), "operation category") + if any(member["label"] != category for member in members): + raise L34DCumulativeCandidateError("operation category changed") + source_scores = [ + _finite(value, "operation source score") + for value in _list(operation.get("source_scores"), "operation scores") + ] + source_boxes = [ + [ + _finite(coordinate, "operation source box") + for coordinate in _list(value, "operation source box") + ] + for value in _list(operation.get("source_boxes_xyxy"), "operation boxes") + ] + if ( + source_scores != [member["score"] for member in members] + or source_boxes != [member["bbox_xyxy"] for member in members] + ): + raise L34DCumulativeCandidateError("operation source payload changed") + merged_box = source_boxes[0] + for source_box in source_boxes[1:]: + merged_box = _union_box(merged_box, source_box) + declared_merged_box = [ + _finite(value, "operation merged box") + for value in _list(operation.get("merged_box_xyxy"), "merged box") + ] + merged_score = _finite(operation.get("merged_score"), "merged score") + if declared_merged_box != merged_box or merged_score != max(source_scores): + raise L34DCumulativeCandidateError("operation projection changed") + source_tiles = [ + _text( + _object(provenance_predictions[index - 1], "provenance prediction").get( + "rectification_tile" + ), + "source tile", + ) + for index in source_indices + ] + if operation_type == "temporal-tile-seam-stitch": + if source_tiles != _list(operation.get("source_tiles"), "stitch tiles"): + raise L34DCumulativeCandidateError("stitch tile provenance changed") + operation["temporal_run_id"] = _text( + operation.get("temporal_run_id"), + "temporal run id", + ) + operation["temporal_run_length"] = _integer( + operation.get("temporal_run_length"), + "temporal run length", + ) + operation["source_prediction_indices"] = source_indices + operation["source_tiles"] = source_tiles + operation["source_scores"] = source_scores + operation["source_boxes_xyxy"] = source_boxes + operation["merged_box_xyxy"] = declared_merged_box + operation["merged_score"] = merged_score + operation["operation_type"] = operation_type + operation.pop("output_prediction_index", None) + return operation + + +def _sequence_map( + values: tuple[dict[str, Any], ...], + label: str, +) -> dict[int, dict[str, Any]]: + result = { + _integer(value.get("truth_island_sequence"), f"{label} sequence"): value + for value in values + } + if len(result) != len(values): + raise L34DCumulativeCandidateError(f"{label} sequences are not unique") + return result + + +def _validate_case_binding( + source: dict[str, Any], + candidate: dict[str, Any], + label: str, +) -> None: + if any( + source.get(key) != candidate.get(key) + for key in ( + "truth_island_sequence", + "image_id", + "frame_index", + "group_id", + "source_image_sha256", + ) + ): + raise L34DCumulativeCandidateError(f"{label} case binding changed") diff --git a/src/k1link/compute/l34e_self_review_diagnostic.py b/src/k1link/compute/l34e_self_review_diagnostic.py new file mode 100644 index 0000000..af94121 --- /dev/null +++ b/src/k1link/compute/l34e_self_review_diagnostic.py @@ -0,0 +1,827 @@ +"""Build the L3.4E diagnostic comparison against a manual self-review. + +L3.4E deliberately does not promote the self-review to ground truth. The +reviewer has seen the candidate identity and the boxes are coarse enough that +strict IoU50 alone would confuse annotation geometry with detector quality. +The artifact therefore preserves the strict metric for reproducibility and +adds a second, explicitly diagnostic association pass for visual triage. +""" + +from __future__ import annotations + +import copy +import hashlib +import os +import re +import shutil +import uuid +from collections import defaultdict +from pathlib import Path +from typing import Any, Final + +from .l34a_assisted_yolox_error_audit import ( + L34AAssistedYoloxErrorAuditError, + _aggregate, + _audit_case, + _iou, + _overlap_over_smaller, + _per_class, +) +from .l34c_tile_seam_stitch_shadow import ( + _artifact, + _canonical_json, + _integer, + _list, + _object, + _read_json, + _read_jsonl, + _sha256, + _text, + _utc_now, + _validated_artifact, + _write_json, + _write_jsonl, +) +from .l34d_cumulative_postprocessing_candidate import ( + L34D_MANIFEST_NAME, + L34DCumulativeCandidateError, + read_l34d_cumulative_postprocessing_candidate, +) + +L34E_RESULT_SCHEMA: Final = "missioncore.l34e-self-review-diagnostic/v1" +L34E_REPORT_SCHEMA: Final = ( + "missioncore.l34e-self-review-diagnostic-report/v1" +) +L34E_CASE_SCHEMA: Final = "missioncore.l34e-self-review-diagnostic-case/v1" +L34E_MANIFEST_NAME: Final = "manifest.json" +L34E_REPORT_NAME: Final = "self-review-diagnostic-report.json" +L34E_CASES_NAME: Final = "self-review-diagnostic-cases.jsonl" + +_RESULT_ID = re.compile(r"^l34e-self-review-diagnostic-[a-f0-9]{64}$") +_SESSION_ID = re.compile(r"^l34-annotation-session-[a-f0-9]{64}$") +_ANNOTATION_SCHEMA: Final = "missioncore.l34-annotation-session/v3" +_STRICT_IOU: Final = 0.5 +_LOOSE_IOU: Final = 0.1 +_LOOSE_OVERLAP: Final = 0.3 +_AUTHORITY: Final = { + "ground_truth": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class L34ESelfReviewDiagnosticError(RuntimeError): + """An L3.4E source, result identity or immutable artifact is invalid.""" + + +def evaluate_l34e_self_review_diagnostic( + *, + l34d_cases: tuple[dict[str, Any], ...], + annotation_frames: tuple[dict[str, Any], ...], +) -> tuple[tuple[dict[str, Any], ...], dict[str, Any]]: + """Evaluate strict IoU50 and diagnostic associations for 32 cases.""" + + if len(l34d_cases) != 32 or len(annotation_frames) != 32: + raise L34ESelfReviewDiagnosticError( + "L3.4E requires exactly 32 candidate and review frames" + ) + candidate_by_sequence = _sequence_map(l34d_cases, "candidate") + review_by_sequence = _sequence_map(annotation_frames, "review") + if set(candidate_by_sequence) != set(range(1, 33)) or set( + review_by_sequence + ) != set(range(1, 33)): + raise L34ESelfReviewDiagnosticError("L3.4E frame coverage differs") + + cases: list[dict[str, Any]] = [] + strict_cases: list[dict[str, Any]] = [] + for sequence in range(1, 33): + source = candidate_by_sequence[sequence] + review = review_by_sequence[sequence] + prediction_row = { + "truth_island_sequence": sequence, + "image_id": source.get("image_id"), + "frame_index": source.get("frame_index"), + "group_id": source.get("group_id"), + "session_seconds": source.get("session_seconds"), + "source_image_sha256": source.get("source_image_sha256"), + "predictions": [ + { + "label": item.get("category"), + "score": item.get("score"), + "bbox_xyxy": copy.deepcopy(item.get("box_xyxy")), + } + for item in ( + _object(value, "L3.4D after prediction") + for value in _list( + source.get("after_predictions"), + "L3.4D after predictions", + ) + ) + ], + } + try: + strict = _audit_case( + prediction_row=prediction_row, + annotation_frame=review, + ) + except L34AAssistedYoloxErrorAuditError as reason: + raise L34ESelfReviewDiagnosticError( + "L3.4E source binding or box contract is invalid" + ) from reason + + source_predictions = _list( + source.get("after_predictions"), + "L3.4D after predictions", + ) + for prediction, source_prediction in zip( + strict["predictions"], + source_predictions, + strict=True, + ): + provenance = _object(source_prediction, "L3.4D prediction") + prediction["source_prediction_indices"] = copy.deepcopy( + provenance.get("source_prediction_indices", []) + ) + prediction["source_rectification_tiles"] = copy.deepcopy( + provenance.get("source_rectification_tiles", []) + ) + prediction["operation_types"] = copy.deepcopy( + provenance.get("operation_types", []) + ) + + associations = _diagnostic_associations( + strict["predictions"], + strict["annotations"], + ) + summary = _diagnostic_case_summary( + strict["predictions"], + strict["annotations"], + associations, + ) + cases.append( + { + "schema_version": L34E_CASE_SCHEMA, + "truth_island_sequence": sequence, + "image_id": strict["image_id"], + "frame_index": strict["frame_index"], + "group_id": strict["group_id"], + "session_seconds": strict["session_seconds"], + "source_image_sha256": strict["source_image_sha256"], + "camera": copy.deepcopy(strict["camera"]), + "predictions": copy.deepcopy(strict["predictions"]), + "references": copy.deepcopy(strict["annotations"]), + "associations": associations, + "strict_summary": copy.deepcopy(strict["summary"]), + "diagnostic_summary": summary, + } + ) + strict_cases.append(strict) + + strict_metrics = _aggregate(tuple(strict_cases)) + strict_metrics["per_class"] = _per_class(tuple(strict_cases)) + diagnostic = _aggregate_diagnostic(tuple(cases)) + temporal = _temporal_count_diagnostics(tuple(cases)) + return tuple(cases), { + "strict_iou50": strict_metrics, + "diagnostic_association": diagnostic, + "frame_count_disagreement": { + "candidate_surplus_lower_bound": sum( + max( + 0, + case["diagnostic_summary"]["prediction_count"] + - case["diagnostic_summary"]["reference_count"], + ) + for case in cases + ), + "reference_surplus_lower_bound": sum( + max( + 0, + case["diagnostic_summary"]["reference_count"] + - case["diagnostic_summary"]["prediction_count"], + ) + for case in cases + ), + "equal_count_frame_count": sum( + case["diagnostic_summary"]["prediction_count"] + == case["diagnostic_summary"]["reference_count"] + for case in cases + ), + }, + "temporal_groups": temporal, + "reference_quality": "not-metric-grade-self-review", + } + + +def build_l34e_self_review_diagnostic( + *, + l34d_candidate_root: Path, + annotation_session_path: Path, + output_root: Path, +) -> dict[str, Any]: + """Build and publish one immutable L3.4E diagnostic result.""" + + try: + candidate = read_l34d_cumulative_postprocessing_candidate( + l34d_candidate_root + ) + except L34DCumulativeCandidateError as reason: + raise L34ESelfReviewDiagnosticError( + "L3.4D candidate is invalid" + ) from reason + session_path = annotation_session_path.expanduser().resolve(strict=True) + if not session_path.is_file() or session_path.is_symlink(): + raise L34ESelfReviewDiagnosticError("self-review session is unavailable") + session = _read_json(session_path) + _validate_self_review_session(session, candidate["result_id"]) + + cases, metrics = evaluate_l34e_self_review_diagnostic( + l34d_cases=candidate["cases"], + annotation_frames=tuple(session["frames"]), + ) + case_order = [ + case["truth_island_sequence"] + for case in sorted( + cases, + key=lambda item: ( + -int(item["diagnostic_summary"]["severity_score"]), + int(item["truth_island_sequence"]), + ), + ) + ] + method = { + "schema_version": "missioncore.laboratory-method/v1", + "completeness": "complete", + "execution_class": "deterministic", + "pipeline_id": "ravnoves00-right-yolox-self-review-diagnostic/v1", + "components": [ + { + "kind": "source", + "name": candidate["result_id"], + "version": "L3.4D frozen cumulative candidate", + "role": "candidate boxes and immutable source binding", + "identity_sha256": _sha256( + candidate["result_root"] / L34D_MANIFEST_NAME + ), + }, + { + "kind": "source", + "name": session["session_id"], + "version": "prediction-hidden manual self-review; not truth", + "role": "coarse human reference boxes for diagnostic triage", + "identity_sha256": _sha256(session_path), + }, + { + "kind": "algorithm", + "name": "strict-plus-diagnostic-spatial-association", + "version": "v1-iou50-then-iou10-or-overlap30", + "role": "separate object association from localization disagreement", + "identity_sha256": _sha256(Path(__file__).resolve(strict=True)), + }, + ], + } + report_basis = { + "schema_version": L34E_REPORT_SCHEMA, + "status": "completed-self-review-diagnostic-not-truth", + "profile": { + "profile_id": "l34e-self-review-diagnostic/v1", + "strict_matcher": "greedy-maximum-iou-0.50", + "diagnostic_matcher": ( + "strict-first-then-greedy-iou-0.10-or-overlap-over-smaller-0.30" + ), + "class_policy": "spatial-association-first-then-class-verdict", + "reference_policy": "manual-self-review-coarse-boxes-not-metric-grade", + "scope": "recorded-right-camera-32-frozen-frames", + }, + "metrics": metrics, + "case_order": case_order, + "decision": { + "self_review_complete": True, + "diagnostic_alignment_available": True, + "metric_grade_reference_available": False, + "independent_truth_available": False, + "detector_retuning_authorized": False, + "candidate_accepted": False, + "l35_blind_gate_open": False, + "next_action": ( + "refine and adjudicate the visual disagreement gallery, then obtain " + "an independent reviewer before one-shot candidate acceptance" + ), + }, + "limitations": [ + ( + "the reviewer had seen the candidate identity, so this result is " + "diagnostic self-review and not independent truth" + ), + ( + "manual boxes are coarse, especially for small distant vehicles; " + "strict IoU50 is not detector accuracy" + ), + ( + "the loose association pass is a visual-triage heuristic and must " + "not be used as an acceptance metric" + ), + "the sample contains only 32 recorded RIGHT-camera frames on one route", + ( + "no live transport, hardware, left camera, LiDAR range, navigation " + "or safety claim is made" + ), + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + identity = { + "schema_version": L34E_RESULT_SCHEMA, + "l34d_candidate": { + "result_id": candidate["result_id"], + "manifest_sha256": _sha256( + candidate["result_root"] / L34D_MANIFEST_NAME + ), + }, + "self_review": { + "session_id": session["session_id"], + "session_sha256": _sha256(session_path), + "revision": session["revision"], + "independent_truth_eligible": False, + "metric_grade_reference": False, + }, + "method": method, + "report_sha256": hashlib.sha256( + _canonical_json(report_basis) + ).hexdigest(), + "cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"l34e-self-review-diagnostic-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_l34e_self_review_diagnostic(destination) + + created_at_utc = _utc_now() + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc, + "source_session_id": "RAVNOVES00", + "camera_source_id": "sensor.camera.right", + "method": method, + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / L34E_REPORT_NAME, report) + _write_jsonl(staging / L34E_CASES_NAME, cases) + manifest = { + "schema_version": L34E_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "diagnostic-self-review-not-truth", + "ground_truth": False, + "artifacts": [ + _artifact(staging / L34E_REPORT_NAME, "diagnostic-report"), + _artifact(staging / L34E_CASES_NAME, "diagnostic-cases"), + ], + "authority": _AUTHORITY, + } + _write_json(staging / L34E_MANIFEST_NAME, manifest) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_l34e_self_review_diagnostic(destination) + + +def read_l34e_self_review_diagnostic(root: Path) -> dict[str, Any]: + """Read and fully revalidate one immutable L3.4E result.""" + + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / L34E_MANIFEST_NAME) + identity = _object(manifest.get("identity"), "L3.4E identity") + identity_sha256 = manifest.get("identity_sha256") + if ( + manifest.get("schema_version") != L34E_RESULT_SCHEMA + or not isinstance(identity_sha256, str) + or hashlib.sha256(_canonical_json(identity)).hexdigest() + != identity_sha256 + or manifest.get("result_id") + != f"l34e-self-review-diagnostic-{identity_sha256}" + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("acceptance_state") != "diagnostic-self-review-not-truth" + or manifest.get("ground_truth") is not False + or manifest.get("authority") != _AUTHORITY + ): + raise L34ESelfReviewDiagnosticError("L3.4E identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 2: + raise L34ESelfReviewDiagnosticError("L3.4E artifacts are invalid") + artifact_by_role = { + _text(item.get("role"), "artifact role"): _object(item, "artifact") + for item in artifacts + if isinstance(item, dict) + } + report = _read_json( + _validated_artifact( + resolved, + artifact_by_role.get("diagnostic-report"), + ) + ) + cases = tuple( + _read_jsonl( + _validated_artifact( + resolved, + artifact_by_role.get("diagnostic-cases"), + ) + ) + ) + if ( + report.get("schema_version") != L34E_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("identity_sha256") != identity_sha256 + or report.get("status") != "completed-self-review-diagnostic-not-truth" + or report.get("ground_truth") is not False + or report.get("authority") != _AUTHORITY + or len(cases) != 32 + or any(case.get("schema_version") != L34E_CASE_SCHEMA for case in cases) + or hashlib.sha256(_canonical_json(cases)).hexdigest() + != identity.get("cases_sha256") + ): + raise L34ESelfReviewDiagnosticError("L3.4E result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "cases": cases, + } + + +def _validate_self_review_session( + session: dict[str, Any], + candidate_result_id: str, +) -> None: + assistance = _object(session.get("assistance"), "self-review assistance") + blindness = _object(session.get("blindness"), "self-review blindness") + frames = session.get("frames") + if ( + session.get("schema_version") != _ANNOTATION_SCHEMA + or not isinstance(session.get("session_id"), str) + or _SESSION_ID.fullmatch(session["session_id"]) is None + or session.get("result_id") != candidate_result_id + or session.get("contract_id") != "l34d-prediction-hidden-review/v1" + or session.get("state") != "saved" + or not isinstance(session.get("revision"), int) + or session["revision"] < 1 + or assistance + != { + "mode": "prediction-hidden-manual", + "independent_truth_eligible": False, + } + or blindness + != { + "candidate_identity_seen": True, + "model_prelabels_seen": False, + "model_predictions_seen": False, + "model_scores_seen": False, + } + or session.get("authority") != _AUTHORITY + or not isinstance(frames, list) + or len(frames) != 32 + ): + raise L34ESelfReviewDiagnosticError( + "self-review session contract is invalid" + ) + sequences: set[int] = set() + for raw_frame in frames: + frame = _object(raw_frame, "self-review frame") + sequence = _integer(frame.get("truth_island_sequence"), "review sequence") + objects = frame.get("objects") + if ( + not 1 <= sequence <= 32 + or sequence in sequences + or frame.get("reviewed") is not True + or not isinstance(frame.get("source_sha256"), str) + or not isinstance(objects, list) + ): + raise L34ESelfReviewDiagnosticError( + "self-review coverage is incomplete" + ) + sequences.add(sequence) + for raw_object in objects: + item = _object(raw_object, "self-review object") + if item.get("origin") != "manual": + raise L34ESelfReviewDiagnosticError( + "self-review contains seeded objects" + ) + category = item.get("category") + if ( + not isinstance(item.get("object_id"), str) + or not isinstance(category, str) + or not _valid_box(item.get("box_xyxy")) + or ( + category == "unmapped" + and not isinstance(item.get("proposed_label"), str) + ) + or ( + category != "unmapped" + and item.get("proposed_label") is not None + ) + ): + raise L34ESelfReviewDiagnosticError( + "self-review object contract is invalid" + ) + + +def _diagnostic_associations( + predictions: list[dict[str, Any]], + references: list[dict[str, Any]], +) -> list[dict[str, Any]]: + remaining_predictions = set(range(len(predictions))) + remaining_references = set(range(len(references))) + associations: list[dict[str, Any]] = [] + + strict_candidates = sorted( + ( + (_iou(prediction["box_xyxy"], reference["box_xyxy"]), p, r) + for p, prediction in enumerate(predictions) + for r, reference in enumerate(references) + ), + reverse=True, + ) + for iou, prediction_index, reference_index in strict_candidates: + if iou < _STRICT_IOU: + break + if ( + prediction_index not in remaining_predictions + or reference_index not in remaining_references + ): + continue + classification = ( + "strict_alignment" + if predictions[prediction_index]["category"] + == references[reference_index]["category"] + else "strict_class_mismatch" + ) + associations.append( + _association( + predictions, + references, + prediction_index, + reference_index, + classification, + ) + ) + remaining_predictions.remove(prediction_index) + remaining_references.remove(reference_index) + + loose_candidates = sorted( + ( + ( + max( + _iou( + predictions[prediction_index]["box_xyxy"], + references[reference_index]["box_xyxy"], + ), + _overlap_over_smaller( + predictions[prediction_index]["box_xyxy"], + references[reference_index]["box_xyxy"], + ), + ), + prediction_index, + reference_index, + ) + for prediction_index in remaining_predictions + for reference_index in remaining_references + ), + reverse=True, + ) + for _, prediction_index, reference_index in loose_candidates: + if ( + prediction_index not in remaining_predictions + or reference_index not in remaining_references + ): + continue + prediction_box = predictions[prediction_index]["box_xyxy"] + reference_box = references[reference_index]["box_xyxy"] + iou = _iou(prediction_box, reference_box) + overlap = _overlap_over_smaller(prediction_box, reference_box) + if iou < _LOOSE_IOU and overlap < _LOOSE_OVERLAP: + continue + classification = ( + "localization_disagreement" + if predictions[prediction_index]["category"] + == references[reference_index]["category"] + else "class_and_localization_disagreement" + ) + associations.append( + _association( + predictions, + references, + prediction_index, + reference_index, + classification, + ) + ) + remaining_predictions.remove(prediction_index) + remaining_references.remove(reference_index) + + for prediction in predictions: + prediction["diagnostic_verdict"] = "prediction_only" + prediction["associated_object_id"] = None + prediction["association_iou"] = None + prediction["association_overlap_over_smaller"] = None + for reference in references: + reference["diagnostic_verdict"] = "reference_only" + reference["associated_prediction_index"] = None + reference["association_iou"] = None + reference["association_overlap_over_smaller"] = None + for association in associations: + prediction = predictions[association["prediction_index"] - 1] + reference = next( + item + for item in references + if item["object_id"] == association["object_id"] + ) + prediction["diagnostic_verdict"] = association["classification"] + prediction["associated_object_id"] = association["object_id"] + prediction["association_iou"] = association["iou"] + prediction["association_overlap_over_smaller"] = association[ + "overlap_over_smaller" + ] + reference["diagnostic_verdict"] = association["classification"] + reference["associated_prediction_index"] = association[ + "prediction_index" + ] + reference["association_iou"] = association["iou"] + reference["association_overlap_over_smaller"] = association[ + "overlap_over_smaller" + ] + return sorted(associations, key=lambda item: item["prediction_index"]) + + +def _association( + predictions: list[dict[str, Any]], + references: list[dict[str, Any]], + prediction_index: int, + reference_index: int, + classification: str, +) -> dict[str, Any]: + prediction = predictions[prediction_index] + reference = references[reference_index] + return { + "prediction_index": prediction["prediction_index"], + "object_id": reference["object_id"], + "prediction_category": prediction["category"], + "reference_category": reference["display_category"], + "iou": _iou(prediction["box_xyxy"], reference["box_xyxy"]), + "overlap_over_smaller": _overlap_over_smaller( + prediction["box_xyxy"], reference["box_xyxy"] + ), + "classification": classification, + } + + +def _diagnostic_case_summary( + predictions: list[dict[str, Any]], + references: list[dict[str, Any]], + associations: list[dict[str, Any]], +) -> dict[str, Any]: + counts: defaultdict[str, int] = defaultdict(int) + for association in associations: + counts[association["classification"]] += 1 + prediction_only = sum( + item["diagnostic_verdict"] == "prediction_only" for item in predictions + ) + reference_only = sum( + item["diagnostic_verdict"] == "reference_only" for item in references + ) + associated = len(associations) + return { + "prediction_count": len(predictions), + "reference_count": len(references), + "associated_pair_count": associated, + "strict_alignment": counts["strict_alignment"], + "strict_class_mismatch": counts["strict_class_mismatch"], + "localization_disagreement": counts["localization_disagreement"], + "class_and_localization_disagreement": counts[ + "class_and_localization_disagreement" + ], + "prediction_only": prediction_only, + "reference_only": reference_only, + "candidate_association_coverage": ( + associated / len(predictions) if predictions else 0.0 + ), + "reference_association_coverage": ( + associated / len(references) if references else 0.0 + ), + "severity_score": ( + prediction_only + + reference_only * 2 + + counts["localization_disagreement"] + + counts["strict_class_mismatch"] * 3 + + counts["class_and_localization_disagreement"] * 4 + ), + } + + +def _aggregate_diagnostic(cases: tuple[dict[str, Any], ...]) -> dict[str, Any]: + keys = ( + "prediction_count", + "reference_count", + "associated_pair_count", + "strict_alignment", + "strict_class_mismatch", + "localization_disagreement", + "class_and_localization_disagreement", + "prediction_only", + "reference_only", + ) + totals = { + key: sum(int(case["diagnostic_summary"][key]) for case in cases) + for key in keys + } + associated = totals["associated_pair_count"] + return { + **totals, + "candidate_association_coverage": ( + associated / totals["prediction_count"] + if totals["prediction_count"] + else 0.0 + ), + "reference_association_coverage": ( + associated / totals["reference_count"] + if totals["reference_count"] + else 0.0 + ), + "error_case_count": sum( + case["diagnostic_summary"]["strict_class_mismatch"] > 0 + or case["diagnostic_summary"]["localization_disagreement"] > 0 + or case["diagnostic_summary"]["class_and_localization_disagreement"] + > 0 + or case["diagnostic_summary"]["prediction_only"] > 0 + or case["diagnostic_summary"]["reference_only"] > 0 + for case in cases + ), + } + + +def _temporal_count_diagnostics( + cases: tuple[dict[str, Any], ...], +) -> list[dict[str, Any]]: + grouped: dict[str, list[dict[str, Any]]] = defaultdict(list) + for case in cases: + grouped[_text(case.get("group_id"), "group id")].append(case) + result: list[dict[str, Any]] = [] + for group_id, members in sorted(grouped.items()): + ordered = sorted(members, key=lambda item: item["truth_island_sequence"]) + candidate_counts = [ + item["diagnostic_summary"]["prediction_count"] for item in ordered + ] + reference_counts = [ + item["diagnostic_summary"]["reference_count"] for item in ordered + ] + result.append( + { + "group_id": group_id, + "sequences": [item["truth_island_sequence"] for item in ordered], + "candidate_counts": candidate_counts, + "reference_counts": reference_counts, + "candidate_count_range": max(candidate_counts) + - min(candidate_counts), + "reference_count_range": max(reference_counts) + - min(reference_counts), + } + ) + return result + + +def _sequence_map( + rows: tuple[dict[str, Any], ...], + label: str, +) -> dict[int, dict[str, Any]]: + result: dict[int, dict[str, Any]] = {} + for raw in rows: + item = _object(raw, f"{label} row") + sequence = _integer(item.get("truth_island_sequence"), f"{label} sequence") + if sequence in result: + raise L34ESelfReviewDiagnosticError(f"duplicate {label} sequence") + result[sequence] = item + return result + + +def _valid_box(value: object) -> bool: + if not isinstance(value, list) or len(value) != 4: + return False + if any( + not isinstance(item, (int, float)) or isinstance(item, bool) + for item in value + ): + return False + left, top, right, bottom = (float(item) for item in value) + return 0 <= left < right <= 800 and 0 <= top < bottom <= 600 diff --git a/src/k1link/compute/l34f_adjudicated_reference.py b/src/k1link/compute/l34f_adjudicated_reference.py new file mode 100644 index 0000000..2559945 --- /dev/null +++ b/src/k1link/compute/l34f_adjudicated_reference.py @@ -0,0 +1,350 @@ +"""Freeze one candidate-visible L3.4F engineering reference. + +The artifact records human adjudication of the L3.4E disagreement gallery. +It is deliberately not independent truth and grants no detector, command, +navigation, or safety authority. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import shutil +import uuid +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from k1link.compute.l34e_self_review_diagnostic import ( + L34E_MANIFEST_NAME, + read_l34e_self_review_diagnostic, +) + +L34F_RESULT_SCHEMA: Final = "missioncore.l34f-adjudicated-reference/v1" +L34F_REPORT_SCHEMA: Final = "missioncore.l34f-adjudicated-reference-report/v1" +L34F_CASE_SCHEMA: Final = "missioncore.l34f-adjudicated-reference-case/v1" +L34F_MANIFEST_NAME: Final = "manifest.json" +L34F_REPORT_NAME: Final = "adjudicated-reference-report.json" +L34F_CASES_NAME: Final = "adjudicated-reference-cases.jsonl" +L34F_SESSION_SCHEMA: Final = "missioncore.l34f-adjudication-session/v1" + +_RESULT_ID = re.compile(r"^l34f-adjudicated-reference-[a-f0-9]{64}$") +_SESSION_ID = re.compile(r"^l34f-adjudication-session-[a-f0-9]{64}$") +_AUTHORITY: Final = { + "ground_truth": False, + "independent_truth": False, + "metric_grade_reference": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class L34FAdjudicatedReferenceError(ValueError): + """Raised when an L3.4F source or immutable artifact is invalid.""" + + +def build_l34f_adjudicated_reference( + *, + diagnostic_root: Path, + adjudication_session_path: Path, + output_root: Path, +) -> dict[str, Any]: + diagnostic = read_l34e_self_review_diagnostic(diagnostic_root) + session_path = adjudication_session_path.expanduser().resolve(strict=True) + if not session_path.is_file() or session_path.is_symlink(): + raise L34FAdjudicatedReferenceError("adjudication session unavailable") + session = _read_json(session_path) + _validate_session(session, diagnostic["result_id"]) + + source_cases = {int(case["truth_island_sequence"]): case for case in diagnostic["cases"]} + cases: list[dict[str, Any]] = [] + totals = { + "frame_count": 32, + "source_reference_count": 0, + "adjudicated_reference_count": 0, + "unchanged_object_count": 0, + "geometry_changed_object_count": 0, + "class_changed_object_count": 0, + "attribute_changed_object_count": 0, + "added_object_count": 0, + "deleted_object_count": 0, + "changed_frame_count": 0, + } + for frame in sorted(session["frames"], key=lambda row: row["truth_island_sequence"]): + sequence = int(frame["truth_island_sequence"]) + source = source_cases[sequence] + source_by_id = {item["object_id"]: item for item in source["references"]} + final_by_id = {item["object_id"]: item for item in frame["objects"]} + changes: list[dict[str, Any]] = [] + for object_id, original in source_by_id.items(): + final = final_by_id.get(object_id) + if final is None: + changes.append({"type": "delete", "object_id": object_id}) + totals["deleted_object_count"] += 1 + continue + changed = False + if original["box_xyxy"] != final["box_xyxy"]: + changes.append( + { + "type": "geometry", + "object_id": object_id, + "before": original["box_xyxy"], + "after": final["box_xyxy"], + } + ) + totals["geometry_changed_object_count"] += 1 + changed = True + if original["category"] != final["category"] or original.get( + "proposed_label" + ) != final.get("proposed_label"): + changes.append( + { + "type": "class", + "object_id": object_id, + "before": { + "category": original["category"], + "proposed_label": original.get("proposed_label"), + }, + "after": { + "category": final["category"], + "proposed_label": final.get("proposed_label"), + }, + } + ) + totals["class_changed_object_count"] += 1 + changed = True + if bool(original["occluded"]) != bool(final["occluded"]) or bool( + original["truncated"] + ) != bool(final["truncated"]): + changes.append({"type": "attributes", "object_id": object_id}) + totals["attribute_changed_object_count"] += 1 + changed = True + if not changed: + totals["unchanged_object_count"] += 1 + for object_id in final_by_id.keys() - source_by_id.keys(): + changes.append({"type": "add", "object_id": object_id}) + totals["added_object_count"] += 1 + if changes: + totals["changed_frame_count"] += 1 + totals["source_reference_count"] += len(source_by_id) + totals["adjudicated_reference_count"] += len(final_by_id) + cases.append( + { + "schema_version": L34F_CASE_SCHEMA, + "truth_island_sequence": sequence, + "image_id": int(source["image_id"]), + "frame_index": int(source["frame_index"]), + "group_id": str(source["group_id"]), + "session_seconds": float(source["session_seconds"]), + "source_image_sha256": str(source["source_image_sha256"]), + "references": frame["objects"], + "source_reference_count": len(source_by_id), + "change_count": len(changes), + "changes": changes, + } + ) + + report_basis = { + "schema_version": L34F_REPORT_SCHEMA, + "status": "completed-candidate-visible-adjudication-not-truth", + "metrics": totals, + "decision": { + "adjudication_complete": True, + "engineering_reference_available": True, + "metric_grade_reference_available": False, + "independent_truth_available": False, + "candidate_accepted": False, + "detector_retuning_authorized": False, + "l35_blind_gate_open": False, + "next_action": ( + "obtain two prediction-free independent reviewer submissions " + "and explicit adjudication before E48 and one-shot L3.5" + ), + }, + "limitations": [ + "candidate predictions were visible during adjudication", + "the artifact is an engineering reference and not independent ground truth", + "the sample contains 32 recorded RIGHT-camera frames on one route", + "no live, left-camera, LiDAR-range, navigation, command, or safety claim is made", + ], + "authority": _AUTHORITY, + "ground_truth": False, + } + identity = { + "schema_version": L34F_RESULT_SCHEMA, + "l34e_diagnostic": { + "result_id": diagnostic["result_id"], + "manifest_sha256": _sha256(diagnostic["result_root"] / L34E_MANIFEST_NAME), + }, + "adjudication_session": { + "session_id": session["session_id"], + "session_sha256": _sha256(session_path), + "revision": session["revision"], + }, + "report_sha256": hashlib.sha256(_canonical_json(report_basis)).hexdigest(), + "cases_sha256": hashlib.sha256(_canonical_json(cases)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"l34f-adjudicated-reference-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_l34f_adjudicated_reference(destination) + created_at_utc = _utc_now() + report = { + **report_basis, + "result_id": result_id, + "identity_sha256": identity_sha256, + "created_at_utc": created_at_utc, + "source_session_id": "RAVNOVES00", + "camera_source_id": "sensor.camera.right", + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / L34F_REPORT_NAME, report) + _write_jsonl(staging / L34F_CASES_NAME, cases) + artifacts = [ + _artifact(staging / L34F_REPORT_NAME, "adjudication-report"), + _artifact(staging / L34F_CASES_NAME, "adjudicated-cases"), + ] + _write_json( + staging / L34F_MANIFEST_NAME, + { + "schema_version": L34F_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": created_at_utc, + "acceptance_state": "engineering-reference-not-truth", + "ground_truth": False, + "artifacts": artifacts, + "authority": _AUTHORITY, + }, + ) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_l34f_adjudicated_reference(destination) + + +def read_l34f_adjudicated_reference(root: Path) -> dict[str, Any]: + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / L34F_MANIFEST_NAME) + identity = manifest.get("identity") + if not isinstance(identity, dict): + raise L34FAdjudicatedReferenceError("L3.4F identity invalid") + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + if ( + manifest.get("schema_version") != L34F_RESULT_SCHEMA + or manifest.get("identity_sha256") != identity_sha256 + or manifest.get("result_id") != f"l34f-adjudicated-reference-{identity_sha256}" + or resolved.name != manifest.get("result_id") + or _RESULT_ID.fullmatch(resolved.name) is None + or manifest.get("acceptance_state") != "engineering-reference-not-truth" + or manifest.get("authority") != _AUTHORITY + or manifest.get("ground_truth") is not False + ): + raise L34FAdjudicatedReferenceError("L3.4F identity invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 2: + raise L34FAdjudicatedReferenceError("L3.4F artifacts invalid") + by_role = {item.get("role"): item for item in artifacts if isinstance(item, dict)} + report = _read_json(_validated_artifact(resolved, by_role.get("adjudication-report"))) + cases = tuple(_read_jsonl(_validated_artifact(resolved, by_role.get("adjudicated-cases")))) + if ( + report.get("schema_version") != L34F_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("status") != "completed-candidate-visible-adjudication-not-truth" + or report.get("authority") != _AUTHORITY + or report.get("ground_truth") is not False + or len(cases) != 32 + or any(case.get("schema_version") != L34F_CASE_SCHEMA for case in cases) + or hashlib.sha256(_canonical_json(cases)).hexdigest() != identity.get("cases_sha256") + ): + raise L34FAdjudicatedReferenceError("L3.4F result changed") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + "cases": cases, + } + + +def _validate_session(session: dict[str, Any], diagnostic_result_id: str) -> None: + frames = session.get("frames") + if ( + session.get("schema_version") != L34F_SESSION_SCHEMA + or not isinstance(session.get("session_id"), str) + or _SESSION_ID.fullmatch(session["session_id"]) is None + or session.get("diagnostic_result_id") != diagnostic_result_id + or session.get("state") != "saved" + or not isinstance(session.get("revision"), int) + or session["revision"] < 1 + or session.get("authority") != _AUTHORITY + or not isinstance(frames, list) + or len(frames) != 32 + or any(frame.get("reviewed") is not True for frame in frames if isinstance(frame, dict)) + ): + raise L34FAdjudicatedReferenceError("adjudication session incomplete") + + +def _artifact(path: Path, role: str) -> dict[str, Any]: + return { + "path": path.name, + "role": role, + "byte_length": path.stat().st_size, + "sha256": _sha256(path), + } + + +def _validated_artifact(root: Path, raw: object) -> Path: + if not isinstance(raw, dict) or not isinstance(raw.get("path"), str): + raise L34FAdjudicatedReferenceError("L3.4F artifact invalid") + path = (root / raw["path"]).resolve(strict=True) + if path.parent != root or path.is_symlink() or _sha256(path) != raw.get("sha256"): + raise L34FAdjudicatedReferenceError("L3.4F artifact changed") + return path + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False + ).encode() + + +def _read_json(path: Path) -> dict[str, Any]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise L34FAdjudicatedReferenceError("expected JSON object") + return value + + +def _read_jsonl(path: Path) -> list[dict[str, Any]]: + values = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line] + if any(not isinstance(value, dict) for value in values): + raise L34FAdjudicatedReferenceError("expected JSONL objects") + return values + + +def _write_json(path: Path, value: object) -> None: + path.write_bytes(_canonical_json(value) + b"\n") + + +def _write_jsonl(path: Path, values: list[dict[str, Any]]) -> None: + path.write_bytes(b"".join(_canonical_json(value) + b"\n" for value in values)) + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def _utc_now() -> str: + return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z") diff --git a/src/k1link/compute/l35_right_yolox_truth_evaluation.py b/src/k1link/compute/l35_right_yolox_truth_evaluation.py new file mode 100644 index 0000000..8766417 --- /dev/null +++ b/src/k1link/compute/l35_right_yolox_truth_evaluation.py @@ -0,0 +1,407 @@ +"""Evaluate the exact L3.4 YOLOX freeze after an accepted E48 truth seal.""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import shutil +import uuid +from datetime import UTC, datetime +from pathlib import Path +from typing import Any, Final + +from .e46_detector_truth_island import ( + E46_MANIFEST_NAME, + E46DetectorTruthIslandError, + read_e46_detector_truth_island, +) +from .e48_detector_truth_seal import ( + E48_MANIFEST_NAME, + E48DetectorTruthSealError, + read_e48_detector_truth_seal, +) +from .e49_detector_truth_evaluation import ( + E49DetectorTruthEvaluationError, + evaluate_frozen_detector_candidates, + read_valid_fov_mask, +) +from .l34_right_yolox_truth_island_freeze import ( + L34_MANIFEST_NAME, + L34_PREDICTION_SCHEMA, + L34RightYoloxTruthIslandError, + read_l34_right_yolox_truth_island_freeze, +) + +L35_RESULT_SCHEMA: Final = "missioncore.l35-right-yolox-truth-evaluation/v1" +L35_REPORT_SCHEMA: Final = "missioncore.l35-right-yolox-evaluation-report/v1" +L35_MANIFEST_NAME: Final = "manifest.json" +L35_REPORT_NAME: Final = "right-yolox-evaluation-report.json" + +_RESULT_ID: Final = re.compile( + r"^l35-right-yolox-truth-evaluation-[a-f0-9]{64}$" +) +_AUTHORITY: Final = { + "commands_enabled": False, + "navigation_or_safety_accepted": False, +} + + +class L35RightYoloxTruthEvaluationError(RuntimeError): + """The L3.4 freeze cannot be joined to accepted independent truth.""" + + +def build_l35_right_yolox_truth_evaluation( + *, + truth_island_root: Path, + truth_seal_root: Path, + l34_freeze_root: Path, + valid_fov_root: Path, + output_root: Path, +) -> dict[str, Any]: + """Join L3.4 to E48 only after truth was sealed after the exact freeze.""" + + try: + truth_island = read_e46_detector_truth_island(truth_island_root) + truth_seal = read_e48_detector_truth_seal(truth_seal_root) + l34_freeze = read_l34_right_yolox_truth_island_freeze(l34_freeze_root) + except ( + E46DetectorTruthIslandError, + E48DetectorTruthSealError, + L34RightYoloxTruthIslandError, + ) as reason: + raise L35RightYoloxTruthEvaluationError( + "L3.5 evaluation input is invalid" + ) from reason + + seal_report = _object(truth_seal.get("report"), "E48 report") + seal_identity = _object( + _object(truth_seal.get("manifest"), "E48 manifest").get("identity"), + "E48 identity", + ) + sealed_island = _object( + seal_identity.get("truth_island"), + "E48 truth island", + ) + l34_identity = _object(l34_freeze.manifest.get("identity"), "L3.4 identity") + l34_island = _object( + l34_identity.get("truth_island"), + "L3.4 truth island", + ) + if ( + seal_report.get("status") != "sealed-adjudicated-independent-truth" + or _object(seal_report.get("decision"), "E48 decision").get( + "candidate_comparison_authorized" + ) + is not True + or sealed_island.get("result_id") != truth_island.result_id + or l34_island.get("result_id") != truth_island.result_id + ): + raise L35RightYoloxTruthEvaluationError( + "truth island, seal and L3.4 identities differ" + ) + provenance = _object(truth_seal.get("provenance"), "E48 provenance") + _require_freeze_before_truth_seal( + freeze_created_at_utc=l34_freeze.manifest.get("created_at_utc"), + truth_sealed_at_utc=provenance.get("sealed_at_utc"), + ) + + source = _object( + _object(truth_island.manifest.get("identity"), "E46 identity").get( + "source" + ), + "E46 source", + ) + try: + valid_fov = read_valid_fov_mask( + valid_fov_root, + calibration_sha256=str(source["calibration_sha256"]), + calibration_slot=str(source["calibration_slot"]), + ) + prediction_rows = l34_rows_for_sealed_truth(l34_freeze.predictions) + metrics = evaluate_frozen_detector_candidates( + truth_rows=tuple(truth_seal["truth_rows"]), + prediction_rows=prediction_rows, + valid_fov_mask=valid_fov["mask"], + ) + except (KeyError, E49DetectorTruthEvaluationError) as reason: + raise L35RightYoloxTruthEvaluationError( + "L3.4 predictions cannot be evaluated against sealed truth" + ) from reason + + profile = { + "profile_id": "l35-ravnoves00-right-yolox-evaluation/v1", + "metric_engine": "e49-frozen-detector-metrics/v1", + "candidate_selection_policy": "no-automatic-winner", + "model_retraining_authorized": False, + } + identity = { + "schema_version": L35_RESULT_SCHEMA, + "truth_island": { + "result_id": truth_island.result_id, + "manifest_sha256": _sha256( + truth_island.result_root / E46_MANIFEST_NAME + ), + }, + "truth_seal": { + "result_id": truth_seal["result_id"], + "manifest_sha256": _sha256( + truth_seal["result_root"] / E48_MANIFEST_NAME + ), + }, + "l34_freeze": { + "result_id": l34_freeze.result_id, + "manifest_sha256": _sha256( + l34_freeze.result_root / L34_MANIFEST_NAME + ), + "prediction_rows_sha256": _object( + l34_identity.get("candidate"), + "L3.4 candidate identity", + )["prediction_rows_sha256"], + }, + "valid_fov": valid_fov["identity"], + "profile": profile, + "metrics_sha256": hashlib.sha256(_canonical_json(metrics)).hexdigest(), + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _AUTHORITY, + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"l35-right-yolox-truth-evaluation-{identity_sha256}" + destination = output_root.expanduser().absolute() / result_id + if destination.exists(): + return read_l35_right_yolox_truth_evaluation(destination) + + report = { + "schema_version": L35_REPORT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "status": "completed-sealed-truth-right-yolox-evaluation", + "frame_count": len(truth_seal["truth_rows"]), + "candidate_count": len(metrics), + "profile": profile, + "candidates": metrics, + "decision": { + "truth_join_performed": True, + "accuracy_metrics_available": True, + "candidate_winner_selected": False, + "model_retraining_authorized": False, + "next_gate": ( + "review the preregistered metrics and explicitly accept or " + "reject the frozen right-camera YOLOX candidate" + ), + }, + "limitations": [ + "source-scoped to the 32-frame RAVNOVES00 right-camera Truth Island", + "recorded replay only; live transport and hardware are out of scope", + "truth evaluation does not authorize navigation or safety claims", + ], + "authority": _AUTHORITY, + } + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / L35_REPORT_NAME, report) + manifest = { + "schema_version": L35_RESULT_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": _utc_now(), + "acceptance_state": "accepted-metrics-only-no-candidate-decision", + "artifacts": [ + _artifact(staging / L35_REPORT_NAME, "evaluation-report") + ], + "authority": _AUTHORITY, + } + _write_json(staging / L35_MANIFEST_NAME, manifest) + os.replace(staging, destination) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return read_l35_right_yolox_truth_evaluation(destination) + + +def l34_rows_for_sealed_truth( + rows: tuple[dict[str, Any], ...], +) -> tuple[dict[str, Any], ...]: + """Adapt immutable L3.4 rows to the shared E49 metric engine contract.""" + + converted: list[dict[str, Any]] = [] + for row in rows: + predictions = row.get("predictions") + if ( + row.get("schema_version") != L34_PREDICTION_SCHEMA + or row.get("truth_joined") is not False + or not isinstance(predictions, list) + ): + raise L35RightYoloxTruthEvaluationError( + "L3.4 prediction row is invalid" + ) + converted.append( + { + "candidate_id": row.get("candidate_id"), + "truth_island_sequence": row.get("truth_island_sequence"), + "image_id": row.get("image_id"), + "frame_index": row.get("frame_index"), + "session_seconds": row.get("session_seconds"), + "source_image_sha256": row.get("source_image_sha256"), + "predictions": [ + { + "category": prediction.get("label"), + "score": prediction.get("score"), + "box_xyxy": prediction.get("bbox_xyxy"), + } + for prediction in predictions + if isinstance(prediction, dict) + ], + "truth_joined": False, + } + ) + if len(converted[-1]["predictions"]) != len(predictions): + raise L35RightYoloxTruthEvaluationError( + "L3.4 prediction entry is invalid" + ) + return tuple(converted) + + +def read_l35_right_yolox_truth_evaluation(root: Path) -> dict[str, Any]: + """Read and revalidate an immutable L3.5 evaluation result.""" + + resolved = root.resolve(strict=True) + manifest = _read_json(resolved / L35_MANIFEST_NAME) + identity = _object(manifest.get("identity"), "L3.5 identity") + identity_sha256 = manifest.get("identity_sha256") + if ( + manifest.get("schema_version") != L35_RESULT_SCHEMA + or not isinstance(identity_sha256, str) + or hashlib.sha256(_canonical_json(identity)).hexdigest() + != identity_sha256 + or manifest.get("result_id") + != f"l35-right-yolox-truth-evaluation-{identity_sha256}" + or not _RESULT_ID.fullmatch(resolved.name) + or resolved.name != manifest.get("result_id") + or manifest.get("acceptance_state") + != "accepted-metrics-only-no-candidate-decision" + or manifest.get("authority") != _AUTHORITY + ): + raise L35RightYoloxTruthEvaluationError("L3.5 identity is invalid") + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list) or len(artifacts) != 1: + raise L35RightYoloxTruthEvaluationError("L3.5 artifacts are invalid") + artifact = _object(artifacts[0], "L3.5 report artifact") + report_path = resolved / str(artifact.get("path")) + if ( + report_path.parent != resolved + or not report_path.is_file() + or report_path.is_symlink() + or artifact.get("byte_length") != report_path.stat().st_size + or artifact.get("sha256") != _sha256(report_path) + ): + raise L35RightYoloxTruthEvaluationError("L3.5 report changed") + report = _read_json(report_path) + if ( + report.get("schema_version") != L35_REPORT_SCHEMA + or report.get("result_id") != resolved.name + or report.get("identity_sha256") != identity_sha256 + or report.get("status") + != "completed-sealed-truth-right-yolox-evaluation" + or hashlib.sha256(_canonical_json(report.get("candidates"))).hexdigest() + != identity.get("metrics_sha256") + ): + raise L35RightYoloxTruthEvaluationError("L3.5 report is invalid") + return { + "result_id": resolved.name, + "result_root": resolved, + "manifest": manifest, + "report": report, + } + + +def _require_freeze_before_truth_seal( + *, + freeze_created_at_utc: object, + truth_sealed_at_utc: object, +) -> None: + freeze = _utc_timestamp(freeze_created_at_utc, "L3.4 created_at_utc") + seal = _utc_timestamp(truth_sealed_at_utc, "E48 sealed_at_utc") + if freeze > seal: + raise L35RightYoloxTruthEvaluationError( + "L3.4 prediction freeze postdates the independent truth seal" + ) + + +def _utc_timestamp(value: object, field: str) -> datetime: + if not isinstance(value, str) or not value.endswith("Z"): + raise L35RightYoloxTruthEvaluationError(f"{field} is invalid") + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as reason: + raise L35RightYoloxTruthEvaluationError(f"{field} is invalid") from reason + if parsed.tzinfo is None: + raise L35RightYoloxTruthEvaluationError(f"{field} is invalid") + return parsed.astimezone(UTC) + + +def _object(value: object, field: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise L35RightYoloxTruthEvaluationError(f"{field} must be an object") + return value + + +def _read_json(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError) as reason: + raise L35RightYoloxTruthEvaluationError( + f"cannot read {path.name}" + ) from reason + return _object(value, path.name) + + +def _canonical_json(value: object) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + allow_nan=False, + ).encode("utf-8") + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _artifact(path: Path, role: str) -> dict[str, Any]: + return { + "path": path.name, + "role": role, + "byte_length": path.stat().st_size, + "sha256": _sha256(path), + } + + +def _write_json(path: Path, value: object) -> None: + path.write_text( + json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + indent=2, + allow_nan=False, + ) + + "\n", + encoding="utf-8", + ) + + +def _utc_now() -> str: + return datetime.now(UTC).isoformat(timespec="milliseconds").replace( + "+00:00", "Z" + ) diff --git a/src/k1link/compute/semantic_geometry_fusion.py b/src/k1link/compute/semantic_geometry_fusion.py index 8ffa3c6..4fe784c 100644 --- a/src/k1link/compute/semantic_geometry_fusion.py +++ b/src/k1link/compute/semantic_geometry_fusion.py @@ -568,21 +568,25 @@ def _semantic_support( reason = "camera-semantic-without-qualified-occupied-lidar-support" if occupied_count: ranges = projected.depths_m[clustered_rows] - range_m = float(np.median(ranges)) + range_estimate_m = float(np.median(ranges)) centroid = np.median(occupied_points, axis=0).astype(np.float64).tolist() height_range = [ float(np.min(point_height_m[occupied_indices])), float(np.max(point_height_m[occupied_indices])), ] else: - range_m = None + range_estimate_m = None centroid = None height_range = None + range_m = range_estimate_m if support_agrees else None base.update( { "geometry_status": status, "geometry_reason": reason, "range_m": range_m, + "range_estimate_m": range_estimate_m, + "range_estimate_available": range_estimate_m is not None, + "range_support_qualified": support_agrees, "occupied_centroid_map_xyz_m": centroid, "occupied_height_range_m": height_range, "support": { @@ -893,6 +897,9 @@ def _empty_geometry(status: str, reason: str) -> dict[str, object]: "geometry_status": status, "geometry_reason": reason, "range_m": None, + "range_estimate_m": None, + "range_estimate_available": False, + "range_support_qualified": False, "occupied_centroid_map_xyz_m": None, "occupied_height_range_m": None, "support": { diff --git a/tests/test_e46a_ai_engineering_preannotation.py b/tests/test_e46a_ai_engineering_preannotation.py new file mode 100644 index 0000000..eff6fa4 --- /dev/null +++ b/tests/test_e46a_ai_engineering_preannotation.py @@ -0,0 +1,325 @@ +from __future__ import annotations + +import copy +import json +from pathlib import Path +from types import SimpleNamespace + +import pytest +from fastapi import APIRouter +from fastapi.routing import APIRoute + +import k1link.compute.e46a_ai_engineering_preannotation as compute +from k1link.web.e46a_ai_engineering_preannotation_api import ( + build_e46a_ai_engineering_preannotation_router, +) +from k1link.web.l34_annotation_api import ( + L34AnnotationCreateRequest, + L34AnnotationFrameRequest, + L34AnnotationObjectRequest, + L34AnnotationSaveRequest, +) + + +def _endpoint(router: APIRouter, path: str, method: str = "GET") -> object: + for route in router.routes: + if ( + isinstance(route, APIRoute) + and route.path == path + and route.methods is not None + and method in route.methods + ): + return route.endpoint + raise AssertionError(f"{method} {path} route is missing") + + +def _build_fixture( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> dict[str, object]: + e46_id = f"e46-detector-truth-island-{'a' * 64}" + e46_root = tmp_path / "e46" / e46_id + e46_root.mkdir(parents=True) + (e46_root / "manifest.json").write_text("{}\n", encoding="utf-8") + references = [] + cases = [] + for sequence in range(1, 33): + source_sha = f"{sequence:064x}" + references.append( + { + "truth_island_sequence": sequence, + "image_id": sequence, + "frame_index": sequence * 100, + "group_id": f"group-{sequence:02d}", + "sha256": source_sha, + } + ) + objects = [ + { + "object_id": f"object-{sequence:02d}-car", + "category": "car", + "proposed_label": None, + "origin": "self_review_seed", + "box_xyxy": [10.0, 20.0, 100.0, 120.0], + "occluded": False, + "truncated": False, + } + ] + if sequence in {5, 29}: + objects.append( + { + "object_id": f"object-{sequence:02d}-custom", + "category": "unmapped", + "proposed_label": ( + "Детская коляска" if sequence == 5 else "Ноутбук" + ), + "origin": "self_review_seed", + "box_xyxy": [120.0, 140.0, 280.0, 320.0], + "occluded": False, + "truncated": sequence == 29, + } + ) + cases.append( + { + "truth_island_sequence": sequence, + "image_id": sequence, + "frame_index": sequence * 100, + "group_id": f"group-{sequence:02d}", + "session_seconds": float(sequence), + "source_image_sha256": source_sha, + "references": objects, + } + ) + (e46_root / "image-references.jsonl").write_text( + "".join(json.dumps(row) + "\n" for row in references), + encoding="utf-8", + ) + l34f_id = f"l34f-adjudicated-reference-{'b' * 64}" + l34f_root = tmp_path / "l34f" / l34f_id + l34f_root.mkdir(parents=True) + (l34f_root / "manifest.json").write_text("{}\n", encoding="utf-8") + e46 = SimpleNamespace( + result_id=e46_id, + result_root=e46_root, + manifest={}, + report={}, + ) + l34f = { + "result_id": l34f_id, + "result_root": l34f_root, + "manifest": {}, + "report": {}, + "cases": tuple(cases), + } + monkeypatch.setattr(compute, "read_e46_detector_truth_island", lambda _: e46) + monkeypatch.setattr(compute, "read_l34f_adjudicated_reference", lambda _: l34f) + return compute.build_e46a_ai_engineering_preannotation( + e46_root=e46_root, + l34f_root=l34f_root, + output_root=tmp_path / "results", + ) + + +def test_e46a_freezes_ai_preannotation_without_truth_authority( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + result = _build_fixture(tmp_path, monkeypatch) + + assert result["report"]["metrics"]["frame_count"] == 32 + assert result["report"]["metrics"]["object_count"] == 34 + assert result["report"]["metrics"]["custom_class_relabel_count"] == 2 + assert result["report"]["metrics"]["independent_review_submission_count"] == 0 + assert result["report"]["decision"]["e48_truth_seal_open"] is False + assert result["manifest"]["authority"]["independent_truth"] is False + custom = { + item["category"] + for case in result["cases"] + for item in case["objects"] + if item["category"] in {"stroller", "laptop"} + } + assert custom == {"stroller", "laptop"} + assert all( + item["category"] != "unmapped" + for case in result["cases"] + for item in case["objects"] + ) + + +def test_e46a_object_level_audit_deletes_snaps_and_relabels( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + result = _build_fixture(tmp_path, monkeypatch) + cases = copy.deepcopy(list(result["cases"])) + rows = tuple( + { + "candidate_id": "geometry-candidate", + "truth_island_sequence": sequence, + "source_image_sha256": cases[sequence - 1][ + "source_image_sha256" + ], + "predictions": [ + { + "category": "car", + "score": 0.95, + "box_xyxy": [12.0, 22.0, 98.0, 118.0], + } + ], + } + for sequence in range(1, 33) + ) + profile = compute.E46AVisualAuditProfile( + profile_id="fixture-object-qa/v1", + geometry_candidate_id="geometry-candidate", + candidate_nms_iou=0.3, + maximum_match_cost=1.3, + expected_source_object_count=34, + expected_final_object_count=33, + expected_geometry_snapped_count=31, + delete_object_ids=("object-01-car",), + category_overrides=(("object-02-car", "heavy_vehicle"),), + ) + + audit = compute._apply_visual_audit( # noqa: SLF001 + cases=cases, + prediction_rows=rows, + profile=profile, + ) + + assert audit["deleted_false_box_count"] == 1 + assert audit["geometry_snapped_object_count"] == 31 + assert audit["source_geometry_retained_object_count"] == 2 + assert cases[0]["objects"] == [] + assert cases[0]["hard_negative"] is True + assert cases[1]["objects"][0]["category"] == "heavy_vehicle" + assert cases[1]["objects"][0]["box_xyxy"] == [12.0, 22.0, 98.0, 118.0] + + +def test_e46a_api_projects_visual_cases_without_truth_escalation( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + result = _build_fixture(tmp_path, monkeypatch) + router = build_e46a_ai_engineering_preannotation_router( + root_provider=lambda: tmp_path / "results" + ) + catalog = _endpoint(router, "/api/v1/laboratory/e46a/results")(limit=1) # type: ignore[operator] + assert catalog["items"][0]["result_id"] == result["result_id"] + assert catalog["items"][0]["ground_truth"] is False + + case = _endpoint( + router, + "/api/v1/laboratory/e46a/results/{result_id}/cases/{sequence}", + )(result_id=result["result_id"], sequence=5) # type: ignore[operator] + assert case["independent_review"] is False + assert case["ground_truth"] is False + assert any(item["category"] == "stroller" for item in case["objects"]) + assert case["camera_url"].endswith("/annotation-source/frames/5/camera") + + +def test_e46a_correction_session_starts_from_editable_ai_seed( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + result = _build_fixture(tmp_path, monkeypatch) + router = build_e46a_ai_engineering_preannotation_router( + root_provider=lambda: tmp_path / "results", + annotation_root_provider=lambda: tmp_path / "annotations", + ) + result_id = str(result["result_id"]) + + source = _endpoint( + router, + "/api/v1/laboratory/e46a/results/{result_id}/annotation-source", + )(result_id=result_id) # type: ignore[operator] + assert source["frame_count"] == 32 + assert source["contract"]["contract_id"] == ( + "e46a-ai-engineering-correction/v1" + ) + assert source["candidate_identity_included"] is True + assert source["candidate_predictions_included"] is False + assert source["prelabels_included"] is False + + seed = _endpoint( + router, + ( + "/api/v1/laboratory/e46a/results/{result_id}" + "/annotation-seed/frames/{sequence}" + ), + )(result_id=result_id, sequence=5) # type: ignore[operator] + custom = next( + item for item in seed["objects"] if item["category"] == "unmapped" + ) + assert custom["proposed_label"] == "Коляска" + assert custom["origin"] == "frozen_candidate_seed" + + create = _endpoint( + router, + "/api/v1/laboratory/e46a/results/{result_id}/annotation-sessions", + "POST", + ) + created = create( # type: ignore[operator] + result_id=result_id, + request=L34AnnotationCreateRequest(idempotency_key="e46a-correction-1"), + ) + assert created["contract_id"] == "e46a-ai-engineering-correction/v1" + assert created["revision"] == 0 + assert created["frames"] == [] + + save = _endpoint( + router, + ( + "/api/v1/laboratory/e46a/results/{result_id}" + "/annotation-sessions/{session_id}" + ), + "PUT", + ) + saved = save( # type: ignore[operator] + result_id=result_id, + session_id=created["session_id"], + request=L34AnnotationSaveRequest( + expected_revision=0, + idempotency_key="e46a-save-1", + title="E46A human correction", + assistance_mode="frozen-candidate-seeded", + frames=[ + L34AnnotationFrameRequest( + truth_island_sequence=5, + reviewed=True, + hard_negative=False, + objects=[ + L34AnnotationObjectRequest( + object_id=str(custom["object_id"]), + category="unmapped", + proposed_label="Коляска", + origin="frozen_candidate_seed", + box_xyxy=[130.0, 150.0, 300.0, 340.0], + occluded=False, + truncated=False, + ) + ], + ) + ], + ), + ) + assert saved["revision"] == 1 + assert saved["frames"][0]["objects"][0]["box_xyxy"] == [ + 130.0, + 150.0, + 300.0, + 340.0, + ] + assert saved["authority"]["ground_truth"] is False + + +def test_e46a_reader_rejects_tampered_cases( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + result = _build_fixture(tmp_path, monkeypatch) + cases_path = Path(result["result_root"]) / compute.E46A_CASES_NAME + cases_path.write_text(cases_path.read_text(encoding="utf-8") + "{}\n", encoding="utf-8") + + with pytest.raises(compute.E46AAiEngineeringPreannotationError): + compute.read_e46a_ai_engineering_preannotation(Path(result["result_root"])) diff --git a/tests/test_e46d_temporal_failure_audit.py b/tests/test_e46d_temporal_failure_audit.py new file mode 100644 index 0000000..3ba5750 --- /dev/null +++ b/tests/test_e46d_temporal_failure_audit.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from k1link.compute.e46d_temporal_failure_audit import analyze_temporal_frames + + +def _object( + track_id: int, + *, + box: list[float] | None = None, + camera_current: bool = True, + world_track_id: int | None = 240001, + motion_state: str = "static", +) -> dict[str, object]: + return { + "bbox_xyxy": box or [100.0, 100.0, 180.0, 220.0], + "category": "car", + "route_track_id": track_id, + "world_track_id": world_track_id, + "motion_state": motion_state, + "camera_evidence_current": camera_current, + } + + +def _frames() -> list[dict[str, object]]: + rows: list[dict[str, object]] = [] + for frame_index in range(30): + rows.append( + { + "frame_index": frame_index, + "session_seconds": 35.0 + frame_index * 0.1, + "objects": [], + } + ) + for frame_index in (0, 1, 2, 6, 7, 8): + rows[frame_index]["objects"] = [_object(10)] + rows[3]["objects"] = [] + rows[4]["objects"] = [] + rows[5]["objects"] = [] + rows[10]["objects"] = [_object(20)] + rows[11]["objects"] = [_object(21)] + rows[12]["objects"] = [_object(30, box=[100.0, 100.0, 180.0, 220.0])] + rows[13]["objects"] = [_object(30, box=[300.0, 100.0, 380.0, 220.0])] + for offset, state in enumerate(("static", "unknown", "static", "unknown", "static")): + rows[15 + offset]["objects"] = [ + _object(40, motion_state=state, world_track_id=240001 + offset % 2) + ] + for frame_index in range(20, 25): + rows[frame_index]["objects"] = [_object(50, camera_current=frame_index in {20, 24})] + for offset, track_id in enumerate((60, 61, 62, 63)): + rows[26 + offset]["objects"] = [_object(track_id)] + return rows + + +def test_e46d_scans_observable_temporal_failures_and_ranks_video_clips() -> None: + signals, clips, metrics = analyze_temporal_frames(_frames()) + kinds = {signal["kind"] for signal in signals} + + assert "layer-blackout" in kinds + assert "route-layer-gap" in kinds + assert "route-id-rebirth-candidate" in kinds + assert "bbox-jump" in kinds + assert "motion-state-flap" in kinds + assert "world-binding-flap" in kinds + assert "short-track-burst" in kinds + assert metrics["temporal_continuity_passed"] is False + assert metrics["failure_signal_count"] == len(signals) + assert metrics["review_clip_count"] == len(clips) + assert clips[0]["priority"] == "critical" + assert all(clip["start_seconds"] <= clip["event_start_seconds"] for clip in clips) + assert all(clip["event_end_seconds"] <= clip["end_seconds"] for clip in clips) + + +def test_e46d_distinguishes_camera_hold_from_route_layer_loss() -> None: + frames = _frames() + signals, _, metrics = analyze_temporal_frames(frames) + + held = [signal for signal in signals if signal["kind"] == "camera-evidence-hold"] + gaps = [signal for signal in signals if signal["kind"] == "route-layer-gap"] + assert held + assert gaps + assert all(signal["evidence"]["camera_evidence_current"] is False for signal in held) + assert all(signal["evidence"]["same_route_identity_returned"] is True for signal in gaps) + assert metrics["detector_hold_episode_count"] == len(held) diff --git a/tests/test_e46e_ready_stack.py b/tests/test_e46e_ready_stack.py new file mode 100644 index 0000000..b3adf0e --- /dev/null +++ b/tests/test_e46e_ready_stack.py @@ -0,0 +1,279 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import pytest + +from k1link.compute.e46e_ready_stack import ( + E46E_RUNTIME_SCHEMA, + E46EReadyStackError, + analyze_e46e_frames, + build_e46e_ready_stack, + read_e46e_ready_stack, +) + + +def test_e46e_projects_stock_deepstream_output_without_custom_tracking( + tmp_path: Path, +) -> None: + source, profile = _source_and_profile(tmp_path, frame_count=6) + raw = _raw_output(tmp_path, profile, frame_count=6) + _detector(raw, 0, "car", (10, 20, 40, 60), 0.91) + _tracker(raw, 0, 7, "car", (10, 20, 40, 60), 0.88) + _tracker(raw, 1, 7, "car", (12, 20, 42, 60), 0.73) + _detector(raw, 5, "person", (100, 80, 130, 160), 0.93) + _tracker(raw, 5, 7, "person", (100, 80, 130, 160), 0.79) + + result = build_e46e_ready_stack( + source_job_root=source, + raw_root=raw, + profile_path=profile, + output_root=tmp_path / "results", + ) + metrics = result["report"]["metrics"] + assert metrics["frame_count"] == 6 + assert metrics["detection_observation_count"] == 2 + assert metrics["track_observation_count"] == 3 + assert metrics["detection_box_clipped_count"] == 0 + assert metrics["track_box_clipped_count"] == 0 + assert metrics["tracker_recovered_frame_count"] == 1 + assert metrics["full_layer_blackout_event_count"] == 1 + assert metrics["route_id_gap_event_count"] == 1 + assert metrics["track_class_switch_count"] == 1 + assert result["frames"][1]["objects"][0]["object_id"] == "nvdcf-7" + assert result["frames"][1]["objects"][0]["bbox"] == [12.0, 20.0, 30.0, 40.0] + assert [ + (component["kind"], component["role"]) + for component in result["report"]["method"]["components"] + ] == [ + ("source", "exact recorded camera evidence"), + ("model", "framewise traffic-object detection"), + ("tool", "official RT-DETR output decoding"), + ("algorithm", "route-local temporal association"), + ("runtime", "GPU inference and media pipeline"), + ] + assert result["report"]["decision"]["custom_temporal_logic_used"] is False + assert result["manifest"]["authority"]["navigation_or_safety_accepted"] is False + + repeated = build_e46e_ready_stack( + source_job_root=source, + raw_root=raw, + profile_path=profile, + output_root=tmp_path / "results", + ) + assert repeated["result_id"] == result["result_id"] + + +def test_e46e_clips_only_display_geometry_at_the_source_plane(tmp_path: Path) -> None: + source, profile = _source_and_profile(tmp_path, frame_count=1) + raw = _raw_output(tmp_path, profile, frame_count=1) + _tracker(raw, 0, 31, "person", (447, 547, 527, 608), 0.69) + + result = build_e46e_ready_stack( + source_job_root=source, + raw_root=raw, + profile_path=profile, + output_root=tmp_path / "results", + ) + + item = result["frames"][0]["objects"][0] + assert item["bbox"] == [447.0, 547.0, 80.0, 53.0] + assert item["source_bbox_ltrb"] == [447.0, 547.0, 527.0, 608.0] + assert item["source_plane_clipped"] is True + assert result["report"]["metrics"]["track_box_clipped_count"] == 1 + assert result["report"]["decision"]["custom_temporal_logic_used"] is False + + +def test_e46e_rejects_tampered_immutable_artifact(tmp_path: Path) -> None: + source, profile = _source_and_profile(tmp_path, frame_count=1) + raw = _raw_output(tmp_path, profile, frame_count=1) + result = build_e46e_ready_stack( + source_job_root=source, + raw_root=raw, + profile_path=profile, + output_root=tmp_path / "results", + ) + (result["result_root"] / "overlay.mp4").write_bytes(b"changed") + with pytest.raises(E46EReadyStackError, match="artifact changed"): + read_e46e_ready_stack(result["result_root"]) + + +def test_e46e_frame_analyzer_requires_contiguous_source_order() -> None: + with pytest.raises(E46EReadyStackError, match="not contiguous"): + analyze_e46e_frames( + [ + { + "frame_index": 1, + "session_seconds": 1.0, + "detections": [], + "objects": [], + } + ] + ) + + +def _source_and_profile(tmp_path: Path, *, frame_count: int) -> tuple[Path, Path]: + source = tmp_path / "source-job" + camera = source / "input" / "camera" / "sensor.camera.right" / "epoch-1" + camera.mkdir(parents=True) + index_rows = [ + { + "schema_version": "missioncore.camera-recording-index/v1", + "kind": "media", + "sequence": sequence, + "session_monotonic_ns": 1_000_000_000 + (sequence - 1) * 100_000_000, + "sha256": hashlib.sha256(f"frame-{sequence}".encode()).hexdigest(), + } + for sequence in range(1, frame_count + 1) + ] + index_path = camera / "index.jsonl" + index_path.write_text( + "".join(json.dumps(row, sort_keys=True) + "\n" for row in index_rows), + encoding="utf-8", + ) + stream_sha = "1" * 64 + summary_path = camera / "summary.json" + summary_path.write_text( + json.dumps( + { + "schema_version": "missioncore.camera-recording/v1", + "stream_sha256": stream_sha, + "segment_count": frame_count, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + index_sha = _sha(index_path) + summary_sha = _sha(summary_path) + job = { + "schema_version": "missioncore.compute-job/v1", + "job_id": "recorded-test", + "input": { + "session_id": "test-session", + "source_id": "sensor.camera.right", + "segment_count": frame_count, + "archive_index_sha256": index_sha, + "archive_summary_sha256": summary_sha, + "timeline": {"start_seconds": 10.0, "end_seconds": 11.0}, + }, + } + (source / "job.json").write_text(json.dumps(job, indent=2) + "\n", encoding="utf-8") + profile_value = { + "schema_version": "missioncore.e46e-ready-stack-profile/v1", + "profile_id": "e46e-test/v1", + "source": { + "camera_source_id": "sensor.camera.right", + "job_id": "recorded-test", + "session_id": "test-session", + "segment_count": frame_count, + "stream_sha256": stream_sha, + "archive_index_sha256": index_sha, + "archive_summary_sha256": summary_sha, + }, + "runtime": { + "container_image": "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:" + + "2" * 64, + "deepstream_version": "9.1", + }, + "detector": { + "name": "NVIDIA TrafficCamNet Transformer Lite", + "version": "test", + "model_sha256": "3" * 64, + "custom_postprocessing": False, + }, + "parser": { + "name": "NVIDIA DeepStream TAO custom bounding-box parser", + "repository": "https://github.com/NVIDIA/DeepStream.git", + "commit": "8" * 40, + "symbol": "NvDsInferParseCustomDDETRTAO", + "library_sha256": "9" * 64, + "custom_mission_core_logic": False, + }, + "tracker": { + "name": "NVIDIA NvDCF", + "configuration": "stock-accuracy", + "custom_association": False, + "custom_hold_or_stitch": False, + }, + "output": {"frame_width": 800, "frame_height": 600}, + "authority": { + "ground_truth": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, + }, + } + profile = tmp_path / "profile.json" + profile.write_text(json.dumps(profile_value, indent=2) + "\n", encoding="utf-8") + return source, profile + + +def _raw_output(tmp_path: Path, profile_path: Path, *, frame_count: int) -> Path: + raw = tmp_path / "raw" + detections = raw / "detections" + tracks = raw / "tracks" + detections.mkdir(parents=True) + tracks.mkdir() + for frame in range(frame_count): + (detections / f"00_000_{frame:06d}.txt").write_text("", encoding="utf-8") + (tracks / f"00_000_{frame:06d}.txt").write_text("", encoding="utf-8") + overlay = raw / "overlay.mp4" + overlay.write_bytes(b"synthetic-overlay") + (raw / "deepstream.log").write_text("synthetic success\n", encoding="utf-8") + profile = json.loads(profile_path.read_text(encoding="utf-8")) + image = profile["runtime"]["container_image"] + runtime = { + "schema_version": E46E_RUNTIME_SCHEMA, + "status": "completed", + "worker_host": "TEST-WORKER-006", + "gpu_name": "Synthetic RTX", + "container_image": image, + "container_image_digest": image.rsplit("@sha256:", 1)[1], + "model_sha256": profile["detector"]["model_sha256"], + "model_engine_sha256": "4" * 64, + "deepstream_config_sha256": "5" * 64, + "detector_config_sha256": "6" * 64, + "parser_library_sha256": profile["parser"]["library_sha256"], + "tracker_config_sha256": "7" * 64, + "input_stream_sha256": profile["source"]["stream_sha256"], + "overlay_sha256": _sha(overlay), + } + (raw / "runtime.json").write_text(json.dumps(runtime, indent=2) + "\n", encoding="utf-8") + return raw + + +def _detector( + raw: Path, + frame: int, + label: str, + box: tuple[int, int, int, int], + confidence: float, +) -> None: + left, top, right, bottom = box + (raw / "detections" / f"00_000_{frame:06d}.txt").write_text( + f"{label} 0.0 0 0.0 {left} {top} {right} {bottom} 0 0 0 0 0 0 0 {confidence}\n", + encoding="utf-8", + ) + + +def _tracker( + raw: Path, + frame: int, + track_id: int, + label: str, + box: tuple[int, int, int, int], + confidence: float, +) -> None: + left, top, right, bottom = box + (raw / "tracks" / f"00_000_{frame:06d}.txt").write_text( + f"{label} {track_id} 0.0 0 0.0 {left} {top} {right} {bottom} 0 0 0 0 0 0 0 {confidence}\n", + encoding="utf-8", + ) + + +def _sha(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() diff --git a/tests/test_e46e_worker_package.py b/tests/test_e46e_worker_package.py new file mode 100644 index 0000000..a4134e9 --- /dev/null +++ b/tests/test_e46e_worker_package.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import hashlib +import importlib.util +import json +import sys +from pathlib import Path + +import pytest + + +def _module() -> object: + path = ( + Path(__file__).resolve().parents[1] + / "experiments" + / "perception" + / "prepare_e46e_worker_package.py" + ) + spec = importlib.util.spec_from_file_location("e46e_worker_package_test", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_e46e_package_is_minimal_deterministic_and_content_addressed( + tmp_path: Path, +) -> None: + module = _module() + repository = Path(__file__).resolve().parents[1] + profile, parser_library = _package_inputs(tmp_path, repository) + kwargs = { + "repository_root": repository, + "profile_path": profile, + "parser_library_path": parser_library, + "output_root": tmp_path, + } + package = module.build_e46e_worker_package(**kwargs) + repeated = module.build_e46e_worker_package(**kwargs) + manifest = module.validate_e46e_worker_package(package) + assert repeated == package + assert package.name == f"e46e-worker-package-{manifest['identity_sha256']}" + assert manifest["identity"]["classification"] == ( + "minimal-stock-nvidia-recorded-right-worker-package" + ) + assert manifest["identity"]["tracker"]["custom_association"] is False + assert manifest["identity"]["tracker"]["custom_hold_or_stitch"] is False + assert manifest["identity"]["parser"]["custom_mission_core_logic"] is False + assert len(manifest["artifacts"]) == 13 + + +def test_e46e_package_rejects_unexpected_member(tmp_path: Path) -> None: + module = _module() + repository = Path(__file__).resolve().parents[1] + profile, parser_library = _package_inputs(tmp_path, repository) + package = module.build_e46e_worker_package( + repository_root=repository, + profile_path=profile, + parser_library_path=parser_library, + output_root=tmp_path, + ) + (package / "unexpected.txt").write_text("not admitted\n", encoding="utf-8") + with pytest.raises(module.E46EWorkerPackageError, match="file set changed"): + module.validate_e46e_worker_package(package) + + +def _package_inputs(tmp_path: Path, repository: Path) -> tuple[Path, Path]: + parser_library = tmp_path / "libnvds_infercustomparser_tao.so" + parser_library.write_bytes(b"synthetic official parser fixture") + profile_value = json.loads( + ( + repository + / "experiments" + / "perception" + / "e46e_ready_stack_profile.json" + ).read_text(encoding="utf-8") + ) + profile_value["parser"]["library_sha256"] = hashlib.sha256( + parser_library.read_bytes() + ).hexdigest() + profile = tmp_path / "profile.json" + profile.write_text(json.dumps(profile_value, indent=2) + "\n", encoding="utf-8") + return profile, parser_library diff --git a/tests/test_e46f_dashcam_bakeoff.py b/tests/test_e46f_dashcam_bakeoff.py new file mode 100644 index 0000000..94764dc --- /dev/null +++ b/tests/test_e46f_dashcam_bakeoff.py @@ -0,0 +1,227 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import pytest + +from k1link.compute.e46f_dashcam_bakeoff import ( + E46F_RUNTIME_SCHEMA, + E46FDashCamBakeoffError, + build_e46f_dashcam_bakeoff, + read_e46f_dashcam_bakeoff, +) + + +def test_e46f_freezes_only_the_stock_detector_change(tmp_path: Path) -> None: + source, profile = _source_and_profile(tmp_path, frame_count=3) + raw = _raw_output(tmp_path, profile, frame_count=3) + _observation(raw, "detections", 0, "car", (10, 20, 40, 60), 0.91) + _observation(raw, "tracks", 0, "car", (10, 20, 40, 60), 0.88, track_id=7) + _observation(raw, "tracks", 1, "car", (12, 20, 42, 60), 0.73, track_id=7) + _observation(raw, "detections", 2, "person", (100, 80, 130, 160), 0.93) + _observation(raw, "tracks", 2, "person", (100, 80, 130, 160), 0.79, track_id=8) + + result = build_e46f_dashcam_bakeoff( + source_job_root=source, + raw_root=raw, + profile_path=profile, + output_root=tmp_path / "results", + ) + + metrics = result["report"]["metrics"] + assert metrics["frame_count"] == 3 + assert metrics["detection_observation_count"] == 2 + assert metrics["track_observation_count"] == 3 + assert metrics["tracker_recovered_frame_count"] == 1 + assert result["frames"][0]["detections"][0]["provenance"] == ("nvidia-dashcamnet-detectnet-v2") + assert result["report"]["comparison_contract"] == { + "baseline_result_id": f"e46e-ready-stack-{'a' * 64}", + "controlled_change": "detector-only", + "held_constant": ["recorded RIGHT source", "DeepStream", "FP16", "NvDCF"], + } + assert result["report"]["decision"]["custom_temporal_logic_used"] is False + assert result["manifest"]["authority"]["navigation_or_safety_accepted"] is False + + repeated = build_e46f_dashcam_bakeoff( + source_job_root=source, + raw_root=raw, + profile_path=profile, + output_root=tmp_path / "results", + ) + assert repeated["result_id"] == result["result_id"] + + +def test_e46f_rejects_tampered_immutable_video(tmp_path: Path) -> None: + source, profile = _source_and_profile(tmp_path, frame_count=1) + raw = _raw_output(tmp_path, profile, frame_count=1) + result = build_e46f_dashcam_bakeoff( + source_job_root=source, + raw_root=raw, + profile_path=profile, + output_root=tmp_path / "results", + ) + (result["result_root"] / "overlay.mp4").write_bytes(b"changed") + with pytest.raises(E46FDashCamBakeoffError, match="artifact changed"): + read_e46f_dashcam_bakeoff(result["result_root"]) + + +def _source_and_profile(tmp_path: Path, *, frame_count: int) -> tuple[Path, Path]: + source = tmp_path / "source-job" + camera = source / "input" / "camera" / "sensor.camera.right" / "epoch-1" + camera.mkdir(parents=True) + rows = [ + { + "schema_version": "missioncore.camera-recording-index/v1", + "kind": "media", + "sequence": sequence, + "session_monotonic_ns": 1_000_000_000 + (sequence - 1) * 100_000_000, + "sha256": hashlib.sha256(f"frame-{sequence}".encode()).hexdigest(), + } + for sequence in range(1, frame_count + 1) + ] + index_path = camera / "index.jsonl" + index_path.write_text( + "".join(json.dumps(row, sort_keys=True) + "\n" for row in rows), + encoding="utf-8", + ) + stream_sha = "1" * 64 + summary_path = camera / "summary.json" + summary_path.write_text( + json.dumps( + { + "schema_version": "missioncore.camera-recording/v1", + "stream_sha256": stream_sha, + "segment_count": frame_count, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + index_sha = _sha(index_path) + summary_sha = _sha(summary_path) + (source / "job.json").write_text( + json.dumps( + { + "schema_version": "missioncore.compute-job/v1", + "job_id": "recorded-test", + "input": { + "session_id": "test-session", + "source_id": "sensor.camera.right", + "segment_count": frame_count, + "archive_index_sha256": index_sha, + "archive_summary_sha256": summary_sha, + "timeline": {"start_seconds": 10.0, "end_seconds": 11.0}, + }, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + profile_value = { + "schema_version": "missioncore.e46f-dashcam-bakeoff-profile/v1", + "profile_id": "e46f-test/v1", + "comparison_contract": { + "baseline_result_id": f"e46e-ready-stack-{'a' * 64}", + "controlled_change": "detector-only", + "held_constant": ["recorded RIGHT source", "DeepStream", "FP16", "NvDCF"], + }, + "source": { + "camera_source_id": "sensor.camera.right", + "job_id": "recorded-test", + "session_id": "test-session", + "segment_count": frame_count, + "stream_sha256": stream_sha, + "archive_index_sha256": index_sha, + "archive_summary_sha256": summary_sha, + }, + "runtime": { + "container_image": "nvcr.io/nvidia/deepstream:9.1-samples-multiarch@sha256:" + "2" * 64, + "deepstream_version": "9.1", + }, + "detector": { + "name": "NVIDIA DashCamNet", + "version": "pruned_onnx_v1.0.4", + "model_sha256": "3" * 64, + "custom_postprocessing": False, + }, + "postprocessor": { + "name": "NVIDIA DeepStream built-in DetectNet_v2 parser and NMS", + "cluster_mode": "NMS", + "reference_commit": "4" * 40, + "reference_config_sha256": "5" * 64, + "custom_mission_core_logic": False, + }, + "tracker": { + "name": "NVIDIA NvDCF", + "configuration": "stock-performance", + "custom_association": False, + "custom_hold_or_stitch": False, + }, + "output": {"frame_width": 800, "frame_height": 600}, + "authority": { + "ground_truth": False, + "candidate_accepted": False, + "commands_enabled": False, + "navigation_or_safety_accepted": False, + }, + } + profile = tmp_path / "profile.json" + profile.write_text(json.dumps(profile_value, indent=2) + "\n", encoding="utf-8") + return source, profile + + +def _raw_output(tmp_path: Path, profile_path: Path, *, frame_count: int) -> Path: + raw = tmp_path / "raw" + for name in ("detections", "tracks"): + directory = raw / name + directory.mkdir(parents=True, exist_ok=True) + for frame in range(frame_count): + (directory / f"00_000_{frame:06d}.txt").write_text("", encoding="utf-8") + overlay = raw / "overlay.mp4" + overlay.write_bytes(b"synthetic-overlay") + (raw / "deepstream.log").write_text("synthetic success\n", encoding="utf-8") + profile = json.loads(profile_path.read_text(encoding="utf-8")) + image = profile["runtime"]["container_image"] + runtime = { + "schema_version": E46F_RUNTIME_SCHEMA, + "status": "completed", + "worker_host": "TEST-WORKER-006", + "gpu_name": "Synthetic RTX", + "container_image": image, + "container_image_digest": image.rsplit("@sha256:", 1)[1], + "model_sha256": profile["detector"]["model_sha256"], + "model_engine_sha256": "6" * 64, + "deepstream_config_sha256": "7" * 64, + "detector_config_sha256": "8" * 64, + "tracker_config_sha256": "9" * 64, + "input_stream_sha256": profile["source"]["stream_sha256"], + "overlay_sha256": _sha(overlay), + } + (raw / "runtime.json").write_text(json.dumps(runtime, indent=2) + "\n", encoding="utf-8") + return raw + + +def _observation( + raw: Path, + directory: str, + frame: int, + label: str, + box: tuple[int, int, int, int], + confidence: float, + *, + track_id: int | None = None, +) -> None: + left, top, right, bottom = box + identity = "" if track_id is None else f" {track_id}" + (raw / directory / f"00_000_{frame:06d}.txt").write_text( + f"{label}{identity} 0.0 0 0.0 {left} {top} {right} {bottom} 0 0 0 0 0 0 0 {confidence}\n", + encoding="utf-8", + ) + + +def _sha(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() diff --git a/tests/test_e46f_worker_package.py b/tests/test_e46f_worker_package.py new file mode 100644 index 0000000..9f4db0a --- /dev/null +++ b/tests/test_e46f_worker_package.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import pytest + + +def _module() -> object: + path = ( + Path(__file__).resolve().parents[1] + / "experiments" + / "perception" + / "prepare_e46f_worker_package.py" + ) + spec = importlib.util.spec_from_file_location("e46f_worker_package_test", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_e46f_package_is_minimal_deterministic_and_detector_only(tmp_path: Path) -> None: + module = _module() + repository = Path(__file__).resolve().parents[1] + kwargs = { + "repository_root": repository, + "profile_path": repository + / "experiments" + / "perception" + / "e46f_dashcam_bakeoff_profile.json", + "output_root": tmp_path, + } + package = module.build_e46f_worker_package(**kwargs) + repeated = module.build_e46f_worker_package(**kwargs) + manifest = module.validate_e46f_worker_package(package) + assert repeated == package + assert package.name == f"e46f-worker-package-{manifest['identity_sha256']}" + assert manifest["identity"]["classification"] == ( + "minimal-stock-nvidia-detector-only-bakeoff-package" + ) + assert manifest["identity"]["baseline_result_id"].startswith("e46e-ready-stack-") + assert manifest["identity"]["tracker"]["custom_association"] is False + assert manifest["identity"]["tracker"]["custom_hold_or_stitch"] is False + assert manifest["identity"]["postprocessor"]["custom_mission_core_logic"] is False + assert len(manifest["artifacts"]) == 11 + + +def test_e46f_package_rejects_unexpected_member(tmp_path: Path) -> None: + module = _module() + repository = Path(__file__).resolve().parents[1] + package = module.build_e46f_worker_package( + repository_root=repository, + profile_path=repository + / "experiments" + / "perception" + / "e46f_dashcam_bakeoff_profile.json", + output_root=tmp_path, + ) + (package / "unexpected.txt").write_text("not admitted\n", encoding="utf-8") + with pytest.raises(module.E46FWorkerPackageError, match="file set changed"): + module.validate_e46f_worker_package(package) diff --git a/tests/test_e46g_rectified_detector_bakeoff.py b/tests/test_e46g_rectified_detector_bakeoff.py new file mode 100644 index 0000000..fe25866 --- /dev/null +++ b/tests/test_e46g_rectified_detector_bakeoff.py @@ -0,0 +1,258 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import pytest + +from k1link.compute.e46g_rectified_detector_bakeoff import ( + E46G_RUNTIME_SCHEMA, + E46GRectifiedDetectorBakeoffError, + build_e46g_rectified_detector_bakeoff, + read_e46g_rectified_detector_bakeoff, +) + + +def test_e46g_freezes_same_calibrated_views_for_both_stock_detectors( + tmp_path: Path, +) -> None: + source, profile = _source_and_profile(tmp_path) + raw = _raw_output(tmp_path, profile) + + result = build_e46g_rectified_detector_bakeoff( + source_job_root=source, + raw_root=raw, + profile_path=profile, + output_root=tmp_path / "results", + ) + + report = result["report"] + assert report["status"] == "completed-awaiting-visual-semantic-adjudication" + assert report["acceptance"]["official_nvidia_dewarper_executed"] is True + assert report["acceptance"]["same_views_and_frames_for_both_candidates"] is True + assert report["decision"]["automatic_winner_selected"] is False + assert report["decision"]["custom_detector_or_tracker_logic_used"] is False + assert {component["kind"] for component in report["method"]["components"]} <= { + "source", + "tool", + "model", + "algorithm", + "runtime", + } + assert set(report["metrics"]) == {"trafficcamnet", "dashcamnet"} + assert report["metrics"]["trafficcamnet"]["view_frame_count"] == 6 + assert report["metrics"]["trafficcamnet"]["track_observation_count"] == 3 + assert report["metrics"]["dashcamnet"]["track_observation_count"] == 3 + assert result["frames"]["trafficcamnet-front"][0]["source_frame_index"] == 0 + assert ( + result["frames"]["dashcamnet-right"][0]["objects"][0]["object_id"] + == "dashcamnet-right-nvdcf-7" + ) + assert result["manifest"]["authority"]["candidate_accepted"] is False + + repeated = build_e46g_rectified_detector_bakeoff( + source_job_root=source, + raw_root=raw, + profile_path=profile, + output_root=tmp_path / "results", + ) + assert repeated["result_id"] == result["result_id"] + + +def test_e46g_rejects_tampered_comparison_video(tmp_path: Path) -> None: + source, profile = _source_and_profile(tmp_path) + raw = _raw_output(tmp_path, profile) + result = build_e46g_rectified_detector_bakeoff( + source_job_root=source, + raw_root=raw, + profile_path=profile, + output_root=tmp_path / "results", + ) + result["comparison_paths"]["trafficcamnet"].write_bytes(b"changed") + with pytest.raises(E46GRectifiedDetectorBakeoffError, match="artifact changed"): + read_e46g_rectified_detector_bakeoff(result["result_root"]) + + +def _source_and_profile(tmp_path: Path) -> tuple[Path, Path]: + repository_profile = ( + Path(__file__).resolve().parents[1] + / "experiments" + / "perception" + / "e46g_rectified_detector_bakeoff_profile.json" + ) + profile_value = json.loads(repository_profile.read_text(encoding="utf-8")) + source = tmp_path / "source-job" + camera = source / "input" / "camera" / "sensor.camera.right" / "epoch-1" + camera.mkdir(parents=True) + rows = [ + { + "schema_version": "missioncore.camera-recording-index/v1", + "kind": "media", + "sequence": sequence, + "session_monotonic_ns": 1_000_000_000 + (sequence - 1) * 100_000_000, + "sha256": hashlib.sha256(f"frame-{sequence}".encode()).hexdigest(), + } + for sequence in range(1, 4490) + ] + index_path = camera / "index.jsonl" + index_path.write_text( + "".join(json.dumps(row, sort_keys=True) + "\n" for row in rows), + encoding="utf-8", + ) + summary_path = camera / "summary.json" + summary_path.write_text( + json.dumps( + { + "schema_version": "missioncore.camera-recording/v1", + "stream_sha256": "1" * 64, + "segment_count": 4489, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + profile_value["source"].update( + { + "job_id": "recorded-test", + "session_id": "test-session", + "stream_sha256": "1" * 64, + "archive_index_sha256": _sha(index_path), + "archive_summary_sha256": _sha(summary_path), + } + ) + profile_value["selection"].update( + { + "first_source_frame_index": 0, + "last_source_frame_index": 1, + "frame_count": 2, + } + ) + (source / "job.json").write_text( + json.dumps( + { + "schema_version": "missioncore.compute-job/v1", + "job_id": "recorded-test", + "input": { + "session_id": "test-session", + "source_id": "sensor.camera.right", + "segment_count": 4489, + "archive_index_sha256": _sha(index_path), + "archive_summary_sha256": _sha(summary_path), + "timeline": {"start_seconds": 10.0, "end_seconds": 459.0}, + }, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + profile = tmp_path / "profile.json" + profile.write_text(json.dumps(profile_value, indent=2) + "\n", encoding="utf-8") + return source, profile + + +def _raw_output(tmp_path: Path, profile_path: Path) -> Path: + profile = json.loads(profile_path.read_text(encoding="utf-8")) + raw = tmp_path / "raw" + geometry_runtime: dict[str, object] = {} + for view in ("left", "front", "right"): + full = _file(raw / "geometry" / f"{view}.mp4", f"full-{view}") + sample = _file(raw / "samples" / f"{view}.mp4", f"sample-{view}") + geometry_runtime[view] = { + "dewarper_config_sha256": profile["rectification"]["views"][view]["config_sha256"], + "dewarper_log_sha256": "2" * 64, + "full_rectified_video_path": f"geometry/{view}.mp4", + "full_rectified_video_sha256": _sha(full), + "sample_video_path": f"samples/{view}.mp4", + "sample_video_sha256": _sha(sample), + "full_frame_count": profile["rectification"]["expected_full_frame_count"], + "retained_source_frame_index_range": profile["rectification"][ + "retained_source_frame_index_range" + ], + "excluded_source_tail_frame_count": profile["rectification"][ + "excluded_source_tail_frame_count" + ], + "sample_frame_count": 2, + } + candidates_runtime: dict[str, object] = {} + for candidate in ("trafficcamnet", "dashcamnet"): + runs: dict[str, object] = {} + for view in ("left", "front", "right"): + root = raw / "runs" / candidate / view + for directory in ("detections", "tracks"): + (root / directory).mkdir(parents=True, exist_ok=True) + for frame in range(2): + target = root / directory / f"00_000_{frame:06d}.txt" + target.write_text( + _kitti_row(directory == "tracks") if frame == 0 else "", + encoding="utf-8", + ) + overlay = _file(root / "overlay.mp4", f"overlay-{candidate}-{view}") + log = _file(root / "deepstream.log", "success") + runs[view] = { + "overlay_path": f"runs/{candidate}/{view}/overlay.mp4", + "overlay_sha256": _sha(overlay), + "deepstream_log_path": f"runs/{candidate}/{view}/deepstream.log", + "deepstream_log_sha256": _sha(log), + "tracker_config_sha256": "3" * 64, + "model_engine_sha256": "4" * 64, + "frame_count": 2, + "deepstream_exit_code": 0, + } + candidates_runtime[candidate] = { + "model_sha256": profile["candidates"][candidate]["model_sha256"], + "deepstream_app_config_sha256": profile["candidates"][candidate][ + "deepstream_app_config_sha256" + ], + "detector_config_sha256": profile["candidates"][candidate]["detector_config_sha256"], + "parser_library_sha256": ( + profile["trafficcamnet_parser"]["library_sha256"] + if candidate == "trafficcamnet" + else None + ), + "runs": runs, + } + comparison_runtime: dict[str, object] = {} + for candidate in ("trafficcamnet", "dashcamnet"): + video = _file(raw / "comparison" / f"{candidate}.mp4", candidate) + comparison_runtime[candidate] = { + "video_path": f"comparison/{candidate}.mp4", + "video_sha256": _sha(video), + "frame_count": 2, + "view_order": ["left", "front", "right"], + } + _file(raw / "worker.log", "completed") + image = profile["runtime"]["container_image"] + runtime = { + "schema_version": E46G_RUNTIME_SCHEMA, + "status": "completed", + "worker_host": "TEST-WORKER-006", + "gpu_name": "Synthetic RTX", + "container_image": image, + "container_image_digest": image.rsplit("@sha256:", 1)[1], + "source_stream_sha256": profile["source"]["stream_sha256"], + "first_source_frame_index": 0, + "sample_frame_count": 2, + "geometry": geometry_runtime, + "candidates": candidates_runtime, + "comparison": comparison_runtime, + } + (raw / "runtime.json").write_text(json.dumps(runtime, indent=2) + "\n", encoding="utf-8") + return raw + + +def _file(path: Path, value: str) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(value.encode()) + return path + + +def _kitti_row(tracked: bool) -> str: + identity = " 7" if tracked else "" + return f"car{identity} 0.0 0 0.0 10 20 300 400 0 0 0 0 0 0 0 0.91\n" + + +def _sha(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() diff --git a/tests/test_e46g_worker_package.py b/tests/test_e46g_worker_package.py new file mode 100644 index 0000000..6f8a3bc --- /dev/null +++ b/tests/test_e46g_worker_package.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import pytest + + +def _module() -> object: + path = ( + Path(__file__).resolve().parents[1] + / "experiments" + / "perception" + / "prepare_e46g_worker_package.py" + ) + spec = importlib.util.spec_from_file_location("e46g_worker_package_test", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_e46g_package_is_deterministic_calibrated_and_stock(tmp_path: Path) -> None: + module = _module() + repository = Path(__file__).resolve().parents[1] + kwargs = { + "repository_root": repository, + "profile_path": repository + / "experiments" + / "perception" + / "e46g_rectified_detector_bakeoff_profile.json", + "parser_library_path": repository + / ".runtime" + / "compute-experiments" + / "e46e" + / "parser" + / "a18d85dae674a088549c5f9b8fda53c640f4fcbd88a41f4c2cb1f4e3ea8878ee" + / "libnvds_infercustomparser_tao.so", + "output_root": tmp_path, + } + package = module.build_e46g_worker_package(**kwargs) + repeated = module.build_e46g_worker_package(**kwargs) + manifest = module.validate_e46g_worker_package(package) + assert repeated == package + assert package.name == f"e46g-worker-package-{manifest['identity_sha256']}" + identity = manifest["identity"] + assert identity["classification"] == ( + "minimal-factory-kb4-stock-nvidia-detector-bakeoff-package" + ) + assert identity["calibration"]["calibration_sha256"] == ( + "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9" + ) + assert identity["rectification"]["provider"] == "NVIDIA Gst-nvdewarper" + assert identity["rectification"]["expected_full_frame_count"] == 4488 + assert identity["rectification"]["retained_source_frame_index_range"] == [0, 4487] + assert identity["selection"]["frame_count"] == 600 + assert all( + candidate["custom_postprocessing"] is False for candidate in identity["candidates"].values() + ) + assert identity["tracker"]["custom_association"] is False + assert identity["tracker"]["custom_hold_or_stitch"] is False + assert len(manifest["artifacts"]) == 19 + + +def test_e46g_package_rejects_unexpected_member(tmp_path: Path) -> None: + module = _module() + repository = Path(__file__).resolve().parents[1] + package = module.build_e46g_worker_package( + repository_root=repository, + profile_path=repository + / "experiments" + / "perception" + / "e46g_rectified_detector_bakeoff_profile.json", + parser_library_path=repository + / ".runtime" + / "compute-experiments" + / "e46e" + / "parser" + / "a18d85dae674a088549c5f9b8fda53c640f4fcbd88a41f4c2cb1f4e3ea8878ee" + / "libnvds_infercustomparser_tao.so", + output_root=tmp_path, + ) + (package / "unexpected.txt").write_text("not admitted\n", encoding="utf-8") + with pytest.raises(module.E46GWorkerPackageError, match="file set changed"): + module.validate_e46g_worker_package(package) diff --git a/tests/test_e46h_full_rectified_front_replay.py b/tests/test_e46h_full_rectified_front_replay.py new file mode 100644 index 0000000..dba9594 --- /dev/null +++ b/tests/test_e46h_full_rectified_front_replay.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import pytest + +from k1link.compute.e46h_full_rectified_front_replay import ( + E46H_RUNTIME_SCHEMA, + E46HFullRectifiedFrontReplayError, + build_e46h_full_rectified_front_replay, + read_e46h_full_rectified_front_replay, +) + + +def test_e46h_freezes_full_retained_front_route(tmp_path: Path) -> None: + source, profile = _source_and_profile(tmp_path) + raw = _raw_output(tmp_path, profile) + result = build_e46h_full_rectified_front_replay( + source_job_root=source, + raw_root=raw, + profile_path=profile, + output_root=tmp_path / "results", + ) + + report = result["report"] + assert report["status"] == "completed-awaiting-full-route-visual-review" + assert report["metrics"]["frame_count"] == 4488 + assert report["acceptance"]["retained_route_accounted"] is True + assert report["acceptance"]["terminal_source_frame_excluded"] is True + assert report["acceptance"]["full_visual_review_completed"] is False + assert report["decision"]["selected_provider"] == "front-trafficcamnet-stock-nvdcf" + assert report["decision"]["custom_detector_or_tracker_logic_used"] is False + assert result["frames"][0]["source_frame_index"] == 0 + assert result["frames"][-1]["source_frame_index"] == 4487 + assert result["manifest"]["authority"]["candidate_accepted"] is False + + result["overlay_path"].write_bytes(b"changed") + with pytest.raises(E46HFullRectifiedFrontReplayError, match="artifact changed"): + read_e46h_full_rectified_front_replay(result["result_root"]) + + +def _source_and_profile(tmp_path: Path) -> tuple[Path, Path]: + repository = Path(__file__).resolve().parents[1] + profile_value = json.loads( + ( + repository + / "experiments" + / "perception" + / "e46h_full_rectified_front_replay_profile.json" + ).read_text(encoding="utf-8") + ) + source = tmp_path / "source-job" + camera = source / "input" / "camera" / "sensor.camera.right" / "epoch-1" + camera.mkdir(parents=True) + rows = [ + { + "schema_version": "missioncore.camera-recording-index/v1", + "kind": "media", + "sequence": sequence, + "session_monotonic_ns": 1_000_000_000 + (sequence - 1) * 100_000_000, + "sha256": hashlib.sha256(f"frame-{sequence}".encode()).hexdigest(), + } + for sequence in range(1, 4490) + ] + index_path = camera / "index.jsonl" + index_path.write_text( + "".join(json.dumps(row, sort_keys=True) + "\n" for row in rows), + encoding="utf-8", + ) + summary_path = camera / "summary.json" + summary_path.write_text( + json.dumps({"stream_sha256": "1" * 64, "segment_count": 4489}) + "\n", + encoding="utf-8", + ) + profile_value["source"].update( + { + "job_id": "recorded-test", + "session_id": "test-session", + "stream_sha256": "1" * 64, + "archive_index_sha256": _sha(index_path), + "archive_summary_sha256": _sha(summary_path), + } + ) + (source / "job.json").write_text( + json.dumps( + { + "schema_version": "missioncore.compute-job/v1", + "job_id": "recorded-test", + "input": { + "session_id": "test-session", + "source_id": "sensor.camera.right", + "segment_count": 4489, + "archive_index_sha256": _sha(index_path), + "archive_summary_sha256": _sha(summary_path), + "timeline": {"start_seconds": 10.0, "end_seconds": 459.0}, + }, + } + ) + + "\n", + encoding="utf-8", + ) + profile = tmp_path / "profile.json" + profile.write_text(json.dumps(profile_value) + "\n", encoding="utf-8") + return source, profile + + +def _raw_output(tmp_path: Path, profile_path: Path) -> Path: + profile = json.loads(profile_path.read_text(encoding="utf-8")) + raw = tmp_path / "raw" + front = _file(raw / "geometry" / "front.mp4", "front") + front_log = _file(raw / "geometry" / "front.log", "dewarper") + overlay = _file(raw / "run" / "overlay.mp4", "overlay") + deepstream_log = _file(raw / "run" / "deepstream.log", "deepstream") + _file(raw / "worker.log", "completed") + for directory in (raw / "run" / "detections", raw / "run" / "tracks"): + directory.mkdir(parents=True) + for frame in range(4488): + (directory / f"00_000_{frame:06d}.txt").touch() + image = profile["runtime"]["container_image"] + runtime = { + "schema_version": E46H_RUNTIME_SCHEMA, + "status": "completed", + "worker_host": "TEST-WORKER-006", + "gpu_name": "Synthetic RTX", + "container_image": image, + "container_image_digest": image.rsplit("@sha256:", 1)[1], + "source_stream_sha256": profile["source"]["stream_sha256"], + "frame_count": 4488, + "retained_source_frame_index_range": [0, 4487], + "geometry": { + "video_path": "geometry/front.mp4", + "video_sha256": _sha(front), + "log_path": "geometry/front.log", + "log_sha256": _sha(front_log), + "config_sha256": profile["rectification"]["config_sha256"], + "frame_count": 4488, + }, + "run": { + "overlay_path": "run/overlay.mp4", + "overlay_sha256": _sha(overlay), + "deepstream_log_path": "run/deepstream.log", + "deepstream_log_sha256": _sha(deepstream_log), + "model_sha256": profile["detector"]["model_sha256"], + "model_engine_sha256": "2" * 64, + "parser_library_sha256": profile["parser"]["library_sha256"], + "deepstream_app_config_sha256": profile["detector"][ + "deepstream_app_config_sha256" + ], + "detector_config_sha256": profile["detector"]["detector_config_sha256"], + "tracker_config_sha256": "3" * 64, + "frame_count": 4488, + }, + } + (raw / "runtime.json").write_text(json.dumps(runtime) + "\n", encoding="utf-8") + return raw + + +def _file(path: Path, value: str) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(value, encoding="utf-8") + return path + + +def _sha(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() diff --git a/tests/test_e46h_worker_package.py b/tests/test_e46h_worker_package.py new file mode 100644 index 0000000..92b2b6f --- /dev/null +++ b/tests/test_e46h_worker_package.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + + +def _module() -> object: + path = ( + Path(__file__).resolve().parents[1] + / "experiments" + / "perception" + / "prepare_e46h_worker_package.py" + ) + spec = importlib.util.spec_from_file_location("e46h_worker_package_test", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_e46h_package_is_deterministic_front_only_and_stock(tmp_path: Path) -> None: + module = _module() + repository = Path(__file__).resolve().parents[1] + kwargs = { + "repository_root": repository, + "profile_path": repository + / "experiments" + / "perception" + / "e46h_full_rectified_front_replay_profile.json", + "parser_library_path": repository + / ".runtime" + / "compute-experiments" + / "e46e" + / "parser" + / "a18d85dae674a088549c5f9b8fda53c640f4fcbd88a41f4c2cb1f4e3ea8878ee" + / "libnvds_infercustomparser_tao.so", + "output_root": tmp_path, + } + package = module.build_e46h_worker_package(**kwargs) + repeated = module.build_e46h_worker_package(**kwargs) + manifest = module.validate_e46h_worker_package(package) + assert repeated == package + assert package.name == f"e46h-worker-package-{manifest['identity_sha256']}" + identity = manifest["identity"] + assert identity["classification"] == "minimal-full-front-stock-nvidia-replay-package" + assert identity["rectification"]["view"] == "front" + assert identity["selection"]["frame_count"] == 4488 + assert identity["selection"]["last_source_frame_index"] == 4487 + assert identity["detector"]["custom_postprocessing"] is False + assert identity["tracker"]["custom_association"] is False + assert identity["tracker"]["custom_hold_or_stitch"] is False diff --git a/tests/test_l34_right_yolox_truth_island_freeze.py b/tests/test_l34_right_yolox_truth_island_freeze.py new file mode 100644 index 0000000..df7cf88 --- /dev/null +++ b/tests/test_l34_right_yolox_truth_island_freeze.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import pytest + +from k1link.compute.l34_right_yolox_truth_island_freeze import ( + L34_PREDICTION_SCHEMA, + L34RightYoloxTruthIslandError, + freeze_l34_candidate_predictions, +) + + +def _reference(frame_index: int, sequence: int) -> dict[str, object]: + return { + "truth_island_sequence": sequence, + "image_id": sequence + 10, + "frame_index": frame_index, + "group_id": "temporal-a", + "session_seconds": 42.5, + "sha256": "a" * 64, + } + + +def _frame(frame_index: int) -> dict[str, object]: + return { + "schema_version": "missioncore.rectified-yolox-frame/v1", + "frame_index": frame_index, + "detections": [ + { + "label": "bus", + "score": 0.91, + "bbox_xyxy": [10.0, 20.0, 30.0, 40.0], + }, + { + "label": "person", + "score": 0.72, + "bbox_xyxy": [1.0, 2.0, 3.0, 4.0], + }, + { + "label": "car", + "score": 0.24, + "bbox_xyxy": [5.0, 6.0, 7.0, 8.0], + }, + ], + } + + +def test_freezes_only_admitted_right_camera_candidate_classes() -> None: + predictions = freeze_l34_candidate_predictions( + references=(_reference(70, 1),), + detector_frames={70: _frame(70)}, + minimum_score=0.25, + ) + + assert predictions == ( + { + "schema_version": L34_PREDICTION_SCHEMA, + "candidate_id": "yolox-s-kb4-core3", + "truth_island_sequence": 1, + "image_id": 11, + "frame_index": 70, + "session_seconds": 42.5, + "source_image_sha256": "a" * 64, + "group_id": "temporal-a", + "predictions": [ + { + "label": "heavy_vehicle", + "score": 0.91, + "bbox_xyxy": [10.0, 20.0, 30.0, 40.0], + }, + { + "label": "person", + "score": 0.72, + "bbox_xyxy": [1.0, 2.0, 3.0, 4.0], + }, + ], + "truth_joined": False, + }, + ) + + +def test_freeze_fails_closed_on_incomplete_frame_coverage() -> None: + with pytest.raises( + L34RightYoloxTruthIslandError, + match="coverage is incomplete", + ): + freeze_l34_candidate_predictions( + references=(_reference(70, 1), _reference(71, 2)), + detector_frames={70: _frame(70)}, + minimum_score=0.25, + ) diff --git a/tests/test_l34a_assisted_yolox_error_audit.py b/tests/test_l34a_assisted_yolox_error_audit.py new file mode 100644 index 0000000..735eff9 --- /dev/null +++ b/tests/test_l34a_assisted_yolox_error_audit.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from k1link.compute.l34a_assisted_yolox_error_audit import ( + _aggregate, + _audit_case, +) + + +def _prediction_row() -> dict[str, object]: + return { + "truth_island_sequence": 1, + "image_id": 2, + "frame_index": 70, + "group_id": "anchor-a", + "session_seconds": 4.2, + "source_image_sha256": "a" * 64, + "predictions": [ + { + "label": "car", + "score": 0.9, + "bbox_xyxy": [100.0, 100.0, 300.0, 300.0], + }, + { + "label": "car", + "score": 0.7, + "bbox_xyxy": [120.0, 120.0, 280.0, 280.0], + }, + { + "label": "motorcycle", + "score": 0.6, + "bbox_xyxy": [400.0, 200.0, 520.0, 420.0], + }, + ], + } + + +def _annotation_frame() -> dict[str, object]: + return { + "truth_island_sequence": 1, + "image_id": 2, + "frame_index": 70, + "source_sha256": "a" * 64, + "objects": [ + { + "object_id": "car-1", + "category": "car", + "proposed_label": None, + "origin": "frozen_candidate_seed", + "box_xyxy": [100.0, 100.0, 300.0, 300.0], + "occluded": False, + "truncated": False, + }, + { + "object_id": "stroller-1", + "category": "unmapped", + "proposed_label": "Детская коляска", + "origin": "manual", + "box_xyxy": [400.0, 200.0, 520.0, 420.0], + "occluded": False, + "truncated": False, + }, + { + "object_id": "person-1", + "category": "person", + "proposed_label": None, + "origin": "manual", + "box_xyxy": [10.0, 10.0, 60.0, 160.0], + "occluded": False, + "truncated": False, + }, + ], + } + + +def test_assisted_audit_distinguishes_duplicate_mismatch_and_miss() -> None: + case = _audit_case( + prediction_row=_prediction_row(), + annotation_frame=_annotation_frame(), + ) + + assert case["summary"] == { + "prediction_count": 3, + "reference_count": 3, + "true_positive": 1, + "false_positive": 2, + "false_negative": 2, + "class_mismatch": 1, + "duplicate_false_positive": 1, + "unmatched_false_positive": 0, + "unmatched_false_negative": 1, + "severity_score": 7, + } + assert [item["verdict"] for item in case["predictions"]] == [ + "true_positive", + "duplicate_false_positive", + "class_mismatch", + ] + assert case["annotations"][1]["display_category"] == ( + "unmapped:Детская коляска" + ) + + +def test_assisted_alignment_metrics_remain_descriptive() -> None: + case = _audit_case( + prediction_row=_prediction_row(), + annotation_frame=_annotation_frame(), + ) + + metrics = _aggregate((case,)) + + assert metrics["precision_iou50"] == 1 / 3 + assert metrics["recall_iou50"] == 1 / 3 + assert metrics["f1_iou50"] == 1 / 3 + assert metrics["custom_reference_count"] == 1 + assert metrics["error_case_count"] == 1 diff --git a/tests/test_l34b_nested_box_consolidation_shadow.py b/tests/test_l34b_nested_box_consolidation_shadow.py new file mode 100644 index 0000000..fb853a7 --- /dev/null +++ b/tests/test_l34b_nested_box_consolidation_shadow.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from k1link.compute.l34a_assisted_yolox_error_audit import _audit_case +from k1link.compute.l34b_nested_box_consolidation_shadow import ( + consolidate_l34b_prediction_row, + evaluate_l34b_shadow, +) + + +def _row(sequence: int = 1) -> dict[str, object]: + return { + "truth_island_sequence": sequence, + "image_id": sequence, + "frame_index": sequence + 10, + "group_id": "nested-box", + "session_seconds": float(sequence), + "source_image_sha256": f"{sequence % 10}" * 64, + "predictions": [ + { + "label": "heavy_vehicle", + "score": 0.71, + "bbox_xyxy": [100.0, 100.0, 140.0, 190.0], + }, + { + "label": "heavy_vehicle", + "score": 0.69, + "bbox_xyxy": [90.0, 99.0, 141.0, 191.0], + }, + { + "label": "car", + "score": 0.8, + "bbox_xyxy": [300.0, 100.0, 360.0, 180.0], + }, + ], + } + + +def _annotation(sequence: int = 1) -> dict[str, object]: + return { + "truth_island_sequence": sequence, + "image_id": sequence, + "frame_index": sequence + 10, + "source_sha256": f"{sequence % 10}" * 64, + "objects": [ + { + "object_id": f"truck-{sequence}", + "category": "heavy_vehicle", + "proposed_label": None, + "origin": "manual", + "box_xyxy": [90.0, 99.0, 141.0, 191.0], + "occluded": False, + "truncated": False, + }, + { + "object_id": f"car-{sequence}", + "category": "car", + "proposed_label": None, + "origin": "manual", + "box_xyxy": [300.0, 100.0, 360.0, 180.0], + "occluded": False, + "truncated": False, + }, + ], + } + + +def test_consolidates_only_same_category_nested_boxes() -> None: + projected, consolidations = consolidate_l34b_prediction_row(_row()) + + assert len(projected["predictions"]) == 2 + assert consolidations[0]["source_prediction_indices"] == [1, 2] + assert projected["predictions"][0] == { + "label": "heavy_vehicle", + "score": 0.71, + "bbox_xyxy": [90.0, 99.0, 141.0, 191.0], + "source_prediction_indices": [1, 2], + } + assert projected["predictions"][1]["source_prediction_indices"] == [3] + + +def test_shadow_reports_regression_free_false_positive_reduction() -> None: + rows = tuple(_row(sequence) for sequence in range(1, 33)) + annotations = tuple(_annotation(sequence) for sequence in range(1, 33)) + before = tuple( + _audit_case(prediction_row=row, annotation_frame=annotation) + for row, annotation in zip(rows, annotations, strict=True) + ) + + cases, metrics = evaluate_l34b_shadow( + prediction_rows=rows, + annotation_frames=annotations, + before_cases=before, + ) + + assert len(cases) == 32 + assert metrics["consolidation_count"] == 32 + assert metrics["delta"]["true_positive"] == 0 + assert metrics["delta"]["false_positive"] == -32 + assert metrics["delta"]["false_negative"] == 0 + assert metrics["assisted_regression_free"] is True diff --git a/tests/test_l34c_tile_seam_stitch_shadow.py b/tests/test_l34c_tile_seam_stitch_shadow.py new file mode 100644 index 0000000..67942fc --- /dev/null +++ b/tests/test_l34c_tile_seam_stitch_shadow.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +import copy + +import pytest + +from k1link.compute.l34c_tile_seam_stitch_shadow import ( + L34C_PROVENANCE_SCHEMA, + L34CTileSeamStitchError, + apply_l34c_stitches, + bind_l34c_prediction_provenance, + find_l34c_temporal_stitches, +) + + +def _frozen_row(*, sequence: int = 1, frame_index: int = 100) -> dict[str, object]: + return { + "schema_version": "missioncore.l34-right-yolox-truth-island-prediction/v1", + "candidate_id": "yolox-s-kb4-core3", + "truth_island_sequence": sequence, + "image_id": sequence, + "frame_index": frame_index, + "session_seconds": frame_index / 10.0, + "source_image_sha256": "a" * 64, + "group_id": "clip-close-car", + "predictions": [ + { + "label": "car", + "score": 0.95, + "bbox_xyxy": [230.0, 220.0, 385.0, 340.0], + }, + { + "label": "car", + "score": 0.90, + "bbox_xyxy": [182.0, 210.0, 266.0, 338.0], + }, + ], + "truth_joined": False, + } + + +def _detector_frame(frame_index: int = 100) -> dict[str, object]: + return { + "schema_version": "missioncore.rectified-yolox-frame/v1", + "frame_index": frame_index, + "detections": [ + { + "label": "car", + "class_id": 2, + "score": 0.95, + "bbox_xyxy": [230.0, 220.0, 385.0, 340.0], + "raw_center_xy": [290.0, 280.0], + "rectification_tile": "front", + "valid_fov_fraction": 1.0, + }, + { + "label": "car", + "class_id": 2, + "score": 0.90, + "bbox_xyxy": [182.0, 210.0, 266.0, 338.0], + "raw_center_xy": [230.0, 275.0], + "rectification_tile": "left", + "valid_fov_fraction": 1.0, + }, + ], + } + + +def _provenance_run(length: int) -> tuple[dict[str, object], ...]: + frozen = tuple( + _frozen_row(sequence=index + 1, frame_index=100 + index) + for index in range(length) + ) + detector = { + 100 + index: _detector_frame(100 + index) + for index in range(length) + } + return bind_l34c_prediction_provenance( + prediction_rows=frozen, + detector_frames=detector, + ) + + +def test_exact_join_preserves_tile_and_raw_detector_identity() -> None: + rows = _provenance_run(1) + + assert rows[0]["schema_version"] == L34C_PROVENANCE_SCHEMA + assert rows[0]["provenance_join"] == "exact-label-score-bbox" + assert rows[0]["predictions"][0]["rectification_tile"] == "front" + assert rows[0]["predictions"][1]["rectification_tile"] == "left" + assert rows[0]["predictions"][0]["class_id"] == 2 + assert rows[0]["predictions"][0]["raw_label"] == "car" + + +def test_exact_join_rejects_missing_or_ambiguous_provenance() -> None: + frame = _detector_frame() + frame["detections"].append(copy.deepcopy(frame["detections"][0])) + + with pytest.raises(L34CTileSeamStitchError, match="one exact"): + bind_l34c_prediction_provenance( + prediction_rows=(_frozen_row(),), + detector_frames={100: frame}, + ) + + +def test_temporal_gate_admits_three_consecutive_frames_but_not_two() -> None: + admitted, static_count = find_l34c_temporal_stitches(_provenance_run(3)) + rejected, rejected_static_count = find_l34c_temporal_stitches( + _provenance_run(2) + ) + + assert static_count == 3 + assert set(admitted) == {1, 2, 3} + assert all(items[0]["temporal_run_length"] == 3 for items in admitted.values()) + assert rejected_static_count == 2 + assert rejected == {} + + +def test_temporal_gate_rejects_same_tile_and_small_pairs() -> None: + rows = list(_provenance_run(3)) + for row in rows: + row["predictions"][1]["rectification_tile"] = "front" + same_tile, same_tile_count = find_l34c_temporal_stitches(tuple(rows)) + + small_rows = list(_provenance_run(3)) + for row in small_rows: + row["predictions"][0]["bbox_xyxy"] = [230.0, 220.0, 260.0, 250.0] + row["predictions"][1]["bbox_xyxy"] = [220.0, 218.0, 245.0, 252.0] + small, small_count = find_l34c_temporal_stitches(tuple(small_rows)) + + assert same_tile_count == 0 + assert same_tile == {} + assert small_count == 0 + assert small == {} + + +def test_apply_stitch_unions_geometry_and_preserves_source_tiles() -> None: + row = _provenance_run(3)[0] + admitted, _ = find_l34c_temporal_stitches(_provenance_run(3)) + + projected = apply_l34c_stitches(row, admitted[1]) + + assert projected["predictions"] == [ + { + "label": "car", + "score": 0.95, + "bbox_xyxy": [182.0, 210.0, 385.0, 340.0], + "source_prediction_indices": [1, 2], + "source_rectification_tiles": ["front", "left"], + "temporal_run_id": "clip-close-car:car:100-102", + "temporal_run_length": 3, + } + ] diff --git a/tests/test_l34d_cumulative_postprocessing_candidate.py b/tests/test_l34d_cumulative_postprocessing_candidate.py new file mode 100644 index 0000000..0a2351e --- /dev/null +++ b/tests/test_l34d_cumulative_postprocessing_candidate.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import copy + +import pytest + +from k1link.compute.l34c_tile_seam_stitch_shadow import ( + bind_l34c_prediction_provenance, +) +from k1link.compute.l34d_cumulative_postprocessing_candidate import ( + L34DCumulativeCandidateError, + compose_l34d_prediction_row, +) + + +def _row() -> dict[str, object]: + return { + "schema_version": "missioncore.l34-right-yolox-truth-island-prediction/v1", + "truth_island_sequence": 1, + "image_id": 1, + "frame_index": 100, + "session_seconds": 1.0, + "source_image_sha256": "a" * 64, + "group_id": "composition", + "predictions": [ + {"label": "heavy_vehicle", "score": 0.71, "bbox_xyxy": [10.0, 10.0, 30.0, 40.0]}, + {"label": "heavy_vehicle", "score": 0.69, "bbox_xyxy": [9.0, 9.0, 31.0, 41.0]}, + {"label": "car", "score": 0.95, "bbox_xyxy": [230.0, 220.0, 385.0, 340.0]}, + {"label": "car", "score": 0.90, "bbox_xyxy": [182.0, 210.0, 266.0, 338.0]}, + ], + } + + +def _detector_frame() -> dict[str, object]: + detections = [] + for index, prediction in enumerate(_row()["predictions"]): + detections.append({ + **prediction, + "class_id": 7 if index < 2 else 2, + "raw_center_xy": [20.0 + index, 20.0 + index], + "rectification_tile": "front" if index in (0, 2) else "left", + "valid_fov_fraction": 1.0, + }) + return { + "schema_version": "missioncore.rectified-yolox-frame/v1", + "frame_index": 100, + "detections": detections, + } + + +def _provenance() -> dict[str, object]: + return bind_l34c_prediction_provenance( + prediction_rows=(_row(),), + detector_frames={100: _detector_frame()}, + )[0] + + +def _operation(indices: list[int], operation_type: str) -> dict[str, object]: + predictions = _row()["predictions"] + members = [predictions[index - 1] for index in indices] + boxes = [member["bbox_xyxy"] for member in members] + operation = { + "category": members[0]["label"], + "source_prediction_indices": indices, + "source_scores": [member["score"] for member in members], + "source_boxes_xyxy": boxes, + "merged_score": max(member["score"] for member in members), + "merged_box_xyxy": [ + min(box[0] for box in boxes), + min(box[1] for box in boxes), + max(box[2] for box in boxes), + max(box[3] for box in boxes), + ], + } + if operation_type == "temporal-tile-seam-stitch": + operation.update({ + "source_tiles": ["front", "left"], + "temporal_run_id": "composition:car:100-102", + "temporal_run_length": 3, + }) + return operation + + +def test_composes_disjoint_nested_and_seam_operations_once() -> None: + projected, operations = compose_l34d_prediction_row( + _provenance(), + consolidations=(_operation([1, 2], "nested-box-consolidation"),), + stitches=(_operation([3, 4], "temporal-tile-seam-stitch"),), + ) + + assert len(projected["predictions"]) == 2 + assert [ + item["source_prediction_indices"] for item in projected["predictions"] + ] == [[1, 2], [3, 4]] + assert [item["operation_types"] for item in projected["predictions"]] == [ + ["nested-box-consolidation"], + ["temporal-tile-seam-stitch"], + ] + assert {item["operation_type"] for item in operations} == { + "nested-box-consolidation", + "temporal-tile-seam-stitch", + } + + +def test_rejects_overlapping_operation_sets() -> None: + with pytest.raises(L34DCumulativeCandidateError, match="overlap"): + compose_l34d_prediction_row( + _provenance(), + consolidations=(_operation([1, 2], "nested-box-consolidation"),), + stitches=(_operation([1, 2], "temporal-tile-seam-stitch"),), + ) + + +def test_rejects_operation_payload_drift() -> None: + operation = _operation([1, 2], "nested-box-consolidation") + operation["source_boxes_xyxy"] = copy.deepcopy(operation["source_boxes_xyxy"]) + operation["source_boxes_xyxy"][0][0] += 1.0 + + with pytest.raises(L34DCumulativeCandidateError, match="payload"): + compose_l34d_prediction_row( + _provenance(), + consolidations=(operation,), + stitches=(), + ) diff --git a/tests/test_l34e_self_review_diagnostic.py b/tests/test_l34e_self_review_diagnostic.py new file mode 100644 index 0000000..41944f1 --- /dev/null +++ b/tests/test_l34e_self_review_diagnostic.py @@ -0,0 +1,152 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from k1link.compute.l34e_self_review_diagnostic import ( + L34ESelfReviewDiagnosticError, + evaluate_l34e_self_review_diagnostic, +) + + +def _candidate_case( + sequence: int, + *, + category: str = "car", + box: list[float] | None = None, +) -> dict[str, Any]: + predictions = [] if box is None else [ + { + "prediction_index": 1, + "category": category, + "score": 0.8, + "box_xyxy": box, + "source_prediction_indices": [1], + "source_rectification_tiles": ["center"], + "operation_types": [], + } + ] + return { + "truth_island_sequence": sequence, + "image_id": sequence, + "frame_index": sequence * 10, + "group_id": f"frame-{sequence:02d}", + "session_seconds": float(sequence), + "source_image_sha256": f"{sequence:064x}", + "after_predictions": predictions, + } + + +def _review_frame( + sequence: int, + *, + category: str = "car", + box: list[float] | None = None, +) -> dict[str, Any]: + objects = [] if box is None else [ + { + "object_id": f"manual-{sequence:02d}", + "category": category, + "proposed_label": None, + "origin": "manual", + "box_xyxy": box, + "occluded": False, + "truncated": False, + } + ] + return { + "truth_island_sequence": sequence, + "image_id": sequence, + "frame_index": sequence * 10, + "source_sha256": f"{sequence:064x}", + "reviewed": True, + "objects": objects, + } + + +def _fixture() -> tuple[tuple[dict[str, Any], ...], tuple[dict[str, Any], ...]]: + candidates = [] + reviews = [] + for sequence in range(1, 33): + candidate_category = "car" + review_category = "car" + candidate_box: list[float] | None = [10.0, 10.0, 30.0, 30.0] + review_box: list[float] | None = [10.0, 10.0, 30.0, 30.0] + if sequence == 2: + review_box = [8.0, 8.0, 40.0, 40.0] + elif sequence == 3: + review_box = [8.0, 8.0, 40.0, 40.0] + review_category = "person" + elif sequence == 4: + review_category = "person" + elif sequence == 5: + review_box = None + elif sequence == 6: + candidate_box = None + candidates.append( + _candidate_case( + sequence, + category=candidate_category, + box=candidate_box, + ) + ) + reviews.append( + _review_frame( + sequence, + category=review_category, + box=review_box, + ) + ) + return tuple(candidates), tuple(reviews) + + +def test_evaluation_separates_localization_from_unmatched_objects() -> None: + candidates, reviews = _fixture() + + cases, metrics = evaluate_l34e_self_review_diagnostic( + l34d_cases=candidates, + annotation_frames=reviews, + ) + + diagnostic = metrics["diagnostic_association"] + assert diagnostic == { + "prediction_count": 31, + "reference_count": 31, + "associated_pair_count": 30, + "strict_alignment": 27, + "strict_class_mismatch": 1, + "localization_disagreement": 1, + "class_and_localization_disagreement": 1, + "prediction_only": 1, + "reference_only": 1, + "candidate_association_coverage": 30 / 31, + "reference_association_coverage": 30 / 31, + "error_case_count": 5, + } + strict = metrics["strict_iou50"] + assert strict["true_positive"] == 27 + assert strict["false_positive"] == 4 + assert strict["false_negative"] == 4 + assert strict["class_mismatch"] == 1 + assert cases[1]["predictions"][0]["diagnostic_verdict"] == ( + "localization_disagreement" + ) + assert cases[2]["references"][0]["diagnostic_verdict"] == ( + "class_and_localization_disagreement" + ) + assert cases[4]["predictions"][0]["diagnostic_verdict"] == "prediction_only" + assert cases[5]["references"][0]["diagnostic_verdict"] == "reference_only" + + +def test_evaluation_rejects_incomplete_coverage() -> None: + candidates, reviews = _fixture() + + with pytest.raises( + L34ESelfReviewDiagnosticError, + match="exactly 32", + ): + evaluate_l34e_self_review_diagnostic( + l34d_cases=candidates[:-1], + annotation_frames=reviews, + ) diff --git a/tests/test_l35_right_yolox_truth_evaluation.py b/tests/test_l35_right_yolox_truth_evaluation.py new file mode 100644 index 0000000..54691e6 --- /dev/null +++ b/tests/test_l35_right_yolox_truth_evaluation.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from typing import Any + +import pytest +from PIL import Image + +from k1link.compute.e49_detector_truth_evaluation import ( + evaluate_frozen_detector_candidates, +) +from k1link.compute.l34_right_yolox_truth_island_freeze import ( + L34_PREDICTION_SCHEMA, +) +from k1link.compute.l35_right_yolox_truth_evaluation import ( + L35RightYoloxTruthEvaluationError, + _require_freeze_before_truth_seal, + l34_rows_for_sealed_truth, +) + + +def _l34_row() -> dict[str, Any]: + return { + "schema_version": L34_PREDICTION_SCHEMA, + "candidate_id": "yolox-s-kb4-core3", + "truth_island_sequence": 1, + "image_id": 11, + "frame_index": 70, + "session_seconds": 42.5, + "source_image_sha256": "a" * 64, + "group_id": "anchor-011", + "predictions": [ + { + "label": "car", + "score": 0.9, + "bbox_xyxy": [10.0, 10.0, 110.0, 110.0], + } + ], + "truth_joined": False, + } + + +def _truth_row() -> dict[str, Any]: + return { + "truth_island_sequence": 1, + "image_id": 11, + "frame_index": 70, + "session_seconds": 42.5, + "role": "anchor", + "group_id": "anchor-011", + "source_image_sha256": "a" * 64, + "hard_negative": False, + "objects": [ + { + "object_id": "car-1", + "category": "car", + "box_xyxy": [10.0, 10.0, 110.0, 110.0], + "occluded": False, + "truncated": False, + "notes": None, + } + ], + "adjudicated": True, + } + + +def test_l35_adapts_l34_without_changing_identity_or_boxes() -> None: + rows = l34_rows_for_sealed_truth((_l34_row(),)) + + assert rows == ( + { + "candidate_id": "yolox-s-kb4-core3", + "truth_island_sequence": 1, + "image_id": 11, + "frame_index": 70, + "session_seconds": 42.5, + "source_image_sha256": "a" * 64, + "predictions": [ + { + "category": "car", + "score": 0.9, + "box_xyxy": [10.0, 10.0, 110.0, 110.0], + } + ], + "truth_joined": False, + }, + ) + metrics = evaluate_frozen_detector_candidates( + truth_rows=(_truth_row(),), + prediction_rows=rows, + valid_fov_mask=Image.new("L", (800, 600), color=255), + ) + assert metrics["yolox-s-kb4-core3"]["ap50"] == 1.0 + assert metrics["yolox-s-kb4-core3"]["candidate_winner_selected"] is False + + +def test_l35_rejects_a_freeze_created_after_truth_was_sealed() -> None: + with pytest.raises( + L35RightYoloxTruthEvaluationError, + match="postdates", + ): + _require_freeze_before_truth_seal( + freeze_created_at_utc="2026-08-01T02:00:00Z", + truth_sealed_at_utc="2026-08-01T01:00:00Z", + ) + + +def test_l35_accepts_a_freeze_created_before_truth_was_sealed() -> None: + _require_freeze_before_truth_seal( + freeze_created_at_utc="2026-08-01T00:30:00Z", + truth_sealed_at_utc="2026-08-01T01:00:00Z", + ) diff --git a/tests/test_semantic_geometry_fusion.py b/tests/test_semantic_geometry_fusion.py index d708f63..87bf64e 100644 --- a/tests/test_semantic_geometry_fusion.py +++ b/tests/test_semantic_geometry_fusion.py @@ -76,6 +76,9 @@ def test_camera_semantic_and_connected_occupied_support_agree() -> None: ) assert support.document["geometry_status"] == "agree" assert support.document["range_m"] == pytest.approx(2.0666666667) + assert support.document["range_estimate_m"] == pytest.approx(2.0666666667) + assert support.document["range_estimate_available"] is True + assert support.document["range_support_qualified"] is True assert support.document["unknown_is_occupied"] is True assert support.document["navigation_or_safety_accepted"] is False assert support.occupied_source_indices.tolist() == [0, 1] @@ -120,8 +123,18 @@ def test_camera_only_and_surface_conflict_remain_explicit() -> None: profile=CameraGeometryFusionProfile(), ) assert camera_only.document["geometry_status"] == "single-source-camera" + assert camera_only.document["range_m"] is None + assert camera_only.document["range_estimate_m"] == pytest.approx(2.0) + assert camera_only.document["range_estimate_available"] is True + assert camera_only.document["range_support_qualified"] is False assert conflict.document["geometry_status"] == "conflict" + assert conflict.document["range_m"] is None + assert conflict.document["range_estimate_available"] is False + assert conflict.document["range_support_qualified"] is False assert unknown.document["geometry_status"] == "unknown" + assert unknown.document["range_m"] is None + assert unknown.document["range_estimate_available"] is False + assert unknown.document["range_support_qualified"] is False def test_unclaimed_occupied_component_is_a_separate_geometry_layer() -> None: