diff --git a/docs/16_ARCHITECTURE_AUDIT_EXECUTION_ROADMAP.md b/docs/16_ARCHITECTURE_AUDIT_EXECUTION_ROADMAP.md index b18661c..006325b 100644 --- a/docs/16_ARCHITECTURE_AUDIT_EXECUTION_ROADMAP.md +++ b/docs/16_ARCHITECTURE_AUDIT_EXECUTION_ROADMAP.md @@ -123,23 +123,44 @@ real authority. confidence `0.8730`. Frames 1213, 162 and 1823 are now explicit automatic engineering outcomes; only geometry frames 2622 and 4147 remain in the human queue. -- [ ] Resolve only the resulting ambiguous/high-impact human-exception queue - and freeze the minimum correction set and cause distribution. +- [x] Resolve the resulting ambiguous/high-impact human-exception queue and + freeze the minimum correction set and cause distribution. The immutable + human generation is + `e30-review-generation-7982a882558d0be690b4c7092e328c080bfcbf52478a220452be7e887a588250`; + coverage is 2/2. Frame 2622 is `background-or-noise` and frame 4147 is + `object-present`. +- [x] Implement the E31 fail-closed source qualification contract over the + immutable E10→E30 chain. It accounts for all 4,489 source frames, audits + recorded host-arrival timing, sweeps nine offset hypotheses, verifies the + exact factory calibration identity and produces a source-scoped self-mask + profile without inferring firing time, sensor height or physical body + dimensions. +- [x] Accept the immutable diagnostic profile + `e31-source-qualification-b2460a5eb143688c7eea6821b2277e13aea79868abe81d83f7e78548c119159a` + for E32 binding. Zero offset retains support for 87/87 evidenced + correspondences. The semantic self-mask is admitted from eight source cases + with zero bbox-centre collateral; the generic geometry point mask is + rejected because it would erase accepted object evidence. Navigation, + safety and command authority remain false. -The current A3 AI-assisted engineering generation covers all 486 items and +The frozen A3 engineering and human generations cover all 486 items and retains the exact A2 materialization/review-pack and 42-sheet evidence identities. It explicitly claims neither human ground truth nor navigation or safety acceptance and retains `lab_published=false`. The dominant conflict cause is detector placement on striped road/construction barriers rather than camera↔LiDAR registration; the `unknown` stratum is dominated by held world-track time freshness; geometry-only contains both expected static scene -geometry and 21 visible missed class-bearing objects. +geometry and 21 visible missed class-bearing objects. Earlier generations and +drafts remain historical and are not rewritten or presented as the current +product workflow. -A3 is not complete until the two routed human exceptions are resolved and the -minimum correction set is frozen. Earlier generations and drafts remain -historical and are not rewritten or presented as the current product workflow. -A4 remains blocked. No perception threshold changes are allowed before the -exception decision is frozen. +A3 and A4 are complete. E31 accepts only the recorded RAVNOVES00 diagnostic +binding and does not claim that host arrival is hardware firing time. The +factory KB4 identity is exact, but a measured calibration-target residual and +physical body/mount dimensions are unavailable. The accepted profile is +therefore source-session scoped and cannot transfer to another mount. A5 +`TrackGeometry v1` is the next critical-path implementation; E32 publication +must bind both that contract and the accepted E31 profile. ### A3 residual and human-exception policy @@ -153,5 +174,6 @@ exception decision is frozen. accuracy, navigation or safety acceptance threshold. - A repeated cause cluster or a high-impact case blocks the gate regardless of percentage. The 1% budget cannot hide a systematic defect. -- The current queue is 2 / 486 (`0.41%`) and therefore bounded, but A3 still - requires those two recorded decisions before its correction set is frozen. +- The routed queue was 2 / 486 (`0.41%`) and is now fully resolved in the + immutable human generation. No unresolved `insufficient-evidence` item + remains. diff --git a/experiments/perception/LAB_E31_REPORT_2026-07-27.md b/experiments/perception/LAB_E31_REPORT_2026-07-27.md new file mode 100644 index 0000000..24d302e --- /dev/null +++ b/experiments/perception/LAB_E31_REPORT_2026-07-27.md @@ -0,0 +1,144 @@ +# LAB E31 — source-time, calibration and self-mask qualification + +Date: 2026-07-27 +Status: accepted for diagnostic E32 binding; navigation, safety and command +authority are false +Immutable result: +`e31-source-qualification-b2460a5eb143688c7eea6821b2277e13aea79868abe81d83f7e78548c119159a` + +## Decision under test + +E31 tests whether the exact RAVNOVES00 source binding used by E29/E30 is +specific and stable enough to enter E32 full replay. It does not tune E29, +infer a raw LiDAR firing timestamp, invent a sensor height or promote the +result to navigation. + +The gate is fail closed. It requires: + +1. the immutable A3 engineering generation and complete human-exception + generation; +2. complete accounting of source-time bindings; +3. the exact factory KB4 calibration identity; +4. a predeclared offset sweep over evidenced camera↔LiDAR correspondences; +5. a source-evidenced self-mask decision that cannot silently erase accepted + object evidence. + +## Immutable input chain + +- Source pack: + `e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b`. +- Local-surface model: + `k1-local-surface-23762244c8bdb97de26fb721ac957d7a00bc9a63571ac4cfa4be19c4effc7d55`. +- E30 materialization: + `e30-materialization-841af926d8d28ab93538c46d8f31278a2234c4d1c12c7dc4dc296b249d59735a`. +- A3 engineering generation: + `e30-engineering-generation-62a4fea10dea9b77f69ceac1af5bf0e4928d9c7716083c22258a03670fe5bd4f`. +- A3 human generation: + `e30-review-generation-7982a882558d0be690b4c7092e328c080bfcbf52478a220452be7e887a588250`. +- Factory calibration content identity: + `05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`. +- Camera binding: `sensor.camera.right`, `camera_1`, KB4, `800×600`. + +Every input result and artifact digest is verified before E31 runs. Existing +results are re-opened only when the complete content identity matches. + +## Source-time audit + +The recording has 4,489 source frames. LiDAR/pose bindings exist for 3,928 +frames (`87.50%`); 561 unavailable frames remain explicitly unavailable. + +| timing relation | absolute p50 | absolute p95 | maximum | declared gate | +| --- | ---: | ---: | ---: | ---: | +| LiDAR ↔ camera | 25.303 ms | 70.789 ms | 99.574 ms | 100 ms | +| pose ↔ point cloud | 6.784 ms | 21.854 ms | 73.798 ms | 100 ms | + +These are recorded host-monotonic arrival relations. They are not hardware +firing timestamps. + +## Offset sensitivity + +The correspondence set contains 87 A3 items where the projection was +`aligned`, point ownership was `object` and the E29 item was semantic. For each +hypothesis E31 selects the nearest available recorded cloud, projects it with +the camera-frame pose and evaluates occupied support inside the exact E29 bbox +inset (`0.03`). + +| host-arrival hypothesis | evaluable | supported | supported fraction | +| ---: | ---: | ---: | ---: | +| -200 ms | 79 | 63 | 79.75% | +| -150 ms | 83 | 66 | 79.52% | +| -100 ms | 81 | 70 | 86.42% | +| -50 ms | 87 | 78 | 89.66% | +| 0 ms | 87 | 87 | 100.00% | +| +50 ms | 87 | 80 | 91.95% | +| +100 ms | 83 | 69 | 83.13% | +| +150 ms | 84 | 71 | 84.52% | +| +200 ms | 83 | 69 | 83.13% | + +Zero offset is retained. Its support deficit relative to the best hypothesis is +zero. The p95 bbox support-centroid residual is `0.3947` bbox diagonals. This is +a diagnostic correspondence residual, not a measured calibration-target +residual. + +## Calibration and self-mask result + +The loaded factory calibration exactly matches the source-pack identity. The +camera-from-LiDAR rotation determinant is `1.00000037`, the maximum +orthonormal residual is `5.30e-7`, and translation norm is `0.1005 m`. + +Eight A3 `self_points` person cases support a source-session semantic exclusion +rectangle: + +```text +normalized xyxy = [0.191196, 0.730990, 0.625038, 0.992935] +application = person bbox centre inside rectangle +observed collateral = 0 +``` + +This mask is bound to the source session and detector rule. It is not a +transferable physical vehicle mask. + +The two geometry `self_points` cases cannot support a generic projected point +mask. Their union would erase 69 selected points from one accepted object and +22 from another. E31 therefore rejects that mask and retains the two exact A3 +correction item identities instead. + +Physical body dimensions and a body-frame mount transform are unavailable. +E31 records both as absent; it does not substitute a guessed height. + +## Decision + +All ten predeclared requirements pass. The result status is +`accepted-diagnostic-source-profile` and `eligible_for_e32=true`. + +Acceptance means only: + +- E32 may bind this exact source session, factory calibration, zero + host-arrival offset and source-scoped semantic mask; +- unavailable source evidence remains unavailable; +- the unsafe generic geometry mask remains disabled; +- the result has no command, navigation or safety authority. + +It does not establish transfer to another physical mount. That remains an E36 +question with a second eligible real source. + +## Reproduction + +```bash +PYTHONPATH=src .venv/bin/python \ + experiments/perception/run_e31_source_qualification.py \ + --source-pack .runtime/compute-experiments/e10/lidar-packs/e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b \ + --local-surface .runtime/compute-experiments/k1-local-surface-v1/models/k1-local-surface-23762244c8bdb97de26fb721ac957d7a00bc9a63571ac4cfa4be19c4effc7d55 \ + --calibration .runtime/mission-core/evidence/sessions/private/device-calibration/20260720T105432Z_k1_factory_calibration_2e0e51ae5485 \ + --materialization .runtime/compute-experiments/e30/materializations/e30-materialization-841af926d8d28ab93538c46d8f31278a2234c4d1c12c7dc4dc296b249d59735a \ + --engineering-generation .runtime/compute-experiments/e30/engineering-generations/e30-engineering-generation-62a4fea10dea9b77f69ceac1af5bf0e4928d9c7716083c22258a03670fe5bd4f \ + --human-generation .runtime/compute-experiments/e30/human-review-generations/e30-review-generation-7982a882558d0be690b4c7092e328c080bfcbf52478a220452be7e887a588250 +``` + +The immutable result contains: + +- `qualification-report.json`; +- `binding-profile.json`; +- `offset-sweep.json`; +- 87 per-item correspondence score rows; +- a digest-bound manifest. diff --git a/experiments/perception/run_e31_source_qualification.py b/experiments/perception/run_e31_source_qualification.py new file mode 100644 index 0000000..63b27e2 --- /dev/null +++ b/experiments/perception/run_e31_source_qualification.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Build the immutable LAB E31 source qualification profile.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from k1link.compute.e31_source_qualification import ( + build_e31_source_qualification, +) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--source-pack", type=Path, required=True) + parser.add_argument("--local-surface", type=Path, required=True) + parser.add_argument("--calibration", type=Path, required=True) + parser.add_argument("--materialization", type=Path, required=True) + parser.add_argument("--engineering-generation", type=Path, required=True) + parser.add_argument("--human-generation", type=Path, required=True) + parser.add_argument( + "--output-root", + type=Path, + default=Path(".runtime/compute-experiments/e31/source-qualifications"), + ) + args = parser.parse_args() + + result = build_e31_source_qualification( + source_pack_root=args.source_pack, + local_surface_root=args.local_surface, + calibration_snapshot_root=args.calibration, + materialization_root=args.materialization, + engineering_generation_root=args.engineering_generation, + human_generation_root=args.human_generation, + output_root=args.output_root, + ) + print( + json.dumps( + { + "result_id": result.result_id, + "result_root": str(result.result_root), + "status": result.report["status"], + "eligible_for_e32": result.report["eligible_for_e32"], + "requirements": result.report["requirements"], + "authority": result.report["authority"], + }, + ensure_ascii=False, + indent=2, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/src/k1link/compute/__init__.py b/src/k1link/compute/__init__.py index 7f5ad50..c31c16f 100644 --- a/src/k1link/compute/__init__.py +++ b/src/k1link/compute/__init__.py @@ -7,6 +7,16 @@ from .annotation_workspace import ( prepare_annotation_workspace, validate_annotation_workspace, ) +from .e31_source_qualification import ( + DEFAULT_E31_SOURCE_QUALIFICATION_PROFILE, + E31_BINDING_PROFILE_SCHEMA, + E31_SOURCE_QUALIFICATION_REPORT_SCHEMA, + E31_SOURCE_QUALIFICATION_SCHEMA, + E31SourceQualification, + E31SourceQualificationError, + E31SourceQualificationProfile, + build_e31_source_qualification, +) from .evaluation_pack import ( ANNOTATION_CONTRACT_SCHEMA, EVALUATION_PACK_SCHEMA, @@ -210,6 +220,13 @@ __all__ = [ "COMPUTE_JOB_SCHEMA", "DEFAULT_QUALIFICATION_FRAME_COUNT", "DEFAULT_K1_LOCAL_SURFACE_PROFILE", + "DEFAULT_E31_SOURCE_QUALIFICATION_PROFILE", + "E31_BINDING_PROFILE_SCHEMA", + "E31_SOURCE_QUALIFICATION_REPORT_SCHEMA", + "E31_SOURCE_QUALIFICATION_SCHEMA", + "E31SourceQualification", + "E31SourceQualificationError", + "E31SourceQualificationProfile", "EVALUATION_PACK_SCHEMA", "EvaluationFrameRequest", "EvaluationPackFrame", @@ -320,6 +337,7 @@ __all__ = [ "prepare_recorded_qualification_slice", "assess_lidar_profile", "build_lidar_replay_pack_v2", + "build_e31_source_qualification", "build_lidar_ground_annotation_template", "build_lidar_ground_benchmark", "build_k1_local_surface", diff --git a/src/k1link/compute/e30_materialization.py b/src/k1link/compute/e30_materialization.py index 51c14ea..c122491 100644 --- a/src/k1link/compute/e30_materialization.py +++ b/src/k1link/compute/e30_materialization.py @@ -28,13 +28,13 @@ from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import ( project_map_points_kb4, ) -from .lidar_field_review import E10LidarFieldSource -from .lidar_local_surface import K1LocalSurfaceV1 from .e30_camera_evidence import ( E30CameraEvidenceSource, materialize_e30_camera_frames, open_e30_camera_evidence_source, ) +from .lidar_field_review import E10LidarFieldSource +from .lidar_local_surface import K1LocalSurfaceV1 from .semantic_geometry_fusion import ( CAMERA_GEOMETRY_FUSION_SCHEMA, CameraGeometryFusionProfile, diff --git a/src/k1link/compute/e31_source_qualification.py b/src/k1link/compute/e31_source_qualification.py new file mode 100644 index 0000000..985db13 --- /dev/null +++ b/src/k1link/compute/e31_source_qualification.py @@ -0,0 +1,1327 @@ +"""E31 source-time, calibration and source-scoped self-mask qualification. + +E31 consumes the immutable E10/E30 evidence chain. It does not tune the E29 +fusion thresholds and does not infer hardware firing time. Its output is a +content-addressed diagnostic binding profile that is either eligible for E32 +replay or fails closed. +""" + +from __future__ import annotations + +import hashlib +import json +import math +import os +import re +import shutil +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any, Final + +import numpy as np +import numpy.typing as npt + +from k1link.artifacts import utc_now_iso +from k1link.device_plugins.xgrids_k1.analyze.calibrated_overlay import ( + _load_calibration_snapshot, +) +from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import ( + Kb4ProjectionProfile, + project_map_points_kb4, +) + +from .lidar_field_review import E10LidarFieldSource +from .lidar_local_surface import POINT_OCCUPIED, K1LocalSurfaceV1 +from .semantic_geometry_fusion import _projection_profile + +E31_SOURCE_QUALIFICATION_SCHEMA: Final = "missioncore.e31-source-qualification/v1" +E31_SOURCE_QUALIFICATION_REPORT_SCHEMA: Final = "missioncore.e31-source-qualification-report/v1" +E31_BINDING_PROFILE_SCHEMA: Final = "missioncore.e31-binding-profile/v1" +E31_OFFSET_SWEEP_SCHEMA: Final = "missioncore.e31-offset-sweep/v1" +E31_CORRESPONDENCE_SCHEMA: Final = "missioncore.e31-correspondence-score/v1" + +E31_MANIFEST_NAME: Final = "manifest.json" +E31_REPORT_NAME: Final = "qualification-report.json" +E31_BINDING_PROFILE_NAME: Final = "binding-profile.json" +E31_OFFSET_SWEEP_NAME: Final = "offset-sweep.json" +E31_CORRESPONDENCES_NAME: Final = "correspondence-scores.jsonl" + +_RESULT_ID = re.compile(r"^e31-source-qualification-[a-f0-9]{64}$") +_MATERIALIZATION_ID = re.compile(r"^e30-materialization-[a-f0-9]{64}$") +_ENGINEERING_ID = re.compile(r"^e30-engineering-generation-[a-f0-9]{64}$") +_HUMAN_ID = re.compile(r"^e30-review-generation-[a-f0-9]{64}$") +_ITEM_ID = re.compile(r"^e30-review-item-[a-f0-9]{64}$") +_SHA256 = re.compile(r"^[a-f0-9]{64}$") + + +class E31SourceQualificationError(RuntimeError): + """An E31 source, decision chain or result violates the contract.""" + + +@dataclass(frozen=True, slots=True) +class E31SourceQualificationProfile: + """Predeclared diagnostic admission rules for the RAVNOVES00 E31 gate.""" + + profile_id: str = "e31-ravnoves00-source-qualification/v1" + offset_hypotheses_ms: tuple[int, ...] = ( + -200, + -150, + -100, + -50, + 0, + 50, + 100, + 150, + 200, + ) + maximum_candidate_gap_ms: float = 100.0 + minimum_correspondence_items: int = 50 + minimum_baseline_supported_fraction: float = 0.95 + maximum_baseline_support_deficit_fraction: float = 0.02 + minimum_occupied_points_per_correspondence: int = 2 + minimum_semantic_self_samples: int = 4 + maximum_semantic_self_mask_collateral_items: int = 0 + + def __post_init__(self) -> None: + offsets = self.offset_hypotheses_ms + numeric = ( + self.maximum_candidate_gap_ms, + self.minimum_baseline_supported_fraction, + self.maximum_baseline_support_deficit_fraction, + ) + if ( + not self.profile_id.strip() + or len(self.profile_id) > 160 + or not offsets + or tuple(sorted(set(offsets))) != offsets + or 0 not in offsets + or any(abs(value) > 1000 for value in offsets) + or not np.isfinite(numeric).all() + or not 1.0 <= self.maximum_candidate_gap_ms <= 500.0 + or not 1 <= self.minimum_correspondence_items <= 10_000 + or not 0.5 <= self.minimum_baseline_supported_fraction <= 1.0 + or not 0.0 <= self.maximum_baseline_support_deficit_fraction <= 0.25 + or not 1 <= self.minimum_occupied_points_per_correspondence <= 64 + or not 1 <= self.minimum_semantic_self_samples <= 1_000 + or not 0 <= self.maximum_semantic_self_mask_collateral_items <= 1_000 + ): + raise E31SourceQualificationError("E31 qualification profile is invalid") + + def to_dict(self) -> dict[str, object]: + return { + "schema_version": "missioncore.e31-source-qualification-profile/v1", + **asdict(self), + "offset_hypotheses_ms": list(self.offset_hypotheses_ms), + "offset_interpretation": "recorded-host-arrival-binding-hypothesis", + "raw_firing_time_inferred": False, + "hardcoded_sensor_height_m": None, + "threshold_tuning_allowed": False, + "navigation_or_safety_accepted": False, + } + + +DEFAULT_E31_SOURCE_QUALIFICATION_PROFILE: Final = E31SourceQualificationProfile() + + +@dataclass(frozen=True, slots=True) +class E31SourceQualification: + result_root: Path + result_id: str + manifest: dict[str, Any] + report: dict[str, Any] + + +@dataclass(frozen=True, slots=True) +class _E30Chain: + materialization_manifest: dict[str, Any] + items: tuple[dict[str, Any], ...] + engineering_manifest: dict[str, Any] + decisions: tuple[dict[str, Any], ...] + exceptions: tuple[dict[str, Any], ...] + human_manifest: dict[str, Any] + human_decisions: tuple[dict[str, Any], ...] + + +def build_e31_source_qualification( + *, + source_pack_root: Path, + local_surface_root: Path, + calibration_snapshot_root: Path, + materialization_root: Path, + engineering_generation_root: Path, + human_generation_root: Path, + output_root: Path, + profile: E31SourceQualificationProfile = DEFAULT_E31_SOURCE_QUALIFICATION_PROFILE, +) -> E31SourceQualification: + """Build or verify one immutable E31 diagnostic source profile.""" + + source = E10LidarFieldSource(source_pack_root) + surface = K1LocalSurfaceV1(local_surface_root) + try: + chain = _load_e30_chain( + materialization_root=materialization_root, + engineering_generation_root=engineering_generation_root, + human_generation_root=human_generation_root, + ) + _validate_source_chain(source=source, surface=surface, chain=chain) + calibration, calibration_identity = _load_calibration_snapshot(calibration_snapshot_root) + projection = Kb4ProjectionProfile.from_factory_calibration( + calibration, + str(source.identity["source_id"]), + ) + _validate_calibration_binding( + source=source, + projection=projection, + calibration_identity=calibration_identity, + ) + + profile_document = profile.to_dict() + identity = { + "schema_version": E31_SOURCE_QUALIFICATION_SCHEMA, + "source": { + "session_id": str(source.identity["session_id"]), + "source_id": str(source.identity["source_id"]), + "source_pack_id": source.pack_id, + "source_pack_artifact_sha256": _source_artifact_sha256(source), + "local_surface_model_id": surface.model_id, + "local_surface_artifact_sha256": _surface_artifact_sha256(surface), + "calibration_content_identity_sha256": calibration_identity, + "calibration_slot": projection.calibration_slot, + "materialization_id": str(chain.materialization_manifest["result_id"]), + "engineering_generation_id": str(chain.engineering_manifest["result_id"]), + "human_generation_id": str(chain.human_manifest["result_id"]), + }, + "method": profile_document, + "producer_sha256": _sha256(Path(__file__).resolve(strict=True)), + "authority": _diagnostic_authority(), + } + identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest() + result_id = f"e31-source-qualification-{identity_sha256}" + destination = output_root.expanduser().absolute() + destination.mkdir(mode=0o700, parents=True, exist_ok=True) + result_root = destination / result_id + if result_root.exists(): + return _read_existing(result_root, identity) + + timing = _timing_report(source) + correspondences = _correspondence_items(chain) + fusion_profile = _object( + _object( + chain.materialization_manifest.get("identity"), + "materialization identity", + ).get("fusion_profile"), + "materialization fusion profile", + ) + bbox_inset_fraction = float(fusion_profile["bbox_inset_fraction"]) + if not math.isfinite(bbox_inset_fraction) or not 0.0 <= bbox_inset_fraction < 0.25: + raise E31SourceQualificationError("materialization bbox inset is invalid") + sweep, correspondence_rows = _offset_sweep( + source=source, + surface=surface, + items=correspondences, + profile=profile, + bbox_inset_fraction=bbox_inset_fraction, + ) + self_mask = _self_mask_report( + materialization_root=materialization_root.resolve(strict=True), + chain=chain, + profile=profile, + ) + calibration_report = _calibration_report( + source=source, + projection=projection, + calibration_identity=calibration_identity, + calibration_snapshot_root=calibration_snapshot_root.resolve(strict=True), + ) + requirements = _admission_requirements( + source=source, + chain=chain, + timing=timing, + sweep=sweep, + self_mask=self_mask, + profile=profile, + ) + accepted = all(bool(value) for value in requirements.values()) + binding_profile = _binding_profile( + source=source, + chain=chain, + projection=projection, + calibration_identity=calibration_identity, + timing=timing, + sweep=sweep, + self_mask=self_mask, + accepted=accepted, + ) + report = { + "schema_version": E31_SOURCE_QUALIFICATION_REPORT_SCHEMA, + "result_id": result_id, + "status": ( + "accepted-diagnostic-source-profile" if accepted else "rejected-fail-closed" + ), + "eligible_for_e32": accepted, + "source_time": timing, + "calibration": calibration_report, + "offset_sensitivity": { + "correspondence_count": len(correspondences), + "selected_offset_ms": sweep["selected_offset_ms"], + "baseline_supported_fraction": sweep["baseline_supported_fraction"], + "best_supported_fraction": sweep["best_supported_fraction"], + "baseline_support_deficit_fraction": sweep["baseline_support_deficit_fraction"], + "baseline_centroid_residual_p95_bbox_diagonal": sweep[ + "baseline_centroid_residual_p95_bbox_diagonal" + ], + }, + "self_mask": self_mask, + "requirements": requirements, + "limitations": [ + "timestamps are recorded host-arrival bindings, not hardware firing time", + "bbox support-centroid residual is diagnostic correspondence " + "evidence, not a measured target", + "physical vehicle/body extrinsics and dimensions are unavailable", + "generic LiDAR point self-mask is rejected when it touches " + "accepted object evidence", + "profile is bound to this source session and cannot transfer to another mount", + ], + "authority": _diagnostic_authority(), + } + return _write_result( + result_root=result_root, + result_id=result_id, + identity=identity, + identity_sha256=identity_sha256, + report=report, + binding_profile=binding_profile, + sweep=sweep, + correspondences=correspondence_rows, + ) + finally: + surface.close() + source.close() + + +def _load_e30_chain( + *, + materialization_root: Path, + engineering_generation_root: Path, + human_generation_root: Path, +) -> _E30Chain: + materialization = _safe_result_root(materialization_root, _MATERIALIZATION_ID) + materialization_manifest = _read_json(materialization / "manifest.json") + _validate_content_identity( + root=materialization, + manifest=materialization_manifest, + schema="missioncore.e30-evidence-materialization/v2", + prefix="e30-materialization-", + ) + materialization_artifacts = _verified_artifacts( + materialization, + materialization_manifest, + ) + index_path = materialization_artifacts.get("materialized-items") + if ( + index_path is None + or materialization_manifest.get("camera_evidence_available") is not True + or materialization_manifest.get("lab_published") is not False + ): + raise E31SourceQualificationError("E30 materialization is incompatible") + items = tuple(_read_jsonl(index_path)) + if len(items) != materialization_manifest.get("item_count") or len( + {item.get("item_id") for item in items} + ) != len(items): + raise E31SourceQualificationError("E30 materialization coverage changed") + + engineering = _safe_result_root(engineering_generation_root, _ENGINEERING_ID) + engineering_manifest = _read_json(engineering / "manifest.json") + _validate_content_identity( + root=engineering, + manifest=engineering_manifest, + schema="missioncore.e30-engineering-generation/v1", + prefix="e30-engineering-generation-", + ) + engineering_artifacts = _verified_artifacts(engineering, engineering_manifest) + decisions_path = engineering_artifacts.get("engineering-decisions") + exceptions_path = engineering_artifacts.get("human-exceptions") + if ( + decisions_path is None + or exceptions_path is None + or engineering_manifest.get("ai_review_complete") is not True + or engineering_manifest.get("lab_published") is not False + ): + raise E31SourceQualificationError("E30 engineering generation is incomplete") + decisions = tuple(_read_jsonl(decisions_path)) + exceptions = tuple(_read_jsonl(exceptions_path)) + engineering_source = _object( + _object(engineering_manifest.get("identity"), "engineering identity").get("source"), + "engineering source", + ) + if ( + engineering_source.get("materialization_id") != materialization.name + or len(decisions) != len(items) + or {item.get("item_id") for item in decisions} != {item.get("item_id") for item in items} + or {item.get("item_id") for item in exceptions} + != { + item.get("item_id") + for item in decisions + if item.get("human_exception_required") is True + } + ): + raise E31SourceQualificationError("E30 engineering source chain changed") + + human = _safe_result_root(human_generation_root, _HUMAN_ID) + human_manifest = _read_json(human / "manifest.json") + _validate_content_identity( + root=human, + manifest=human_manifest, + schema="missioncore.e30-human-review-generation/v2", + prefix="e30-review-generation-", + ) + human_artifacts = _verified_artifacts(human, human_manifest) + human_decisions_path = human_artifacts.get("review-decisions") + human_source = _object( + _object(human_manifest.get("identity"), "human identity").get("source"), + "human source", + ) + coverage = _object(human_manifest.get("coverage"), "human coverage") + if ( + human_decisions_path is None + or human_source.get("materialization_id") != materialization.name + or human_source.get("engineering_generation_id") != engineering.name + or human_manifest.get("human_review_complete") is not True + or human_manifest.get("lab_published") is not False + or coverage.get("complete") is not True + or coverage.get("expected_item_count") != len(exceptions) + or coverage.get("reviewed_item_count") != len(exceptions) + ): + raise E31SourceQualificationError("E30 human exception gate is incomplete") + human_decisions = tuple(_read_jsonl(human_decisions_path)) + if ( + len(human_decisions) != len(exceptions) + or {item.get("item_id") for item in human_decisions} + != {item.get("item_id") for item in exceptions} + or any( + item.get("disposition") + not in {"object-present", "background-or-noise", "insufficient-evidence"} + for item in human_decisions + ) + ): + raise E31SourceQualificationError("E30 human decisions changed") + return _E30Chain( + materialization_manifest=materialization_manifest, + items=items, + engineering_manifest=engineering_manifest, + decisions=decisions, + exceptions=exceptions, + human_manifest=human_manifest, + human_decisions=human_decisions, + ) + + +def _validate_source_chain( + *, + source: E10LidarFieldSource, + surface: K1LocalSurfaceV1, + chain: _E30Chain, +) -> None: + materialization_identity = _object( + chain.materialization_manifest.get("identity"), + "materialization identity", + ) + materialization_source = _object( + materialization_identity.get("source"), + "materialization source", + ) + if ( + materialization_source.get("lidar_pack_id") != source.pack_id + or materialization_source.get("local_surface_model_id") != surface.model_id + or materialization_source.get("source_session_id") != source.identity.get("session_id") + or surface.identity.get("source_pack_id") != source.pack_id + or surface.identity.get("frame_count") != source.frame_count + or surface.identity.get("point_count") != source.point_count + ): + raise E31SourceQualificationError("E31 source chain is inconsistent") + + +def _validate_calibration_binding( + *, + source: E10LidarFieldSource, + projection: Kb4ProjectionProfile, + calibration_identity: str, +) -> None: + source_projection = _projection_profile(source) + if ( + calibration_identity != source.identity.get("calibration_sha256") + or projection.source_id != source_projection.source_id + or projection.calibration_slot != source_projection.calibration_slot + or projection.width != source_projection.width + or projection.height != source_projection.height + or not np.allclose( + projection.intrinsic_fx_fy_cx_cy, + source_projection.intrinsic_fx_fy_cx_cy, + rtol=0.0, + atol=1e-12, + ) + or not np.allclose( + projection.distortion_kb4, + source_projection.distortion_kb4, + rtol=0.0, + atol=1e-12, + ) + or not np.allclose( + projection.t_camera_from_lidar, + source_projection.t_camera_from_lidar, + rtol=0.0, + atol=1e-12, + ) + ): + raise E31SourceQualificationError("factory calibration binding changed") + + +def _timing_report(source: E10LidarFieldSource) -> dict[str, object]: + available = np.asarray(source.arrays["sample_available"], dtype=np.bool_) + lidar_delta = np.asarray(source.arrays["lidar_camera_delta_ms"], dtype=np.float64) + pose_delta = np.asarray(source.arrays["pose_point_delta_ms"], dtype=np.float64) + lidar_values = lidar_delta[np.isfinite(lidar_delta)] + pose_values = pose_delta[np.isfinite(pose_delta)] + if lidar_values.size != int(np.count_nonzero(available)) or pose_values.size != int( + np.count_nonzero(available) + ): + raise E31SourceQualificationError("source timing coverage is inconsistent") + temporal_policy = _object( + source.identity.get("temporal_policy"), + "source temporal policy", + ) + return { + "clock_source": temporal_policy.get("clock_source"), + "binding": temporal_policy.get("binding"), + "raw_firing_time_available": False, + "frame_count": source.frame_count, + "available_binding_count": int(lidar_values.size), + "unavailable_binding_count": int(source.frame_count - lidar_values.size), + "available_fraction": float(lidar_values.size / source.frame_count), + "lidar_camera_delta_ms": _distribution(lidar_values), + "lidar_camera_abs_delta_ms": _distribution(np.abs(lidar_values)), + "pose_point_delta_ms": _distribution(pose_values), + "pose_point_abs_delta_ms": _distribution(np.abs(pose_values)), + "declared_maximum_lidar_camera_delta_ms": float( + temporal_policy["maximum_lidar_camera_delta_ms"] + ), + "declared_maximum_pose_point_delta_ms": float( + temporal_policy["maximum_pose_point_delta_ms"] + ), + } + + +def _correspondence_items(chain: _E30Chain) -> tuple[dict[str, Any], ...]: + by_id = {str(item["item_id"]): item for item in chain.items} + result = [] + for decision in chain.decisions: + item = by_id[str(decision["item_id"])] + locator = _object(item.get("e29_locator"), "E30 locator") + if ( + decision.get("projection_assessment") == "aligned" + and decision.get("point_ownership") == "object" + and locator.get("kind") == "semantic-observation" + ): + result.append(item) + return tuple(result) + + +def _offset_sweep( + *, + source: E10LidarFieldSource, + surface: K1LocalSurfaceV1, + items: tuple[dict[str, Any], ...], + profile: E31SourceQualificationProfile, + bbox_inset_fraction: float, +) -> tuple[dict[str, Any], tuple[dict[str, Any], ...]]: + arrays = source.arrays + times = np.asarray(arrays["session_seconds"], dtype=np.float64) + available = np.asarray(arrays["sample_available"], dtype=np.bool_) + available_rows = np.flatnonzero(available) + offsets = np.asarray(arrays["cloud_offsets"], dtype=np.int64) + points = np.asarray(arrays["cloud_points_map"], dtype=np.float32) + positions = np.asarray(arrays["pose_positions_map"], dtype=np.float64) + orientations = np.asarray( + arrays["pose_quaternions_map_from_lidar"], + dtype=np.float64, + ) + point_class = np.asarray(surface.arrays["point_class"], dtype=np.uint8) + projection = _projection_profile(source) + sweep_rows: list[dict[str, Any]] = [] + item_rows: dict[str, dict[str, Any]] = { + str(item["item_id"]): { + "schema_version": E31_CORRESPONDENCE_SCHEMA, + "item_id": str(item["item_id"]), + "review_key": str(item["review_key"]), + "frame_index": int( + _object(item.get("evidence_binding"), "evidence binding")["frame_index"] + ), + "scores": [], + } + for item in items + } + for hypothesis_ms in profile.offset_hypotheses_ms: + support_counts: list[int] = [] + centroid_residuals: list[float] = [] + candidate_gaps: list[float] = [] + frame_shifts: list[int] = [] + supported = 0 + evaluable = 0 + for item in items: + binding = _object(item.get("evidence_binding"), "evidence binding") + frame_index = int(binding["frame_index"]) + target = float(times[frame_index]) + hypothesis_ms / 1000.0 + candidate_index = int( + available_rows[int(np.argmin(np.abs(times[available_rows] - target)))] + ) + candidate_gap_ms = abs(float(times[candidate_index] - target) * 1000.0) + candidate_gaps.append(candidate_gap_ms) + frame_shifts.append(candidate_index - frame_index) + score: dict[str, object] = { + "offset_ms": hypothesis_ms, + "candidate_frame_index": candidate_index, + "candidate_gap_ms": candidate_gap_ms, + "occupied_points_in_bbox": None, + "centroid_residual_bbox_diagonal": None, + "evaluable": False, + } + if candidate_gap_ms <= profile.maximum_candidate_gap_ms: + evaluable += 1 + start = int(offsets[candidate_index]) + end = int(offsets[candidate_index + 1]) + position = positions[frame_index] + orientation = orientations[frame_index] + projected = project_map_points_kb4( + points[start:end], + position_map_xyz=( + float(position[0]), + float(position[1]), + float(position[2]), + ), + orientation_map_from_lidar_xyzw=( + float(orientation[0]), + float(orientation[1]), + float(orientation[2]), + float(orientation[3]), + ), + profile=projection, + ) + occupied = point_class[start:end][projected.source_indices] == POINT_OCCUPIED + pixels = projected.pixels_xy[occupied] + snapshot = _object(item.get("e29_snapshot"), "E30 snapshot") + bbox = np.asarray(snapshot.get("bbox_xyxy"), dtype=np.float64) + if bbox.shape != (4,) or not np.isfinite(bbox).all(): + raise E31SourceQualificationError("correspondence bbox is invalid") + width = float(bbox[2] - bbox[0]) + height = float(bbox[3] - bbox[1]) + inset_bbox = np.asarray( + [ + bbox[0] + bbox_inset_fraction * width, + bbox[1] + bbox_inset_fraction * height, + bbox[2] - bbox_inset_fraction * width, + bbox[3] - bbox_inset_fraction * height, + ], + dtype=np.float64, + ) + inside = ( + (pixels[:, 0] >= inset_bbox[0]) + & (pixels[:, 0] < inset_bbox[2]) + & (pixels[:, 1] >= inset_bbox[1]) + & (pixels[:, 1] < inset_bbox[3]) + ) + inside_pixels = pixels[inside] + count = int(inside_pixels.shape[0]) + support_counts.append(count) + if count >= profile.minimum_occupied_points_per_correspondence: + supported += 1 + residual: float | None = None + if count: + pixel_center = np.median(inside_pixels, axis=0) + bbox_center = np.asarray( + [ + (inset_bbox[0] + inset_bbox[2]) * 0.5, + (inset_bbox[1] + inset_bbox[3]) * 0.5, + ] + ) + diagonal = math.hypot( + float(inset_bbox[2] - inset_bbox[0]), + float(inset_bbox[3] - inset_bbox[1]), + ) + residual = float(np.linalg.norm(pixel_center - bbox_center) / diagonal) + centroid_residuals.append(residual) + score.update( + { + "occupied_points_in_bbox": count, + "centroid_residual_bbox_diagonal": residual, + "evaluable": True, + } + ) + item_rows[str(item["item_id"])]["scores"].append(score) + supported_fraction = float(supported / evaluable) if evaluable else 0.0 + sweep_rows.append( + { + "offset_ms": hypothesis_ms, + "evaluable_count": evaluable, + "unavailable_count": len(items) - evaluable, + "supported_count": supported, + "supported_fraction": supported_fraction, + "occupied_support_total": int(sum(support_counts)), + "occupied_support_p50": _percentile_or_none(support_counts, 50), + "occupied_support_p95": _percentile_or_none(support_counts, 95), + "centroid_residual_p50_bbox_diagonal": _percentile_or_none( + centroid_residuals, + 50, + ), + "centroid_residual_p95_bbox_diagonal": _percentile_or_none( + centroid_residuals, + 95, + ), + "candidate_gap_p95_ms": _percentile_or_none(candidate_gaps, 95), + "frame_shift_p50": _percentile_or_none(frame_shifts, 50), + } + ) + baseline = next(item for item in sweep_rows if item["offset_ms"] == 0) + best_supported_fraction = max(float(item["supported_fraction"]) for item in sweep_rows) + sweep = { + "schema_version": E31_OFFSET_SWEEP_SCHEMA, + "interpretation": "recorded-host-arrival-binding-hypothesis", + "raw_firing_time_inferred": False, + "correspondence_count": len(items), + "bbox_inset_fraction": bbox_inset_fraction, + "minimum_occupied_points": (profile.minimum_occupied_points_per_correspondence), + "selected_offset_ms": 0, + "baseline_supported_fraction": baseline["supported_fraction"], + "best_supported_fraction": best_supported_fraction, + "baseline_support_deficit_fraction": ( + best_supported_fraction - float(baseline["supported_fraction"]) + ), + "baseline_centroid_residual_p95_bbox_diagonal": baseline[ + "centroid_residual_p95_bbox_diagonal" + ], + "hypotheses": sweep_rows, + } + return sweep, tuple(item_rows[item_id] for item_id in sorted(item_rows)) + + +def _self_mask_report( + *, + materialization_root: Path, + chain: _E30Chain, + profile: E31SourceQualificationProfile, +) -> dict[str, Any]: + items = {str(item["item_id"]): item for item in chain.items} + self_decisions = [ + decision + for decision in chain.decisions + if decision.get("cause_code") == "self_points" and decision.get("point_ownership") == "self" + ] + semantic_self = [] + geometry_self = [] + for decision in self_decisions: + item = items[str(decision["item_id"])] + bbox = _object(item.get("e29_snapshot"), "E30 snapshot").get("bbox_xyxy") + if isinstance(bbox, list) and len(bbox) == 4: + semantic_self.append((item, np.asarray(bbox, dtype=np.float64))) + else: + geometry_self.append(item) + if not semantic_self: + return { + "semantic_mask": {"status": "unavailable"}, + "geometry_point_mask": {"status": "unavailable"}, + "exact_correction_item_ids": sorted(str(item["item_id"]) for item in geometry_self), + } + projection = _object( + _object( + chain.materialization_manifest.get("identity"), + "materialization identity", + ).get("projection"), + "materialization projection", + ) + width = float(projection["width"]) + height = float(projection["height"]) + bboxes = np.vstack([bbox for _item, bbox in semantic_self]) + rectangle = np.asarray( + [ + np.min(bboxes[:, 0]), + np.min(bboxes[:, 1]), + np.max(bboxes[:, 2]), + np.max(bboxes[:, 3]), + ], + dtype=np.float64, + ) + collateral: list[str] = [] + for decision in chain.decisions: + if decision.get("cause_code") == "self_points": + continue + item = items[str(decision["item_id"])] + bbox = _object(item.get("e29_snapshot"), "E30 snapshot").get("bbox_xyxy") + if not isinstance(bbox, list) or len(bbox) != 4: + continue + values = np.asarray(bbox, dtype=np.float64) + center_x = float((values[0] + values[2]) * 0.5) + center_y = float((values[1] + values[3]) * 0.5) + if rectangle[0] <= center_x <= rectangle[2] and rectangle[1] <= center_y <= rectangle[3]: + collateral.append(str(item["item_id"])) + semantic_admitted = ( + len(semantic_self) >= profile.minimum_semantic_self_samples + and len(collateral) <= profile.maximum_semantic_self_mask_collateral_items + ) + geometry_mask = _geometry_mask_candidate( + materialization_root=materialization_root, + geometry_self=geometry_self, + items=items, + decisions=chain.decisions, + ) + return { + "semantic_mask": { + "status": "admitted" if semantic_admitted else "rejected", + "kind": "camera-semantic-exclusion-rectangle", + "class_allowlist": ["person"], + "application_rule": "bbox-center-inside-rectangle", + "rectangle_normalized_xyxy": [ + float(rectangle[0] / width), + float(rectangle[1] / height), + float(rectangle[2] / width), + float(rectangle[3] / height), + ], + "source_item_ids": sorted(str(item["item_id"]) for item, _ in semantic_self), + "source_sample_count": len(semantic_self), + "collateral_item_ids": sorted(collateral), + "collateral_item_count": len(collateral), + "scope": "source-session-diagnostic-only", + }, + "geometry_point_mask": geometry_mask, + "exact_correction_item_ids": sorted(str(item["item_id"]) for item in geometry_self), + } + + +def _geometry_mask_candidate( + *, + materialization_root: Path, + geometry_self: list[dict[str, Any]], + items: dict[str, dict[str, Any]], + decisions: tuple[dict[str, Any], ...], +) -> dict[str, Any]: + if not geometry_self: + return {"status": "unavailable", "reason": "no-geometry-self-evidence"} + source_pixels: list[npt.NDArray[np.float32]] = [] + for item in geometry_self: + pixels = _selected_projected_pixels(materialization_root, item) + if pixels.size: + source_pixels.append(pixels) + if not source_pixels: + return {"status": "unavailable", "reason": "self-points-not-projectable"} + combined = np.concatenate(source_pixels) + rectangle = np.asarray( + [ + np.min(combined[:, 0]), + np.min(combined[:, 1]), + np.max(combined[:, 0]), + np.max(combined[:, 1]), + ], + dtype=np.float64, + ) + collateral: list[dict[str, object]] = [] + for decision in decisions: + if decision.get("point_ownership") != "object": + continue + item = items[str(decision["item_id"])] + pixels = _selected_projected_pixels(materialization_root, item) + if not pixels.size: + continue + inside = ( + (pixels[:, 0] >= rectangle[0]) + & (pixels[:, 0] <= rectangle[2]) + & (pixels[:, 1] >= rectangle[1]) + & (pixels[:, 1] <= rectangle[3]) + ) + count = int(np.count_nonzero(inside)) + if count: + collateral.append( + { + "item_id": str(item["item_id"]), + "masked_selected_point_count": count, + } + ) + return { + "status": "rejected" if collateral else "admitted", + "kind": "camera-projected-point-exclusion-rectangle", + "rectangle_pixels_xyxy": [float(value) for value in rectangle], + "source_item_ids": sorted(str(item["item_id"]) for item in geometry_self), + "collateral": collateral, + "reason": ( + "candidate-touches-accepted-object-evidence" if collateral else "no-observed-collateral" + ), + } + + +def _selected_projected_pixels( + materialization_root: Path, + item: dict[str, Any], +) -> npt.NDArray[np.float32]: + artifact = _object(item.get("artifact"), "materialized artifact") + path = _verified_file( + materialization_root, + artifact, + ) + with np.load(path, allow_pickle=False) as arrays: + pixels = np.asarray(arrays["projected_pixels_xy"], dtype=np.float32) + selected = np.asarray( + arrays["projected_selected_mask"], + dtype=np.uint8, + ).astype(np.bool_) + if selected.shape != (pixels.shape[0],): + raise E31SourceQualificationError("materialized projection mask changed") + return pixels[selected] + + +def _calibration_report( + *, + source: E10LidarFieldSource, + projection: Kb4ProjectionProfile, + calibration_identity: str, + calibration_snapshot_root: Path, +) -> dict[str, object]: + manifest = _read_json(calibration_snapshot_root / "manifest.json") + transform = np.asarray(projection.t_camera_from_lidar, dtype=np.float64) + rotation = transform[:3, :3] + orthonormal_residual = float(np.max(np.abs(rotation.T @ rotation - np.eye(3)))) + return { + "content_identity_sha256": calibration_identity, + "snapshot_id": manifest.get("snapshot_id"), + "captured_at_utc": manifest.get("captured_at_utc"), + "compatibility_profile_id": manifest.get("compatibility_profile_id"), + "source_id": projection.source_id, + "calibration_slot": projection.calibration_slot, + "model": "kb4", + "resolution": [projection.width, projection.height], + "intrinsic_fx_fy_cx_cy": list(projection.intrinsic_fx_fy_cx_cy), + "distortion_kb4": list(projection.distortion_kb4), + "t_camera_from_lidar": transform.tolist(), + "rotation_determinant": float(np.linalg.det(rotation)), + "rotation_orthonormal_max_residual": orthonormal_residual, + "translation_norm_m": float(np.linalg.norm(transform[:3, 3])), + "matches_source_pack": (calibration_identity == source.identity.get("calibration_sha256")), + "measured_target_residual_available": False, + } + + +def _admission_requirements( + *, + source: E10LidarFieldSource, + chain: _E30Chain, + timing: dict[str, object], + sweep: dict[str, Any], + self_mask: dict[str, Any], + profile: E31SourceQualificationProfile, +) -> dict[str, bool]: + lidar_abs = _object( + timing.get("lidar_camera_abs_delta_ms"), + "LiDAR timing distribution", + ) + pose_abs = _object( + timing.get("pose_point_abs_delta_ms"), + "pose timing distribution", + ) + return { + "a3_human_exception_coverage_complete": ( + chain.human_manifest.get("human_review_complete") is True + and not any( + item.get("disposition") == "insufficient-evidence" for item in chain.human_decisions + ) + ), + "calibration_identity_pinned": bool(source.identity.get("calibration_sha256")), + "source_time_accounting_complete": ( + _integer(timing.get("available_binding_count"), "available binding count") + + _integer( + timing.get("unavailable_binding_count"), + "unavailable binding count", + ) + == source.frame_count + ), + "lidar_camera_p95_within_declared_gate": ( + _number(lidar_abs.get("p95"), "LiDAR timing p95") + <= _number( + timing.get("declared_maximum_lidar_camera_delta_ms"), + "declared LiDAR timing gate", + ) + ), + "pose_point_p95_within_declared_gate": ( + _number(pose_abs.get("p95"), "pose timing p95") + <= _number( + timing.get("declared_maximum_pose_point_delta_ms"), + "declared pose timing gate", + ) + ), + "correspondence_sample_sufficient": ( + int(sweep["correspondence_count"]) >= profile.minimum_correspondence_items + ), + "zero_offset_support_sufficient": ( + float(sweep["baseline_supported_fraction"]) + >= profile.minimum_baseline_supported_fraction + ), + "zero_offset_not_materially_worse_than_sweep": ( + float(sweep["baseline_support_deficit_fraction"]) + <= profile.maximum_baseline_support_deficit_fraction + ), + "semantic_self_mask_source_evidenced": ( + _object(self_mask.get("semantic_mask"), "semantic self mask").get("status") + == "admitted" + ), + "unsafe_geometry_point_mask_not_admitted": ( + _object( + self_mask.get("geometry_point_mask"), + "geometry point mask", + ).get("status") + != "admitted" + ), + } + + +def _binding_profile( + *, + source: E10LidarFieldSource, + chain: _E30Chain, + projection: Kb4ProjectionProfile, + calibration_identity: str, + timing: dict[str, object], + sweep: dict[str, Any], + self_mask: dict[str, Any], + accepted: bool, +) -> dict[str, Any]: + return { + "schema_version": E31_BINDING_PROFILE_SCHEMA, + "status": "accepted" if accepted else "rejected", + "eligible_for_e32": accepted, + "scope": { + "session_id": str(source.identity["session_id"]), + "source_id": str(source.identity["source_id"]), + "source_pack_id": source.pack_id, + "transferable_to_other_mounts": False, + }, + "time_binding": { + "basis": timing["binding"], + "clock_source": timing["clock_source"], + "selected_host_arrival_offset_ms": sweep["selected_offset_ms"], + "maximum_lidar_camera_delta_ms": timing["declared_maximum_lidar_camera_delta_ms"], + "maximum_pose_point_delta_ms": timing["declared_maximum_pose_point_delta_ms"], + "raw_firing_time_available": False, + }, + "calibration": { + "content_identity_sha256": calibration_identity, + "slot": projection.calibration_slot, + "model": "kb4", + "resolution": [projection.width, projection.height], + }, + "mount_and_self_mask": { + "physical_mount_transform": None, + "physical_body_dimensions": None, + "hardcoded_sensor_height_m": None, + "semantic_mask": self_mask["semantic_mask"], + "generic_geometry_point_mask": None, + "exact_geometry_correction_item_ids": self_mask["exact_correction_item_ids"], + }, + "a3_decision_chain": { + "engineering_generation_id": chain.engineering_manifest["result_id"], + "human_generation_id": chain.human_manifest["result_id"], + }, + "authority": _diagnostic_authority(), + } + + +def _write_result( + *, + result_root: Path, + result_id: str, + identity: dict[str, Any], + identity_sha256: str, + report: dict[str, Any], + binding_profile: dict[str, Any], + sweep: dict[str, Any], + correspondences: tuple[dict[str, Any], ...], +) -> E31SourceQualification: + staging = result_root.parent / f".{result_id}.{os.getpid()}.incomplete" + staging.mkdir(mode=0o700, exist_ok=False) + try: + _write_json(staging / E31_REPORT_NAME, report) + _write_json(staging / E31_BINDING_PROFILE_NAME, binding_profile) + _write_json(staging / E31_OFFSET_SWEEP_NAME, sweep) + with (staging / E31_CORRESPONDENCES_NAME).open( + "x", + encoding="utf-8", + ) as stream: + for item in correspondences: + stream.write(_canonical_json(item).decode("utf-8") + "\n") + artifacts = [ + _artifact("qualification-report", staging / E31_REPORT_NAME), + _artifact("binding-profile", staging / E31_BINDING_PROFILE_NAME), + _artifact("offset-sweep", staging / E31_OFFSET_SWEEP_NAME), + _artifact("correspondence-scores", staging / E31_CORRESPONDENCES_NAME), + ] + manifest = { + "schema_version": E31_SOURCE_QUALIFICATION_SCHEMA, + "result_id": result_id, + "identity_sha256": identity_sha256, + "identity": identity, + "created_at_utc": utc_now_iso(), + "status": report["status"], + "eligible_for_e32": report["eligible_for_e32"], + "lab_published": False, + "artifacts": artifacts, + "authority": _diagnostic_authority(), + } + _write_json(staging / E31_MANIFEST_NAME, manifest) + os.replace(staging, result_root) + except BaseException: + shutil.rmtree(staging, ignore_errors=True) + raise + return E31SourceQualification(result_root, result_id, manifest, report) + + +def _read_existing( + root: Path, + expected_identity: dict[str, Any], +) -> E31SourceQualification: + manifest = _read_json(root / E31_MANIFEST_NAME) + _validate_content_identity( + root=root, + manifest=manifest, + schema=E31_SOURCE_QUALIFICATION_SCHEMA, + prefix="e31-source-qualification-", + ) + if manifest.get("identity") != expected_identity: + raise E31SourceQualificationError("existing E31 identity collision") + artifacts = _verified_artifacts(root, manifest) + report_path = artifacts.get("qualification-report") + if report_path is None: + raise E31SourceQualificationError("existing E31 report is unavailable") + report = _read_json(report_path) + return E31SourceQualification(root, root.name, manifest, report) + + +def _safe_result_root(path: Path, pattern: re.Pattern[str]) -> Path: + root = path.expanduser().absolute() + if root.is_symlink(): + raise E31SourceQualificationError("source result cannot be a symlink") + root = root.resolve(strict=True) + if not root.is_dir() or pattern.fullmatch(root.name) is None: + raise E31SourceQualificationError("source result id is invalid") + return root + + +def _validate_content_identity( + *, + root: Path, + manifest: dict[str, Any], + schema: str, + prefix: str, +) -> None: + identity = manifest.get("identity") + identity_sha256 = manifest.get("identity_sha256") + if ( + manifest.get("schema_version") != schema + or manifest.get("result_id") != root.name + or not isinstance(identity, dict) + or not isinstance(identity_sha256, str) + or _SHA256.fullmatch(identity_sha256) is None + or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256 + or root.name != f"{prefix}{identity_sha256}" + or not _authority_is_diagnostic(manifest.get("authority")) + ): + raise E31SourceQualificationError("source result identity changed") + + +def _verified_artifacts( + root: Path, + manifest: dict[str, Any], +) -> dict[str, Path]: + values = manifest.get("artifacts") + if not isinstance(values, list): + raise E31SourceQualificationError("artifact list is invalid") + result: dict[str, Path] = {} + for value in values: + artifact = _object(value, "artifact") + role = artifact.get("role") + if not isinstance(role, str) or role in result: + raise E31SourceQualificationError("artifact role is invalid") + result[role] = _verified_file(root, artifact) + return result + + +def _verified_file(root: Path, artifact: dict[str, Any]) -> Path: + relative = artifact.get("path") + byte_length = artifact.get("byte_length") + digest = artifact.get("sha256") + if ( + not isinstance(relative, str) + or not relative + or relative.startswith("/") + or not isinstance(byte_length, int) + or isinstance(byte_length, bool) + or byte_length <= 0 + or not isinstance(digest, str) + or _SHA256.fullmatch(digest) is None + ): + raise E31SourceQualificationError("artifact metadata is invalid") + path = root / relative + if ( + path.is_symlink() + or not path.is_file() + or root.resolve() not in path.resolve().parents + or path.stat().st_size != byte_length + or _sha256(path) != digest + ): + raise E31SourceQualificationError("artifact content changed") + return path + + +def _source_artifact_sha256(source: E10LidarFieldSource) -> str: + return str(_object(source.manifest.get("artifact"), "source artifact")["sha256"]) + + +def _surface_artifact_sha256(surface: K1LocalSurfaceV1) -> str: + artifacts = _verified_artifact_documents(surface.manifest) + return str(artifacts["local-surface"]["sha256"]) + + +def _verified_artifact_documents( + manifest: dict[str, Any], +) -> dict[str, dict[str, Any]]: + values = manifest.get("artifacts") + if not isinstance(values, list): + raise E31SourceQualificationError("artifact documents are invalid") + result = {} + for value in values: + artifact = _object(value, "artifact") + role = artifact.get("role") + if not isinstance(role, str) or role in result: + raise E31SourceQualificationError("artifact role is invalid") + result[role] = artifact + return result + + +def _distribution(values: npt.NDArray[np.float64]) -> dict[str, float | int]: + if values.ndim != 1 or not values.size or not np.isfinite(values).all(): + raise E31SourceQualificationError("distribution input is invalid") + return { + "count": int(values.size), + "min": float(np.min(values)), + "p01": float(np.percentile(values, 1)), + "p05": float(np.percentile(values, 5)), + "p50": float(np.percentile(values, 50)), + "p95": float(np.percentile(values, 95)), + "p99": float(np.percentile(values, 99)), + "max": float(np.max(values)), + "mean": float(np.mean(values)), + } + + +def _percentile_or_none(values: list[float] | list[int], percentile: int) -> float | None: + if not values: + return None + return float(np.percentile(np.asarray(values, dtype=np.float64), percentile)) + + +def _read_json(path: Path) -> dict[str, Any]: + if path.is_symlink() or not path.is_file(): + raise E31SourceQualificationError("required JSON is unavailable") + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: + raise E31SourceQualificationError("required JSON is invalid") from exc + if not isinstance(value, dict): + raise E31SourceQualificationError("required JSON must be an object") + return value + + +def _read_jsonl(path: Path) -> list[dict[str, Any]]: + if path.is_symlink() or not path.is_file(): + raise E31SourceQualificationError("required JSONL is unavailable") + result = [] + with path.open("r", encoding="utf-8") as stream: + for line_number, line in enumerate(stream, start=1): + try: + value = json.loads(line) + except json.JSONDecodeError as exc: + raise E31SourceQualificationError(f"invalid JSONL line {line_number}") from exc + if not isinstance(value, dict): + raise E31SourceQualificationError("JSONL item must be an object") + result.append(value) + return result + + +def _write_json(path: Path, value: dict[str, Any]) -> None: + path.write_bytes( + json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + indent=2, + allow_nan=False, + ).encode("utf-8") + + b"\n" + ) + + +def _artifact(role: str, path: Path) -> dict[str, object]: + return { + "role": role, + "path": path.name, + "byte_length": path.stat().st_size, + "sha256": _sha256(path), + } + + +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: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _object(value: object, label: str) -> dict[str, Any]: + if not isinstance(value, dict): + raise E31SourceQualificationError(f"{label} is invalid") + return value + + +def _integer(value: object, label: str) -> int: + if not isinstance(value, int) or isinstance(value, bool): + raise E31SourceQualificationError(f"{label} is invalid") + 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 E31SourceQualificationError(f"{label} is invalid") + return float(value) + + +def _diagnostic_authority() -> dict[str, bool]: + return { + "commands_enabled": False, + "navigation_or_safety_accepted": False, + } + + +def _authority_is_diagnostic(value: object) -> bool: + return ( + isinstance(value, dict) + and value.get("commands_enabled") is False + and value.get("navigation_or_safety_accepted") is False + ) diff --git a/src/k1link/web/e30_human_review_api.py b/src/k1link/web/e30_human_review_api.py index 87d9d70..427fd61 100644 --- a/src/k1link/web/e30_human_review_api.py +++ b/src/k1link/web/e30_human_review_api.py @@ -9,12 +9,12 @@ from fastapi import Path as ApiPath from pydantic import BaseModel, ConfigDict, Field from k1link.compute.e30_human_review import ( + E30ExceptionDisposition, E30HumanReviewConflictError, E30HumanReviewIntegrityError, E30HumanReviewNotFoundError, E30HumanReviewStore, E30HumanReviewValidationError, - E30ExceptionDisposition, E30ReviewSubject, E30ReviewSubstrate, ) diff --git a/tests/test_e31_source_qualification.py b/tests/test_e31_source_qualification.py new file mode 100644 index 0000000..a700d11 --- /dev/null +++ b/tests/test_e31_source_qualification.py @@ -0,0 +1,238 @@ +from __future__ import annotations + +import hashlib +from pathlib import Path +from typing import Any + +import numpy as np +import pytest + +from k1link.compute.e31_source_qualification import ( + E31SourceQualificationError, + E31SourceQualificationProfile, + _E30Chain, + _offset_sweep, + _self_mask_report, +) +from k1link.compute.lidar_local_surface import POINT_OCCUPIED + + +class _SweepSource: + def __init__(self) -> None: + self.arrays = { + "session_seconds": np.asarray([0.0, 0.1, 0.2], dtype=np.float64), + "sample_available": np.ones(3, dtype=np.bool_), + "cloud_offsets": np.asarray([0, 2, 4, 6], dtype=np.int64), + "cloud_points_map": np.asarray( + [ + [1.0, 0.0, 2.0], + [1.1, 0.0, 2.0], + [0.0, 0.0, 2.0], + [0.1, 0.0, 2.0], + [-1.0, 0.0, 2.0], + [-1.1, 0.0, 2.0], + ], + dtype=np.float32, + ), + "pose_positions_map": np.zeros((3, 3), dtype=np.float64), + "pose_quaternions_map_from_lidar": np.asarray( + [[0.0, 0.0, 0.0, 1.0]] * 3, + dtype=np.float64, + ), + "intrinsic_fx_fy_cx_cy": np.asarray( + [100.0, 100.0, 50.0, 50.0], + dtype=np.float64, + ), + "distortion_kb4": np.zeros(4, dtype=np.float64), + "t_camera_from_lidar": np.eye(4, dtype=np.float64), + } + self.identity: dict[str, Any] = { + "source_id": "sensor.camera.right", + "camera_slot": "camera_1", + "projection": {"width": 100, "height": 100}, + } + + +class _SweepSurface: + def __init__(self) -> None: + self.arrays = { + "point_class": np.full(6, POINT_OCCUPIED, dtype=np.uint8), + } + + +def _artifact(path: Path) -> dict[str, object]: + return { + "path": path.name, + "byte_length": path.stat().st_size, + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + } + + +def _projection_artifact( + root: Path, + name: str, + pixels: list[list[float]], +) -> dict[str, object]: + path = root / f"{name}.npz" + np.savez( + path, + projected_pixels_xy=np.asarray(pixels, dtype=np.float32), + projected_selected_mask=np.ones(len(pixels), dtype=np.uint8), + ) + return _artifact(path) + + +def test_profile_rejects_ambiguous_offset_hypotheses() -> None: + with pytest.raises(E31SourceQualificationError): + E31SourceQualificationProfile(offset_hypotheses_ms=(0, -50, 50)) + with pytest.raises(E31SourceQualificationError): + E31SourceQualificationProfile(offset_hypotheses_ms=(-50, 50)) + + +def test_offset_sweep_keeps_evidenced_zero_binding() -> None: + source = _SweepSource() + surface = _SweepSurface() + profile = E31SourceQualificationProfile( + offset_hypotheses_ms=(-100, 0, 100), + minimum_correspondence_items=1, + ) + item = { + "item_id": "item-1", + "review_key": "semantic:1:1", + "evidence_binding": {"frame_index": 1}, + "e29_snapshot": {"bbox_xyxy": [40.0, 40.0, 60.0, 60.0]}, + } + + sweep, rows = _offset_sweep( + source=source, # type: ignore[arg-type] + surface=surface, # type: ignore[arg-type] + items=(item,), + profile=profile, + bbox_inset_fraction=0.03, + ) + + assert sweep["selected_offset_ms"] == 0 + assert sweep["baseline_supported_fraction"] == 1.0 + assert sweep["best_supported_fraction"] == 1.0 + assert sweep["baseline_support_deficit_fraction"] == 0.0 + assert [row["supported_count"] for row in sweep["hypotheses"]] == [0, 1, 0] + assert [score["candidate_frame_index"] for score in rows[0]["scores"]] == [ + 0, + 1, + 2, + ] + + +def test_self_mask_admits_only_non_colliding_semantic_rule( + tmp_path: Path, +) -> None: + items: list[dict[str, Any]] = [] + decisions: list[dict[str, Any]] = [] + for index in range(4): + item = { + "item_id": f"semantic-self-{index}", + "e29_snapshot": { + "label": "person", + "bbox_xyxy": [ + 20.0 + index, + 75.0, + 40.0 + index, + 99.0, + ], + }, + } + items.append(item) + decisions.append( + { + "item_id": item["item_id"], + "cause_code": "self_points", + "point_ownership": "self", + } + ) + + geometry_self = { + "item_id": "geometry-self", + "e29_snapshot": {}, + "artifact": _projection_artifact( + tmp_path, + "geometry-self", + [[20.0, 20.0], [30.0, 30.0]], + ), + } + accepted_object = { + "item_id": "accepted-object", + "e29_snapshot": {}, + "artifact": _projection_artifact( + tmp_path, + "accepted-object", + [[25.0, 25.0], [80.0, 80.0]], + ), + } + accepted_person = { + "item_id": "accepted-person", + "e29_snapshot": { + "label": "person", + "bbox_xyxy": [70.0, 50.0, 90.0, 90.0], + }, + "artifact": _projection_artifact( + tmp_path, + "accepted-person", + [[80.0, 80.0]], + ), + } + items.extend([geometry_self, accepted_object, accepted_person]) + decisions.extend( + [ + { + "item_id": geometry_self["item_id"], + "cause_code": "self_points", + "point_ownership": "self", + }, + { + "item_id": accepted_object["item_id"], + "cause_code": "none", + "point_ownership": "object", + }, + { + "item_id": accepted_person["item_id"], + "cause_code": "none", + "point_ownership": "object", + }, + ] + ) + chain = _E30Chain( + materialization_manifest={ + "identity": { + "projection": { + "width": 100, + "height": 100, + } + } + }, + items=tuple(items), + engineering_manifest={}, + decisions=tuple(decisions), + exceptions=(), + human_manifest={}, + human_decisions=(), + ) + + report = _self_mask_report( + materialization_root=tmp_path, + chain=chain, + profile=E31SourceQualificationProfile( + minimum_semantic_self_samples=4, + ), + ) + + assert report["semantic_mask"]["status"] == "admitted" + assert report["semantic_mask"]["application_rule"] == "bbox-center-inside-rectangle" + assert report["semantic_mask"]["collateral_item_count"] == 0 + assert report["geometry_point_mask"]["status"] == "rejected" + assert report["geometry_point_mask"]["collateral"] == [ + { + "item_id": "accepted-object", + "masked_selected_point_count": 1, + } + ] + assert report["exact_correction_item_ids"] == ["geometry-self"]