356 lines
13 KiB
Python
356 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from itertools import pairwise
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
from k1link.laboratory.m47_reference_graph import M47_REFERENCE_GRAPH_LAB_SCHEMA
|
|
from k1link.laboratory.m48_object_quality import read_m48_object_quality_pack
|
|
from k1link.laboratory.m48_ravnoves00_pack import (
|
|
M48_FRAME_COUNT,
|
|
M48_SELECTION_SCHEMA,
|
|
_prediction_objects,
|
|
prepare_m48_ravnoves00_pack,
|
|
)
|
|
|
|
|
|
def _canonical_json(value: object) -> str:
|
|
return json.dumps(value, sort_keys=True, separators=(",", ":"))
|
|
|
|
|
|
def _write_json(path: Path, value: object) -> None:
|
|
path.write_text(_canonical_json(value) + "\n", encoding="utf-8")
|
|
|
|
|
|
def _write_jsonl(path: Path, rows: list[dict[str, object]]) -> str:
|
|
raw = "".join(_canonical_json(row) + "\n" for row in rows).encode()
|
|
path.write_bytes(raw)
|
|
return hashlib.sha256(raw).hexdigest()
|
|
|
|
|
|
def _recursive_keys(value: object) -> set[str]:
|
|
if isinstance(value, dict):
|
|
return set(value) | {key for item in value.values() for key in _recursive_keys(item)}
|
|
if isinstance(value, list):
|
|
return {key for item in value for key in _recursive_keys(item)}
|
|
return set()
|
|
|
|
|
|
def test_prediction_projection_is_class_free_and_conservative() -> None:
|
|
rows = _prediction_objects(
|
|
[
|
|
{
|
|
"proposal_id": "proposal-0-1",
|
|
"bbox_xyxy": [80.0, 60.0, 400.0, 300.0],
|
|
"occupied_support": False,
|
|
"threat_decision": "unknown",
|
|
"semantic_hint": "person",
|
|
"objectness": 0.99,
|
|
},
|
|
{
|
|
"proposal_id": "proposal-0-2",
|
|
"bbox_xyxy": [400.0, 300.0, 720.0, 540.0],
|
|
"occupied_support": True,
|
|
"threat_decision": "threat",
|
|
"semantic_hint": "car",
|
|
"objectness": 0.98,
|
|
},
|
|
],
|
|
geometry_observations=[
|
|
{
|
|
"proposal_ids": ["proposal-0-1"],
|
|
"currentness": "current",
|
|
"metric_geometry": None,
|
|
},
|
|
{
|
|
"proposal_ids": ["proposal-0-2"],
|
|
"currentness": "current",
|
|
"metric_geometry": {"centroid_xyz_m": [1.0, 2.0, 3.0]},
|
|
},
|
|
],
|
|
metric_obstacles=[
|
|
{
|
|
"centroid_map_xyz_m": [1.0, 2.0, 3.0],
|
|
"motion": "moving",
|
|
"assessment": {"decision": "threat"},
|
|
}
|
|
],
|
|
)
|
|
|
|
assert rows == [
|
|
{
|
|
"prediction_id": "proposal-0-1",
|
|
"extent_xyxy": [0.1, 0.1, 0.5, 0.5],
|
|
"geometry_association": "unknown",
|
|
"freshness": "current",
|
|
"motion": "unsupported",
|
|
"threat": "unknown",
|
|
"unknown_causes": [
|
|
"insufficient-geometry-support",
|
|
"threat-evidence-insufficient",
|
|
],
|
|
},
|
|
{
|
|
"prediction_id": "proposal-0-2",
|
|
"extent_xyxy": [0.5, 0.5, 0.9, 0.9],
|
|
"geometry_association": "associated",
|
|
"freshness": "current",
|
|
"motion": "moving",
|
|
"threat": "threat",
|
|
"unknown_causes": [],
|
|
},
|
|
]
|
|
assert "semantic_hint" not in _canonical_json(rows)
|
|
assert "objectness" not in _canonical_json(rows)
|
|
|
|
|
|
def test_real_selection_contract_is_balanced_and_prediction_blind() -> None:
|
|
repository_root = Path(__file__).resolve().parents[1]
|
|
document = json.loads(
|
|
(repository_root / "config/perception/m48-object-quality-selection-v1.json").read_text()
|
|
)
|
|
clips = document["clips"]
|
|
|
|
assert document["schema_version"] == M48_SELECTION_SCHEMA
|
|
assert len(clips) == 24
|
|
assert {clip["split"] for clip in clips} == {"development", "validation"}
|
|
assert all("strata" not in clip and "hypotheses" not in clip for clip in clips)
|
|
assert document["selection_hypothesis_profile"] == {
|
|
"derivation": "exact-frozen-prediction-rows-before-independent-truth",
|
|
"small_obstacle_max_normalized_area": 0.001,
|
|
"fisheye_edge_margin_normalized": 0.08,
|
|
"sparse_scene_max_median_prediction_count": 2.0,
|
|
}
|
|
for field in ("component_id", "route_block", "time_block"):
|
|
group_splits: dict[str, set[str]] = {}
|
|
for clip in clips:
|
|
group_splits.setdefault(clip[field], set()).add(clip["split"])
|
|
assert all(len(splits) == 1 for splits in group_splits.values())
|
|
assert len(group_splits) < len(clips)
|
|
assert all(left["end_sequence"] < right["start_sequence"] for left, right in pairwise(clips))
|
|
forbidden = {"label", "labels", "truth", "review", "adjudication"}
|
|
assert forbidden.isdisjoint(document)
|
|
|
|
|
|
def test_prepare_pack_binds_all_source_ledgers_and_freezes_selected_frames(
|
|
tmp_path: Path,
|
|
monkeypatch,
|
|
) -> None:
|
|
repository_root = Path(__file__).resolve().parents[1]
|
|
graph_root = tmp_path / ("m47-reference-graph-" + "a" * 64)
|
|
threat_root = tmp_path / ("m4-threat-replay-" + "b" * 64)
|
|
geometry_root = tmp_path / ("m4-geometry-replay-" + "e" * 64)
|
|
lab_root = tmp_path / ("m47-reference-graph-lab-" + "c" * 64)
|
|
graph_root.mkdir()
|
|
threat_root.mkdir()
|
|
geometry_root.mkdir()
|
|
lab_root.mkdir()
|
|
_write_json(lab_root / "manifest.json", {"fixture": True})
|
|
|
|
graph_rows: list[dict[str, object]] = []
|
|
threat_rows: list[dict[str, object]] = []
|
|
geometry_rows: list[dict[str, object]] = []
|
|
camera_rows: list[dict[str, object]] = []
|
|
for frame_index in range(M48_FRAME_COUNT):
|
|
source_time_ns = 35_421_857_292 + frame_index * 100_000_000
|
|
graph_rows.append(
|
|
{
|
|
"sequence": frame_index,
|
|
"obstacle_map": {
|
|
"schema_version": "missioncore.local-obstacle-map/v1",
|
|
"frame_id": f"frame-{frame_index:06d}",
|
|
"free_space_claimed": False,
|
|
},
|
|
"threats": [],
|
|
}
|
|
)
|
|
threat_rows.append(
|
|
{
|
|
"schema_version": "missioncore.perception-threat-replay-frame/v2",
|
|
"sequence": frame_index,
|
|
"frame_id": f"frame-{frame_index:06d}",
|
|
"source_time_ns": source_time_ns,
|
|
"source_available": True,
|
|
"camera_proposals": [
|
|
{
|
|
"proposal_id": f"proposal-{frame_index}-0",
|
|
"bbox_xyxy": [0.0, 0.0, 20.0, 20.0],
|
|
"occupied_support": True,
|
|
"threat_decision": "threat" if frame_index % 2 == 0 else "not-threat",
|
|
},
|
|
{
|
|
"proposal_id": f"proposal-{frame_index}-1",
|
|
"bbox_xyxy": [80.0, 60.0, 400.0, 300.0],
|
|
"occupied_support": False,
|
|
"threat_decision": "unknown",
|
|
},
|
|
],
|
|
"metric_obstacles": [
|
|
{
|
|
"centroid_map_xyz_m": [1.0, 2.0, 3.0],
|
|
"motion": "moving" if frame_index % 2 == 0 else "stationary",
|
|
"assessment": {
|
|
"decision": "threat" if frame_index % 2 == 0 else "not-threat"
|
|
},
|
|
}
|
|
],
|
|
}
|
|
)
|
|
geometry_rows.append(
|
|
{
|
|
"schema_version": "missioncore.perception-geometry-replay-frame/v1",
|
|
"sequence": frame_index,
|
|
"frame_id": f"frame-{frame_index:06d}",
|
|
"source_available": True,
|
|
"observations": [
|
|
{
|
|
"proposal_ids": [f"proposal-{frame_index}-0"],
|
|
"currentness": "current",
|
|
"metric_geometry": {"centroid_xyz_m": [1.0, 2.0, 3.0]},
|
|
},
|
|
{
|
|
"proposal_ids": [f"proposal-{frame_index}-1"],
|
|
"currentness": "current",
|
|
"metric_geometry": None,
|
|
},
|
|
],
|
|
}
|
|
)
|
|
camera_rows.append(
|
|
{
|
|
"schema_version": "missioncore.camera-recording-index/v1",
|
|
"sequence": frame_index + 1,
|
|
"kind": "media",
|
|
"session_monotonic_ns": frame_index + 1,
|
|
"sha256": hashlib.sha256(f"camera-{frame_index}".encode()).hexdigest(),
|
|
}
|
|
)
|
|
graph_sha256 = _write_jsonl(graph_root / "frames.jsonl", graph_rows)
|
|
threat_sha256 = _write_jsonl(threat_root / "frames.jsonl", threat_rows)
|
|
geometry_sha256 = _write_jsonl(geometry_root / "frames.jsonl", geometry_rows)
|
|
camera_index = tmp_path / "index.jsonl"
|
|
_write_jsonl(camera_index, camera_rows)
|
|
_write_json(
|
|
graph_root / "manifest.json",
|
|
{
|
|
"schema_version": "missioncore.reference-perception-graph-manifest/v1",
|
|
"result_id": graph_root.name,
|
|
"accepted": True,
|
|
"graph_id": "reference-perception-graph/v2",
|
|
"run_mode": "lossless-replay",
|
|
"files": {
|
|
"frames.jsonl": {
|
|
"bytes": (graph_root / "frames.jsonl").stat().st_size,
|
|
"sha256": graph_sha256,
|
|
}
|
|
},
|
|
},
|
|
)
|
|
_write_json(
|
|
threat_root / "manifest.json",
|
|
{
|
|
"schema_version": "missioncore.perception-threat-replay-result/v2",
|
|
"result_id": threat_root.name,
|
|
"accepted": True,
|
|
"identity": {
|
|
"source_session_id": "20260720T065719Z_viewer_live",
|
|
"frames_sha256": threat_sha256,
|
|
"geometry_result_id": geometry_root.name,
|
|
"geometry_frames_sha256": geometry_sha256,
|
|
},
|
|
},
|
|
)
|
|
_write_json(
|
|
geometry_root / "manifest.json",
|
|
{
|
|
"schema_version": "missioncore.perception-geometry-replay-result/v1",
|
|
"identity": {
|
|
"accepted": True,
|
|
"source_pack_id": (
|
|
"e10-lidar-pack-"
|
|
"576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b"
|
|
),
|
|
"frames_sha256": geometry_sha256,
|
|
},
|
|
},
|
|
)
|
|
authority = {
|
|
"mode": "replay-simulated",
|
|
"physical_live": False,
|
|
"commands_enabled": False,
|
|
"actuation_allowed": False,
|
|
"navigation_or_safety_accepted": False,
|
|
}
|
|
lab = SimpleNamespace(
|
|
result_id=lab_root.name,
|
|
result_root=lab_root,
|
|
manifest={
|
|
"schema_version": M47_REFERENCE_GRAPH_LAB_SCHEMA,
|
|
"accepted": True,
|
|
"ground_truth": False,
|
|
},
|
|
report={
|
|
"source": {
|
|
"graph_result_id": graph_root.name,
|
|
"visual_result_id": threat_root.name,
|
|
"threat_frames_sha256": threat_sha256,
|
|
"source_id": "RAVNOVES00",
|
|
"source_session_id": "20260720T065719Z_viewer_live",
|
|
},
|
|
"method": {
|
|
"graph_id": "reference-perception-graph/v2",
|
|
"run_mode": "lossless-replay",
|
|
"canonical_payload_sha256": "d" * 64,
|
|
},
|
|
"decision": {
|
|
"state": "accepted-reference-graph-replay",
|
|
"next_gate": "independent-object-centric-detection-quality",
|
|
},
|
|
"acceptance": {"accepted": True},
|
|
"authority": authority,
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
"k1link.laboratory.m48_ravnoves00_pack.read_m47_reference_graph_lab",
|
|
lambda _: lab,
|
|
)
|
|
monkeypatch.setattr(
|
|
"k1link.laboratory.m48_object_quality.read_m47_reference_graph_lab",
|
|
lambda _: lab,
|
|
)
|
|
|
|
result = prepare_m48_ravnoves00_pack(
|
|
m47_lab_root=lab_root,
|
|
graph_result_root=graph_root,
|
|
threat_result_root=threat_root,
|
|
geometry_result_root=geometry_root,
|
|
camera_index_path=camera_index,
|
|
selection_path=(repository_root / "config/perception/m48-object-quality-selection-v1.json"),
|
|
frozen_at_utc="2026-08-24T00:00:00Z",
|
|
output_root=tmp_path / "runtime/m48/object-quality-packs",
|
|
)
|
|
|
|
assert read_m48_object_quality_pack(result.result_root) == result
|
|
assert result.report["metrics"]["clip_count"] == 24
|
|
assert result.report["metrics"]["frame_count"] == 24 * 61
|
|
assert len(result.predictions) == 24 * 61
|
|
assert result.manifest["identity"]["preparation"]["adapter"]["sha256"] == (
|
|
hashlib.sha256(
|
|
(repository_root / "src/k1link/laboratory/m48_ravnoves00_pack.py").read_bytes()
|
|
).hexdigest()
|
|
)
|
|
assert result.manifest["identity"]["preparation"]["selection"]["sha256"] == (
|
|
hashlib.sha256(
|
|
(
|
|
repository_root / "config/perception/m48-object-quality-selection-v1.json"
|
|
).read_bytes()
|
|
).hexdigest()
|
|
)
|
|
reviewer_package = json.loads((result.result_root / "reviewer-package.json").read_text())
|
|
reviewer_keys = _recursive_keys(reviewer_package)
|
|
assert "strata" not in reviewer_keys
|
|
assert "prediction_id" not in reviewer_keys
|
|
assert "semantic_hint" not in reviewer_keys
|