Files
NODEDC_MISSION_CORE/tests/test_m48_object_quality.py
T

614 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import copy
import hashlib
import json
from pathlib import Path
from types import SimpleNamespace
from typing import Any
import pytest
import k1link.laboratory.m48_object_quality as m48
from k1link.laboratory.evidence_registry import LaboratoryEvidenceRegistry
from k1link.laboratory.evidence_report import verify_laboratory_evidence_result
def _write_json(path: Path, value: object) -> None:
path.write_text(json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n")
def _frame_catalog() -> list[dict[str, object]]:
return [
{
"sequence": sequence,
"source_time_ns": (sequence - 1) * 100_000_000,
"camera_fragment_sha256": hashlib.sha256(f"frame-{sequence}".encode()).hexdigest(),
}
for sequence in range(1, 4490)
]
def _clips() -> list[dict[str, object]]:
rows = []
for index in range(20):
start = 1 + index * 100
split = "development" if index < 10 else "validation"
split_index = index if index < 10 else index - 10
rows.append(
{
"clip_id": f"clip-{index:02d}",
"component_id": f"component-{split}-{split_index // 2:02d}",
"route_block": f"route-{split}-{split_index // 3:02d}",
"time_block": f"time-{split}-{split_index // 2:02d}",
"split": split,
"start_sequence": start,
"end_sequence": start + 50,
}
)
return rows
def _clip_fixture_state(clip_id: str) -> dict[str, object]:
local_index = int(clip_id.rsplit("-", 1)[1]) % 10
if local_index == 0:
return {
"extent_xyxy": [0.2, 0.2, 0.22, 0.22],
"geometry_association": "unknown",
"motion": "unsupported",
"threat": "unknown",
"unknown_causes": [
"insufficient-geometry-support",
"threat-evidence-insufficient",
],
}
if local_index == 1:
return {
"extent_xyxy": [0.01, 0.2, 0.2, 0.4],
"geometry_association": "associated",
"motion": "static",
"threat": "not-threat",
"unknown_causes": [],
}
if local_index == 2:
return {
"extent_xyxy": [0.1, 0.1, 0.3, 0.4],
"geometry_association": "associated",
"motion": "moving",
"threat": "threat",
"unknown_causes": [],
}
return {
"extent_xyxy": [0.1, 0.1, 0.3, 0.4],
"geometry_association": "associated",
"motion": "static",
"threat": "not-threat",
"unknown_causes": [],
}
def _preparation_provenance() -> dict[str, object]:
return {
"schema_version": m48.M48_PREPARATION_PROVENANCE_SCHEMA,
"adapter": {
"module": "k1link.laboratory.m48_ravnoves00_pack",
"sha256": "1" * 64,
},
"selection": {
"selection_id": "m48-ravnoves00-balanced-connected-clips/v1",
"sha256": "2" * 64,
},
"camera_index": {
"source_session_id": "20260720T065719Z_viewer_live",
"sha256": "3" * 64,
"byte_length": 1234,
"frame_count": 4489,
},
"graph": {
"result_id": "m47-reference-graph-" + "4" * 64,
"manifest_sha256": "5" * 64,
"frames_sha256": "6" * 64,
},
"threat": {
"result_id": "m4-threat-replay-" + "7" * 64,
"manifest_sha256": "8" * 64,
"frames_sha256": "9" * 64,
},
"geometry": {
"result_id": "m4-geometry-replay-" + "a" * 64,
"manifest_sha256": "b" * 64,
"frames_sha256": "c" * 64,
},
}
def _prediction_rows(
clips: list[dict[str, object]],
*,
unsafe_free_space: bool,
unsafe_free_space_split: str | None = None,
) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
for clip in clips:
fixture = _clip_fixture_state(str(clip["clip_id"]))
unsafe = unsafe_free_space and (
unsafe_free_space_split is None or clip["split"] == unsafe_free_space_split
)
for sequence in range(int(clip["start_sequence"]), int(clip["end_sequence"]) + 1):
objects = [
{
"prediction_id": f"prediction-{sequence}",
"extent_xyxy": fixture["extent_xyxy"],
"geometry_association": fixture["geometry_association"],
"freshness": "current",
"motion": fixture["motion"],
"threat": fixture["threat"],
"unknown_causes": fixture["unknown_causes"],
}
]
rows.append(
{
"sequence": sequence,
"source_time_ns": (sequence - 1) * 100_000_000,
"terminal_outcome": "delivered",
"terminal_reason": None,
"free_space_claimed": unsafe,
"objects": objects,
}
)
return rows
def _fake_m47(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
root = tmp_path / f"m47-reference-graph-lab-{'a' * 64}"
root.mkdir()
manifest = {
"schema_version": "missioncore.reference-perception-graph-lab/v2",
"accepted": True,
"ground_truth": False,
}
_write_json(root / "manifest.json", manifest)
report = {
"source": {
"source_id": "RAVNOVES00",
"source_session_id": "20260720T065719Z_viewer_live",
"graph_result_id": "m47-reference-graph-" + "b" * 64,
},
"method": {
"graph_id": "reference-perception-graph/v2",
"run_mode": "lossless-replay",
"canonical_payload_sha256": "c" * 64,
},
"decision": {
"state": "accepted-reference-graph-replay",
"next_gate": "independent-object-centric-detection-quality",
},
"acceptance": {"accepted": True},
"authority": {
"mode": "replay-simulated",
"physical_live": False,
"commands_enabled": False,
"actuation_allowed": False,
"navigation_or_safety_accepted": False,
"ground_truth": False,
},
}
monkeypatch.setattr(
m48,
"read_m47_reference_graph_lab",
lambda _: SimpleNamespace(
result_id=root.name,
result_root=root,
manifest=manifest,
report=report,
),
)
def _review_clips(clips: tuple[dict[str, Any], ...], *, state: str) -> list[dict[str, Any]]:
rows = []
for clip in clips:
fixture = _clip_fixture_state(str(clip["clip_id"]))
start = int(clip["start_sequence"])
end = int(clip["end_sequence"])
rows.append(
{
"clip_id": clip["clip_id"],
"start_sequence": start,
"end_sequence": end,
"review_state": state,
"no_object": False,
"tracklets": [
{
"object_id": "object-1",
"first_sequence": start,
"last_sequence": end,
"keyframes": [
{
"sequence": start,
"extent_xyxy": fixture["extent_xyxy"],
"visibility": "visible",
},
{
"sequence": end,
"extent_xyxy": fixture["extent_xyxy"],
"visibility": "partial",
},
],
"state_segments": [
{
"start_sequence": start,
"end_sequence": end,
"geometry_association": fixture["geometry_association"],
"freshness": "current",
"motion": fixture["motion"],
"threat": fixture["threat"],
"critical_corridor_obstacle": True,
}
],
"notes": None,
}
],
"notes": None,
}
)
return rows
def _review(pack: m48.M48ObjectQualityPack, reviewer_id: str) -> dict[str, Any]:
return {
"schema_version": m48.M48_REVIEW_SCHEMA,
"pack_id": pack.result_id,
"state": "completed-independent-no-predictions",
"reviewer_id": reviewer_id,
"review_round": 1,
"blindness": {
"candidate_identity_seen": False,
"model_predictions_seen": False,
"model_scores_seen": False,
"semantic_class_task_seen": False,
},
"clips": _review_clips(pack.clips, state="reviewed"),
"acceptance": {
"all_clips_reviewed": True,
"independent": True,
"submitted_at_utc": "2026-08-24T11:00:00Z",
},
}
def _build_generations(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
*,
unsafe_free_space: bool = False,
unsafe_free_space_split: str | None = None,
) -> tuple[m48.M48ObjectQualityPack, m48.M48ObjectTruthSeal]:
_fake_m47(tmp_path, monkeypatch)
clips = _clips()
pack = m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=_prediction_rows(
clips,
unsafe_free_space=unsafe_free_space,
unsafe_free_space_split=unsafe_free_space_split,
),
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs",
)
review_a = _review(pack, "reviewer-a")
review_b = _review(pack, "reviewer-b")
review_a_path = tmp_path / "review-a.json"
review_b_path = tmp_path / "review-b.json"
_write_json(review_a_path, review_a)
_write_json(review_b_path, review_b)
adjudication = {
"schema_version": m48.M48_ADJUDICATION_SCHEMA,
"pack_id": pack.result_id,
"state": "completed-adjudicated",
"adjudicator_id": "adjudicator-1",
"review_submission_sha256": sorted(
(m48._canonical_sha256(review_a), m48._canonical_sha256(review_b))
),
"clips": _review_clips(pack.clips, state="adjudicated"),
"acceptance": {
"all_clips_adjudicated": True,
"all_disagreements_resolved": True,
"sealed_at_utc": "2026-08-24T12:00:00Z",
},
}
adjudication_path = tmp_path / "adjudication.json"
_write_json(adjudication_path, adjudication)
truth = m48.build_m48_object_truth_seal(
pack_root=pack.result_root,
reviewer_a_path=review_a_path,
reviewer_b_path=review_b_path,
adjudication_path=adjudication_path,
output_root=tmp_path / "truth",
)
return pack, truth
def test_m48_pack_is_neutral_tracklet_review_evidence(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
pack, truth = _build_generations(tmp_path, monkeypatch)
reviewer_package = json.loads(
(pack.result_root / "reviewer-package.json").read_text(encoding="utf-8")
)
assert reviewer_package["strata_included"] is False
assert reviewer_package["frozen_predictions_included"] is False
assert all(
"strata" not in clip and "selection_hypotheses" not in clip
for clip in reviewer_package["clips"]
)
assert {
hypothesis
for clip in pack.clips
if clip["split"] == "validation"
for hypothesis in clip["selection_hypotheses"]
} == set(pack.manifest["identity"]["profile"]["required_validation_hypotheses"])
review_template = json.loads(
(pack.result_root / "review-template.json").read_text(encoding="utf-8")
)
assert "clips" in review_template and "frames" not in review_template
assert "tracklets" in review_template["clips"][0]
assert len(truth.truth_rows) == len(pack.frame_references)
assert truth.truth_rows[0]["objects"][0]["visibility"] == "visible"
assert truth.truth_rows[50]["objects"][0]["visibility"] == "partial"
def test_m48_perfect_class_free_result_passes_all_gates_and_registry(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
pack, truth = _build_generations(tmp_path, monkeypatch)
result = m48.score_m48_object_quality(
pack_root=pack.result_root,
truth_seal_root=truth.result_root,
output_root=tmp_path / "results",
)
assert result.report["acceptance"]["accepted"] is True
assert all(result.report["acceptance"]["gates"].values())
assert result.report["acceptance"]["scope"] == "validation-only"
assert result.report["metrics"] == result.report["metrics_by_split"]["validation"]
assert result.report["method"]["semantic_class_scored"] is False
assert result.report["decision"]["next_gate"] == ("m4.9-recorded-realtime-release-candidate")
repository_root = Path(__file__).resolve().parents[1]
registry = LaboratoryEvidenceRegistry.from_directory(repository_root / "config/laboratories")
definitions = {definition.work_id: definition for definition in registry.definitions}
pack_proof = verify_laboratory_evidence_result(
definitions["m48-object-centric-quality"], pack.result_root
)
result_proof = verify_laboratory_evidence_result(
definitions["m48-object-centric-quality"], result.result_root
)
assert pack_proof["artifact_count"] == 7
assert result_proof["artifact_count"] == 3
def test_m48_review_rejects_semantic_class_and_same_reviewer(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_fake_m47(tmp_path, monkeypatch)
clips = _clips()
pack = m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=_prediction_rows(clips, unsafe_free_space=False),
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs",
)
review_a = _review(pack, "reviewer-a")
review_a["clips"][0]["tracklets"][0]["category"] = "car"
review_a_path = tmp_path / "review-a.json"
_write_json(review_a_path, review_a)
with pytest.raises(m48.M48ObjectQualityError, match="fields"):
m48.validate_m48_review_submission(pack_root=pack.result_root, review_path=review_a_path)
review_a = _review(pack, "reviewer-a")
review_b = copy.deepcopy(review_a)
review_a_path = tmp_path / "review-a-clean.json"
review_b_path = tmp_path / "review-b-same.json"
_write_json(review_a_path, review_a)
_write_json(review_b_path, review_b)
adjudication = {
"schema_version": m48.M48_ADJUDICATION_SCHEMA,
"pack_id": pack.result_id,
"state": "completed-adjudicated",
"adjudicator_id": "adjudicator-1",
"review_submission_sha256": [m48._canonical_sha256(review_a)] * 2,
"clips": _review_clips(pack.clips, state="adjudicated"),
"acceptance": {
"all_clips_adjudicated": True,
"all_disagreements_resolved": True,
"sealed_at_utc": "2026-08-24T12:00:00Z",
},
}
adjudication_path = tmp_path / "adjudication.json"
_write_json(adjudication_path, adjudication)
with pytest.raises(m48.M48ObjectQualityError, match="must differ"):
m48.build_m48_object_truth_seal(
pack_root=pack.result_root,
reviewer_a_path=review_a_path,
reviewer_b_path=review_b_path,
adjudication_path=adjudication_path,
output_root=tmp_path / "truth",
)
def test_m48_unsafe_free_space_fails_with_bounded_atlas(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
pack, truth = _build_generations(tmp_path, monkeypatch, unsafe_free_space=True)
result = m48.score_m48_object_quality(
pack_root=pack.result_root,
truth_seal_root=truth.result_root,
output_root=tmp_path / "results",
)
assert result.report["acceptance"]["accepted"] is False
assert result.report["acceptance"]["gates"]["false_free_space_claims"] is False
assert result.report["acceptance"]["gates"]["critical_corridor_obstacle_recall"] is True
assert result.failure_atlas
assert any("false-free-space-claim" in row["causes"] for row in result.failure_atlas)
assert result.report["decision"]["next_gate"] == (
"bounded-cause-remediation-on-failed-m48-clusters"
)
def test_m48_development_failures_are_reported_but_cannot_fail_release(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
pack, truth = _build_generations(
tmp_path,
monkeypatch,
unsafe_free_space=True,
unsafe_free_space_split="development",
)
result = m48.score_m48_object_quality(
pack_root=pack.result_root,
truth_seal_root=truth.result_root,
output_root=tmp_path / "results",
)
assert result.report["acceptance"]["accepted"] is True
assert result.report["metrics_by_split"]["development"]["false_free_space_claims"] > 0
assert result.report["metrics"]["false_free_space_claims"] == 0
assert all(result.report["acceptance"]["gates"].values())
assert any(row["split"] == "development" for row in result.failure_atlas)
def test_m48_rejects_incomplete_clip_contract(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_fake_m47(tmp_path, monkeypatch)
clips = _clips()[:19]
with pytest.raises(m48.M48ObjectQualityError, match="2030"):
m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=_prediction_rows(clips, unsafe_free_space=False),
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs",
)
def test_m48_rejects_cross_split_and_vacuous_grouping(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_fake_m47(tmp_path, monkeypatch)
clips = _clips()
clips[10]["route_block"] = clips[0]["route_block"]
with pytest.raises(m48.M48ObjectQualityError, match="route_block crosses"):
m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=_prediction_rows(clips, unsafe_free_space=False),
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs-cross-split",
)
clips = _clips()
for index, clip in enumerate(clips):
clip["component_id"] = f"unique-component-{index:02d}"
with pytest.raises(m48.M48ObjectQualityError, match="non-vacuous"):
m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=_prediction_rows(clips, unsafe_free_space=False),
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs-vacuous",
)
def test_m48_profile_config_matches_executable_contract() -> None:
repository_root = Path(__file__).resolve().parents[1]
document = json.loads(
(repository_root / "config/perception/m48-object-quality-v1.json").read_text(
encoding="utf-8"
)
)
profile = m48.DEFAULT_M48_OBJECT_QUALITY_PROFILE
assert document["schema_version"] == m48.M48_PROFILE_SCHEMA
assert document["profile_id"] == profile.profile_id
assert document["clip_contract"]["minimum_clip_count"] == profile.minimum_clip_count
assert document["clip_contract"]["maximum_clip_count"] == profile.maximum_clip_count
assert document["review_contract"]["review_unit"] == "clip-local-object-tracklet"
assert document["review_contract"]["semantic_class_labels_allowed"] is False
assert document["review_contract"]["selection_hypotheses_visible_to_reviewers"] is False
assert document["clip_contract"]["release_gate_split"] == "validation"
assert document["clip_contract"]["required_validation_hypotheses"] == sorted(
m48.DEFAULT_M48_OBJECT_QUALITY_PROFILE.to_dict()["required_validation_hypotheses"]
)
assert document["release_thresholds"]["obstacle_presence_precision"] == (
profile.obstacle_presence_precision
)
assert document["release_thresholds"]["critical_corridor_obstacle_recall"] == (
profile.critical_corridor_obstacle_recall
)
def test_m48_pack_identity_is_stable_across_clip_and_prediction_input_order(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
_fake_m47(tmp_path, monkeypatch)
clips = _clips()
predictions = _prediction_rows(clips, unsafe_free_space=False)
first = m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=predictions,
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs-a",
)
second = m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=reversed(clips),
predictions=reversed(predictions),
preparation_provenance=_preparation_provenance(),
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs-b",
)
assert first.result_id == second.result_id
assert first.manifest["identity_sha256"] == second.manifest["identity_sha256"]
changed_provenance = _preparation_provenance()
changed_provenance["camera_index"]["sha256"] = "d" * 64
third = m48.build_m48_object_quality_pack(
m47_lab_root=tmp_path / "ignored-m47",
frame_catalog=_frame_catalog(),
clips=clips,
predictions=predictions,
preparation_provenance=changed_provenance,
frozen_at_utc="2026-08-24T10:00:00Z",
output_root=tmp_path / "packs-c",
)
assert third.result_id != first.result_id
assert third.manifest["identity"]["preparation"]["camera_index"]["sha256"] == ("d" * 64)