feat(perception): freeze blind detector gates
This commit is contained in:
@@ -14,6 +14,9 @@ Each gate produces evidence and an explicit GO, PAUSE or BLOCKED result.
|
||||
| Native pipeline telemetry | GO for producer/normalizer contract — lifecycle events and JSONL/MQTT sink boundaries exist and an E41 smoke run records real stage accounting. Durable Worker 006 MQTT wiring/deployment remains pending. |
|
||||
| Evidence storage | MEASURED — E44 finds 525,471,092 logical bytes, 312,753,179 unique-content bytes and 1.680146× amplification across 14 E30–E40 roots. Exact-content references/deduplication precede any format migration. |
|
||||
| Future transfer | PREREGISTERED — E43 freezes same-K1/mount/calibration/firmware, required streams, connected-component split and independent label reveal. Capture and labels do not yet exist. |
|
||||
| E31 binding sensitivity | MEASURED — E45 closes accounting for 87/87 accepted correspondences and finds no material monotonic residual association with represented image radius, rig speed or pose age. It does not supply calibration-target truth or outer-fisheye coverage. |
|
||||
| Detector Truth Island | PREPARED — E46 freezes 32 references with no prelabels, predictions, scores or candidate identity in the review package. Two independent reviews and adjudication are still required. |
|
||||
| Detector candidate comparison | FROZEN BEFORE TRUTH — E47 freezes raw-KB4 and fixed-valid-FOV-fill predictions from the same exact Mask R-CNN checkpoint. No accuracy result or winner exists before the E46 truth seal. |
|
||||
| Product interface | DEFERRED — no new windows, page anatomy or design changes are part of this stabilization increment. |
|
||||
|
||||
The governing decision is
|
||||
|
||||
@@ -142,6 +142,34 @@ across 14 admitted E30–E40 roots: `1.680146×` amplification and
|
||||
content-addressed referencing and exact deduplication, not an unmeasured
|
||||
format/database migration.
|
||||
|
||||
E45 immutable diagnostic
|
||||
`e45-binding-sensitivity-b10fa117f110d5ac7942f73c62f1300c7d9ce833a187542b992e58fb24d1be72`
|
||||
accounts for all 87 accepted E31 correspondences and stratifies the existing
|
||||
support-centroid residual by KB4 image radius, rig motion and recorded source
|
||||
age. Residual rank association is small for image radius (`ρ=0.0421`),
|
||||
translation speed (`ρ=0.0076`) and pose age (`ρ=-0.0181`), so the accepted E31
|
||||
zero-offset binding remains unchanged. This is not calibration-target truth:
|
||||
the outer fisheye stratum is empty, measured static-landmark reprojection and
|
||||
physical mount dimensions remain unavailable, and calibrated-perception P0
|
||||
stays open.
|
||||
|
||||
E46 immutable preparation
|
||||
`e46-detector-truth-island-d8ab2745679636dce374b720b562fce05d6d0a26be3eac88650224d7aa92267d`
|
||||
freezes 32 references: 16 anchors across eight route-time bins and all 16
|
||||
frames from four temporal groups. The reviewer package contains no model
|
||||
prelabels, predictions, scores or candidate identity. It references
|
||||
14,919,621 source bytes without copying images. The package is prepared, not
|
||||
truth: two different human reviewers and adjudication remain mandatory.
|
||||
|
||||
E47 immutable candidate freeze
|
||||
`e47-detector-candidate-freeze-514bcca7a8a26313cab5ffcacca053d7a0ec6fe7cbef25f15faf3a11e48ee92f`
|
||||
stores raw-KB4 and fixed-valid-FOV-fill predictions for those exact 32 frames.
|
||||
Both candidates use the same Mask R-CNN checkpoint; only preprocessing differs.
|
||||
The freeze contains 417 and 394 task-ontology predictions respectively, but
|
||||
these counts are descriptive rather than accuracy evidence. No truth was
|
||||
joined, no winner was selected and no retraining is authorized before the E46
|
||||
truth seal.
|
||||
|
||||
E43 immutable protocol
|
||||
`e43-future-capture-protocol-28f091b9648daffce988d44c183e21f56d77988061630de934f8003fb13701d8`
|
||||
preregisters the later same-K1/new-route transfer. It requires the same mount,
|
||||
@@ -177,6 +205,11 @@ The current non-UI priority order is:
|
||||
4. design exact-content references/deduplication from E44 before any storage
|
||||
migration;
|
||||
5. preserve E43 unchanged until the owner supplies the new capture.
|
||||
6. complete two independent E46 reviews and adjudication without exposing
|
||||
model prelabels or E47 predictions;
|
||||
7. reveal the sealed truth only after the E47 prediction freeze, then calculate
|
||||
the preregistered detector metrics and select or reject a preprocessing
|
||||
candidate.
|
||||
|
||||
The following remain outside the present stabilization increment:
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# LAB E45 · RAVNOVES00 binding sensitivity
|
||||
|
||||
Date: 2026-07-29
|
||||
|
||||
Status: accepted diagnostic measurement only
|
||||
|
||||
Immutable result:
|
||||
`e45-binding-sensitivity-b10fa117f110d5ac7942f73c62f1300c7d9ce833a187542b992e58fb24d1be72`
|
||||
|
||||
## Task
|
||||
|
||||
E31 accepted zero recorded-host-arrival offset for all 87 evidenced
|
||||
camera↔LiDAR correspondences, but its residual is a support-centroid location
|
||||
inside a reviewed detector box. It is not a calibration-target measurement.
|
||||
E45 asks whether that already accepted diagnostic residual visibly degrades
|
||||
with fisheye radius, rig motion, LiDAR↔camera age, or pose↔point age.
|
||||
|
||||
E45 does not:
|
||||
|
||||
- re-run projection or inference;
|
||||
- change E31 thresholds or the selected offset;
|
||||
- infer hardware firing time;
|
||||
- infer vehicle body or physical mount dimensions;
|
||||
- rename a detector-box residual as calibration truth.
|
||||
|
||||
## Immutable inputs
|
||||
|
||||
- RAVNOVES00 session `20260720T065719Z_viewer_live`;
|
||||
- E10 LiDAR pack
|
||||
`e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b`;
|
||||
- accepted E31
|
||||
`e31-source-qualification-b2460a5eb143688c7eea6821b2277e13aea79868abe81d83f7e78548c119159a`;
|
||||
- E30 materialization
|
||||
`e30-materialization-841af926d8d28ab93538c46d8f31278a2234c4d1c12c7dc4dc296b249d59735a`;
|
||||
- factory calibration identity
|
||||
`05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`;
|
||||
- right camera, `camera_1`, KB4, 800×600.
|
||||
|
||||
## Method
|
||||
|
||||
For every accepted E31 correspondence, E45 joins the zero-offset score with:
|
||||
|
||||
- normalized detector-box-centre radius from the 800×600 image centre;
|
||||
- centred translational speed from adjacent valid recorded poses;
|
||||
- centred quaternion angular speed from adjacent valid recorded poses;
|
||||
- absolute LiDAR↔camera host-arrival age;
|
||||
- absolute pose↔point host-arrival age;
|
||||
- translation and rotation exposure proxies over the combined source age.
|
||||
|
||||
The 561 unavailable E10 LiDAR/pose rows remain unavailable. They are neither
|
||||
interpolated nor replaced. Motion is evaluated only between adjacent valid
|
||||
pose rows. Every derived row preserves the exact E31 item and source frame
|
||||
identity.
|
||||
|
||||
The frozen E45 profile uses descriptive low/middle/high strata. Correlations
|
||||
are Spearman rank coefficients against the existing
|
||||
`centroid_residual_bbox_diagonal`. They are diagnostic associations, not
|
||||
causal calibration-error estimates.
|
||||
|
||||
## Result
|
||||
|
||||
Evidence accounting closes at 87/87 rows. All 87 retain at least two occupied
|
||||
points in their reviewed box.
|
||||
|
||||
| Measurement | Value |
|
||||
| --- | ---: |
|
||||
| Support fraction | 1.000 |
|
||||
| Residual p50 | 0.22045 bbox diagonal |
|
||||
| Residual p95 | 0.39466 bbox diagonal |
|
||||
| Occupied support p50 | 6 points |
|
||||
| Occupied support p95 | 57 points |
|
||||
| Image radius max | 0.72639 normalized |
|
||||
| Translation speed p50 / p95 | 1.325 / 2.709 m/s |
|
||||
| Angular speed p50 / p95 | 13.941 / 34.579 deg/s |
|
||||
| LiDAR↔camera age p50 / p95 | 22.633 / 81.594 ms |
|
||||
| Pose↔point age p50 / p95 | 6.060 / 16.829 ms |
|
||||
| Translation exposure p50 / p95 | 0.0383 / 0.1159 m |
|
||||
| Rotation exposure p50 / p95 | 0.4157 / 1.3000 deg |
|
||||
|
||||
Residual correlations are small:
|
||||
|
||||
| Variable | Spearman ρ |
|
||||
| --- | ---: |
|
||||
| Image radius | +0.0421 |
|
||||
| Translation speed | +0.0076 |
|
||||
| Angular speed | −0.0669 |
|
||||
| LiDAR↔camera age | +0.1016 |
|
||||
| Pose↔point age | −0.0181 |
|
||||
| Translation exposure | +0.0051 |
|
||||
| Rotation exposure | −0.0312 |
|
||||
|
||||
The high LiDAR↔camera-age stratum contains 12 correspondences and still closes
|
||||
12/12 support. The high translation-speed stratum contains 57 and closes
|
||||
57/57. The high angular-speed stratum contains 48 and closes 48/48.
|
||||
|
||||
The image-radius high stratum is empty. The accepted E31 correspondence set
|
||||
does not test the outer KB4 fisheye belt beyond normalized radius `0.85`.
|
||||
|
||||
## Failed attempt retained as implementation evidence
|
||||
|
||||
The first bounded E45 execution failed closed because the source pack contains
|
||||
561 deliberately unavailable LiDAR/pose rows with non-finite pose arrays. The
|
||||
initial implementation incorrectly required finite pose values for all 4,489
|
||||
camera rows. The implementation was corrected to derive motion only between
|
||||
adjacent valid pose rows. No missing pose was interpolated and no E31 input was
|
||||
changed.
|
||||
|
||||
The successful local run completed in 0.74 seconds. Maximum RSS was
|
||||
249,446,400 bytes and peak memory footprint was 213,025,728 bytes. No Docker
|
||||
container, Worker 006 job, model, video decode, network change, or second
|
||||
Mission Core service was started.
|
||||
|
||||
## Decision
|
||||
|
||||
The accepted E31 zero-offset source binding is retained unchanged. E45 finds
|
||||
no material monotonic relationship between the existing detector-box
|
||||
support-centroid residual and the measured motion/age/radius variables inside
|
||||
the represented source envelope.
|
||||
|
||||
This result does not close calibrated-perception P0:
|
||||
|
||||
- measured static-landmark or calibration-target reprojection error remains
|
||||
unavailable;
|
||||
- the outer fisheye belt is not represented;
|
||||
- physical mount/body dimensions remain unavailable;
|
||||
- hardware firing time remains unavailable;
|
||||
- cross-route or changed-mount transfer remains unproved.
|
||||
|
||||
The next detector-quality gate is an independent Truth Island with no model
|
||||
prelabels exposed to reviewers and with prediction frozen before labels are
|
||||
revealed. A later physical calibration-target capture can close the remaining
|
||||
P0 measurement, but E45 does not fabricate it from RAVNOVES00.
|
||||
|
||||
Navigation, safety and command authority remain false.
|
||||
@@ -0,0 +1,95 @@
|
||||
# LAB E46 · RAVNOVES00 detector Truth Island preparation
|
||||
|
||||
Date: 2026-07-29
|
||||
|
||||
Status: prepared; awaiting two independent human reviews
|
||||
|
||||
Immutable result:
|
||||
`e46-detector-truth-island-d8ab2745679636dce374b720b562fce05d6d0a26be3eac88650224d7aa92267d`
|
||||
|
||||
## Task
|
||||
|
||||
E46 prepares a small, bounded and reviewable detector Truth Island on the
|
||||
known RAVNOVES00 right-camera source. It breaks the circular evaluation pattern
|
||||
identified by E41: reviewers receive source-image references and an empty
|
||||
annotation template, while model prelabels, predictions, scores and candidate
|
||||
identities stay outside the review package.
|
||||
|
||||
E46 does not yet create truth, calculate accuracy or select a detector.
|
||||
|
||||
## Immutable input
|
||||
|
||||
- E2 evaluation pack
|
||||
`evaluation-pack-7a983bba75d46c7c260252cb2d461e1384dcb92cda9e164397e841e6ebb37789`;
|
||||
- source job `recorded-camera-602ac89026ed12978619801d`;
|
||||
- source input SHA-256
|
||||
`602ac89026ed12978619801d4edea0cae24b5cc3afabd9f7af2858de6505a20e`;
|
||||
- session `20260720T065719Z_viewer_live`;
|
||||
- right camera, `camera_1`, KB4, 800×600;
|
||||
- fixed valid-FOV-fill preprocessing identity retained only as source
|
||||
provenance, not shown as a model hint.
|
||||
|
||||
## Frozen selection
|
||||
|
||||
The deterministic profile selects 32 frames:
|
||||
|
||||
- 16 anchors: two from each of eight route-time bins;
|
||||
- 16 temporal frames: all four frames from each of four existing temporal
|
||||
groups;
|
||||
- source frame range: 70–4436;
|
||||
- referenced source image bytes: 14,919,621;
|
||||
- copied source images: zero.
|
||||
|
||||
The four temporal groups are:
|
||||
|
||||
- `clip-close-car`;
|
||||
- `clip-near-structure`;
|
||||
- `clip-stroller-person`;
|
||||
- `clip-vehicle-occlusion`.
|
||||
|
||||
The package is content-addressed through
|
||||
`image-references.jsonl`. Every reference retains source path, byte length,
|
||||
file SHA-256 and pixel SHA-256.
|
||||
|
||||
## Review contract
|
||||
|
||||
Two different human reviewers must independently annotate all identifiable
|
||||
instances inside the valid camera field of view. The target classes are
|
||||
`person`, `bicycle`, `motorcycle`, `car`, `heavy_vehicle`,
|
||||
`static_obstacle` and `animal`. Hard-negative status plus occlusion and
|
||||
truncation flags are required. Disagreements require adjudication.
|
||||
|
||||
Neither reviewer may see:
|
||||
|
||||
- model prelabels;
|
||||
- model predictions or scores;
|
||||
- candidate identity;
|
||||
- candidate comparison results.
|
||||
|
||||
Prediction must be frozen before labels are revealed. E47 performs that freeze
|
||||
in a separate immutable result.
|
||||
|
||||
## Result and resource boundary
|
||||
|
||||
The package contains 32/32 valid references, an empty review template, the
|
||||
blind contract and a preparation report. Its state is
|
||||
`prepared-awaiting-independent-human-review`; truth remains unavailable and
|
||||
candidate comparison remains unauthorized.
|
||||
|
||||
The local preparation completed in 0.54 seconds. Maximum RSS was 102,596,608
|
||||
bytes and peak memory footprint was 64,406,344 bytes. No Docker container,
|
||||
Worker 006 job, model inference, image copy, network mutation or second Mission
|
||||
Core service was started.
|
||||
|
||||
## Decision
|
||||
|
||||
Truth Island preparation is complete, but the island is not sealed. The next
|
||||
manual gate is two independent reviews followed by adjudication. Only the
|
||||
sealed adjudicated labels may be joined with the already-frozen E47
|
||||
predictions.
|
||||
|
||||
This is still same-source truth on RAVNOVES00, not cross-route validation.
|
||||
E38–E40 source-trained candidates remain ineligible for this gate because the
|
||||
source already contains dense engineering labels.
|
||||
|
||||
Navigation, safety and command authority remain false.
|
||||
@@ -0,0 +1,72 @@
|
||||
# LAB E47 · detector candidate freeze before truth reveal
|
||||
|
||||
Date: 2026-07-29
|
||||
|
||||
Status: predictions frozen; awaiting E46 truth seal
|
||||
|
||||
Immutable result:
|
||||
`e47-detector-candidate-freeze-514bcca7a8a26313cab5ffcacca053d7a0ec6fe7cbef25f15faf3a11e48ee92f`
|
||||
|
||||
## Task
|
||||
|
||||
E47 freezes two detector prediction candidates for the exact 32 E46 frames
|
||||
before either independent review is revealed. This makes the later
|
||||
raw-versus-calibrated-preprocessing comparison reproducible and prevents
|
||||
post-label tuning.
|
||||
|
||||
E47 deliberately computes no accuracy metric and selects no winner.
|
||||
|
||||
## Candidates
|
||||
|
||||
Both candidates use the same exact generic Mask R-CNN checkpoint:
|
||||
`73cbd0190fcbe3ba339921fbce2c3a0b6bb9126c9a133c85e43a2a8e060a109e`.
|
||||
The only intended comparison variable is camera preprocessing:
|
||||
|
||||
| Candidate | Input profile | Admitted boxes | Frames |
|
||||
| --- | --- | ---: | ---: |
|
||||
| `maskrcnn-kb4-raw` | raw KB4 800×600 | 417 | 32 |
|
||||
| `maskrcnn-kb4-valid-fov-fill` | fixed valid-FOV fill 800×600 | 394 | 32 |
|
||||
|
||||
The raw candidate contained 432 source instances. Fifteen categories outside
|
||||
the frozen task ontology were ignored, leaving 417 admitted predictions. The
|
||||
valid-FOV-fill candidate contained and admitted 394 predictions. Both
|
||||
candidates produced at least one admitted prediction for every selected frame.
|
||||
|
||||
These counts are descriptive only. Fewer or more boxes do not imply better
|
||||
accuracy.
|
||||
|
||||
## Frozen output
|
||||
|
||||
E47 writes 64 content-addressed prediction rows: one row for each candidate and
|
||||
E46 image. The combined prediction row digest is
|
||||
`0aec660d33c002b3e0578841b09e9bbb25e15c425b2428ebbf5a1957d23371fa`.
|
||||
Every row retains the E46 sequence and source-image digest while excluding
|
||||
truth and review fields.
|
||||
|
||||
The E46 reviewer package is not modified. The result records:
|
||||
|
||||
- truth labels unavailable;
|
||||
- no truth join performed;
|
||||
- accuracy metrics unavailable;
|
||||
- no candidate winner;
|
||||
- model retraining unauthorized.
|
||||
|
||||
## Resource boundary
|
||||
|
||||
The local freeze completed in 0.51 seconds. Maximum RSS was 105,103,360 bytes
|
||||
and peak memory footprint was 67,634,016 bytes. It reused existing immutable
|
||||
prediction artifacts; no model inference, video decode, Docker container,
|
||||
Worker 006 job, network mutation or second Mission Core service was started.
|
||||
|
||||
## Decision
|
||||
|
||||
The two preprocessing candidates are now frozen before label reveal. The next
|
||||
gate is to complete and adjudicate both E46 reviews, seal the resulting truth,
|
||||
then compute COCO AP/AR, per-class recall, person/vehicle miss rate,
|
||||
false-large-box rate, valid-FOV boundary leakage and temporal flicker.
|
||||
|
||||
Until that gate, neither candidate is preferred and no detector-quality claim
|
||||
is made. The result remains source-scoped to the known RAVNOVES00 right camera
|
||||
and cannot establish cross-route generalization.
|
||||
|
||||
Navigation, safety and command authority remain false.
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the source-scoped E45 binding sensitivity audit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e45_binding_sensitivity import (
|
||||
build_e45_binding_sensitivity,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--e31-result-root", type=Path, required=True)
|
||||
parser.add_argument("--source-pack-root", type=Path, required=True)
|
||||
parser.add_argument("--materialization-root", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e45_binding_sensitivity(
|
||||
e31_result_root=args.e31_result_root,
|
||||
source_pack_root=args.source_pack_root,
|
||||
materialization_root=args.materialization_root,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result.result_id,
|
||||
"result_root": str(result.result_root),
|
||||
"analysis": result.report["analysis"],
|
||||
"decision": result.report["decision"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare the references-only E46 detector Truth Island."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e46_detector_truth_island import (
|
||||
build_e46_detector_truth_island,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--evaluation-pack-root", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e46_detector_truth_island(
|
||||
evaluation_pack_root=args.evaluation_pack_root,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result.result_id,
|
||||
"result_root": str(result.result_root),
|
||||
"status": result.report["status"],
|
||||
"selection": result.report["selection"],
|
||||
"blindness": result.report["blindness"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze E47 detector candidates before E46 truth reveal."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.compute.e47_detector_candidate_freeze import (
|
||||
build_e47_detector_candidate_freeze,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--truth-island-root", type=Path, required=True)
|
||||
parser.add_argument("--raw-result-root", type=Path, required=True)
|
||||
parser.add_argument("--valid-fov-result-root", type=Path, required=True)
|
||||
parser.add_argument("--output-root", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
result = build_e47_detector_candidate_freeze(
|
||||
truth_island_root=args.truth_island_root,
|
||||
raw_result_root=args.raw_result_root,
|
||||
valid_fov_result_root=args.valid_fov_result_root,
|
||||
output_root=args.output_root,
|
||||
)
|
||||
report = result["report"]
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result["result_id"],
|
||||
"result_root": str(result["result_root"]),
|
||||
"status": report["status"],
|
||||
"candidates": report["candidates"],
|
||||
"blindness": report["blindness"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,988 @@
|
||||
"""E45 source-scoped binding sensitivity audit over accepted E31 evidence.
|
||||
|
||||
E45 does not re-run projection, tune E31, or manufacture calibration target
|
||||
truth. It joins accepted E31 correspondences with the exact E10 pose/timing
|
||||
arrays and E30 camera geometry, then measures how the existing diagnostic
|
||||
residual behaves across image radius, rig motion, and source age.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from .e31_source_qualification import (
|
||||
E31_CORRESPONDENCES_NAME,
|
||||
E31SourceQualificationError,
|
||||
read_e31_source_qualification,
|
||||
)
|
||||
from .lidar_field_review import E10LidarFieldSource
|
||||
|
||||
E45_RESULT_SCHEMA: Final = "missioncore.e45-binding-sensitivity/v1"
|
||||
E45_REPORT_SCHEMA: Final = "missioncore.e45-binding-sensitivity-report/v1"
|
||||
E45_ROW_SCHEMA: Final = "missioncore.e45-binding-sensitivity-row/v1"
|
||||
E45_PROFILE_SCHEMA: Final = "missioncore.e45-binding-sensitivity-profile/v1"
|
||||
E45_MANIFEST_NAME: Final = "manifest.json"
|
||||
E45_REPORT_NAME: Final = "binding-sensitivity-report.json"
|
||||
E45_ROWS_NAME: Final = "binding-sensitivity-rows.jsonl"
|
||||
|
||||
_AUTHORITY: Final = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class E45BindingSensitivityError(RuntimeError):
|
||||
"""An E45 input, analysis, or immutable result violates the contract."""
|
||||
|
||||
|
||||
def _valid_edges(
|
||||
values: tuple[float, float],
|
||||
*,
|
||||
lower: float,
|
||||
upper: float,
|
||||
) -> bool:
|
||||
return bool(
|
||||
len(values) == 2
|
||||
and np.isfinite(values).all()
|
||||
and lower <= values[0] < values[1] <= upper
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class E45BindingSensitivityProfile:
|
||||
"""Frozen descriptive strata for the source-scoped E45 audit."""
|
||||
|
||||
profile_id: str = "e45-ravnoves00-binding-sensitivity/v1"
|
||||
image_radius_edges: tuple[float, float] = (0.5, 0.85)
|
||||
translation_speed_edges_mps: tuple[float, float] = (0.1, 1.0)
|
||||
angular_speed_edges_deg_s: tuple[float, float] = (2.0, 12.0)
|
||||
lidar_camera_age_edges_ms: tuple[float, float] = (25.0, 60.0)
|
||||
pose_point_age_edges_ms: tuple[float, float] = (10.0, 25.0)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if (
|
||||
not self.profile_id.strip()
|
||||
or len(self.profile_id) > 160
|
||||
or not _valid_edges(self.image_radius_edges, lower=0.0, upper=2.0)
|
||||
or not _valid_edges(
|
||||
self.translation_speed_edges_mps,
|
||||
lower=0.0,
|
||||
upper=30.0,
|
||||
)
|
||||
or not _valid_edges(
|
||||
self.angular_speed_edges_deg_s,
|
||||
lower=0.0,
|
||||
upper=720.0,
|
||||
)
|
||||
or not _valid_edges(
|
||||
self.lidar_camera_age_edges_ms,
|
||||
lower=0.0,
|
||||
upper=500.0,
|
||||
)
|
||||
or not _valid_edges(
|
||||
self.pose_point_age_edges_ms,
|
||||
lower=0.0,
|
||||
upper=500.0,
|
||||
)
|
||||
):
|
||||
raise E45BindingSensitivityError("E45 profile is invalid")
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": E45_PROFILE_SCHEMA,
|
||||
**asdict(self),
|
||||
"image_radius_edges": list(self.image_radius_edges),
|
||||
"translation_speed_edges_mps": list(
|
||||
self.translation_speed_edges_mps
|
||||
),
|
||||
"angular_speed_edges_deg_s": list(
|
||||
self.angular_speed_edges_deg_s
|
||||
),
|
||||
"lidar_camera_age_edges_ms": list(
|
||||
self.lidar_camera_age_edges_ms
|
||||
),
|
||||
"pose_point_age_edges_ms": list(self.pose_point_age_edges_ms),
|
||||
"analysis_kind": "descriptive-source-scoped-sensitivity",
|
||||
"threshold_tuning_allowed": False,
|
||||
"calibration_target_truth_available": False,
|
||||
"physical_mount_inferred": False,
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_E45_BINDING_SENSITIVITY_PROFILE: Final = (
|
||||
E45BindingSensitivityProfile()
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class E45BindingSensitivity:
|
||||
result_id: str
|
||||
result_root: Path
|
||||
manifest: dict[str, Any]
|
||||
report: dict[str, Any]
|
||||
|
||||
|
||||
def build_e45_binding_sensitivity(
|
||||
*,
|
||||
e31_result_root: Path,
|
||||
source_pack_root: Path,
|
||||
materialization_root: Path,
|
||||
output_root: Path,
|
||||
profile: E45BindingSensitivityProfile = (
|
||||
DEFAULT_E45_BINDING_SENSITIVITY_PROFILE
|
||||
),
|
||||
) -> E45BindingSensitivity:
|
||||
"""Build or verify one immutable E45 diagnostic sensitivity result."""
|
||||
|
||||
try:
|
||||
e31 = read_e31_source_qualification(e31_result_root)
|
||||
except E31SourceQualificationError as reason:
|
||||
raise E45BindingSensitivityError("E31 source is invalid") from reason
|
||||
if (
|
||||
e31.report.get("eligible_for_e32") is not True
|
||||
or e31.report.get("status") != "accepted-diagnostic-source-profile"
|
||||
):
|
||||
raise E45BindingSensitivityError("E31 source is not accepted")
|
||||
|
||||
e31_source = _object(
|
||||
_object(e31.manifest.get("identity"), "E31 identity").get("source"),
|
||||
"E31 source",
|
||||
)
|
||||
source = E10LidarFieldSource(source_pack_root)
|
||||
try:
|
||||
if (
|
||||
source.pack_id != e31_source.get("source_pack_id")
|
||||
or source.identity.get("session_id") != e31_source.get("session_id")
|
||||
or source.identity.get("source_id") != e31_source.get("source_id")
|
||||
):
|
||||
raise E45BindingSensitivityError("E10/E31 source binding changed")
|
||||
items = _load_materialized_items(
|
||||
materialization_root=materialization_root,
|
||||
expected_result_id=str(e31_source.get("materialization_id", "")),
|
||||
)
|
||||
correspondence_rows = tuple(
|
||||
_read_jsonl(e31.result_root / E31_CORRESPONDENCES_NAME)
|
||||
)
|
||||
rows = _build_rows(
|
||||
correspondence_rows=correspondence_rows,
|
||||
items=items,
|
||||
source=source,
|
||||
profile=profile,
|
||||
)
|
||||
analysis = analyze_binding_sensitivity(rows, profile=profile)
|
||||
profile_document = profile.to_dict()
|
||||
rows_sha256 = hashlib.sha256(
|
||||
b"".join(_canonical_json(row) + b"\n" for row in rows)
|
||||
).hexdigest()
|
||||
analysis_sha256 = hashlib.sha256(
|
||||
_canonical_json(analysis)
|
||||
).hexdigest()
|
||||
identity = {
|
||||
"schema_version": E45_RESULT_SCHEMA,
|
||||
"source": {
|
||||
"session_id": str(source.identity["session_id"]),
|
||||
"source_id": str(source.identity["source_id"]),
|
||||
"source_pack_id": source.pack_id,
|
||||
"e31_result_id": e31.result_id,
|
||||
"materialization_id": materialization_root.resolve(
|
||||
strict=True
|
||||
).name,
|
||||
"calibration_content_identity_sha256": e31_source.get(
|
||||
"calibration_content_identity_sha256"
|
||||
),
|
||||
},
|
||||
"profile": profile_document,
|
||||
"rows_sha256": rows_sha256,
|
||||
"analysis_sha256": analysis_sha256,
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(
|
||||
_canonical_json(identity)
|
||||
).hexdigest()
|
||||
result_id = f"e45-binding-sensitivity-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e45_binding_sensitivity(destination)
|
||||
|
||||
report = {
|
||||
"schema_version": E45_REPORT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"status": "completed-source-scoped-binding-sensitivity",
|
||||
"analysis": analysis,
|
||||
"decision": {
|
||||
"e31_binding_retained": True,
|
||||
"e31_thresholds_changed": False,
|
||||
"p0_calibration_gate_closed": False,
|
||||
"measured_calibration_target_residual_available": False,
|
||||
"physical_mount_dimensions_available": False,
|
||||
"next_gate": (
|
||||
"collect explicit static-landmark or calibration-target "
|
||||
"correspondences without changing the accepted E31 source"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
(
|
||||
"the residual is the median occupied-support centroid "
|
||||
"relative to a reviewed 2D box, not calibration-target truth"
|
||||
),
|
||||
(
|
||||
"motion and source-age strata diagnose the recorded "
|
||||
"host-arrival binding; they do not recover hardware firing time"
|
||||
),
|
||||
(
|
||||
"physical mount transform, vehicle body dimensions and "
|
||||
"cross-route transfer remain unavailable"
|
||||
),
|
||||
(
|
||||
"correlation is descriptive on 87 accepted correspondences "
|
||||
"and must not be interpreted as causal calibration error"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / (
|
||||
f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
)
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_jsonl(staging / E45_ROWS_NAME, rows)
|
||||
_write_json(staging / E45_REPORT_NAME, report)
|
||||
manifest = {
|
||||
"schema_version": E45_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": _utc_now(),
|
||||
"acceptance_state": "accepted-diagnostic-measurement-only",
|
||||
"artifacts": [
|
||||
_artifact(staging / E45_REPORT_NAME, "sensitivity-report"),
|
||||
_artifact(staging / E45_ROWS_NAME, "sensitivity-rows"),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
_write_json(staging / E45_MANIFEST_NAME, manifest)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e45_binding_sensitivity(destination)
|
||||
finally:
|
||||
source.close()
|
||||
|
||||
|
||||
def read_e45_binding_sensitivity(root: Path) -> E45BindingSensitivity:
|
||||
"""Read and fully validate one immutable E45 result."""
|
||||
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / E45_MANIFEST_NAME)
|
||||
identity = _object(manifest.get("identity"), "E45 identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != E45_RESULT_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
!= identity_sha256
|
||||
or manifest.get("result_id")
|
||||
!= f"e45-binding-sensitivity-{identity_sha256}"
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or manifest.get("acceptance_state")
|
||||
!= "accepted-diagnostic-measurement-only"
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise E45BindingSensitivityError("E45 result identity is invalid")
|
||||
|
||||
artifact_rows = manifest.get("artifacts")
|
||||
if not isinstance(artifact_rows, list) or len(artifact_rows) != 2:
|
||||
raise E45BindingSensitivityError("E45 artifact catalog is invalid")
|
||||
artifacts = {
|
||||
str(item.get("role")): item
|
||||
for item in artifact_rows
|
||||
if isinstance(item, dict)
|
||||
}
|
||||
for role, name in (
|
||||
("sensitivity-report", E45_REPORT_NAME),
|
||||
("sensitivity-rows", E45_ROWS_NAME),
|
||||
):
|
||||
item = artifacts.get(role)
|
||||
path = resolved / name
|
||||
if (
|
||||
item is None
|
||||
or item.get("path") != name
|
||||
or not path.is_file()
|
||||
or item.get("byte_length") != path.stat().st_size
|
||||
or item.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise E45BindingSensitivityError("E45 artifact content changed")
|
||||
|
||||
report = _read_json(resolved / E45_REPORT_NAME)
|
||||
analysis = _object(report.get("analysis"), "E45 analysis")
|
||||
rows = tuple(_read_jsonl(resolved / E45_ROWS_NAME))
|
||||
if (
|
||||
report.get("schema_version") != E45_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("identity_sha256") != identity_sha256
|
||||
or report.get("authority") != _AUTHORITY
|
||||
or hashlib.sha256(_canonical_json(analysis)).hexdigest()
|
||||
!= identity.get("analysis_sha256")
|
||||
or hashlib.sha256(
|
||||
b"".join(_canonical_json(row) + b"\n" for row in rows)
|
||||
).hexdigest()
|
||||
!= identity.get("rows_sha256")
|
||||
or len(rows) != analysis.get("correspondence_count")
|
||||
or len({row.get("item_id") for row in rows}) != len(rows)
|
||||
or any(row.get("schema_version") != E45_ROW_SCHEMA for row in rows)
|
||||
or report.get("decision", {}).get("p0_calibration_gate_closed")
|
||||
is not False
|
||||
):
|
||||
raise E45BindingSensitivityError("E45 report is invalid")
|
||||
return E45BindingSensitivity(
|
||||
result_id=resolved.name,
|
||||
result_root=resolved,
|
||||
manifest=manifest,
|
||||
report=report,
|
||||
)
|
||||
|
||||
|
||||
def analyze_binding_sensitivity(
|
||||
rows: Iterable[dict[str, Any]],
|
||||
*,
|
||||
profile: E45BindingSensitivityProfile = (
|
||||
DEFAULT_E45_BINDING_SENSITIVITY_PROFILE
|
||||
),
|
||||
) -> dict[str, Any]:
|
||||
"""Aggregate validated E45 rows into descriptive source-scoped strata."""
|
||||
|
||||
materialized = tuple(rows)
|
||||
if not materialized:
|
||||
raise E45BindingSensitivityError("E45 rows are empty")
|
||||
item_ids: set[str] = set()
|
||||
for row in materialized:
|
||||
item_id = row.get("item_id")
|
||||
values = (
|
||||
row.get("image_radius_normalized"),
|
||||
row.get("translation_speed_mps"),
|
||||
row.get("angular_speed_deg_s"),
|
||||
row.get("lidar_camera_age_ms"),
|
||||
row.get("pose_point_age_ms"),
|
||||
row.get("motion_exposure_translation_m"),
|
||||
row.get("motion_exposure_rotation_deg"),
|
||||
row.get("centroid_residual_bbox_diagonal"),
|
||||
)
|
||||
if (
|
||||
row.get("schema_version") != E45_ROW_SCHEMA
|
||||
or not isinstance(item_id, str)
|
||||
or not item_id
|
||||
or item_id in item_ids
|
||||
or not all(
|
||||
isinstance(value, (int, float))
|
||||
and not isinstance(value, bool)
|
||||
and math.isfinite(float(value))
|
||||
and float(value) >= 0.0
|
||||
for value in values
|
||||
)
|
||||
or not isinstance(row.get("occupied_points_in_bbox"), int)
|
||||
or int(row["occupied_points_in_bbox"]) < 0
|
||||
or not isinstance(row.get("supported"), bool)
|
||||
):
|
||||
raise E45BindingSensitivityError("E45 row is invalid")
|
||||
item_ids.add(item_id)
|
||||
|
||||
supported = [row for row in materialized if row["supported"]]
|
||||
if len(supported) != len(materialized):
|
||||
raise E45BindingSensitivityError(
|
||||
"E45 requires every accepted E31 correspondence to remain supported"
|
||||
)
|
||||
residual = _array(
|
||||
materialized,
|
||||
"centroid_residual_bbox_diagonal",
|
||||
)
|
||||
variables = {
|
||||
"image_radius_normalized": _array(
|
||||
materialized,
|
||||
"image_radius_normalized",
|
||||
),
|
||||
"translation_speed_mps": _array(
|
||||
materialized,
|
||||
"translation_speed_mps",
|
||||
),
|
||||
"angular_speed_deg_s": _array(
|
||||
materialized,
|
||||
"angular_speed_deg_s",
|
||||
),
|
||||
"lidar_camera_age_ms": _array(
|
||||
materialized,
|
||||
"lidar_camera_age_ms",
|
||||
),
|
||||
"pose_point_age_ms": _array(
|
||||
materialized,
|
||||
"pose_point_age_ms",
|
||||
),
|
||||
"motion_exposure_translation_m": _array(
|
||||
materialized,
|
||||
"motion_exposure_translation_m",
|
||||
),
|
||||
"motion_exposure_rotation_deg": _array(
|
||||
materialized,
|
||||
"motion_exposure_rotation_deg",
|
||||
),
|
||||
}
|
||||
return {
|
||||
"correspondence_count": len(materialized),
|
||||
"supported_count": len(supported),
|
||||
"supported_fraction": float(len(supported) / len(materialized)),
|
||||
"residual_bbox_diagonal": _distribution(residual),
|
||||
"occupied_points_in_bbox": _distribution(
|
||||
_array(materialized, "occupied_points_in_bbox")
|
||||
),
|
||||
"motion_and_age": {
|
||||
name: _distribution(values)
|
||||
for name, values in variables.items()
|
||||
},
|
||||
"strata": {
|
||||
"image_radius": _stratify(
|
||||
materialized,
|
||||
key="image_radius_normalized",
|
||||
edges=profile.image_radius_edges,
|
||||
),
|
||||
"translation_speed": _stratify(
|
||||
materialized,
|
||||
key="translation_speed_mps",
|
||||
edges=profile.translation_speed_edges_mps,
|
||||
),
|
||||
"angular_speed": _stratify(
|
||||
materialized,
|
||||
key="angular_speed_deg_s",
|
||||
edges=profile.angular_speed_edges_deg_s,
|
||||
),
|
||||
"lidar_camera_age": _stratify(
|
||||
materialized,
|
||||
key="lidar_camera_age_ms",
|
||||
edges=profile.lidar_camera_age_edges_ms,
|
||||
),
|
||||
"pose_point_age": _stratify(
|
||||
materialized,
|
||||
key="pose_point_age_ms",
|
||||
edges=profile.pose_point_age_edges_ms,
|
||||
),
|
||||
},
|
||||
"spearman_residual_correlation": {
|
||||
name: _spearman(values, residual)
|
||||
for name, values in variables.items()
|
||||
},
|
||||
"evidence_accounting_complete": True,
|
||||
"measured_calibration_target_residual_available": False,
|
||||
"raw_firing_time_inferred": False,
|
||||
"physical_mount_inferred": False,
|
||||
}
|
||||
|
||||
|
||||
def _build_rows(
|
||||
*,
|
||||
correspondence_rows: tuple[dict[str, Any], ...],
|
||||
items: dict[str, dict[str, Any]],
|
||||
source: E10LidarFieldSource,
|
||||
profile: E45BindingSensitivityProfile,
|
||||
) -> tuple[dict[str, Any], ...]:
|
||||
times = np.asarray(source.arrays["session_seconds"], dtype=np.float64)
|
||||
positions = np.asarray(
|
||||
source.arrays["pose_positions_map"],
|
||||
dtype=np.float64,
|
||||
)
|
||||
quaternions = np.asarray(
|
||||
source.arrays["pose_quaternions_map_from_lidar"],
|
||||
dtype=np.float64,
|
||||
)
|
||||
lidar_age = np.abs(
|
||||
np.asarray(source.arrays["lidar_camera_delta_ms"], dtype=np.float64)
|
||||
)
|
||||
pose_age = np.abs(
|
||||
np.asarray(source.arrays["pose_point_delta_ms"], dtype=np.float64)
|
||||
)
|
||||
available = np.asarray(
|
||||
source.arrays["sample_available"],
|
||||
dtype=np.bool_,
|
||||
)
|
||||
translation_speed, angular_speed = _rig_motion(
|
||||
times=times,
|
||||
positions=positions,
|
||||
quaternions=quaternions,
|
||||
available=available,
|
||||
)
|
||||
projection = _object(source.identity.get("projection"), "E10 projection")
|
||||
width = float(projection["width"])
|
||||
height = float(projection["height"])
|
||||
rows: list[dict[str, Any]] = []
|
||||
for correspondence in correspondence_rows:
|
||||
item_id = str(correspondence.get("item_id", ""))
|
||||
item = items.get(item_id)
|
||||
if item is None:
|
||||
raise E45BindingSensitivityError(
|
||||
"E31 correspondence is missing from E30"
|
||||
)
|
||||
frame_index = _integer(
|
||||
correspondence.get("frame_index"),
|
||||
"E31 frame index",
|
||||
)
|
||||
if not 0 <= frame_index < source.frame_count:
|
||||
raise E45BindingSensitivityError("E31 frame index is invalid")
|
||||
scores = correspondence.get("scores")
|
||||
if not isinstance(scores, list):
|
||||
raise E45BindingSensitivityError("E31 scores are invalid")
|
||||
baseline = next(
|
||||
(
|
||||
score
|
||||
for score in scores
|
||||
if isinstance(score, dict) and score.get("offset_ms") == 0
|
||||
),
|
||||
None,
|
||||
)
|
||||
if (
|
||||
baseline is None
|
||||
or baseline.get("evaluable") is not True
|
||||
or not isinstance(baseline.get("occupied_points_in_bbox"), int)
|
||||
or baseline.get("centroid_residual_bbox_diagonal") is None
|
||||
):
|
||||
raise E45BindingSensitivityError(
|
||||
"E31 zero-offset correspondence is incomplete"
|
||||
)
|
||||
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()
|
||||
or bbox[2] <= bbox[0]
|
||||
or bbox[3] <= bbox[1]
|
||||
):
|
||||
raise E45BindingSensitivityError("E30 bbox is invalid")
|
||||
center_x = float((bbox[0] + bbox[2]) * 0.5)
|
||||
center_y = float((bbox[1] + bbox[3]) * 0.5)
|
||||
radius = math.hypot(
|
||||
(center_x - width * 0.5) / (width * 0.5),
|
||||
(center_y - height * 0.5) / (height * 0.5),
|
||||
)
|
||||
age_seconds = (
|
||||
float(lidar_age[frame_index] + pose_age[frame_index]) / 1000.0
|
||||
)
|
||||
row = {
|
||||
"schema_version": E45_ROW_SCHEMA,
|
||||
"item_id": item_id,
|
||||
"review_key": str(correspondence.get("review_key", "")),
|
||||
"frame_index": frame_index,
|
||||
"source_frame_index": _integer(
|
||||
_object(
|
||||
item.get("evidence_binding"),
|
||||
"E30 evidence binding",
|
||||
).get("source_frame_index"),
|
||||
"E30 source frame index",
|
||||
),
|
||||
"session_seconds": float(times[frame_index]),
|
||||
"label": str(snapshot.get("label", "object")),
|
||||
"bbox_xyxy": [float(value) for value in bbox],
|
||||
"image_radius_normalized": radius,
|
||||
"image_radius_stratum": _bin_label(
|
||||
radius,
|
||||
profile.image_radius_edges,
|
||||
),
|
||||
"translation_speed_mps": float(
|
||||
translation_speed[frame_index]
|
||||
),
|
||||
"translation_speed_stratum": _bin_label(
|
||||
float(translation_speed[frame_index]),
|
||||
profile.translation_speed_edges_mps,
|
||||
),
|
||||
"angular_speed_deg_s": float(angular_speed[frame_index]),
|
||||
"angular_speed_stratum": _bin_label(
|
||||
float(angular_speed[frame_index]),
|
||||
profile.angular_speed_edges_deg_s,
|
||||
),
|
||||
"lidar_camera_age_ms": float(lidar_age[frame_index]),
|
||||
"lidar_camera_age_stratum": _bin_label(
|
||||
float(lidar_age[frame_index]),
|
||||
profile.lidar_camera_age_edges_ms,
|
||||
),
|
||||
"pose_point_age_ms": float(pose_age[frame_index]),
|
||||
"pose_point_age_stratum": _bin_label(
|
||||
float(pose_age[frame_index]),
|
||||
profile.pose_point_age_edges_ms,
|
||||
),
|
||||
"motion_exposure_translation_m": float(
|
||||
translation_speed[frame_index] * age_seconds
|
||||
),
|
||||
"motion_exposure_rotation_deg": float(
|
||||
angular_speed[frame_index] * age_seconds
|
||||
),
|
||||
"occupied_points_in_bbox": int(
|
||||
baseline["occupied_points_in_bbox"]
|
||||
),
|
||||
"centroid_residual_bbox_diagonal": float(
|
||||
baseline["centroid_residual_bbox_diagonal"]
|
||||
),
|
||||
"supported": int(baseline["occupied_points_in_bbox"]) >= 2,
|
||||
"residual_interpretation": (
|
||||
"diagnostic-support-centroid-not-calibration-target"
|
||||
),
|
||||
}
|
||||
if not all(
|
||||
math.isfinite(_number(row[key], key))
|
||||
for key in (
|
||||
"image_radius_normalized",
|
||||
"translation_speed_mps",
|
||||
"angular_speed_deg_s",
|
||||
"lidar_camera_age_ms",
|
||||
"pose_point_age_ms",
|
||||
"motion_exposure_translation_m",
|
||||
"motion_exposure_rotation_deg",
|
||||
"centroid_residual_bbox_diagonal",
|
||||
)
|
||||
):
|
||||
raise E45BindingSensitivityError(
|
||||
"E45 derived measurement is not finite"
|
||||
)
|
||||
rows.append(row)
|
||||
rows.sort(key=lambda row: (int(row["frame_index"]), str(row["item_id"])))
|
||||
if len(rows) != len({str(row["item_id"]) for row in rows}):
|
||||
raise E45BindingSensitivityError("E45 item identity is duplicated")
|
||||
return tuple(rows)
|
||||
|
||||
|
||||
def _rig_motion(
|
||||
*,
|
||||
times: npt.NDArray[np.float64],
|
||||
positions: npt.NDArray[np.float64],
|
||||
quaternions: npt.NDArray[np.float64],
|
||||
available: npt.NDArray[np.bool_],
|
||||
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
|
||||
count = int(times.size)
|
||||
valid_indices = np.flatnonzero(available)
|
||||
if (
|
||||
count < 2
|
||||
or positions.shape != (count, 3)
|
||||
or quaternions.shape != (count, 4)
|
||||
or available.shape != (count,)
|
||||
or not np.isfinite(times).all()
|
||||
or valid_indices.size < 2
|
||||
or not np.isfinite(positions[valid_indices]).all()
|
||||
or not np.isfinite(quaternions[valid_indices]).all()
|
||||
or np.any(np.diff(times) <= 0.0)
|
||||
):
|
||||
raise E45BindingSensitivityError("E10 pose timeline is invalid")
|
||||
valid_rows = np.arange(valid_indices.size)
|
||||
left = valid_indices[np.maximum(valid_rows - 1, 0)]
|
||||
right = valid_indices[
|
||||
np.minimum(valid_rows + 1, valid_indices.size - 1)
|
||||
]
|
||||
duration = times[right] - times[left]
|
||||
if np.any(duration <= 0.0):
|
||||
raise E45BindingSensitivityError("E10 pose duration is invalid")
|
||||
valid_translation = np.linalg.norm(
|
||||
positions[right] - positions[left],
|
||||
axis=1,
|
||||
) / duration
|
||||
q_left = quaternions[left].copy()
|
||||
q_right = quaternions[right].copy()
|
||||
q_left /= np.linalg.norm(q_left, axis=1, keepdims=True)
|
||||
q_right /= np.linalg.norm(q_right, axis=1, keepdims=True)
|
||||
dot = np.clip(
|
||||
np.abs(np.sum(q_left * q_right, axis=1)),
|
||||
0.0,
|
||||
1.0,
|
||||
)
|
||||
valid_angular = np.degrees(2.0 * np.arccos(dot)) / duration
|
||||
translation = np.full(count, np.nan, dtype=np.float64)
|
||||
angular = np.full(count, np.nan, dtype=np.float64)
|
||||
translation[valid_indices] = valid_translation
|
||||
angular[valid_indices] = valid_angular
|
||||
return (
|
||||
np.asarray(translation, dtype=np.float64),
|
||||
np.asarray(angular, dtype=np.float64),
|
||||
)
|
||||
|
||||
|
||||
def _load_materialized_items(
|
||||
*,
|
||||
materialization_root: Path,
|
||||
expected_result_id: str,
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
root = materialization_root.resolve(strict=True)
|
||||
manifest = _read_json(root / "manifest.json")
|
||||
if (
|
||||
root.name != expected_result_id
|
||||
or manifest.get("result_id") != expected_result_id
|
||||
or manifest.get("schema_version")
|
||||
!= "missioncore.e30-evidence-materialization/v2"
|
||||
):
|
||||
raise E45BindingSensitivityError(
|
||||
"E30 materialization binding changed"
|
||||
)
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list):
|
||||
raise E45BindingSensitivityError(
|
||||
"E30 materialization catalog is invalid"
|
||||
)
|
||||
artifact = next(
|
||||
(
|
||||
item
|
||||
for item in artifacts
|
||||
if isinstance(item, dict)
|
||||
and item.get("role") == "materialized-items"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if artifact is None:
|
||||
raise E45BindingSensitivityError(
|
||||
"E30 materialized item index is absent"
|
||||
)
|
||||
path = root / str(artifact.get("path", ""))
|
||||
if (
|
||||
not path.is_file()
|
||||
or artifact.get("byte_length") != path.stat().st_size
|
||||
or artifact.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise E45BindingSensitivityError(
|
||||
"E30 materialized item index changed"
|
||||
)
|
||||
rows = tuple(_read_jsonl(path))
|
||||
result = {str(row.get("item_id", "")): row for row in rows}
|
||||
if (
|
||||
len(rows) != manifest.get("item_count")
|
||||
or len(result) != len(rows)
|
||||
or "" in result
|
||||
):
|
||||
raise E45BindingSensitivityError(
|
||||
"E30 materialized item coverage changed"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _stratify(
|
||||
rows: tuple[dict[str, Any], ...],
|
||||
*,
|
||||
key: str,
|
||||
edges: tuple[float, float],
|
||||
) -> list[dict[str, Any]]:
|
||||
result = []
|
||||
for label in ("low", "middle", "high"):
|
||||
selected = [
|
||||
row
|
||||
for row in rows
|
||||
if _bin_label(float(row[key]), edges) == label
|
||||
]
|
||||
result.append(
|
||||
{
|
||||
"stratum": label,
|
||||
"minimum_inclusive": (
|
||||
None
|
||||
if label == "low"
|
||||
else edges[0] if label == "middle" else edges[1]
|
||||
),
|
||||
"maximum_exclusive": (
|
||||
edges[0]
|
||||
if label == "low"
|
||||
else edges[1] if label == "middle" else None
|
||||
),
|
||||
"count": len(selected),
|
||||
"supported_count": sum(
|
||||
1 for row in selected if row["supported"]
|
||||
),
|
||||
"supported_fraction": (
|
||||
float(
|
||||
sum(1 for row in selected if row["supported"])
|
||||
/ len(selected)
|
||||
)
|
||||
if selected
|
||||
else None
|
||||
),
|
||||
"centroid_residual_bbox_diagonal": (
|
||||
_distribution(
|
||||
_array(
|
||||
selected,
|
||||
"centroid_residual_bbox_diagonal",
|
||||
)
|
||||
)
|
||||
if selected
|
||||
else None
|
||||
),
|
||||
"occupied_points_in_bbox": (
|
||||
_distribution(
|
||||
_array(selected, "occupied_points_in_bbox")
|
||||
)
|
||||
if selected
|
||||
else None
|
||||
),
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _spearman(
|
||||
left: npt.NDArray[np.float64],
|
||||
right: npt.NDArray[np.float64],
|
||||
) -> float | None:
|
||||
if left.size != right.size or left.size < 3:
|
||||
return None
|
||||
left_rank = _rank(left)
|
||||
right_rank = _rank(right)
|
||||
if np.std(left_rank) == 0.0 or np.std(right_rank) == 0.0:
|
||||
return None
|
||||
value = float(np.corrcoef(left_rank, right_rank)[0, 1])
|
||||
return value if math.isfinite(value) else None
|
||||
|
||||
|
||||
def _rank(values: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
|
||||
order = np.argsort(values, kind="mergesort")
|
||||
ranks = np.empty(values.size, dtype=np.float64)
|
||||
start = 0
|
||||
while start < values.size:
|
||||
end = start + 1
|
||||
while end < values.size and values[order[end]] == values[order[start]]:
|
||||
end += 1
|
||||
ranks[order[start:end]] = (start + end - 1) * 0.5 + 1.0
|
||||
start = end
|
||||
return ranks
|
||||
|
||||
|
||||
def _distribution(values: npt.NDArray[np.float64]) -> dict[str, object]:
|
||||
if values.size == 0 or not np.isfinite(values).all():
|
||||
raise E45BindingSensitivityError("E45 distribution is invalid")
|
||||
return {
|
||||
"count": int(values.size),
|
||||
"min": float(np.min(values)),
|
||||
"p05": float(np.percentile(values, 5)),
|
||||
"p50": float(np.percentile(values, 50)),
|
||||
"p95": float(np.percentile(values, 95)),
|
||||
"max": float(np.max(values)),
|
||||
"mean": float(np.mean(values)),
|
||||
}
|
||||
|
||||
|
||||
def _array(
|
||||
rows: Iterable[dict[str, Any]],
|
||||
key: str,
|
||||
) -> npt.NDArray[np.float64]:
|
||||
return np.asarray([float(row[key]) for row in rows], dtype=np.float64)
|
||||
|
||||
|
||||
def _bin_label(value: float, edges: tuple[float, float]) -> str:
|
||||
if value < edges[0]:
|
||||
return "low"
|
||||
if value < edges[1]:
|
||||
return "middle"
|
||||
return "high"
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, object]:
|
||||
return {
|
||||
"path": path.name,
|
||||
"role": role,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _integer(value: object, label: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
raise E45BindingSensitivityError(f"{label} must be an integer")
|
||||
return value
|
||||
|
||||
|
||||
def _number(value: object, label: str) -> float:
|
||||
if (
|
||||
not isinstance(value, (int, float))
|
||||
or isinstance(value, bool)
|
||||
or not math.isfinite(float(value))
|
||||
):
|
||||
raise E45BindingSensitivityError(f"{label} must be finite")
|
||||
return float(value)
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise E45BindingSensitivityError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(value, dict):
|
||||
raise E45BindingSensitivityError(
|
||||
f"JSON object expected: {path.name}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> Iterable[dict[str, Any]]:
|
||||
with path.open("r", encoding="utf-8-sig") as stream:
|
||||
for line_number, line in enumerate(stream, start=1):
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict):
|
||||
raise E45BindingSensitivityError(
|
||||
f"JSON object expected at {path.name}:{line_number}"
|
||||
)
|
||||
yield value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
with path.open("x", encoding="utf-8", newline="\n") as stream:
|
||||
json.dump(value, stream, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def _write_jsonl(
|
||||
path: Path,
|
||||
rows: Iterable[dict[str, Any]],
|
||||
) -> None:
|
||||
with path.open("x", encoding="utf-8", newline="\n") as stream:
|
||||
for row in rows:
|
||||
stream.write(
|
||||
json.dumps(
|
||||
row,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
)
|
||||
)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
||||
"+00:00",
|
||||
"Z",
|
||||
)
|
||||
@@ -0,0 +1,633 @@
|
||||
"""Prepare a prelabel-free detector Truth Island from immutable E2 images."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
E46_RESULT_SCHEMA: Final = "missioncore.e46-detector-truth-island/v1"
|
||||
E46_REPORT_SCHEMA: Final = "missioncore.e46-truth-island-preparation-report/v1"
|
||||
E46_REFERENCE_SCHEMA: Final = "missioncore.e46-truth-island-image-reference/v1"
|
||||
E46_REVIEW_SCHEMA: Final = "missioncore.e46-detector-review-template/v1"
|
||||
E46_CONTRACT_SCHEMA: Final = "missioncore.e46-detector-blind-contract/v1"
|
||||
E46_PROFILE_SCHEMA: Final = "missioncore.e46-truth-island-profile/v1"
|
||||
|
||||
E46_MANIFEST_NAME: Final = "manifest.json"
|
||||
E46_REPORT_NAME: Final = "preparation-report.json"
|
||||
E46_REFERENCES_NAME: Final = "image-references.jsonl"
|
||||
E46_REVIEW_NAME: Final = "review-template.json"
|
||||
E46_CONTRACT_NAME: Final = "blind-contract.json"
|
||||
|
||||
_E2_SCHEMA: Final = "missioncore.perception-evaluation-pack/v1"
|
||||
_E2_IDENTITY_SCHEMA: Final = "missioncore.perception-evaluation-pack-identity/v1"
|
||||
_AUTHORITY: Final = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class E46DetectorTruthIslandError(RuntimeError):
|
||||
"""An E46 source, selection, or blind-review package is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class E46DetectorTruthIslandProfile:
|
||||
profile_id: str = "e46-ravnoves00-detector-truth-island/v1"
|
||||
anchor_time_bins: int = 8
|
||||
anchors_per_bin: int = 2
|
||||
include_all_temporal_groups: bool = True
|
||||
independent_reviewers_required: int = 2
|
||||
selection_seed: str = "missioncore-e46-detector-truth-island-20260729"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if (
|
||||
not self.profile_id.strip()
|
||||
or not 2 <= self.anchor_time_bins <= 32
|
||||
or not 1 <= self.anchors_per_bin <= 8
|
||||
or self.include_all_temporal_groups is not True
|
||||
or self.independent_reviewers_required != 2
|
||||
or not 16 <= len(self.selection_seed) <= 160
|
||||
):
|
||||
raise E46DetectorTruthIslandError("E46 profile is invalid")
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"schema_version": E46_PROFILE_SCHEMA,
|
||||
**asdict(self),
|
||||
"prelabels_allowed_in_reviewer_package": False,
|
||||
"predictions_visible_during_review": False,
|
||||
"labels_revealed_before_prediction_freeze": False,
|
||||
"selection_uses_model_output": False,
|
||||
}
|
||||
|
||||
|
||||
DEFAULT_E46_DETECTOR_TRUTH_ISLAND_PROFILE: Final = (
|
||||
E46DetectorTruthIslandProfile()
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class E46DetectorTruthIsland:
|
||||
result_id: str
|
||||
result_root: Path
|
||||
manifest: dict[str, Any]
|
||||
report: dict[str, Any]
|
||||
|
||||
|
||||
def build_e46_detector_truth_island(
|
||||
*,
|
||||
evaluation_pack_root: Path,
|
||||
output_root: Path,
|
||||
profile: E46DetectorTruthIslandProfile = (
|
||||
DEFAULT_E46_DETECTOR_TRUTH_ISLAND_PROFILE
|
||||
),
|
||||
) -> E46DetectorTruthIsland:
|
||||
"""Create a references-only blind review package with no model payload."""
|
||||
|
||||
source_root = evaluation_pack_root.resolve(strict=True)
|
||||
manifest = _read_json(source_root / "manifest.json")
|
||||
identity = _object(manifest.get("identity"), "E2 identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != _E2_SCHEMA
|
||||
or identity.get("schema_version") != _E2_IDENTITY_SCHEMA
|
||||
or manifest.get("generation_id") != source_root.name
|
||||
or not isinstance(identity_sha256, str)
|
||||
or source_root.name != f"evaluation-pack-{identity_sha256}"
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
!= identity_sha256
|
||||
or identity.get("preprocessing_profile") != "fixed-valid-fov-fill/v1"
|
||||
or identity.get("resolution") != [800, 600]
|
||||
):
|
||||
raise E46DetectorTruthIslandError("E2 evaluation pack is invalid")
|
||||
frames = identity.get("frames")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(frames, list) or not isinstance(artifacts, list):
|
||||
raise E46DetectorTruthIslandError("E2 frame catalog is invalid")
|
||||
artifact_by_path = {
|
||||
str(item.get("path")): item
|
||||
for item in artifacts
|
||||
if isinstance(item, dict) and isinstance(item.get("path"), str)
|
||||
}
|
||||
selected = select_truth_island_frames(frames, profile=profile)
|
||||
references: list[dict[str, Any]] = []
|
||||
for sequence, frame in enumerate(selected, start=1):
|
||||
image_id = _integer(frame.get("image_id"), "E2 image id")
|
||||
frame_index = _integer(frame.get("frame_index"), "E2 frame index")
|
||||
relative = (
|
||||
"images/valid-fov-fill/"
|
||||
f"image-{image_id:03d}-frame-{frame_index:06d}.png"
|
||||
)
|
||||
artifact = artifact_by_path.get(relative)
|
||||
path = source_root / relative
|
||||
if (
|
||||
artifact is None
|
||||
or not path.is_file()
|
||||
or artifact.get("byte_length") != path.stat().st_size
|
||||
or artifact.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise E46DetectorTruthIslandError(
|
||||
"selected E2 image content changed"
|
||||
)
|
||||
references.append(
|
||||
{
|
||||
"schema_version": E46_REFERENCE_SCHEMA,
|
||||
"truth_island_sequence": sequence,
|
||||
"image_id": image_id,
|
||||
"frame_index": frame_index,
|
||||
"source_sequence": _integer(
|
||||
frame.get("sequence"),
|
||||
"E2 source sequence",
|
||||
),
|
||||
"session_seconds": _number(
|
||||
frame.get("session_seconds"),
|
||||
"E2 session time",
|
||||
),
|
||||
"role": str(frame.get("role")),
|
||||
"group_id": str(frame.get("group_id")),
|
||||
"source_path": relative,
|
||||
"byte_length": int(artifact["byte_length"]),
|
||||
"sha256": str(artifact["sha256"]),
|
||||
"pixel_sha256": str(
|
||||
frame.get("valid_fov_fill_rgb_sha256", "")
|
||||
),
|
||||
}
|
||||
)
|
||||
references_sha256 = hashlib.sha256(
|
||||
b"".join(_canonical_json(row) + b"\n" for row in references)
|
||||
).hexdigest()
|
||||
profile_document = profile.to_dict()
|
||||
package_identity = {
|
||||
"schema_version": E46_RESULT_SCHEMA,
|
||||
"source": {
|
||||
"evaluation_pack_id": source_root.name,
|
||||
"evaluation_identity_sha256": identity_sha256,
|
||||
"job_id": identity.get("job_id"),
|
||||
"input_sha256": identity.get("input_sha256"),
|
||||
"session_id": identity.get("session_id"),
|
||||
"source_id": identity.get("source_id"),
|
||||
"calibration_sha256": identity.get("calibration_sha256"),
|
||||
"calibration_slot": identity.get("calibration_slot"),
|
||||
"preprocessing_profile": identity.get("preprocessing_profile"),
|
||||
},
|
||||
"profile": profile_document,
|
||||
"references_sha256": references_sha256,
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_digest = hashlib.sha256(
|
||||
_canonical_json(package_identity)
|
||||
).hexdigest()
|
||||
result_id = f"e46-detector-truth-island-{identity_digest}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e46_detector_truth_island(destination)
|
||||
|
||||
temporal_groups = sorted(
|
||||
{
|
||||
str(row["group_id"])
|
||||
for row in references
|
||||
if row["role"] == "temporal"
|
||||
}
|
||||
)
|
||||
contract = {
|
||||
"schema_version": E46_CONTRACT_SCHEMA,
|
||||
"task": "task-relevant-2d-object-detection",
|
||||
"truth_state": "labels-unavailable",
|
||||
"reviewer_package": {
|
||||
"source_images": "references-only-to-immutable-e2",
|
||||
"model_prelabels_included": False,
|
||||
"model_predictions_included": False,
|
||||
"model_scores_included": False,
|
||||
"candidate_identity_included": False,
|
||||
},
|
||||
"annotation": {
|
||||
"classes": [
|
||||
"person",
|
||||
"bicycle",
|
||||
"motorcycle",
|
||||
"car",
|
||||
"heavy_vehicle",
|
||||
"static_obstacle",
|
||||
"animal",
|
||||
],
|
||||
"box_format": "xyxy-pixels-800x600",
|
||||
"inside_valid_fov_only": True,
|
||||
"all_identifiable_instances_required": True,
|
||||
"hard_negative_frame_flag_required": True,
|
||||
"occluded_and_truncated_flags_required": True,
|
||||
},
|
||||
"review": {
|
||||
"independent_reviewers_required": (
|
||||
profile.independent_reviewers_required
|
||||
),
|
||||
"reviewer_identity_must_differ": True,
|
||||
"adjudication_required_on_disagreement": True,
|
||||
"review_order_must_not_reveal_predictions": True,
|
||||
},
|
||||
"prediction_freeze": {
|
||||
"required_before_label_reveal": True,
|
||||
"candidate_profile_hash_required": True,
|
||||
"per-image prediction_hash_required": True,
|
||||
"source-trained_e38_e40_candidates_eligible": False,
|
||||
},
|
||||
"metrics_after_truth_seal": [
|
||||
"coco_ap_50_95",
|
||||
"ap50",
|
||||
"ap75",
|
||||
"ar100",
|
||||
"per_class_recall",
|
||||
"person_vehicle_miss_rate",
|
||||
"false_large_box_rate",
|
||||
"valid_fov_boundary_leakage",
|
||||
"temporal_detection_flicker",
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
review_template = {
|
||||
"schema_version": E46_REVIEW_SCHEMA,
|
||||
"truth_island_id": result_id,
|
||||
"state": "prepared-unreviewed-no-prelabels",
|
||||
"reviewer_id": None,
|
||||
"review_round": None,
|
||||
"images": [
|
||||
{
|
||||
"truth_island_sequence": row["truth_island_sequence"],
|
||||
"image_id": row["image_id"],
|
||||
"frame_index": row["frame_index"],
|
||||
"session_seconds": row["session_seconds"],
|
||||
"role": row["role"],
|
||||
"group_id": row["group_id"],
|
||||
"source_path": row["source_path"],
|
||||
"source_sha256": row["sha256"],
|
||||
"review_state": "pending",
|
||||
"hard_negative": None,
|
||||
"objects": [],
|
||||
"notes": None,
|
||||
}
|
||||
for row in references
|
||||
],
|
||||
"acceptance": None,
|
||||
}
|
||||
report = {
|
||||
"schema_version": E46_REPORT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_digest,
|
||||
"status": "prepared-awaiting-independent-human-review",
|
||||
"selection": {
|
||||
"frame_count": len(references),
|
||||
"anchor_count": sum(
|
||||
1 for row in references if row["role"] == "anchor"
|
||||
),
|
||||
"temporal_frame_count": sum(
|
||||
1 for row in references if row["role"] == "temporal"
|
||||
),
|
||||
"temporal_group_count": len(temporal_groups),
|
||||
"temporal_groups": temporal_groups,
|
||||
"minimum_frame_index": min(
|
||||
int(row["frame_index"]) for row in references
|
||||
),
|
||||
"maximum_frame_index": max(
|
||||
int(row["frame_index"]) for row in references
|
||||
),
|
||||
"source_images_copied": 0,
|
||||
"source_reference_bytes": sum(
|
||||
int(row["byte_length"]) for row in references
|
||||
),
|
||||
},
|
||||
"blindness": {
|
||||
"model_prelabels_included": False,
|
||||
"predictions_included": False,
|
||||
"truth_labels_available": False,
|
||||
"candidate_comparison_authorized": False,
|
||||
},
|
||||
"decision": {
|
||||
"truth_island_preparation_complete": True,
|
||||
"truth_island_sealed": False,
|
||||
"human_review_required": True,
|
||||
"next_gate": (
|
||||
"complete two independent reviews and adjudication, while "
|
||||
"freezing E47 candidate predictions before label reveal"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
(
|
||||
"the island is an independent blind review generation on the "
|
||||
"known RAVNOVES00 source, not cross-route truth"
|
||||
),
|
||||
(
|
||||
"E37 reviewed frames are dense across the route; E38-E40 "
|
||||
"source-trained predictors are therefore explicitly ineligible"
|
||||
),
|
||||
(
|
||||
"no accuracy metric exists until two human reviews are sealed "
|
||||
"and adjudicated"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_jsonl(staging / E46_REFERENCES_NAME, references)
|
||||
_write_json(staging / E46_CONTRACT_NAME, contract)
|
||||
_write_json(staging / E46_REVIEW_NAME, review_template)
|
||||
_write_json(staging / E46_REPORT_NAME, report)
|
||||
output_manifest = {
|
||||
"schema_version": E46_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_digest,
|
||||
"identity": package_identity,
|
||||
"created_at_utc": _utc_now(),
|
||||
"acceptance_state": "prepared-not-truth",
|
||||
"artifacts": [
|
||||
_artifact(staging / E46_REPORT_NAME, "preparation-report"),
|
||||
_artifact(staging / E46_REFERENCES_NAME, "image-references"),
|
||||
_artifact(staging / E46_CONTRACT_NAME, "blind-contract"),
|
||||
_artifact(staging / E46_REVIEW_NAME, "review-template"),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
_write_json(staging / E46_MANIFEST_NAME, output_manifest)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e46_detector_truth_island(destination)
|
||||
|
||||
|
||||
def select_truth_island_frames(
|
||||
frames: list[object],
|
||||
*,
|
||||
profile: E46DetectorTruthIslandProfile = (
|
||||
DEFAULT_E46_DETECTOR_TRUTH_ISLAND_PROFILE
|
||||
),
|
||||
) -> tuple[dict[str, Any], ...]:
|
||||
"""Select temporal groups whole plus evenly distributed anchor frames."""
|
||||
|
||||
normalized = []
|
||||
previous = -1
|
||||
image_ids: set[int] = set()
|
||||
for value in frames:
|
||||
row = _object(value, "E2 frame")
|
||||
image_id = _integer(row.get("image_id"), "E2 image id")
|
||||
frame_index = _integer(row.get("frame_index"), "E2 frame index")
|
||||
if (
|
||||
image_id in image_ids
|
||||
or frame_index <= previous
|
||||
or row.get("role") not in {"anchor", "temporal"}
|
||||
or not isinstance(row.get("group_id"), str)
|
||||
or not str(row["group_id"])
|
||||
):
|
||||
raise E46DetectorTruthIslandError("E2 frame ordering is invalid")
|
||||
image_ids.add(image_id)
|
||||
previous = frame_index
|
||||
normalized.append(row)
|
||||
anchors = [row for row in normalized if row["role"] == "anchor"]
|
||||
temporal = [row for row in normalized if row["role"] == "temporal"]
|
||||
if (
|
||||
len(anchors)
|
||||
< profile.anchor_time_bins * profile.anchors_per_bin
|
||||
or not temporal
|
||||
):
|
||||
raise E46DetectorTruthIslandError(
|
||||
"E2 does not cover the frozen E46 selection"
|
||||
)
|
||||
bins = _partition(anchors, profile.anchor_time_bins)
|
||||
selected_anchors = []
|
||||
for values in bins:
|
||||
ranked = sorted(
|
||||
values,
|
||||
key=lambda row: hashlib.sha256(
|
||||
(
|
||||
f"{profile.selection_seed}:"
|
||||
f"{row['group_id']}:{row['frame_index']}"
|
||||
).encode()
|
||||
).hexdigest(),
|
||||
)
|
||||
selected_anchors.extend(ranked[: profile.anchors_per_bin])
|
||||
|
||||
by_group: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for row in temporal:
|
||||
by_group[str(row["group_id"])].append(row)
|
||||
for group_rows in by_group.values():
|
||||
indices = [int(row["frame_index"]) for row in group_rows]
|
||||
if len(group_rows) < 2 or indices != list(
|
||||
range(indices[0], indices[0] + len(indices))
|
||||
):
|
||||
raise E46DetectorTruthIslandError(
|
||||
"E2 temporal group is not consecutive"
|
||||
)
|
||||
selected = selected_anchors + [
|
||||
row for group in sorted(by_group) for row in by_group[group]
|
||||
]
|
||||
selected.sort(key=lambda row: int(row["frame_index"]))
|
||||
expected = (
|
||||
profile.anchor_time_bins * profile.anchors_per_bin + len(temporal)
|
||||
)
|
||||
if len(selected) != expected or len(
|
||||
{int(row["image_id"]) for row in selected}
|
||||
) != expected:
|
||||
raise E46DetectorTruthIslandError("E46 selection is inconsistent")
|
||||
return tuple(selected)
|
||||
|
||||
|
||||
def read_e46_detector_truth_island(root: Path) -> E46DetectorTruthIsland:
|
||||
"""Read and validate a prepared, explicitly non-truth E46 generation."""
|
||||
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / E46_MANIFEST_NAME)
|
||||
identity = _object(manifest.get("identity"), "E46 identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != E46_RESULT_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
!= identity_sha256
|
||||
or manifest.get("result_id")
|
||||
!= f"e46-detector-truth-island-{identity_sha256}"
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or manifest.get("acceptance_state") != "prepared-not-truth"
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise E46DetectorTruthIslandError("E46 identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 4:
|
||||
raise E46DetectorTruthIslandError("E46 artifacts are invalid")
|
||||
for item in artifacts:
|
||||
if not isinstance(item, dict) or not isinstance(item.get("path"), str):
|
||||
raise E46DetectorTruthIslandError("E46 artifact row is invalid")
|
||||
path = resolved / str(item["path"])
|
||||
if (
|
||||
not path.is_file()
|
||||
or item.get("byte_length") != path.stat().st_size
|
||||
or item.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise E46DetectorTruthIslandError("E46 artifact changed")
|
||||
report = _read_json(resolved / E46_REPORT_NAME)
|
||||
contract = _read_json(resolved / E46_CONTRACT_NAME)
|
||||
review = _read_json(resolved / E46_REVIEW_NAME)
|
||||
references = tuple(_read_jsonl(resolved / E46_REFERENCES_NAME))
|
||||
blindness = _object(report.get("blindness"), "E46 blindness")
|
||||
reviewer_package = _object(
|
||||
contract.get("reviewer_package"),
|
||||
"E46 reviewer package",
|
||||
)
|
||||
if (
|
||||
report.get("schema_version") != E46_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("identity_sha256") != identity_sha256
|
||||
or contract.get("schema_version") != E46_CONTRACT_SCHEMA
|
||||
or contract.get("truth_state") != "labels-unavailable"
|
||||
or review.get("schema_version") != E46_REVIEW_SCHEMA
|
||||
or review.get("state") != "prepared-unreviewed-no-prelabels"
|
||||
or blindness
|
||||
!= {
|
||||
"candidate_comparison_authorized": False,
|
||||
"model_prelabels_included": False,
|
||||
"predictions_included": False,
|
||||
"truth_labels_available": False,
|
||||
}
|
||||
or reviewer_package.get("model_prelabels_included") is not False
|
||||
or reviewer_package.get("model_predictions_included") is not False
|
||||
or any(row.get("schema_version") != E46_REFERENCE_SCHEMA for row in references)
|
||||
or len(references) != report.get("selection", {}).get("frame_count")
|
||||
or any(
|
||||
image.get("objects") != []
|
||||
or image.get("hard_negative") is not None
|
||||
or image.get("review_state") != "pending"
|
||||
for image in review.get("images", [])
|
||||
if isinstance(image, dict)
|
||||
)
|
||||
):
|
||||
raise E46DetectorTruthIslandError("E46 blind package is invalid")
|
||||
return E46DetectorTruthIsland(
|
||||
result_id=resolved.name,
|
||||
result_root=resolved,
|
||||
manifest=manifest,
|
||||
report=report,
|
||||
)
|
||||
|
||||
|
||||
def _partition(
|
||||
rows: list[dict[str, Any]],
|
||||
count: int,
|
||||
) -> tuple[list[dict[str, Any]], ...]:
|
||||
quotient, remainder = divmod(len(rows), count)
|
||||
result = []
|
||||
start = 0
|
||||
for index in range(count):
|
||||
size = quotient + (1 if index < remainder else 0)
|
||||
result.append(rows[start : start + size])
|
||||
start += size
|
||||
return tuple(result)
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, object]:
|
||||
return {
|
||||
"path": path.name,
|
||||
"role": role,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _integer(value: object, label: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
raise E46DetectorTruthIslandError(f"{label} must be an integer")
|
||||
return value
|
||||
|
||||
|
||||
def _number(value: object, label: str) -> float:
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
||||
raise E46DetectorTruthIslandError(f"{label} must be numeric")
|
||||
number = float(value)
|
||||
if not math.isfinite(number):
|
||||
raise E46DetectorTruthIslandError(f"{label} must be finite")
|
||||
return number
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise E46DetectorTruthIslandError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(value, dict):
|
||||
raise E46DetectorTruthIslandError(
|
||||
f"JSON object expected: {path.name}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> Iterable[dict[str, Any]]:
|
||||
with path.open("r", encoding="utf-8-sig") as stream:
|
||||
for line_number, line in enumerate(stream, start=1):
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict):
|
||||
raise E46DetectorTruthIslandError(
|
||||
f"JSON object expected at {path.name}:{line_number}"
|
||||
)
|
||||
yield value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
with path.open("x", encoding="utf-8", newline="\n") as stream:
|
||||
json.dump(value, stream, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None:
|
||||
with path.open("x", encoding="utf-8", newline="\n") as stream:
|
||||
for row in rows:
|
||||
stream.write(
|
||||
json.dumps(
|
||||
row,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
)
|
||||
)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
||||
"+00:00",
|
||||
"Z",
|
||||
)
|
||||
@@ -0,0 +1,740 @@
|
||||
"""Freeze detector candidates before E46 Truth Island label reveal."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from collections.abc import Iterable
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from .e46_detector_truth_island import (
|
||||
E46_REFERENCES_NAME,
|
||||
E46DetectorTruthIslandError,
|
||||
read_e46_detector_truth_island,
|
||||
)
|
||||
|
||||
E47_RESULT_SCHEMA: Final = "missioncore.e47-detector-candidate-freeze/v1"
|
||||
E47_REPORT_SCHEMA: Final = "missioncore.e47-detector-candidate-report/v1"
|
||||
E47_PREDICTION_SCHEMA: Final = "missioncore.e47-detector-prediction-row/v1"
|
||||
E47_MANIFEST_NAME: Final = "manifest.json"
|
||||
E47_REPORT_NAME: Final = "candidate-freeze-report.json"
|
||||
E47_PREDICTIONS_NAME: Final = "candidate-predictions.jsonl"
|
||||
|
||||
_RAW_RESULT_SCHEMA: Final = "missioncore.recorded-perception-result/v2"
|
||||
_FILL_RESULT_SCHEMA: Final = "missioncore.perception-evaluation-prelabels/v1"
|
||||
_RAW_CANDIDATE: Final = "maskrcnn-kb4-raw"
|
||||
_FILL_CANDIDATE: Final = "maskrcnn-kb4-valid-fov-fill"
|
||||
_AUTHORITY: Final = {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
class E47DetectorCandidateFreezeError(RuntimeError):
|
||||
"""An E47 candidate input or frozen prediction set is invalid."""
|
||||
|
||||
|
||||
def build_e47_detector_candidate_freeze(
|
||||
*,
|
||||
truth_island_root: Path,
|
||||
raw_result_root: Path,
|
||||
valid_fov_result_root: Path,
|
||||
output_root: Path,
|
||||
) -> dict[str, Any]:
|
||||
"""Freeze two exact-checkpoint preprocessing candidates without truth."""
|
||||
|
||||
try:
|
||||
truth_island = read_e46_detector_truth_island(truth_island_root)
|
||||
except E46DetectorTruthIslandError as reason:
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"E46 truth island is invalid"
|
||||
) from reason
|
||||
if (
|
||||
truth_island.report.get("status")
|
||||
!= "prepared-awaiting-independent-human-review"
|
||||
or truth_island.report.get("blindness", {}).get(
|
||||
"truth_labels_available"
|
||||
)
|
||||
is not False
|
||||
):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"E46 truth state is incompatible"
|
||||
)
|
||||
references = tuple(
|
||||
_read_jsonl(
|
||||
truth_island.result_root / E46_REFERENCES_NAME
|
||||
)
|
||||
)
|
||||
selected_frames = {
|
||||
_integer(row.get("frame_index"), "E46 frame index"): row
|
||||
for row in references
|
||||
}
|
||||
selected_images = {
|
||||
_integer(row.get("image_id"), "E46 image id"): row
|
||||
for row in references
|
||||
}
|
||||
if len(selected_frames) != len(references) or len(selected_images) != len(
|
||||
references
|
||||
):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"E46 selected identity is duplicated"
|
||||
)
|
||||
|
||||
raw_root = raw_result_root.resolve(strict=True)
|
||||
raw_result = _read_json(raw_root / "result.json")
|
||||
raw_identity = _object(raw_result.get("identity"), "raw identity")
|
||||
raw_frames_path = _verified_result_artifact(
|
||||
root=raw_root,
|
||||
result=raw_result,
|
||||
expected_schema=_RAW_RESULT_SCHEMA,
|
||||
artifact_path="frames.jsonl",
|
||||
)
|
||||
fill_root = valid_fov_result_root.resolve(strict=True)
|
||||
fill_result = _read_json(fill_root / "result.json")
|
||||
fill_identity = _object(fill_result.get("identity"), "fill identity")
|
||||
fill_frames_path = _verified_result_artifact(
|
||||
root=fill_root,
|
||||
result=fill_result,
|
||||
expected_schema=_FILL_RESULT_SCHEMA,
|
||||
artifact_path="frames.jsonl",
|
||||
)
|
||||
truth_source = _object(
|
||||
truth_island.manifest["identity"].get("source"),
|
||||
"E46 source identity",
|
||||
)
|
||||
if (
|
||||
raw_identity.get("input_sha256")
|
||||
!= truth_source.get("input_sha256")
|
||||
or raw_identity.get("job_id") != truth_source.get("job_id")
|
||||
):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"raw candidate source changed"
|
||||
)
|
||||
if (
|
||||
fill_identity.get("evaluation_pack_id")
|
||||
!= truth_source.get("evaluation_pack_id")
|
||||
or fill_identity.get("evaluation_identity_sha256")
|
||||
!= truth_source.get("evaluation_identity_sha256")
|
||||
):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"valid-FOV candidate source changed"
|
||||
)
|
||||
raw_weight = _instance_weight_sha256(raw_identity)
|
||||
fill_weight = _instance_weight_sha256(fill_identity)
|
||||
if raw_weight != fill_weight:
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate checkpoint identity differs"
|
||||
)
|
||||
instance_mapping = _object(
|
||||
fill_identity.get("instance_mapping"),
|
||||
"candidate instance mapping",
|
||||
)
|
||||
target_categories = _target_categories(fill_identity)
|
||||
|
||||
raw_source = {
|
||||
_integer(row.get("frame_index"), "raw frame index"): row
|
||||
for row in _read_jsonl(raw_frames_path)
|
||||
if row.get("frame_index") in selected_frames
|
||||
}
|
||||
fill_source = {
|
||||
_integer(row.get("image_id"), "fill image id"): row
|
||||
for row in _read_jsonl(fill_frames_path)
|
||||
if row.get("image_id") in selected_images
|
||||
}
|
||||
if (
|
||||
set(raw_source) != set(selected_frames)
|
||||
or set(fill_source) != set(selected_images)
|
||||
):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate frame coverage is incomplete"
|
||||
)
|
||||
|
||||
rows: list[dict[str, Any]] = []
|
||||
for reference in references:
|
||||
frame_index = int(reference["frame_index"])
|
||||
image_id = int(reference["image_id"])
|
||||
rows.append(
|
||||
_normalize_raw_row(
|
||||
source=raw_source[frame_index],
|
||||
reference=reference,
|
||||
instance_mapping=instance_mapping,
|
||||
target_categories=target_categories,
|
||||
)
|
||||
)
|
||||
rows.append(
|
||||
_normalize_fill_row(
|
||||
source=fill_source[image_id],
|
||||
reference=reference,
|
||||
target_categories=target_categories,
|
||||
)
|
||||
)
|
||||
rows.sort(
|
||||
key=lambda row: (
|
||||
int(row["truth_island_sequence"]),
|
||||
str(row["candidate_id"]),
|
||||
)
|
||||
)
|
||||
candidate_summary = {
|
||||
candidate_id: _candidate_summary(
|
||||
tuple(row for row in rows if row["candidate_id"] == candidate_id)
|
||||
)
|
||||
for candidate_id in (_RAW_CANDIDATE, _FILL_CANDIDATE)
|
||||
}
|
||||
predictions_sha256 = hashlib.sha256(
|
||||
b"".join(_canonical_json(row) + b"\n" for row in rows)
|
||||
).hexdigest()
|
||||
identity = {
|
||||
"schema_version": E47_RESULT_SCHEMA,
|
||||
"truth_island": {
|
||||
"result_id": truth_island.result_id,
|
||||
"state": "prepared-unreviewed-no-prelabels",
|
||||
"truth_labels_available": False,
|
||||
},
|
||||
"candidates": [
|
||||
{
|
||||
"candidate_id": _RAW_CANDIDATE,
|
||||
"preprocessing": "raw-kb4-800x600",
|
||||
"source_result_id": raw_result.get("result_id"),
|
||||
"source_result_sha256": _sha256(raw_root / "result.json"),
|
||||
"source_frames_sha256": _sha256(raw_frames_path),
|
||||
"checkpoint_sha256": raw_weight,
|
||||
},
|
||||
{
|
||||
"candidate_id": _FILL_CANDIDATE,
|
||||
"preprocessing": "fixed-valid-fov-fill-800x600",
|
||||
"source_result_id": fill_root.name,
|
||||
"source_result_sha256": _sha256(fill_root / "result.json"),
|
||||
"source_frames_sha256": _sha256(fill_frames_path),
|
||||
"checkpoint_sha256": fill_weight,
|
||||
},
|
||||
],
|
||||
"prediction_rows_sha256": predictions_sha256,
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e47-detector-candidate-freeze-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute() / result_id
|
||||
if destination.exists():
|
||||
return read_e47_detector_candidate_freeze(destination)
|
||||
|
||||
report = {
|
||||
"schema_version": E47_REPORT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"status": "predictions-frozen-awaiting-truth-reveal",
|
||||
"frame_count": len(references),
|
||||
"prediction_row_count": len(rows),
|
||||
"checkpoint_relation": "same-exact-maskrcnn-checkpoint",
|
||||
"comparison_variable": "raw-kb4-vs-fixed-valid-fov-fill",
|
||||
"candidates": candidate_summary,
|
||||
"blindness": {
|
||||
"truth_labels_available": False,
|
||||
"truth_join_performed": False,
|
||||
"accuracy_metrics_available": False,
|
||||
"reviewer_package_modified": False,
|
||||
},
|
||||
"decision": {
|
||||
"candidate_predictions_frozen": True,
|
||||
"candidate_winner_selected": False,
|
||||
"model_retraining_authorized": False,
|
||||
"next_gate": (
|
||||
"seal E46 independent reviews, reveal truth only after this "
|
||||
"prediction generation, then compute detection metrics"
|
||||
),
|
||||
},
|
||||
"limitations": [
|
||||
(
|
||||
"truth-free prediction counts are descriptive and cannot rank "
|
||||
"candidate accuracy"
|
||||
),
|
||||
(
|
||||
"both candidates use the same generic COCO Mask R-CNN weights; "
|
||||
"E47 currently isolates only calibrated valid-FOV preprocessing"
|
||||
),
|
||||
(
|
||||
"the comparison remains source-scoped to the known "
|
||||
"RAVNOVES00 camera"
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
staging = destination.parent / f".{result_id}.{uuid.uuid4().hex}.tmp"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
_write_jsonl(staging / E47_PREDICTIONS_NAME, rows)
|
||||
_write_json(staging / E47_REPORT_NAME, report)
|
||||
manifest = {
|
||||
"schema_version": E47_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": _utc_now(),
|
||||
"acceptance_state": "accepted-prediction-freeze-only",
|
||||
"artifacts": [
|
||||
_artifact(staging / E47_REPORT_NAME, "candidate-report"),
|
||||
_artifact(
|
||||
staging / E47_PREDICTIONS_NAME,
|
||||
"candidate-predictions",
|
||||
),
|
||||
],
|
||||
"authority": _AUTHORITY,
|
||||
}
|
||||
_write_json(staging / E47_MANIFEST_NAME, manifest)
|
||||
os.replace(staging, destination)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e47_detector_candidate_freeze(destination)
|
||||
|
||||
|
||||
def read_e47_detector_candidate_freeze(root: Path) -> dict[str, Any]:
|
||||
"""Read and validate an E47 prediction-only generation."""
|
||||
|
||||
resolved = root.resolve(strict=True)
|
||||
manifest = _read_json(resolved / E47_MANIFEST_NAME)
|
||||
identity = _object(manifest.get("identity"), "E47 identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != E47_RESULT_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
!= identity_sha256
|
||||
or manifest.get("result_id")
|
||||
!= f"e47-detector-candidate-freeze-{identity_sha256}"
|
||||
or resolved.name != manifest.get("result_id")
|
||||
or manifest.get("acceptance_state")
|
||||
!= "accepted-prediction-freeze-only"
|
||||
or manifest.get("authority") != _AUTHORITY
|
||||
):
|
||||
raise E47DetectorCandidateFreezeError("E47 identity is invalid")
|
||||
artifacts = manifest.get("artifacts")
|
||||
if not isinstance(artifacts, list) or len(artifacts) != 2:
|
||||
raise E47DetectorCandidateFreezeError("E47 artifacts are invalid")
|
||||
for item in artifacts:
|
||||
if not isinstance(item, dict) or not isinstance(item.get("path"), str):
|
||||
raise E47DetectorCandidateFreezeError("E47 artifact row is invalid")
|
||||
path = resolved / str(item["path"])
|
||||
if (
|
||||
not path.is_file()
|
||||
or item.get("byte_length") != path.stat().st_size
|
||||
or item.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise E47DetectorCandidateFreezeError("E47 artifact changed")
|
||||
report = _read_json(resolved / E47_REPORT_NAME)
|
||||
rows = tuple(_read_jsonl(resolved / E47_PREDICTIONS_NAME))
|
||||
if (
|
||||
report.get("schema_version") != E47_REPORT_SCHEMA
|
||||
or report.get("result_id") != resolved.name
|
||||
or report.get("identity_sha256") != identity_sha256
|
||||
or report.get("status")
|
||||
!= "predictions-frozen-awaiting-truth-reveal"
|
||||
or report.get("blindness")
|
||||
!= {
|
||||
"accuracy_metrics_available": False,
|
||||
"reviewer_package_modified": False,
|
||||
"truth_join_performed": False,
|
||||
"truth_labels_available": False,
|
||||
}
|
||||
or len(rows) != report.get("prediction_row_count")
|
||||
or any(row.get("schema_version") != E47_PREDICTION_SCHEMA for row in rows)
|
||||
or hashlib.sha256(
|
||||
b"".join(_canonical_json(row) + b"\n" for row in rows)
|
||||
).hexdigest()
|
||||
!= identity.get("prediction_rows_sha256")
|
||||
):
|
||||
raise E47DetectorCandidateFreezeError("E47 report is invalid")
|
||||
return {
|
||||
"result_id": resolved.name,
|
||||
"result_root": resolved,
|
||||
"manifest": manifest,
|
||||
"report": report,
|
||||
}
|
||||
|
||||
|
||||
def normalize_candidate_predictions(
|
||||
*,
|
||||
instances: object,
|
||||
label_mapping: dict[str, int],
|
||||
target_categories: dict[int, str],
|
||||
source_kind: str,
|
||||
) -> tuple[dict[str, object], ...]:
|
||||
"""Normalize raw or E2 draft instances into one detection contract."""
|
||||
|
||||
if not isinstance(instances, list):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate instances must be a list"
|
||||
)
|
||||
normalized: list[dict[str, Any]] = []
|
||||
for instance in instances:
|
||||
item = _object(instance, "candidate instance")
|
||||
if source_kind == "raw":
|
||||
source_label = str(item.get("label", ""))
|
||||
category_id = label_mapping.get(source_label)
|
||||
score = _number(item.get("score"), "candidate score")
|
||||
bbox = item.get("box_xyxy")
|
||||
elif source_kind == "fill":
|
||||
category_id = _integer(
|
||||
item.get("draft_category_id"),
|
||||
"candidate category",
|
||||
)
|
||||
source_label = str(item.get("source_model_category", ""))
|
||||
score = _number(item.get("score"), "candidate score")
|
||||
bbox = item.get("box_xyxy")
|
||||
else:
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate source kind is invalid"
|
||||
)
|
||||
if category_id is None:
|
||||
continue
|
||||
target_label = target_categories.get(category_id)
|
||||
if target_label is None:
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate category is outside the target ontology"
|
||||
)
|
||||
if (
|
||||
not isinstance(bbox, list)
|
||||
or len(bbox) != 4
|
||||
or not all(
|
||||
isinstance(value, (int, float))
|
||||
and not isinstance(value, bool)
|
||||
and math.isfinite(float(value))
|
||||
for value in bbox
|
||||
)
|
||||
or float(bbox[2]) <= float(bbox[0])
|
||||
or float(bbox[3]) <= float(bbox[1])
|
||||
or not 0.0 <= score <= 1.0
|
||||
):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate box is invalid"
|
||||
)
|
||||
normalized.append(
|
||||
{
|
||||
"category_id": category_id,
|
||||
"category": target_label,
|
||||
"source_category": source_label,
|
||||
"score": score,
|
||||
"box_xyxy": [float(value) for value in bbox],
|
||||
}
|
||||
)
|
||||
normalized.sort(
|
||||
key=lambda row: (
|
||||
-float(row["score"]),
|
||||
int(row["category_id"]),
|
||||
tuple(float(value) for value in row["box_xyxy"]),
|
||||
)
|
||||
)
|
||||
return tuple(normalized)
|
||||
|
||||
|
||||
def _normalize_raw_row(
|
||||
*,
|
||||
source: dict[str, Any],
|
||||
reference: dict[str, Any],
|
||||
instance_mapping: dict[str, Any],
|
||||
target_categories: dict[int, str],
|
||||
) -> dict[str, Any]:
|
||||
mapping = {
|
||||
str(label): _integer(category, "raw category mapping")
|
||||
for label, category in instance_mapping.items()
|
||||
}
|
||||
predictions = normalize_candidate_predictions(
|
||||
instances=source.get("instances"),
|
||||
label_mapping=mapping,
|
||||
target_categories=target_categories,
|
||||
source_kind="raw",
|
||||
)
|
||||
source_instances = source.get("instances")
|
||||
if not isinstance(source_instances, list):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"raw source instances are invalid"
|
||||
)
|
||||
return _prediction_row(
|
||||
candidate_id=_RAW_CANDIDATE,
|
||||
reference=reference,
|
||||
source_instance_count=len(source_instances),
|
||||
predictions=predictions,
|
||||
)
|
||||
|
||||
|
||||
def _normalize_fill_row(
|
||||
*,
|
||||
source: dict[str, Any],
|
||||
reference: dict[str, Any],
|
||||
target_categories: dict[int, str],
|
||||
) -> dict[str, Any]:
|
||||
predictions = normalize_candidate_predictions(
|
||||
instances=source.get("instances"),
|
||||
label_mapping={},
|
||||
target_categories=target_categories,
|
||||
source_kind="fill",
|
||||
)
|
||||
source_instances = source.get("instances")
|
||||
if not isinstance(source_instances, list):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"fill source instances are invalid"
|
||||
)
|
||||
return _prediction_row(
|
||||
candidate_id=_FILL_CANDIDATE,
|
||||
reference=reference,
|
||||
source_instance_count=len(source_instances),
|
||||
predictions=predictions,
|
||||
)
|
||||
|
||||
|
||||
def _prediction_row(
|
||||
*,
|
||||
candidate_id: str,
|
||||
reference: dict[str, Any],
|
||||
source_instance_count: int,
|
||||
predictions: tuple[dict[str, object], ...],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": E47_PREDICTION_SCHEMA,
|
||||
"candidate_id": candidate_id,
|
||||
"truth_island_sequence": int(reference["truth_island_sequence"]),
|
||||
"image_id": int(reference["image_id"]),
|
||||
"frame_index": int(reference["frame_index"]),
|
||||
"session_seconds": float(reference["session_seconds"]),
|
||||
"source_image_sha256": str(reference["sha256"]),
|
||||
"source_instance_count": source_instance_count,
|
||||
"admitted_prediction_count": len(predictions),
|
||||
"ignored_source_instance_count": source_instance_count - len(predictions),
|
||||
"predictions": list(predictions),
|
||||
"truth_joined": False,
|
||||
}
|
||||
|
||||
|
||||
def _candidate_summary(rows: tuple[dict[str, Any], ...]) -> dict[str, Any]:
|
||||
if not rows:
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate prediction rows are empty"
|
||||
)
|
||||
class_counts: Counter[str] = Counter()
|
||||
total_predictions = 0
|
||||
source_instances = 0
|
||||
ignored = 0
|
||||
per_frame = []
|
||||
for row in rows:
|
||||
predictions = row.get("predictions")
|
||||
if not isinstance(predictions, list):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate predictions are invalid"
|
||||
)
|
||||
total_predictions += len(predictions)
|
||||
source_instances += int(row["source_instance_count"])
|
||||
ignored += int(row["ignored_source_instance_count"])
|
||||
per_frame.append(len(predictions))
|
||||
for item in predictions:
|
||||
if isinstance(item, dict):
|
||||
class_counts[str(item.get("category"))] += 1
|
||||
return {
|
||||
"frame_count": len(rows),
|
||||
"source_instance_count": source_instances,
|
||||
"admitted_prediction_count": total_predictions,
|
||||
"ignored_source_instance_count": ignored,
|
||||
"frames_without_predictions": sum(1 for value in per_frame if value == 0),
|
||||
"predictions_per_frame": {
|
||||
"min": min(per_frame),
|
||||
"p50": _percentile(per_frame, 50),
|
||||
"p95": _percentile(per_frame, 95),
|
||||
"max": max(per_frame),
|
||||
"mean": float(sum(per_frame) / len(per_frame)),
|
||||
},
|
||||
"class_counts": dict(sorted(class_counts.items())),
|
||||
"accuracy_metrics_available": False,
|
||||
}
|
||||
|
||||
|
||||
def _verified_result_artifact(
|
||||
*,
|
||||
root: Path,
|
||||
result: dict[str, Any],
|
||||
expected_schema: str,
|
||||
artifact_path: str,
|
||||
) -> Path:
|
||||
if result.get("schema_version") != expected_schema:
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate result schema changed"
|
||||
)
|
||||
artifacts = result.get("artifacts")
|
||||
if not isinstance(artifacts, list):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate artifact catalog is invalid"
|
||||
)
|
||||
artifact = next(
|
||||
(
|
||||
item
|
||||
for item in artifacts
|
||||
if isinstance(item, dict) and item.get("path") == artifact_path
|
||||
),
|
||||
None,
|
||||
)
|
||||
path = root / artifact_path
|
||||
if (
|
||||
artifact is None
|
||||
or not path.is_file()
|
||||
or artifact.get("byte_length") != path.stat().st_size
|
||||
or artifact.get("sha256") != _sha256(path)
|
||||
):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate prediction artifact changed"
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def _instance_weight_sha256(identity: dict[str, Any]) -> str:
|
||||
models = _object(identity.get("models"), "candidate models")
|
||||
files = models.get("files")
|
||||
if not isinstance(files, list):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate model files are invalid"
|
||||
)
|
||||
matches = [
|
||||
str(item.get("sha256"))
|
||||
for item in files
|
||||
if isinstance(item, dict)
|
||||
and "maskrcnn_resnet50_fpn_v2" in str(item.get("name"))
|
||||
]
|
||||
if len(matches) != 1 or len(matches[0]) != 64:
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"candidate Mask R-CNN weight identity is invalid"
|
||||
)
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _target_categories(identity: dict[str, Any]) -> dict[int, str]:
|
||||
value = _object(identity.get("target_categories"), "target categories")
|
||||
result = {}
|
||||
for category_id, label in value.items():
|
||||
try:
|
||||
numeric = int(category_id)
|
||||
except ValueError as reason:
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"target category id is invalid"
|
||||
) from reason
|
||||
if not isinstance(label, str) or not label:
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
"target category label is invalid"
|
||||
)
|
||||
result[numeric] = label
|
||||
return result
|
||||
|
||||
|
||||
def _percentile(values: list[int], percentile: int) -> float:
|
||||
ordered = sorted(values)
|
||||
position = (len(ordered) - 1) * percentile / 100.0
|
||||
lower = int(math.floor(position))
|
||||
upper = int(math.ceil(position))
|
||||
if lower == upper:
|
||||
return float(ordered[lower])
|
||||
fraction = position - lower
|
||||
return float(
|
||||
ordered[lower] * (1.0 - fraction) + ordered[upper] * fraction
|
||||
)
|
||||
|
||||
|
||||
def _artifact(path: Path, role: str) -> dict[str, object]:
|
||||
return {
|
||||
"path": path.name,
|
||||
"role": role,
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _integer(value: object, label: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
raise E47DetectorCandidateFreezeError(f"{label} must be an integer")
|
||||
return value
|
||||
|
||||
|
||||
def _number(value: object, label: str) -> float:
|
||||
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
||||
raise E47DetectorCandidateFreezeError(f"{label} must be numeric")
|
||||
result = float(value)
|
||||
if not math.isfinite(result):
|
||||
raise E47DetectorCandidateFreezeError(f"{label} must be finite")
|
||||
return result
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise E47DetectorCandidateFreezeError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
if not isinstance(value, dict):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
f"JSON object expected: {path.name}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _read_jsonl(path: Path) -> Iterable[dict[str, Any]]:
|
||||
with path.open("r", encoding="utf-8-sig") as stream:
|
||||
for line_number, line in enumerate(stream, start=1):
|
||||
value = json.loads(line)
|
||||
if not isinstance(value, dict):
|
||||
raise E47DetectorCandidateFreezeError(
|
||||
f"JSON object expected at {path.name}:{line_number}"
|
||||
)
|
||||
yield value
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
with path.open("x", encoding="utf-8", newline="\n") as stream:
|
||||
json.dump(value, stream, indent=2, sort_keys=True)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def _write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> None:
|
||||
with path.open("x", encoding="utf-8", newline="\n") as stream:
|
||||
for row in rows:
|
||||
stream.write(
|
||||
json.dumps(
|
||||
row,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
)
|
||||
)
|
||||
stream.write("\n")
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(UTC).isoformat(timespec="milliseconds").replace(
|
||||
"+00:00",
|
||||
"Z",
|
||||
)
|
||||
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.e45_binding_sensitivity import (
|
||||
E45_ROW_SCHEMA,
|
||||
E45BindingSensitivityError,
|
||||
E45BindingSensitivityProfile,
|
||||
analyze_binding_sensitivity,
|
||||
)
|
||||
|
||||
|
||||
def _row(
|
||||
item_id: str,
|
||||
*,
|
||||
radius: float,
|
||||
speed: float,
|
||||
angular: float,
|
||||
lidar_age: float,
|
||||
pose_age: float,
|
||||
residual: float,
|
||||
points: int,
|
||||
) -> dict[str, object]:
|
||||
age_seconds = (lidar_age + pose_age) / 1000.0
|
||||
return {
|
||||
"schema_version": E45_ROW_SCHEMA,
|
||||
"item_id": item_id,
|
||||
"image_radius_normalized": radius,
|
||||
"translation_speed_mps": speed,
|
||||
"angular_speed_deg_s": angular,
|
||||
"lidar_camera_age_ms": lidar_age,
|
||||
"pose_point_age_ms": pose_age,
|
||||
"motion_exposure_translation_m": speed * age_seconds,
|
||||
"motion_exposure_rotation_deg": angular * age_seconds,
|
||||
"centroid_residual_bbox_diagonal": residual,
|
||||
"occupied_points_in_bbox": points,
|
||||
"supported": points >= 2,
|
||||
}
|
||||
|
||||
|
||||
def test_e45_stratifies_existing_diagnostic_residual_without_target_claim() -> None:
|
||||
rows = [
|
||||
_row(
|
||||
"one",
|
||||
radius=0.2,
|
||||
speed=0.0,
|
||||
angular=0.0,
|
||||
lidar_age=5.0,
|
||||
pose_age=2.0,
|
||||
residual=0.1,
|
||||
points=5,
|
||||
),
|
||||
_row(
|
||||
"two",
|
||||
radius=0.7,
|
||||
speed=0.5,
|
||||
angular=6.0,
|
||||
lidar_age=40.0,
|
||||
pose_age=15.0,
|
||||
residual=0.2,
|
||||
points=8,
|
||||
),
|
||||
_row(
|
||||
"three",
|
||||
radius=1.0,
|
||||
speed=2.0,
|
||||
angular=20.0,
|
||||
lidar_age=80.0,
|
||||
pose_age=35.0,
|
||||
residual=0.4,
|
||||
points=12,
|
||||
),
|
||||
]
|
||||
|
||||
analysis = analyze_binding_sensitivity(rows)
|
||||
|
||||
assert analysis["correspondence_count"] == 3
|
||||
assert analysis["supported_fraction"] == 1.0
|
||||
assert [
|
||||
item["count"] for item in analysis["strata"]["image_radius"]
|
||||
] == [1, 1, 1]
|
||||
assert math.isclose(
|
||||
analysis["spearman_residual_correlation"][
|
||||
"image_radius_normalized"
|
||||
],
|
||||
1.0,
|
||||
)
|
||||
assert (
|
||||
analysis["measured_calibration_target_residual_available"] is False
|
||||
)
|
||||
assert analysis["physical_mount_inferred"] is False
|
||||
|
||||
|
||||
def test_e45_rejects_missing_support_in_accepted_correspondence() -> None:
|
||||
rows = [
|
||||
_row(
|
||||
"one",
|
||||
radius=0.2,
|
||||
speed=0.0,
|
||||
angular=0.0,
|
||||
lidar_age=5.0,
|
||||
pose_age=2.0,
|
||||
residual=0.1,
|
||||
points=1,
|
||||
)
|
||||
]
|
||||
|
||||
with pytest.raises(E45BindingSensitivityError, match="supported"):
|
||||
analyze_binding_sensitivity(rows)
|
||||
|
||||
|
||||
def test_e45_profile_rejects_overlapping_or_reversed_edges() -> None:
|
||||
with pytest.raises(E45BindingSensitivityError):
|
||||
E45BindingSensitivityProfile(image_radius_edges=(0.8, 0.4))
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.e46_detector_truth_island import (
|
||||
E46DetectorTruthIslandError,
|
||||
E46DetectorTruthIslandProfile,
|
||||
select_truth_island_frames,
|
||||
)
|
||||
|
||||
|
||||
def _frames() -> list[dict[str, object]]:
|
||||
rows = []
|
||||
image_id = 1
|
||||
for frame_index in range(48):
|
||||
rows.append(
|
||||
{
|
||||
"image_id": image_id,
|
||||
"frame_index": frame_index * 10,
|
||||
"role": "anchor",
|
||||
"group_id": f"anchor-{image_id:03d}",
|
||||
}
|
||||
)
|
||||
image_id += 1
|
||||
for group_index, start in enumerate((500, 600, 700, 800), start=1):
|
||||
for offset in range(4):
|
||||
rows.append(
|
||||
{
|
||||
"image_id": image_id,
|
||||
"frame_index": start + offset,
|
||||
"role": "temporal",
|
||||
"group_id": f"clip-{group_index}",
|
||||
}
|
||||
)
|
||||
image_id += 1
|
||||
rows.sort(key=lambda row: int(row["frame_index"]))
|
||||
return rows
|
||||
|
||||
|
||||
def test_e46_selects_two_anchors_per_bin_and_all_temporal_groups() -> None:
|
||||
frames = _frames()
|
||||
|
||||
selected = select_truth_island_frames(frames)
|
||||
|
||||
assert len(selected) == 32
|
||||
assert sum(row["role"] == "anchor" for row in selected) == 16
|
||||
assert sum(row["role"] == "temporal" for row in selected) == 16
|
||||
assert {
|
||||
row["group_id"]
|
||||
for row in selected
|
||||
if row["role"] == "temporal"
|
||||
} == {"clip-1", "clip-2", "clip-3", "clip-4"}
|
||||
assert selected == select_truth_island_frames(frames)
|
||||
|
||||
|
||||
def test_e46_rejects_partial_or_nonconsecutive_temporal_group() -> None:
|
||||
frames = _frames()
|
||||
frames[-1]["frame_index"] = 900
|
||||
frames.sort(key=lambda row: int(row["frame_index"]))
|
||||
|
||||
with pytest.raises(E46DetectorTruthIslandError, match="consecutive"):
|
||||
select_truth_island_frames(frames)
|
||||
|
||||
|
||||
def test_e46_profile_requires_two_reviewers_and_no_prelabel_mode() -> None:
|
||||
with pytest.raises(E46DetectorTruthIslandError):
|
||||
E46DetectorTruthIslandProfile(independent_reviewers_required=1)
|
||||
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.compute.e47_detector_candidate_freeze import (
|
||||
E47DetectorCandidateFreezeError,
|
||||
normalize_candidate_predictions,
|
||||
)
|
||||
|
||||
|
||||
def test_e47_normalizes_raw_predictions_and_drops_outside_ontology() -> None:
|
||||
result = normalize_candidate_predictions(
|
||||
instances=[
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.8,
|
||||
"box_xyxy": [10.0, 20.0, 30.0, 40.0],
|
||||
},
|
||||
{
|
||||
"label": "laptop",
|
||||
"score": 0.9,
|
||||
"box_xyxy": [20.0, 30.0, 40.0, 50.0],
|
||||
},
|
||||
],
|
||||
label_mapping={"car": 4},
|
||||
target_categories={4: "car"},
|
||||
source_kind="raw",
|
||||
)
|
||||
|
||||
assert result == (
|
||||
{
|
||||
"category_id": 4,
|
||||
"category": "car",
|
||||
"source_category": "car",
|
||||
"score": 0.8,
|
||||
"box_xyxy": [10.0, 20.0, 30.0, 40.0],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_e47_normalizes_fill_predictions_without_truth_fields() -> None:
|
||||
result = normalize_candidate_predictions(
|
||||
instances=[
|
||||
{
|
||||
"draft_category_id": 1,
|
||||
"source_model_category": "person",
|
||||
"score": 0.95,
|
||||
"box_xyxy": [1.0, 2.0, 3.0, 4.0],
|
||||
"review_state": "unreviewed-model-draft",
|
||||
}
|
||||
],
|
||||
label_mapping={},
|
||||
target_categories={1: "person"},
|
||||
source_kind="fill",
|
||||
)
|
||||
|
||||
assert result[0]["category"] == "person"
|
||||
assert "review_state" not in result[0]
|
||||
assert "truth" not in result[0]
|
||||
|
||||
|
||||
def test_e47_rejects_invalid_box() -> None:
|
||||
with pytest.raises(E47DetectorCandidateFreezeError, match="box"):
|
||||
normalize_candidate_predictions(
|
||||
instances=[
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.8,
|
||||
"box_xyxy": [30.0, 20.0, 10.0, 40.0],
|
||||
}
|
||||
],
|
||||
label_mapping={"car": 4},
|
||||
target_categories={4: "car"},
|
||||
source_kind="raw",
|
||||
)
|
||||
Reference in New Issue
Block a user