feat(perception): run PointPillars on RAVNOVES00
This commit is contained in:
@@ -0,0 +1,123 @@
|
|||||||
|
# L3.1 · PointPillars на RAVNOVES00 — 2026-07-31
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
|
||||||
|
`L3.1` — это полный transfer-прогон текущего PointPillars-кандидата по
|
||||||
|
реальной записи нашего сканера `RAVNOVES00`. Внешний KITTI в эту лабораторную
|
||||||
|
работу не входит: прежний `L3` сохранён отдельно как публичный benchmark для
|
||||||
|
проверки измерителя и идеального box-truth контура.
|
||||||
|
|
||||||
|
Полный runtime-контракт на RAVNOVES00 выполнен, но модель не допускается в
|
||||||
|
operational detector. У записи нет независимых ориентированных 3D truth-боксов,
|
||||||
|
поэтому полученные боксы являются только визуально проверяемыми гипотезами.
|
||||||
|
Кроме того, повтор выбранных кадров совпал только в `50%` случаев.
|
||||||
|
|
||||||
|
Неизменяемый результат:
|
||||||
|
|
||||||
|
`l31-pointpillars-ravnoves-80a9715f64ea397222fbcfd700803f9152009e3751caf87e2e9dc5ec6fc01b72`
|
||||||
|
|
||||||
|
## Источник
|
||||||
|
|
||||||
|
- запись: `RAVNOVES00`;
|
||||||
|
- канонический session id: `20260720T065719Z_viewer_live`;
|
||||||
|
- replay pack:
|
||||||
|
`lidar-replay-pack-8fc0fb418578b8ee2ac88d502d2acbc63ae533437a9da14f1a9f9d8916f613ce`;
|
||||||
|
- logical content SHA-256:
|
||||||
|
`8d5642e5f394a48f5e07322e813384c601c96880f1ec349075f064a2d077b4ac`;
|
||||||
|
- LiDAR-кадров: `4,570`;
|
||||||
|
- pose-кадров: `4,598`;
|
||||||
|
- исходных точек: `10,751,258`.
|
||||||
|
|
||||||
|
Каждая порция point cloud была связана с ближайшей pose. Точки, сохранённые в
|
||||||
|
map frame, детерминированно преобразованы обратно в sensor-frame XYZI,
|
||||||
|
ожидаемый моделью. Возраст pose binding не превысил `73.797625 ms`.
|
||||||
|
|
||||||
|
## Исполнение
|
||||||
|
|
||||||
|
Прогон выполнен на Worker 006 внутри уже работающего канонического контейнера
|
||||||
|
`ndc-mission-core-triton`:
|
||||||
|
|
||||||
|
- второй контейнер и второй serving stack не создавались;
|
||||||
|
- существующий Triton не перезапускался;
|
||||||
|
- один последовательный worker;
|
||||||
|
- source pacing отключён;
|
||||||
|
- Synology не использовался;
|
||||||
|
- модель ONNX SHA-256:
|
||||||
|
`2dcabddc3a365e9608a112d7bbbb7db769a6dddeeaa59aa03611a83113326da1`;
|
||||||
|
- TensorRT engine SHA-256:
|
||||||
|
`12005d972a4632d56342a5da44442b632c1dcc5144fa3c70b162dec334532481`;
|
||||||
|
- worker package:
|
||||||
|
`l3-pointpillars-worker-package-6799b6f4ad776a4a959dc2d6366c6bcdf8ddb922990571c46841acf186e28c9b`.
|
||||||
|
|
||||||
|
## Полный результат
|
||||||
|
|
||||||
|
| Метрика | Значение |
|
||||||
|
| --- | ---: |
|
||||||
|
| Принято входных кадров | `4,570 / 4,570` (`100%`) |
|
||||||
|
| Валидных выходных контрактов | `4,570 / 4,570` (`100%`) |
|
||||||
|
| Кадров с гипотезами | `4,570` |
|
||||||
|
| Кадров с Vehicle-гипотезами | `4,559` |
|
||||||
|
| Всего гипотез | `48,005` |
|
||||||
|
| Vehicle | `28,097` |
|
||||||
|
| Pedestrian | `1,095` |
|
||||||
|
| Cyclist | `18,813` |
|
||||||
|
| Inference mean | `27.857662 ms` |
|
||||||
|
| Inference p50 | `26.324656 ms` |
|
||||||
|
| Inference p95 | `39.919944 ms` |
|
||||||
|
| Inference max | `143.260391 ms` |
|
||||||
|
| Повтор выбранных визуальных кадров | `9 / 18` (`50%`) |
|
||||||
|
|
||||||
|
Идентичность полного набора per-frame результатов:
|
||||||
|
|
||||||
|
`9e4bb0fe1fc7a31d77bbc7d4de08e9e42c1456d28728ae25f9cc88598f0ebac0`
|
||||||
|
|
||||||
|
## Визуальное доказательство
|
||||||
|
|
||||||
|
Из полного маршрута детерминированно выбраны `18` кадров: маршрут разделён на
|
||||||
|
равные временные окна, внутри каждого приоритет имеет число Vehicle-гипотез,
|
||||||
|
затем общее число гипотез и близость к центру окна.
|
||||||
|
|
||||||
|
Для каждого выбранного кадра опубликованы:
|
||||||
|
|
||||||
|
- ограниченная равномерная выборка реальных XYZI-точек RAVNOVES00;
|
||||||
|
- ориентированные 3D-боксы PointPillars;
|
||||||
|
- классы и confidence;
|
||||||
|
- исходная latency;
|
||||||
|
- результат повторного запуска того же кадра.
|
||||||
|
|
||||||
|
Mission Core загружает только выбранный кадр и показывает его в общем
|
||||||
|
лабораторном viewer в режимах `3D` и `BEV`. В интерфейсе боксы явно обозначены
|
||||||
|
как гипотезы модели, а не ground truth, TP/FP/FN или подтверждённые объекты.
|
||||||
|
|
||||||
|
## Что доказано
|
||||||
|
|
||||||
|
- реальный RAVNOVES00 проходит через текущий PointPillars input/output seam без
|
||||||
|
пропуска кадров и нарушения схемы;
|
||||||
|
- существующий Worker 006 и Triton способны последовательно обработать полный
|
||||||
|
маршрут;
|
||||||
|
- задержка inference измерена на всём наборе, а не на коротком smoke;
|
||||||
|
- фактические точки и гипотезы можно проверять визуально в Mission Core.
|
||||||
|
|
||||||
|
## Что не доказано
|
||||||
|
|
||||||
|
- точность, полнота и классовая корректность детектора;
|
||||||
|
- TP/FP/FN и пригодность боксов для продукта;
|
||||||
|
- source-paced queue/drop/deadline поведение;
|
||||||
|
- навигация, команды или safety;
|
||||||
|
- замена camera-first семантики LiDAR-only моделью.
|
||||||
|
|
||||||
|
Текущий source — vendor map increment без ring, firing time, scan geometry и
|
||||||
|
IMU. Модель обучена на другом proprietary solid-state LiDAR domain.
|
||||||
|
|
||||||
|
## Следующий gate
|
||||||
|
|
||||||
|
1. Разобрать визуально `18` опубликованных кадров и локализовать характер
|
||||||
|
доменного расхождения.
|
||||||
|
2. Выяснить причину `50%` повторяемости боксов при неизменном входе и sealed
|
||||||
|
engine.
|
||||||
|
3. Зафиксировать небольшой независимый RAVNOVES box-truth набор до любого
|
||||||
|
обучения или подбора порога.
|
||||||
|
4. Только после этого допускать новый train/adaptation candidate на Worker 006.
|
||||||
|
5. Основную продуктовую архитектуру сохранять как
|
||||||
|
`camera-first semantics + LiDAR metric geometry`; LiDAR-native detector
|
||||||
|
остаётся отдельным доказательным кандидатом.
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
# L3 PointPillars transfer report — 2026-07-31
|
# L3 PointPillars transfer report — 2026-07-31
|
||||||
|
|
||||||
|
> Контур размещения: это отдельный публичный benchmark на KITTI. Он сохранён
|
||||||
|
> для проверки измерителя, box-truth и cross-domain ideal/reference path, но не
|
||||||
|
> является основной лабораторной работой нашего сканера. Реальный прогон
|
||||||
|
> RAVNOVES00 оформлен отдельно как `L3.1`; см.
|
||||||
|
> `L31_POINTPILLARS_RAVNOVES_REPORT_2026-07-31.md`.
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
The LiDAR-native detector seam is operational on Worker 006, but the frozen
|
The LiDAR-native detector seam is operational on Worker 006, but the frozen
|
||||||
@@ -230,10 +236,10 @@ the sensor-domain mismatch.
|
|||||||
4. Export a new ONNX, build its TensorRT engine on Worker 006 and evaluate
|
4. Export a new ONNX, build its TensorRT engine on Worker 006 and evaluate
|
||||||
through the already proven canonical Triton seam.
|
through the already proven canonical Triton seam.
|
||||||
5. Require a useful public box-truth result before K1 accuracy language.
|
5. Require a useful public box-truth result before K1 accuracy language.
|
||||||
6. Run `RAVNOVES01` as a K1 representation/stability transfer after the public
|
6. Keep the public KITTI benchmark outside the primary scanner profile.
|
||||||
candidate is useful. Because that session has no camera and no independent
|
RAVNOVES transfer is now the separate `L3.1` laboratory work; its completed
|
||||||
3D cuboids, it can prove deterministic replay, output stability, latency,
|
baseline and limitations are recorded in
|
||||||
queue/drop behavior and visual plausibility, but not detector accuracy.
|
`L31_POINTPILLARS_RAVNOVES_REPORT_2026-07-31.md`.
|
||||||
|
|
||||||
The adaptation task is a new admission. This report does not authorize a
|
The adaptation task is a new admission. This report does not authorize a
|
||||||
training container, a second serving stack, K1 quality claims or navigation
|
training container, a second serving stack, K1 quality claims or navigation
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ _SOURCE_FILES = {
|
|||||||
"runtime/k1link/compute/pointpillars_postprocess.py": (
|
"runtime/k1link/compute/pointpillars_postprocess.py": (
|
||||||
"src/k1link/compute/pointpillars_postprocess.py"
|
"src/k1link/compute/pointpillars_postprocess.py"
|
||||||
),
|
),
|
||||||
|
"runtime/k1link/compute/l31_pointpillars_ravnoves.py": (
|
||||||
|
"src/k1link/compute/l31_pointpillars_ravnoves.py"
|
||||||
|
),
|
||||||
"runtime/k1link/datasets/kitti_3d_admission.py": (
|
"runtime/k1link/datasets/kitti_3d_admission.py": (
|
||||||
"src/k1link/datasets/kitti_3d_admission.py"
|
"src/k1link/datasets/kitti_3d_admission.py"
|
||||||
),
|
),
|
||||||
@@ -41,6 +44,9 @@ _SOURCE_FILES = {
|
|||||||
"runtime/run_l3_pointpillars_public_baseline.py": (
|
"runtime/run_l3_pointpillars_public_baseline.py": (
|
||||||
"experiments/perception/worker/run_l3_pointpillars_public_baseline.py"
|
"experiments/perception/worker/run_l3_pointpillars_public_baseline.py"
|
||||||
),
|
),
|
||||||
|
"runtime/run_l31_pointpillars_ravnoves.py": (
|
||||||
|
"experiments/perception/worker/run_l31_pointpillars_ravnoves.py"
|
||||||
|
),
|
||||||
"runtime/smoke_l3_pointpillars_live.py": (
|
"runtime/smoke_l3_pointpillars_live.py": (
|
||||||
"experiments/perception/worker/smoke_l3_pointpillars_live.py"
|
"experiments/perception/worker/smoke_l3_pointpillars_live.py"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,777 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run PointPillars sequentially over the sealed RAVNOVES00 K1 replay."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from k1link.compute.l31_pointpillars_ravnoves import (
|
||||||
|
nearest_pose_indices,
|
||||||
|
select_visual_frame_indices,
|
||||||
|
sensor_frame_xyzi,
|
||||||
|
)
|
||||||
|
from k1link.compute.pointpillars_postprocess import (
|
||||||
|
POINTPILLARS_EMBEDDED_SCORE_THRESHOLD,
|
||||||
|
POINTPILLARS_MODEL_POINT_CLOUD_RANGE,
|
||||||
|
PointPillarsBox,
|
||||||
|
decode_pointpillars_output,
|
||||||
|
)
|
||||||
|
|
||||||
|
RUN_SCHEMA = "missioncore.l31-pointpillars-ravnoves/v1"
|
||||||
|
FRAME_SCHEMA = "missioncore.l31-pointpillars-ravnoves-frame/v1"
|
||||||
|
CATALOG_SCHEMA = "missioncore.l31-pointpillars-ravnoves-catalog/v1"
|
||||||
|
VISUAL_FRAME_SCHEMA = "missioncore.l31-pointpillars-ravnoves-visual-frame/v1"
|
||||||
|
WORKER_PACKAGE_SCHEMA = "missioncore.l3-pointpillars-worker-package/v1"
|
||||||
|
REPLAY_SCHEMA = "missioncore.lidar-replay-pack/v2"
|
||||||
|
EXPECTED_PACK_ID = (
|
||||||
|
"lidar-replay-pack-"
|
||||||
|
"8fc0fb418578b8ee2ac88d502d2acbc63ae533437a9da14f1a9f9d8916f613ce"
|
||||||
|
)
|
||||||
|
EXPECTED_REPLAY_SHA256 = (
|
||||||
|
"cbb75341ca0d82aea6e59bec26636d06aa58d9c300e6fb5555033ab35b2bed7e"
|
||||||
|
)
|
||||||
|
EXPECTED_SESSION_ID = "20260720T065719Z_viewer_live"
|
||||||
|
EXPECTED_MODEL_SHA256 = (
|
||||||
|
"2dcabddc3a365e9608a112d7bbbb7db769a6dddeeaa59aa03611a83113326da1"
|
||||||
|
)
|
||||||
|
EXPECTED_ENGINE_SHA256 = (
|
||||||
|
"12005d972a4632d56342a5da44442b632c1dcc5144fa3c70b162dec334532481"
|
||||||
|
)
|
||||||
|
MODEL_NAME = "pointpillars"
|
||||||
|
MAXIMUM_POINTS = 204_800
|
||||||
|
MAXIMUM_VISUAL_BOXES = 512
|
||||||
|
MAXIMUM_VISUAL_FRAMES = 18
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--replay-pack", type=Path, required=True)
|
||||||
|
parser.add_argument("--worker-package", type=Path, required=True)
|
||||||
|
parser.add_argument("--triton-url", default="http://127.0.0.1:8000")
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-root",
|
||||||
|
type=Path,
|
||||||
|
default=Path(
|
||||||
|
"/mnt/d/NDC_MISSIONCORE/runtime/experiments/l3/"
|
||||||
|
"pointpillars-ravnoves"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
replay_root, replay_manifest, arrays = _read_replay(args.replay_pack)
|
||||||
|
package = _read_worker_package(args.worker_package)
|
||||||
|
_require_triton_ready(args.triton_url)
|
||||||
|
point_times = arrays["point_received_monotonic_ns"]
|
||||||
|
pose_indices, pose_age_ms = nearest_pose_indices(
|
||||||
|
point_times,
|
||||||
|
arrays["pose_received_monotonic_ns"],
|
||||||
|
)
|
||||||
|
if float(np.max(pose_age_ms)) > 100.0:
|
||||||
|
raise RuntimeError("RAVNOVES00 contains a pose binding older than 100 ms")
|
||||||
|
|
||||||
|
identity = {
|
||||||
|
"schema_version": RUN_SCHEMA,
|
||||||
|
"source_pack_id": replay_manifest["pack_id"],
|
||||||
|
"source_pack_identity_sha256": replay_manifest["identity_sha256"],
|
||||||
|
"source_logical_content_sha256": replay_manifest["identity"][
|
||||||
|
"logical_content_sha256"
|
||||||
|
],
|
||||||
|
"source_session_id": EXPECTED_SESSION_ID,
|
||||||
|
"point_frame_count": int(point_times.size),
|
||||||
|
"model": {
|
||||||
|
"name": MODEL_NAME,
|
||||||
|
"source_model_sha256": EXPECTED_MODEL_SHA256,
|
||||||
|
"engine_sha256": EXPECTED_ENGINE_SHA256,
|
||||||
|
"embedded_score_threshold": POINTPILLARS_EMBEDDED_SCORE_THRESHOLD,
|
||||||
|
"point_cloud_range": list(POINTPILLARS_MODEL_POINT_CLOUD_RANGE),
|
||||||
|
},
|
||||||
|
"worker_package_id": package["package_id"],
|
||||||
|
"worker_package_identity_sha256": package["identity_sha256"],
|
||||||
|
"producer_sha256": _sha256(Path(__file__)),
|
||||||
|
"execution": {
|
||||||
|
"worker_host_id": "worker-006",
|
||||||
|
"sequential": True,
|
||||||
|
"parallel_workers": 1,
|
||||||
|
"source_paced": False,
|
||||||
|
"existing_triton_only": True,
|
||||||
|
},
|
||||||
|
"authority": {
|
||||||
|
"shadow_only": True,
|
||||||
|
"commands_enabled": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
"accuracy_accepted": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||||
|
result_id = f"l31-pointpillars-ravnoves-{identity_sha256}"
|
||||||
|
result_root = args.output_root.expanduser().absolute() / result_id
|
||||||
|
summaries_root = result_root / "frame-results"
|
||||||
|
visual_root = result_root / "visual-frames"
|
||||||
|
summaries_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
visual_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
_write_once(result_root / "identity.json", identity)
|
||||||
|
|
||||||
|
point_offsets = arrays["point_offsets"]
|
||||||
|
first_time_ns = int(point_times[0])
|
||||||
|
summaries: list[dict[str, Any]] = []
|
||||||
|
for completed, frame_index in enumerate(range(point_times.size), start=1):
|
||||||
|
xyzi = _frame_xyzi(arrays, point_offsets, pose_indices, frame_index)
|
||||||
|
input_sha256 = hashlib.sha256(
|
||||||
|
np.ascontiguousarray(xyzi, dtype=np.float32).tobytes()
|
||||||
|
).hexdigest()
|
||||||
|
frame_path = summaries_root / f"{frame_index:06d}.json"
|
||||||
|
if frame_path.exists():
|
||||||
|
summary = _read_frame_summary(
|
||||||
|
frame_path,
|
||||||
|
frame_index=frame_index,
|
||||||
|
input_sha256=input_sha256,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
boxes, elapsed_ms = _run_frame(args.triton_url, xyzi)
|
||||||
|
summary = _frame_summary(
|
||||||
|
frame_index=frame_index,
|
||||||
|
session_seconds=(int(point_times[frame_index]) - first_time_ns)
|
||||||
|
/ 1_000_000_000.0,
|
||||||
|
source_point_count=int(xyzi.shape[0]),
|
||||||
|
pose_index=int(pose_indices[frame_index]),
|
||||||
|
pose_binding_age_ms=float(pose_age_ms[frame_index]),
|
||||||
|
input_sha256=input_sha256,
|
||||||
|
inference_ms=elapsed_ms,
|
||||||
|
boxes=boxes,
|
||||||
|
)
|
||||||
|
_atomic_json(frame_path, summary)
|
||||||
|
summaries.append(summary)
|
||||||
|
if completed == 1 or completed % 100 == 0:
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"result_id": result_id,
|
||||||
|
"completed": completed,
|
||||||
|
"total": int(point_times.size),
|
||||||
|
"frame_index": frame_index,
|
||||||
|
},
|
||||||
|
sort_keys=True,
|
||||||
|
),
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
selected_indices = select_visual_frame_indices(
|
||||||
|
[int(row["class_counts"]["Vehicle"]) for row in summaries],
|
||||||
|
[int(row["prediction_count"]) for row in summaries],
|
||||||
|
maximum_frames=MAXIMUM_VISUAL_FRAMES,
|
||||||
|
)
|
||||||
|
visual_descriptors: list[dict[str, Any]] = []
|
||||||
|
deterministic_matches = 0
|
||||||
|
for frame_index in selected_indices:
|
||||||
|
xyzi = _frame_xyzi(arrays, point_offsets, pose_indices, frame_index)
|
||||||
|
boxes, elapsed_ms = _run_frame(args.triton_url, xyzi)
|
||||||
|
observed_sha256 = _boxes_sha256(boxes)
|
||||||
|
if observed_sha256 == summaries[frame_index]["boxes_sha256"]:
|
||||||
|
deterministic_matches += 1
|
||||||
|
visual_payload = _visual_frame(
|
||||||
|
summary=summaries[frame_index],
|
||||||
|
xyzi=xyzi,
|
||||||
|
boxes=boxes,
|
||||||
|
replay_inference_ms=elapsed_ms,
|
||||||
|
deterministic_replay=(
|
||||||
|
observed_sha256 == summaries[frame_index]["boxes_sha256"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
visual_path = visual_root / f"{frame_index:06d}.json"
|
||||||
|
_atomic_json(visual_path, visual_payload)
|
||||||
|
descriptor = _artifact(
|
||||||
|
visual_path,
|
||||||
|
"visual-frame",
|
||||||
|
relative_to=result_root,
|
||||||
|
)
|
||||||
|
visual_descriptors.append(
|
||||||
|
{
|
||||||
|
"frame_id": f"{frame_index:06d}",
|
||||||
|
"frame_index": frame_index,
|
||||||
|
"session_seconds": summaries[frame_index]["session_seconds"],
|
||||||
|
"source_point_count": summaries[frame_index]["source_point_count"],
|
||||||
|
"prediction_count": summaries[frame_index]["prediction_count"],
|
||||||
|
"class_counts": summaries[frame_index]["class_counts"],
|
||||||
|
"inference_ms": summaries[frame_index]["inference_ms"],
|
||||||
|
"detail_path": descriptor["path"],
|
||||||
|
"detail_sha256": descriptor["sha256"],
|
||||||
|
"detail_byte_length": descriptor["byte_length"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
metrics = _metrics(
|
||||||
|
summaries,
|
||||||
|
deterministic_matches=deterministic_matches,
|
||||||
|
deterministic_total=len(selected_indices),
|
||||||
|
)
|
||||||
|
catalog = {
|
||||||
|
"schema_version": CATALOG_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"source_session_id": EXPECTED_SESSION_ID,
|
||||||
|
"selection": {
|
||||||
|
"strategy": (
|
||||||
|
"18 equal route-time bins; prefer Vehicle count, then total "
|
||||||
|
"prediction count, then bin-centre proximity"
|
||||||
|
),
|
||||||
|
"maximum_frames": MAXIMUM_VISUAL_FRAMES,
|
||||||
|
"maximum_boxes_per_frame": MAXIMUM_VISUAL_BOXES,
|
||||||
|
},
|
||||||
|
"frame_count": len(visual_descriptors),
|
||||||
|
"frames": visual_descriptors,
|
||||||
|
}
|
||||||
|
_atomic_json(result_root / "catalog.json", catalog)
|
||||||
|
manifest = {
|
||||||
|
"schema_version": RUN_SCHEMA,
|
||||||
|
"result_id": result_id,
|
||||||
|
"identity_sha256": identity_sha256,
|
||||||
|
"identity": identity,
|
||||||
|
"created_at_utc": datetime.now(UTC)
|
||||||
|
.isoformat(timespec="milliseconds")
|
||||||
|
.replace("+00:00", "Z"),
|
||||||
|
"status": "k1-cross-domain-transfer-measured-visual-review-required",
|
||||||
|
"metrics": metrics,
|
||||||
|
"catalog": _artifact(
|
||||||
|
result_root / "catalog.json",
|
||||||
|
"visual-frame-catalog",
|
||||||
|
relative_to=result_root,
|
||||||
|
),
|
||||||
|
"frame_results": {
|
||||||
|
"count": len(summaries),
|
||||||
|
"identity_sha256": _directory_identity(summaries_root),
|
||||||
|
},
|
||||||
|
"limitations": [
|
||||||
|
"RAVNOVES00 has no independent oriented 3D cuboid ground truth.",
|
||||||
|
"Predicted boxes are model hypotheses, not TP/FP/FN or accepted objects.",
|
||||||
|
"The K1 source is a vendor map increment without ring, firing time, "
|
||||||
|
"scan geometry, or IMU.",
|
||||||
|
"Execution is offline sequential and does not establish source-paced "
|
||||||
|
"drop or deadline behaviour.",
|
||||||
|
"The model was trained on another proprietary solid-state LiDAR domain.",
|
||||||
|
],
|
||||||
|
"authority": identity["authority"],
|
||||||
|
}
|
||||||
|
_atomic_json(result_root / "manifest.json", manifest)
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"result_id": result_id,
|
||||||
|
"frame_count": len(summaries),
|
||||||
|
"frames_with_predictions": metrics["frames_with_predictions"],
|
||||||
|
"vehicle_predictions": metrics["class_counts"]["Vehicle"],
|
||||||
|
"inference_p95_ms": metrics["inference_latency_ms"]["p95"],
|
||||||
|
"deterministic_replay_fraction": metrics[
|
||||||
|
"deterministic_replay_fraction"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
sort_keys=True,
|
||||||
|
),
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
del replay_root
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _read_replay(
|
||||||
|
path: Path,
|
||||||
|
) -> tuple[Path, dict[str, Any], dict[str, np.ndarray]]:
|
||||||
|
root = path.expanduser().resolve(strict=True)
|
||||||
|
manifest = _read_json(root / "manifest.json")
|
||||||
|
identity = manifest.get("identity")
|
||||||
|
artifacts = manifest.get("artifacts")
|
||||||
|
if (
|
||||||
|
not root.is_dir()
|
||||||
|
or root.is_symlink()
|
||||||
|
or root.name != EXPECTED_PACK_ID
|
||||||
|
or manifest.get("schema_version") != REPLAY_SCHEMA
|
||||||
|
or manifest.get("pack_id") != EXPECTED_PACK_ID
|
||||||
|
or manifest.get("identity_sha256") != EXPECTED_PACK_ID.removeprefix(
|
||||||
|
"lidar-replay-pack-"
|
||||||
|
)
|
||||||
|
or not isinstance(identity, dict)
|
||||||
|
or identity.get("session_id") != EXPECTED_SESSION_ID
|
||||||
|
or identity.get("point_frame_count") != 4570
|
||||||
|
or identity.get("pose_frame_count") != 4598
|
||||||
|
or not isinstance(artifacts, list)
|
||||||
|
):
|
||||||
|
raise RuntimeError("RAVNOVES00 replay identity is invalid")
|
||||||
|
descriptor = next(
|
||||||
|
(
|
||||||
|
row
|
||||||
|
for row in artifacts
|
||||||
|
if isinstance(row, dict) and row.get("path") == "lidar-replay.npz"
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
replay_path = root / "lidar-replay.npz"
|
||||||
|
if (
|
||||||
|
not isinstance(descriptor, dict)
|
||||||
|
or descriptor.get("sha256") != EXPECTED_REPLAY_SHA256
|
||||||
|
or descriptor.get("byte_length") != replay_path.stat().st_size
|
||||||
|
or _sha256(replay_path) != EXPECTED_REPLAY_SHA256
|
||||||
|
):
|
||||||
|
raise RuntimeError("RAVNOVES00 replay artifact changed")
|
||||||
|
expected = {
|
||||||
|
"point_offsets",
|
||||||
|
"point_xyz_map",
|
||||||
|
"point_intensity",
|
||||||
|
"point_received_monotonic_ns",
|
||||||
|
"pose_positions_map",
|
||||||
|
"pose_quaternions_map_from_lidar",
|
||||||
|
"pose_received_monotonic_ns",
|
||||||
|
}
|
||||||
|
with np.load(replay_path, allow_pickle=False) as archive:
|
||||||
|
if not expected.issubset(archive.files):
|
||||||
|
raise RuntimeError("RAVNOVES00 replay arrays changed")
|
||||||
|
# Load each required member once. Reopening a compressed NPZ member for
|
||||||
|
# every frame would repeatedly inflate the complete 10.7M-point array.
|
||||||
|
arrays = {name: archive[name] for name in expected}
|
||||||
|
if (
|
||||||
|
arrays["point_offsets"].shape != (4571,)
|
||||||
|
or arrays["point_offsets"].dtype != np.int64
|
||||||
|
or arrays["point_xyz_map"].shape != (10_751_258, 3)
|
||||||
|
or arrays["point_xyz_map"].dtype != np.float64
|
||||||
|
or arrays["point_intensity"].shape != (10_751_258,)
|
||||||
|
or arrays["point_intensity"].dtype != np.uint8
|
||||||
|
or arrays["point_received_monotonic_ns"].shape != (4570,)
|
||||||
|
or arrays["point_received_monotonic_ns"].dtype != np.int64
|
||||||
|
or arrays["pose_positions_map"].shape != (4598, 3)
|
||||||
|
or arrays["pose_positions_map"].dtype != np.float64
|
||||||
|
or arrays["pose_quaternions_map_from_lidar"].shape != (4598, 4)
|
||||||
|
or arrays["pose_quaternions_map_from_lidar"].dtype != np.float64
|
||||||
|
or arrays["pose_received_monotonic_ns"].shape != (4598,)
|
||||||
|
or arrays["pose_received_monotonic_ns"].dtype != np.int64
|
||||||
|
or int(arrays["point_offsets"][0]) != 0
|
||||||
|
or int(arrays["point_offsets"][-1]) != 10_751_258
|
||||||
|
or np.any(np.diff(arrays["point_offsets"]) <= 0)
|
||||||
|
):
|
||||||
|
raise RuntimeError("RAVNOVES00 replay array contract changed")
|
||||||
|
return root, manifest, arrays
|
||||||
|
|
||||||
|
|
||||||
|
def _frame_xyzi(
|
||||||
|
arrays: dict[str, np.ndarray],
|
||||||
|
offsets: np.ndarray,
|
||||||
|
pose_indices: np.ndarray,
|
||||||
|
frame_index: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
start = int(offsets[frame_index])
|
||||||
|
stop = int(offsets[frame_index + 1])
|
||||||
|
pose_index = int(pose_indices[frame_index])
|
||||||
|
xyzi = sensor_frame_xyzi(
|
||||||
|
arrays["point_xyz_map"][start:stop],
|
||||||
|
arrays["point_intensity"][start:stop],
|
||||||
|
position_map_xyz=arrays["pose_positions_map"][pose_index],
|
||||||
|
orientation_map_from_lidar_xyzw=arrays[
|
||||||
|
"pose_quaternions_map_from_lidar"
|
||||||
|
][pose_index],
|
||||||
|
)
|
||||||
|
if not 1 <= xyzi.shape[0] <= MAXIMUM_POINTS:
|
||||||
|
raise RuntimeError("RAVNOVES00 frame violates PointPillars input bounds")
|
||||||
|
return xyzi
|
||||||
|
|
||||||
|
|
||||||
|
def _run_frame(
|
||||||
|
triton_url: str,
|
||||||
|
native: np.ndarray,
|
||||||
|
) -> tuple[tuple[PointPillarsBox, ...], float]:
|
||||||
|
points = np.zeros((1, MAXIMUM_POINTS, 4), dtype=np.float32)
|
||||||
|
points[0, : native.shape[0]] = native
|
||||||
|
num_points = np.asarray([native.shape[0]], dtype=np.int32)
|
||||||
|
output_boxes, output_count, elapsed_ms = _infer(
|
||||||
|
triton_url,
|
||||||
|
points,
|
||||||
|
num_points,
|
||||||
|
)
|
||||||
|
return decode_pointpillars_output(output_boxes, output_count), elapsed_ms
|
||||||
|
|
||||||
|
|
||||||
|
def _infer(
|
||||||
|
triton_url: str,
|
||||||
|
points: np.ndarray,
|
||||||
|
num_points: np.ndarray,
|
||||||
|
) -> tuple[np.ndarray, np.ndarray, float]:
|
||||||
|
points_binary = np.ascontiguousarray(points, dtype=np.float32).tobytes()
|
||||||
|
count_binary = np.ascontiguousarray(num_points, dtype=np.int32).tobytes()
|
||||||
|
header = {
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "points",
|
||||||
|
"shape": [1, MAXIMUM_POINTS, 4],
|
||||||
|
"datatype": "FP32",
|
||||||
|
"parameters": {"binary_data_size": len(points_binary)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "num_points",
|
||||||
|
"shape": [1],
|
||||||
|
"datatype": "INT32",
|
||||||
|
"parameters": {"binary_data_size": len(count_binary)},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{"name": "output_boxes", "parameters": {"binary_data": True}},
|
||||||
|
{"name": "num_boxes", "parameters": {"binary_data": True}},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
encoded = _canonical_json(header)
|
||||||
|
request = urllib.request.Request(
|
||||||
|
f"{triton_url.rstrip('/')}/v2/models/{MODEL_NAME}/infer",
|
||||||
|
data=encoded + points_binary + count_binary,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
"Inference-Header-Content-Length": str(len(encoded)),
|
||||||
|
},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
started = time.perf_counter()
|
||||||
|
with urllib.request.urlopen(request, timeout=120) as response:
|
||||||
|
payload = response.read()
|
||||||
|
header_length = int(response.headers["Inference-Header-Content-Length"])
|
||||||
|
elapsed_ms = (time.perf_counter() - started) * 1000.0
|
||||||
|
response_header = json.loads(payload[:header_length])
|
||||||
|
outputs = response_header.get("outputs")
|
||||||
|
if not isinstance(outputs, list) or len(outputs) != 2:
|
||||||
|
raise RuntimeError("Triton PointPillars output set changed")
|
||||||
|
offset = header_length
|
||||||
|
decoded: dict[str, np.ndarray] = {}
|
||||||
|
for descriptor in outputs:
|
||||||
|
if not isinstance(descriptor, dict):
|
||||||
|
raise RuntimeError("Triton output descriptor is invalid")
|
||||||
|
name = descriptor.get("name")
|
||||||
|
parameters = descriptor.get("parameters")
|
||||||
|
if not isinstance(name, str) or not isinstance(parameters, dict):
|
||||||
|
raise RuntimeError("Triton output descriptor is invalid")
|
||||||
|
byte_length = parameters.get("binary_data_size")
|
||||||
|
if isinstance(byte_length, bool) or not isinstance(byte_length, int):
|
||||||
|
raise RuntimeError("Triton output byte length is invalid")
|
||||||
|
binary = payload[offset : offset + byte_length]
|
||||||
|
offset += byte_length
|
||||||
|
if name == "output_boxes" and descriptor.get("datatype") == "FP32":
|
||||||
|
decoded[name] = np.frombuffer(binary, dtype="<f4").reshape(
|
||||||
|
1, 393_216, 9
|
||||||
|
)
|
||||||
|
elif name == "num_boxes" and descriptor.get("datatype") == "INT32":
|
||||||
|
decoded[name] = np.frombuffer(binary, dtype="<i4").reshape(1)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Triton PointPillars output contract changed")
|
||||||
|
if offset != len(payload) or set(decoded) != {"output_boxes", "num_boxes"}:
|
||||||
|
raise RuntimeError("Triton PointPillars output payload is invalid")
|
||||||
|
return decoded["output_boxes"], decoded["num_boxes"], elapsed_ms
|
||||||
|
|
||||||
|
|
||||||
|
def _box_payload(box: PointPillarsBox) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"x_m": box.x_m,
|
||||||
|
"y_m": box.y_m,
|
||||||
|
"z_m": box.z_m,
|
||||||
|
"length_m": box.length_m,
|
||||||
|
"width_m": box.width_m,
|
||||||
|
"height_m": box.height_m,
|
||||||
|
"yaw_rad": box.yaw_rad,
|
||||||
|
"class_id": box.class_id,
|
||||||
|
"model_class": box.model_class,
|
||||||
|
"score": box.score,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _boxes_sha256(boxes: tuple[PointPillarsBox, ...]) -> str:
|
||||||
|
return hashlib.sha256(
|
||||||
|
_canonical_json([_box_payload(box) for box in boxes])
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _frame_summary(
|
||||||
|
*,
|
||||||
|
frame_index: int,
|
||||||
|
session_seconds: float,
|
||||||
|
source_point_count: int,
|
||||||
|
pose_index: int,
|
||||||
|
pose_binding_age_ms: float,
|
||||||
|
input_sha256: str,
|
||||||
|
inference_ms: float,
|
||||||
|
boxes: tuple[PointPillarsBox, ...],
|
||||||
|
) -> dict[str, object]:
|
||||||
|
class_counts = {
|
||||||
|
name: sum(box.model_class == name for box in boxes)
|
||||||
|
for name in ("Vehicle", "Pedestrian", "Cyclist")
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"schema_version": FRAME_SCHEMA,
|
||||||
|
"frame_id": f"{frame_index:06d}",
|
||||||
|
"frame_index": frame_index,
|
||||||
|
"session_seconds": session_seconds,
|
||||||
|
"source_point_count": source_point_count,
|
||||||
|
"pose_index": pose_index,
|
||||||
|
"pose_binding_age_ms": pose_binding_age_ms,
|
||||||
|
"input_xyzi_sha256": input_sha256,
|
||||||
|
"inference_ms": inference_ms,
|
||||||
|
"prediction_count": len(boxes),
|
||||||
|
"class_counts": class_counts,
|
||||||
|
"boxes_sha256": _boxes_sha256(boxes),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_frame_summary(
|
||||||
|
path: Path,
|
||||||
|
*,
|
||||||
|
frame_index: int,
|
||||||
|
input_sha256: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
payload = _read_json(path)
|
||||||
|
if (
|
||||||
|
payload.get("schema_version") != FRAME_SCHEMA
|
||||||
|
or payload.get("frame_id") != f"{frame_index:06d}"
|
||||||
|
or payload.get("frame_index") != frame_index
|
||||||
|
or payload.get("input_xyzi_sha256") != input_sha256
|
||||||
|
or not isinstance(payload.get("class_counts"), dict)
|
||||||
|
or not isinstance(payload.get("boxes_sha256"), str)
|
||||||
|
):
|
||||||
|
raise RuntimeError("cached L3.1 frame summary is invalid")
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _visual_frame(
|
||||||
|
*,
|
||||||
|
summary: dict[str, Any],
|
||||||
|
xyzi: np.ndarray,
|
||||||
|
boxes: tuple[PointPillarsBox, ...],
|
||||||
|
replay_inference_ms: float,
|
||||||
|
deterministic_replay: bool,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
minimum_x, minimum_y, minimum_z, maximum_x, maximum_y, maximum_z = (
|
||||||
|
POINTPILLARS_MODEL_POINT_CLOUD_RANGE
|
||||||
|
)
|
||||||
|
mask = (
|
||||||
|
(xyzi[:, 0] >= minimum_x)
|
||||||
|
& (xyzi[:, 0] <= maximum_x)
|
||||||
|
& (xyzi[:, 1] >= minimum_y)
|
||||||
|
& (xyzi[:, 1] <= maximum_y)
|
||||||
|
& (xyzi[:, 2] >= minimum_z)
|
||||||
|
& (xyzi[:, 2] <= maximum_z)
|
||||||
|
)
|
||||||
|
visible_points = xyzi[mask]
|
||||||
|
visible_boxes = boxes[:MAXIMUM_VISUAL_BOXES]
|
||||||
|
return {
|
||||||
|
"schema_version": VISUAL_FRAME_SCHEMA,
|
||||||
|
"frame_id": summary["frame_id"],
|
||||||
|
"summary": {
|
||||||
|
**summary,
|
||||||
|
"replay_inference_ms": replay_inference_ms,
|
||||||
|
"deterministic_replay": deterministic_replay,
|
||||||
|
"visual_box_count": len(visible_boxes),
|
||||||
|
"visual_box_truncated": len(boxes) > len(visible_boxes),
|
||||||
|
},
|
||||||
|
"points": {
|
||||||
|
"layout": "flat-xyzi",
|
||||||
|
"source_point_count": int(xyzi.shape[0]),
|
||||||
|
"model_range_point_count": int(visible_points.shape[0]),
|
||||||
|
"sampled_point_count": int(visible_points.shape[0]),
|
||||||
|
"values": visible_points.reshape(-1).tolist(),
|
||||||
|
},
|
||||||
|
"prediction_boxes": [_box_payload(box) for box in visible_boxes],
|
||||||
|
"interpretation": {
|
||||||
|
"ground_truth_available": False,
|
||||||
|
"boxes_are_model_hypotheses": True,
|
||||||
|
"accuracy_claim_allowed": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _metrics(
|
||||||
|
summaries: list[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
deterministic_matches: int,
|
||||||
|
deterministic_total: int,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
latency = np.asarray([row["inference_ms"] for row in summaries], dtype=np.float64)
|
||||||
|
pose_age = np.asarray(
|
||||||
|
[row["pose_binding_age_ms"] for row in summaries],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
class_counts = {
|
||||||
|
name: sum(int(row["class_counts"][name]) for row in summaries)
|
||||||
|
for name in ("Vehicle", "Pedestrian", "Cyclist")
|
||||||
|
}
|
||||||
|
predictions = [int(row["prediction_count"]) for row in summaries]
|
||||||
|
return {
|
||||||
|
"frame_count": len(summaries),
|
||||||
|
"input_admission_fraction": 1.0,
|
||||||
|
"output_schema_valid_fraction": 1.0,
|
||||||
|
"frames_with_predictions": sum(value > 0 for value in predictions),
|
||||||
|
"frames_with_vehicle_predictions": sum(
|
||||||
|
int(row["class_counts"]["Vehicle"]) > 0 for row in summaries
|
||||||
|
),
|
||||||
|
"prediction_count": sum(predictions),
|
||||||
|
"class_counts": class_counts,
|
||||||
|
"predictions_per_frame": _distribution(np.asarray(predictions)),
|
||||||
|
"inference_latency_ms": _distribution(latency),
|
||||||
|
"pose_binding_age_ms": _distribution(pose_age),
|
||||||
|
"deterministic_replay_fraction": (
|
||||||
|
deterministic_matches / deterministic_total
|
||||||
|
if deterministic_total
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
|
"deterministic_replay_frames": deterministic_total,
|
||||||
|
"source_paced": False,
|
||||||
|
"input_drop_count": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _distribution(values: np.ndarray) -> dict[str, float]:
|
||||||
|
return {
|
||||||
|
"minimum": float(np.min(values)),
|
||||||
|
"p50": float(np.percentile(values, 50)),
|
||||||
|
"p95": float(np.percentile(values, 95)),
|
||||||
|
"maximum": float(np.max(values)),
|
||||||
|
"mean": float(np.mean(values)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_worker_package(path: Path) -> dict[str, Any]:
|
||||||
|
root = path.expanduser().resolve(strict=True)
|
||||||
|
manifest = _read_json(root / "manifest.json")
|
||||||
|
identity = manifest.get("identity")
|
||||||
|
artifacts = manifest.get("artifacts")
|
||||||
|
identity_sha256 = manifest.get("identity_sha256")
|
||||||
|
package_id = manifest.get("package_id")
|
||||||
|
if (
|
||||||
|
root.is_symlink()
|
||||||
|
or manifest.get("schema_version") != WORKER_PACKAGE_SCHEMA
|
||||||
|
or not isinstance(identity, dict)
|
||||||
|
or not isinstance(artifacts, list)
|
||||||
|
or not isinstance(identity_sha256, str)
|
||||||
|
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||||
|
or package_id != f"l3-pointpillars-worker-package-{identity_sha256}"
|
||||||
|
or root.name != package_id
|
||||||
|
or Path(__file__).resolve(strict=True).parents[1] != root
|
||||||
|
):
|
||||||
|
raise RuntimeError("L3.1 worker package identity is invalid")
|
||||||
|
expected = set(identity.get("artifact_paths", []))
|
||||||
|
actual = {
|
||||||
|
item.relative_to(root).as_posix()
|
||||||
|
for item in root.rglob("*")
|
||||||
|
if item.is_file()
|
||||||
|
}
|
||||||
|
if actual != expected | {"manifest.json"}:
|
||||||
|
raise RuntimeError("L3.1 worker package file set changed")
|
||||||
|
for descriptor in artifacts:
|
||||||
|
if not isinstance(descriptor, dict):
|
||||||
|
raise RuntimeError("L3.1 worker package artifact is invalid")
|
||||||
|
relative = descriptor.get("path")
|
||||||
|
member = root / str(relative)
|
||||||
|
if (
|
||||||
|
not isinstance(relative, str)
|
||||||
|
or relative not in expected
|
||||||
|
or not member.is_file()
|
||||||
|
or member.is_symlink()
|
||||||
|
or descriptor.get("byte_length") != member.stat().st_size
|
||||||
|
or descriptor.get("sha256") != _sha256(member)
|
||||||
|
):
|
||||||
|
raise RuntimeError("L3.1 worker package artifact changed")
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
|
||||||
|
def _require_triton_ready(url: str) -> None:
|
||||||
|
for endpoint in ("/v2/health/ready", f"/v2/models/{MODEL_NAME}/ready"):
|
||||||
|
request = urllib.request.Request(f"{url.rstrip('/')}{endpoint}", method="GET")
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=10) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
raise RuntimeError("canonical Triton is not ready")
|
||||||
|
except OSError as exc:
|
||||||
|
raise RuntimeError("canonical Triton or PointPillars is not ready") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _artifact(path: Path, kind: str, *, relative_to: Path) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"kind": kind,
|
||||||
|
"path": path.relative_to(relative_to).as_posix(),
|
||||||
|
"byte_length": path.stat().st_size,
|
||||||
|
"sha256": _sha256(path),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _directory_identity(root: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
for path in sorted(root.glob("*.json"), key=lambda item: item.name):
|
||||||
|
digest.update(path.name.encode("ascii"))
|
||||||
|
digest.update(b"\0")
|
||||||
|
digest.update(bytes.fromhex(_sha256(path)))
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_json(value: object) -> bytes:
|
||||||
|
return json.dumps(
|
||||||
|
value,
|
||||||
|
ensure_ascii=False,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
allow_nan=False,
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _read_json(path: Path) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
value = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
raise RuntimeError(f"invalid JSON artifact: {path.name}") from exc
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise RuntimeError(f"invalid JSON artifact: {path.name}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _write_once(path: Path, payload: object) -> None:
|
||||||
|
encoded = _canonical_json(payload)
|
||||||
|
if path.exists():
|
||||||
|
if path.read_bytes() != encoded:
|
||||||
|
raise RuntimeError(f"immutable artifact changed: {path.name}")
|
||||||
|
return
|
||||||
|
_atomic_bytes(path, encoded)
|
||||||
|
|
||||||
|
|
||||||
|
def _atomic_json(path: Path, payload: object) -> None:
|
||||||
|
_atomic_bytes(path, _canonical_json(payload))
|
||||||
|
|
||||||
|
|
||||||
|
def _atomic_bytes(path: Path, payload: bytes) -> None:
|
||||||
|
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
descriptor, temporary = tempfile.mkstemp(
|
||||||
|
prefix=f".{path.name}.",
|
||||||
|
suffix=".tmp",
|
||||||
|
dir=path.parent,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with os.fdopen(descriptor, "wb") as handle:
|
||||||
|
handle.write(payload)
|
||||||
|
handle.flush()
|
||||||
|
os.fsync(handle.fileno())
|
||||||
|
os.replace(temporary, path)
|
||||||
|
except BaseException:
|
||||||
|
try:
|
||||||
|
os.unlink(temporary)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(block)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"""Deterministic helpers for the L3.1 PointPillars transfer on RAVNOVES00."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import numpy.typing as npt
|
||||||
|
|
||||||
|
|
||||||
|
class L31PointPillarsRavnovesError(RuntimeError):
|
||||||
|
"""The RAVNOVES transfer input violates the frozen L3.1 contract."""
|
||||||
|
|
||||||
|
|
||||||
|
def nearest_pose_indices(
|
||||||
|
point_times_ns: npt.NDArray[np.int64],
|
||||||
|
pose_times_ns: npt.NDArray[np.int64],
|
||||||
|
) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float64]]:
|
||||||
|
"""Bind each point frame to the nearest pose on the host monotonic clock."""
|
||||||
|
|
||||||
|
point_times = np.asarray(point_times_ns, dtype=np.int64)
|
||||||
|
pose_times = np.asarray(pose_times_ns, dtype=np.int64)
|
||||||
|
if (
|
||||||
|
point_times.ndim != 1
|
||||||
|
or pose_times.ndim != 1
|
||||||
|
or not point_times.size
|
||||||
|
or not pose_times.size
|
||||||
|
or np.any(np.diff(point_times) < 0)
|
||||||
|
or np.any(np.diff(pose_times) < 0)
|
||||||
|
):
|
||||||
|
raise L31PointPillarsRavnovesError("LiDAR/pose time axes are invalid")
|
||||||
|
|
||||||
|
right = np.searchsorted(pose_times, point_times, side="left")
|
||||||
|
right = np.clip(right, 0, pose_times.size - 1)
|
||||||
|
left = np.clip(right - 1, 0, pose_times.size - 1)
|
||||||
|
right_delta = np.abs(pose_times[right] - point_times)
|
||||||
|
left_delta = np.abs(point_times - pose_times[left])
|
||||||
|
indices = np.where(left_delta <= right_delta, left, right).astype(np.int64)
|
||||||
|
age_ms = (
|
||||||
|
np.abs(pose_times[indices] - point_times).astype(np.float64) / 1_000_000.0
|
||||||
|
)
|
||||||
|
if not np.isfinite(age_ms).all():
|
||||||
|
raise L31PointPillarsRavnovesError("LiDAR/pose binding age is invalid")
|
||||||
|
return indices, age_ms
|
||||||
|
|
||||||
|
|
||||||
|
def sensor_frame_xyzi(
|
||||||
|
points_map_xyz: npt.NDArray[np.float64],
|
||||||
|
intensities: npt.NDArray[np.uint8],
|
||||||
|
*,
|
||||||
|
position_map_xyz: npt.NDArray[np.float64],
|
||||||
|
orientation_map_from_lidar_xyzw: npt.NDArray[np.float64],
|
||||||
|
) -> npt.NDArray[np.float32]:
|
||||||
|
"""Convert verified map-frame K1 points into the model's sensor frame."""
|
||||||
|
|
||||||
|
points = np.asarray(points_map_xyz, dtype=np.float64)
|
||||||
|
intensity = np.asarray(intensities, dtype=np.uint8)
|
||||||
|
position = np.asarray(position_map_xyz, dtype=np.float64)
|
||||||
|
quaternion = np.asarray(orientation_map_from_lidar_xyzw, dtype=np.float64)
|
||||||
|
if (
|
||||||
|
points.ndim != 2
|
||||||
|
or points.shape[1:] != (3,)
|
||||||
|
or intensity.shape != (points.shape[0],)
|
||||||
|
or position.shape != (3,)
|
||||||
|
or quaternion.shape != (4,)
|
||||||
|
or not np.isfinite(points).all()
|
||||||
|
or not np.isfinite(position).all()
|
||||||
|
or not np.isfinite(quaternion).all()
|
||||||
|
):
|
||||||
|
raise L31PointPillarsRavnovesError("K1 point/pose arrays are invalid")
|
||||||
|
norm = float(np.linalg.norm(quaternion))
|
||||||
|
if not math.isfinite(norm) or not 0.99 <= norm <= 1.01:
|
||||||
|
raise L31PointPillarsRavnovesError("K1 pose quaternion is not normalized")
|
||||||
|
x, y, z, w = quaternion / norm
|
||||||
|
rotation_map_from_lidar = np.asarray(
|
||||||
|
[
|
||||||
|
[1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)],
|
||||||
|
[2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)],
|
||||||
|
[2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)],
|
||||||
|
],
|
||||||
|
dtype=np.float64,
|
||||||
|
)
|
||||||
|
sensor_xyz = (points - position) @ rotation_map_from_lidar
|
||||||
|
result = np.empty((points.shape[0], 4), dtype=np.float32)
|
||||||
|
result[:, :3] = sensor_xyz.astype(np.float32)
|
||||||
|
result[:, 3] = intensity.astype(np.float32) / 255.0
|
||||||
|
if not np.isfinite(result).all():
|
||||||
|
raise L31PointPillarsRavnovesError("K1 sensor-frame XYZI is non-finite")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def select_visual_frame_indices(
|
||||||
|
vehicle_counts: Sequence[int],
|
||||||
|
total_counts: Sequence[int],
|
||||||
|
*,
|
||||||
|
maximum_frames: int = 18,
|
||||||
|
) -> tuple[int, ...]:
|
||||||
|
"""Select route-wide evidence, preferring frames with Vehicle predictions."""
|
||||||
|
|
||||||
|
vehicles = tuple(vehicle_counts)
|
||||||
|
totals = tuple(total_counts)
|
||||||
|
if (
|
||||||
|
len(vehicles) != len(totals)
|
||||||
|
or not vehicles
|
||||||
|
or isinstance(maximum_frames, bool)
|
||||||
|
or maximum_frames < 1
|
||||||
|
or any(
|
||||||
|
isinstance(value, bool) or not isinstance(value, int) or value < 0
|
||||||
|
for value in (*vehicles, *totals)
|
||||||
|
)
|
||||||
|
or any(vehicle > total for vehicle, total in zip(vehicles, totals, strict=True))
|
||||||
|
):
|
||||||
|
raise L31PointPillarsRavnovesError("L3.1 visual selection input is invalid")
|
||||||
|
|
||||||
|
count = min(maximum_frames, len(vehicles))
|
||||||
|
boundaries = np.linspace(0, len(vehicles), count + 1, dtype=np.int64)
|
||||||
|
selected: list[int] = []
|
||||||
|
for bin_index in range(count):
|
||||||
|
start = int(boundaries[bin_index])
|
||||||
|
stop = int(boundaries[bin_index + 1])
|
||||||
|
if stop <= start:
|
||||||
|
continue
|
||||||
|
center = (start + stop - 1) / 2.0
|
||||||
|
chosen = max(
|
||||||
|
range(start, stop),
|
||||||
|
key=lambda index: (
|
||||||
|
vehicles[index],
|
||||||
|
totals[index],
|
||||||
|
-abs(index - center),
|
||||||
|
-index,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
selected.append(chosen)
|
||||||
|
return tuple(selected)
|
||||||
@@ -10,8 +10,6 @@ from typing import Any, Final
|
|||||||
|
|
||||||
from fastapi import APIRouter, Query
|
from fastapi import APIRouter, Query
|
||||||
|
|
||||||
from k1link.web.l3_pointpillars_visual_api import latest_l3_visual_identity
|
|
||||||
|
|
||||||
from k1link.compute.e31_source_qualification import (
|
from k1link.compute.e31_source_qualification import (
|
||||||
E31SourceQualification,
|
E31SourceQualification,
|
||||||
E31SourceQualificationError,
|
E31SourceQualificationError,
|
||||||
@@ -57,6 +55,8 @@ from k1link.compute.e40_perception_product_gate import (
|
|||||||
E40PerceptionProductGateError,
|
E40PerceptionProductGateError,
|
||||||
read_e40_perception_product_gate,
|
read_e40_perception_product_gate,
|
||||||
)
|
)
|
||||||
|
from k1link.web.l3_pointpillars_visual_api import latest_l3_visual_identity
|
||||||
|
from k1link.web.l31_pointpillars_ravnoves_api import latest_l31_identity
|
||||||
|
|
||||||
LABORATORY_ADVANCED_CATALOG_SCHEMA: Final = (
|
LABORATORY_ADVANCED_CATALOG_SCHEMA: Final = (
|
||||||
"missioncore.laboratory-advanced-catalog/v1"
|
"missioncore.laboratory-advanced-catalog/v1"
|
||||||
@@ -828,6 +828,7 @@ def build_advanced_laboratory_router(
|
|||||||
e39_root_provider: RootProvider = lambda: None,
|
e39_root_provider: RootProvider = lambda: None,
|
||||||
e40_root_provider: RootProvider = lambda: None,
|
e40_root_provider: RootProvider = lambda: None,
|
||||||
l3_visual_root_provider: RootProvider = lambda: None,
|
l3_visual_root_provider: RootProvider = lambda: None,
|
||||||
|
l31_ravnoves_root_provider: RootProvider = lambda: None,
|
||||||
) -> APIRouter:
|
) -> APIRouter:
|
||||||
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
|
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
|
||||||
|
|
||||||
@@ -909,6 +910,15 @@ def build_advanced_laboratory_router(
|
|||||||
"access": "read-only",
|
"access": "read-only",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
l31_identity = latest_l31_identity(l31_ravnoves_root_provider)
|
||||||
|
if l31_identity is not None:
|
||||||
|
result["items"].append(
|
||||||
|
{
|
||||||
|
"work_id": "l31-pointpillars-ravnoves",
|
||||||
|
**l31_identity,
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@router.get("/e31/results")
|
@router.get("/e31/results")
|
||||||
|
|||||||
+24
-3
@@ -34,9 +34,6 @@ from k1link.sessions import (
|
|||||||
SessionStore,
|
SessionStore,
|
||||||
)
|
)
|
||||||
from k1link.web.advanced_laboratory_api import build_advanced_laboratory_router
|
from k1link.web.advanced_laboratory_api import build_advanced_laboratory_router
|
||||||
from k1link.web.l3_pointpillars_visual_api import (
|
|
||||||
build_l3_pointpillars_visual_router,
|
|
||||||
)
|
|
||||||
from k1link.web.artifact_health_api import build_artifact_health_router
|
from k1link.web.artifact_health_api import build_artifact_health_router
|
||||||
from k1link.web.compute_contour_api import build_compute_contour_router
|
from k1link.web.compute_contour_api import build_compute_contour_router
|
||||||
from k1link.web.device_plugin_composition import load_installed_device_plugins
|
from k1link.web.device_plugin_composition import load_installed_device_plugins
|
||||||
@@ -45,6 +42,12 @@ from k1link.web.e30_human_review_api import build_e30_human_review_router
|
|||||||
from k1link.web.e30_review_api import build_e30_review_router
|
from k1link.web.e30_review_api import build_e30_review_router
|
||||||
from k1link.web.e40_case_review_api import build_e40_case_review_router
|
from k1link.web.e40_case_review_api import build_e40_case_review_router
|
||||||
from k1link.web.environment_api import build_environment_router
|
from k1link.web.environment_api import build_environment_router
|
||||||
|
from k1link.web.l3_pointpillars_visual_api import (
|
||||||
|
build_l3_pointpillars_visual_router,
|
||||||
|
)
|
||||||
|
from k1link.web.l31_pointpillars_ravnoves_api import (
|
||||||
|
build_l31_pointpillars_ravnoves_router,
|
||||||
|
)
|
||||||
from k1link.web.laboratory_api import build_laboratory_router
|
from k1link.web.laboratory_api import build_laboratory_router
|
||||||
from k1link.web.lidar_api import build_lidar_router
|
from k1link.web.lidar_api import build_lidar_router
|
||||||
from k1link.web.lidar_local_surface_service import K1LocalSurfaceReadService
|
from k1link.web.lidar_local_surface_service import K1LocalSurfaceReadService
|
||||||
@@ -609,6 +612,13 @@ app.include_router(
|
|||||||
/ "l3"
|
/ "l3"
|
||||||
/ "visual-audits"
|
/ "visual-audits"
|
||||||
),
|
),
|
||||||
|
l31_ravnoves_root_provider=lambda: (
|
||||||
|
REPOSITORY_ROOT
|
||||||
|
/ ".runtime"
|
||||||
|
/ "compute-experiments"
|
||||||
|
/ "l3"
|
||||||
|
/ "pointpillars-ravnoves"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
app.include_router(
|
app.include_router(
|
||||||
@@ -622,6 +632,17 @@ app.include_router(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
app.include_router(
|
||||||
|
build_l31_pointpillars_ravnoves_router(
|
||||||
|
root_provider=lambda: (
|
||||||
|
REPOSITORY_ROOT
|
||||||
|
/ ".runtime"
|
||||||
|
/ "compute-experiments"
|
||||||
|
/ "l3"
|
||||||
|
/ "pointpillars-ravnoves"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
app.include_router(
|
app.include_router(
|
||||||
build_e30_review_router(
|
build_e30_review_router(
|
||||||
materialization_root_provider=lambda: (
|
materialization_root_provider=lambda: (
|
||||||
|
|||||||
@@ -0,0 +1,392 @@
|
|||||||
|
"""Read-only projection of the sealed L3.1 PointPillars RAVNOVES evidence."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
|
|
||||||
|
RootProvider = Callable[[], Path | None]
|
||||||
|
|
||||||
|
RESULT_SCHEMA: Final = "missioncore.l31-pointpillars-ravnoves/v1"
|
||||||
|
CATALOG_SCHEMA: Final = "missioncore.l31-pointpillars-ravnoves-catalog/v1"
|
||||||
|
VISUAL_FRAME_SCHEMA: Final = (
|
||||||
|
"missioncore.l31-pointpillars-ravnoves-visual-frame/v1"
|
||||||
|
)
|
||||||
|
RESULT_PROJECTION_SCHEMA: Final = (
|
||||||
|
"missioncore.l31-pointpillars-ravnoves-result/v1"
|
||||||
|
)
|
||||||
|
RESULT_CATALOG_SCHEMA: Final = (
|
||||||
|
"missioncore.l31-pointpillars-ravnoves-catalog-results/v1"
|
||||||
|
)
|
||||||
|
RESULT_ID: Final = re.compile(r"^l31-pointpillars-ravnoves-[a-f0-9]{64}$")
|
||||||
|
FRAME_ID: Final = re.compile(r"^[0-9]{6}$")
|
||||||
|
SHA256: Final = re.compile(r"^[a-f0-9]{64}$")
|
||||||
|
MAX_JSON_BYTES: Final = 16 * 1024 * 1024
|
||||||
|
MAX_CANDIDATES: Final = 16
|
||||||
|
MAX_FRAMES: Final = 18
|
||||||
|
|
||||||
|
|
||||||
|
def build_l31_pointpillars_ravnoves_router(
|
||||||
|
*,
|
||||||
|
root_provider: RootProvider = lambda: None,
|
||||||
|
) -> APIRouter:
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/api/v1/laboratory/l31/pointpillars-ravnoves",
|
||||||
|
tags=["laboratory"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/results")
|
||||||
|
def list_results(
|
||||||
|
limit: int = Query(default=1, ge=1, le=10),
|
||||||
|
) -> dict[str, object]:
|
||||||
|
root = _configured_root(root_provider)
|
||||||
|
if root is None:
|
||||||
|
return _empty_catalog(False)
|
||||||
|
candidates = _candidates(root)
|
||||||
|
items: list[dict[str, object]] = []
|
||||||
|
invalid_total = 0
|
||||||
|
for candidate in candidates:
|
||||||
|
try:
|
||||||
|
items.append(_project_result(candidate))
|
||||||
|
except RuntimeError:
|
||||||
|
invalid_total += 1
|
||||||
|
items.sort(
|
||||||
|
key=lambda item: (
|
||||||
|
str(item["created_at_utc"]),
|
||||||
|
str(item["result_id"]),
|
||||||
|
),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"schema_version": RESULT_CATALOG_SCHEMA,
|
||||||
|
"configured": True,
|
||||||
|
"items": items[:limit],
|
||||||
|
"candidate_total": len(candidates),
|
||||||
|
"invalid_total": invalid_total,
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.get("/{result_id}/frames/{frame_id}")
|
||||||
|
def get_frame(result_id: str, frame_id: str) -> dict[str, object]:
|
||||||
|
if not RESULT_ID.fullmatch(result_id) or not FRAME_ID.fullmatch(frame_id):
|
||||||
|
raise HTTPException(status_code=404, detail="L3.1 frame not found")
|
||||||
|
root = _configured_root(root_provider)
|
||||||
|
if root is None:
|
||||||
|
raise HTTPException(status_code=404, detail="L3.1 result not found")
|
||||||
|
candidate = root / result_id
|
||||||
|
try:
|
||||||
|
result = _load_result(candidate)
|
||||||
|
descriptor = next(
|
||||||
|
item
|
||||||
|
for item in result["catalog"]["frames"]
|
||||||
|
if item["frame_id"] == frame_id
|
||||||
|
)
|
||||||
|
relative = descriptor["detail_path"]
|
||||||
|
if relative != f"visual-frames/{frame_id}.json":
|
||||||
|
raise RuntimeError("L3.1 visual path changed")
|
||||||
|
path = candidate / relative
|
||||||
|
payload = _read_json(path)
|
||||||
|
if (
|
||||||
|
payload.get("schema_version") != VISUAL_FRAME_SCHEMA
|
||||||
|
or payload.get("frame_id") != frame_id
|
||||||
|
or descriptor["detail_sha256"] != _sha256(path)
|
||||||
|
or descriptor["detail_byte_length"] != path.stat().st_size
|
||||||
|
or not _valid_visual_payload(payload)
|
||||||
|
):
|
||||||
|
raise RuntimeError("L3.1 visual identity changed")
|
||||||
|
except (RuntimeError, StopIteration):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="L3.1 frame not found",
|
||||||
|
) from None
|
||||||
|
return {**copy.deepcopy(payload), "access": "read-only"}
|
||||||
|
|
||||||
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
def latest_l31_identity(
|
||||||
|
root_provider: RootProvider,
|
||||||
|
) -> dict[str, str] | None:
|
||||||
|
root = _configured_root(root_provider)
|
||||||
|
if root is None:
|
||||||
|
return None
|
||||||
|
valid: list[dict[str, object]] = []
|
||||||
|
for candidate in _candidates(root):
|
||||||
|
try:
|
||||||
|
valid.append(_project_result(candidate))
|
||||||
|
except RuntimeError:
|
||||||
|
continue
|
||||||
|
if not valid:
|
||||||
|
return None
|
||||||
|
latest = max(
|
||||||
|
valid,
|
||||||
|
key=lambda item: (
|
||||||
|
str(item["created_at_utc"]),
|
||||||
|
str(item["result_id"]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"result_id": str(latest["result_id"]),
|
||||||
|
"created_at_utc": str(latest["created_at_utc"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _project_result(candidate: Path) -> dict[str, object]:
|
||||||
|
result = _load_result(candidate)
|
||||||
|
manifest = result["manifest"]
|
||||||
|
identity = manifest["identity"]
|
||||||
|
return {
|
||||||
|
"schema_version": RESULT_PROJECTION_SCHEMA,
|
||||||
|
"result_id": manifest["result_id"],
|
||||||
|
"created_at_utc": manifest["created_at_utc"],
|
||||||
|
"status": "cross-domain-transfer-measured-visual-review-required",
|
||||||
|
"source_session_id": identity["source_session_id"],
|
||||||
|
"source_pack_id": identity["source_pack_id"],
|
||||||
|
"source_logical_content_sha256": identity[
|
||||||
|
"source_logical_content_sha256"
|
||||||
|
],
|
||||||
|
"model": copy.deepcopy(identity["model"]),
|
||||||
|
"execution": copy.deepcopy(identity["execution"]),
|
||||||
|
"metrics": copy.deepcopy(manifest["metrics"]),
|
||||||
|
"frames": copy.deepcopy(result["catalog"]["frames"]),
|
||||||
|
"limitations": copy.deepcopy(manifest["limitations"]),
|
||||||
|
"authority": copy.deepcopy(manifest["authority"]),
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_result(candidate: Path) -> dict[str, Any]:
|
||||||
|
if (
|
||||||
|
not candidate.is_dir()
|
||||||
|
or candidate.is_symlink()
|
||||||
|
or not RESULT_ID.fullmatch(candidate.name)
|
||||||
|
):
|
||||||
|
raise RuntimeError("L3.1 candidate is invalid")
|
||||||
|
manifest = _read_json(candidate / "manifest.json")
|
||||||
|
identity = manifest.get("identity")
|
||||||
|
authority = manifest.get("authority")
|
||||||
|
catalog_descriptor = manifest.get("catalog")
|
||||||
|
limitations = manifest.get("limitations")
|
||||||
|
metrics = manifest.get("metrics")
|
||||||
|
if (
|
||||||
|
manifest.get("schema_version") != RESULT_SCHEMA
|
||||||
|
or manifest.get("result_id") != candidate.name
|
||||||
|
or manifest.get("status")
|
||||||
|
!= "k1-cross-domain-transfer-measured-visual-review-required"
|
||||||
|
or not isinstance(manifest.get("created_at_utc"), str)
|
||||||
|
or not isinstance(identity, dict)
|
||||||
|
or identity.get("source_session_id")
|
||||||
|
!= "20260720T065719Z_viewer_live"
|
||||||
|
or not isinstance(authority, dict)
|
||||||
|
or authority.get("shadow_only") is not True
|
||||||
|
or authority.get("commands_enabled") is not False
|
||||||
|
or authority.get("navigation_or_safety_accepted") is not False
|
||||||
|
or authority.get("accuracy_accepted") is not False
|
||||||
|
or manifest.get("identity_sha256")
|
||||||
|
!= hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||||
|
or candidate.name
|
||||||
|
!= f"l31-pointpillars-ravnoves-{manifest.get('identity_sha256')}"
|
||||||
|
or not isinstance(metrics, dict)
|
||||||
|
or metrics.get("frame_count") != 4570
|
||||||
|
or metrics.get("input_admission_fraction") != 1.0
|
||||||
|
or metrics.get("output_schema_valid_fraction") != 1.0
|
||||||
|
or not isinstance(limitations, list)
|
||||||
|
or not limitations
|
||||||
|
or not isinstance(catalog_descriptor, dict)
|
||||||
|
or catalog_descriptor.get("path") != "catalog.json"
|
||||||
|
or catalog_descriptor.get("kind") != "visual-frame-catalog"
|
||||||
|
):
|
||||||
|
raise RuntimeError("L3.1 manifest is invalid")
|
||||||
|
catalog_path = candidate / "catalog.json"
|
||||||
|
if (
|
||||||
|
catalog_descriptor.get("sha256") != _sha256(catalog_path)
|
||||||
|
or catalog_descriptor.get("byte_length") != catalog_path.stat().st_size
|
||||||
|
):
|
||||||
|
raise RuntimeError("L3.1 catalog changed")
|
||||||
|
catalog = _read_json(catalog_path)
|
||||||
|
frames = catalog.get("frames")
|
||||||
|
if (
|
||||||
|
catalog.get("schema_version") != CATALOG_SCHEMA
|
||||||
|
or catalog.get("result_id") != candidate.name
|
||||||
|
or catalog.get("source_session_id") != identity["source_session_id"]
|
||||||
|
or not isinstance(frames, list)
|
||||||
|
or not 1 <= len(frames) <= MAX_FRAMES
|
||||||
|
or catalog.get("frame_count") != len(frames)
|
||||||
|
or len({item.get("frame_id") for item in frames if isinstance(item, dict)})
|
||||||
|
!= len(frames)
|
||||||
|
or any(not _valid_frame_descriptor(item) for item in frames)
|
||||||
|
):
|
||||||
|
raise RuntimeError("L3.1 catalog is invalid")
|
||||||
|
return {"manifest": manifest, "catalog": catalog}
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_frame_descriptor(value: object) -> bool:
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return False
|
||||||
|
frame_id = value.get("frame_id")
|
||||||
|
class_counts = value.get("class_counts")
|
||||||
|
return (
|
||||||
|
isinstance(frame_id, str)
|
||||||
|
and FRAME_ID.fullmatch(frame_id) is not None
|
||||||
|
and value.get("detail_path") == f"visual-frames/{frame_id}.json"
|
||||||
|
and isinstance(value.get("detail_sha256"), str)
|
||||||
|
and SHA256.fullmatch(value["detail_sha256"]) is not None
|
||||||
|
and isinstance(value.get("detail_byte_length"), int)
|
||||||
|
and 0 < value["detail_byte_length"] <= MAX_JSON_BYTES
|
||||||
|
and isinstance(value.get("frame_index"), int)
|
||||||
|
and value["frame_index"] >= 0
|
||||||
|
and isinstance(value.get("source_point_count"), int)
|
||||||
|
and value["source_point_count"] > 0
|
||||||
|
and isinstance(value.get("prediction_count"), int)
|
||||||
|
and value["prediction_count"] >= 0
|
||||||
|
and isinstance(value.get("session_seconds"), (int, float))
|
||||||
|
and value["session_seconds"] >= 0
|
||||||
|
and isinstance(value.get("inference_ms"), (int, float))
|
||||||
|
and 0 < value["inference_ms"] < 60_000
|
||||||
|
and isinstance(class_counts, dict)
|
||||||
|
and set(class_counts) == {"Vehicle", "Pedestrian", "Cyclist"}
|
||||||
|
and all(
|
||||||
|
isinstance(count, int) and not isinstance(count, bool) and count >= 0
|
||||||
|
for count in class_counts.values()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_visual_payload(value: dict[str, Any]) -> bool:
|
||||||
|
points = value.get("points")
|
||||||
|
boxes = value.get("prediction_boxes")
|
||||||
|
interpretation = value.get("interpretation")
|
||||||
|
if (
|
||||||
|
not isinstance(points, dict)
|
||||||
|
or points.get("layout") != "flat-xyzi"
|
||||||
|
or not isinstance(boxes, list)
|
||||||
|
or len(boxes) > 512
|
||||||
|
or not isinstance(interpretation, dict)
|
||||||
|
or interpretation.get("ground_truth_available") is not False
|
||||||
|
or interpretation.get("boxes_are_model_hypotheses") is not True
|
||||||
|
or interpretation.get("accuracy_claim_allowed") is not False
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
values = points.get("values")
|
||||||
|
sampled = points.get("sampled_point_count")
|
||||||
|
return (
|
||||||
|
isinstance(values, list)
|
||||||
|
and isinstance(sampled, int)
|
||||||
|
and 0 < sampled <= 12_000
|
||||||
|
and len(values) == sampled * 4
|
||||||
|
and all(
|
||||||
|
isinstance(item, (int, float))
|
||||||
|
and not isinstance(item, bool)
|
||||||
|
and -1_000_000 < item < 1_000_000
|
||||||
|
for item in values
|
||||||
|
)
|
||||||
|
and all(_valid_box(box) for box in boxes)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_box(value: object) -> bool:
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
return False
|
||||||
|
numeric = (
|
||||||
|
"x_m",
|
||||||
|
"y_m",
|
||||||
|
"z_m",
|
||||||
|
"length_m",
|
||||||
|
"width_m",
|
||||||
|
"height_m",
|
||||||
|
"yaw_rad",
|
||||||
|
"score",
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
value.get("model_class") in {"Vehicle", "Pedestrian", "Cyclist"}
|
||||||
|
and isinstance(value.get("class_id"), int)
|
||||||
|
and value["class_id"] in {0, 1, 2}
|
||||||
|
and all(
|
||||||
|
isinstance(value.get(key), (int, float))
|
||||||
|
and not isinstance(value.get(key), bool)
|
||||||
|
and math_is_finite(float(value[key]))
|
||||||
|
for key in numeric
|
||||||
|
)
|
||||||
|
and value["length_m"] > 0
|
||||||
|
and value["width_m"] > 0
|
||||||
|
and value["height_m"] > 0
|
||||||
|
and 0.1 <= value["score"] <= 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def math_is_finite(value: float) -> bool:
|
||||||
|
return value == value and value not in (float("inf"), float("-inf"))
|
||||||
|
|
||||||
|
|
||||||
|
def _configured_root(provider: RootProvider) -> Path | None:
|
||||||
|
value = provider()
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
root = value.expanduser().absolute()
|
||||||
|
if not root.is_dir() or root.is_symlink():
|
||||||
|
return None
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def _candidates(root: Path) -> list[Path]:
|
||||||
|
candidates = [
|
||||||
|
path
|
||||||
|
for path in root.iterdir()
|
||||||
|
if path.is_dir()
|
||||||
|
and not path.is_symlink()
|
||||||
|
and RESULT_ID.fullmatch(path.name)
|
||||||
|
]
|
||||||
|
if len(candidates) > MAX_CANDIDATES:
|
||||||
|
raise RuntimeError("L3.1 candidate bound exceeded")
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_catalog(configured: bool) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"schema_version": RESULT_CATALOG_SCHEMA,
|
||||||
|
"configured": configured,
|
||||||
|
"items": [],
|
||||||
|
"candidate_total": 0,
|
||||||
|
"invalid_total": 0,
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_json(path: Path) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
if (
|
||||||
|
not path.is_file()
|
||||||
|
or path.is_symlink()
|
||||||
|
or not 0 < path.stat().st_size <= MAX_JSON_BYTES
|
||||||
|
):
|
||||||
|
raise RuntimeError(f"{path.name} is unavailable")
|
||||||
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError) as exc:
|
||||||
|
raise RuntimeError(f"{path.name} is invalid") from exc
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise RuntimeError(f"{path.name} is not an object")
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as source:
|
||||||
|
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_json(payload: object) -> bytes:
|
||||||
|
return json.dumps(
|
||||||
|
payload,
|
||||||
|
ensure_ascii=False,
|
||||||
|
separators=(",", ":"),
|
||||||
|
sort_keys=True,
|
||||||
|
).encode("utf-8")
|
||||||
@@ -53,6 +53,36 @@ def test_advanced_index_is_empty_when_not_configured() -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_advanced_index_publishes_l31_ravnoves_identity(
|
||||||
|
tmp_path: Path,
|
||||||
|
monkeypatch: MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
result_id = f"l31-pointpillars-ravnoves-{'b' * 64}"
|
||||||
|
|
||||||
|
def fake_latest(provider: object) -> dict[str, str]:
|
||||||
|
assert callable(provider)
|
||||||
|
assert provider() == tmp_path # type: ignore[operator]
|
||||||
|
return {
|
||||||
|
"result_id": result_id,
|
||||||
|
"created_at_utc": "2026-07-31T10:56:45.861Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(advanced_api, "latest_l31_identity", fake_latest)
|
||||||
|
router = build_advanced_laboratory_router(
|
||||||
|
l31_ravnoves_root_provider=lambda: tmp_path,
|
||||||
|
)
|
||||||
|
route = _endpoint(router, "/api/v1/laboratory/advanced-index")
|
||||||
|
|
||||||
|
assert route()["items"] == [ # type: ignore[index,operator]
|
||||||
|
{
|
||||||
|
"work_id": "l31-pointpillars-ravnoves",
|
||||||
|
"result_id": result_id,
|
||||||
|
"created_at_utc": "2026-07-31T10:56:45.861Z",
|
||||||
|
"access": "read-only",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_advanced_index_reads_only_bounded_identity_documents(
|
def test_advanced_index_reads_only_bounded_identity_documents(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from k1link.compute.l31_pointpillars_ravnoves import (
|
||||||
|
L31PointPillarsRavnovesError,
|
||||||
|
nearest_pose_indices,
|
||||||
|
select_visual_frame_indices,
|
||||||
|
sensor_frame_xyzi,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_nearest_pose_binding_prefers_earlier_pose_on_tie() -> None:
|
||||||
|
indices, age_ms = nearest_pose_indices(
|
||||||
|
np.asarray([10, 20, 31], dtype=np.int64) * 1_000_000,
|
||||||
|
np.asarray([5, 15, 30], dtype=np.int64) * 1_000_000,
|
||||||
|
)
|
||||||
|
assert indices.tolist() == [0, 1, 2]
|
||||||
|
assert age_ms.tolist() == [5.0, 5.0, 1.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_sensor_frame_xyzi_inverts_map_pose() -> None:
|
||||||
|
points = np.asarray([[11.0, 20.0, 30.0], [10.0, 22.0, 30.0]])
|
||||||
|
intensity = np.asarray([255, 0], dtype=np.uint8)
|
||||||
|
result = sensor_frame_xyzi(
|
||||||
|
points,
|
||||||
|
intensity,
|
||||||
|
position_map_xyz=np.asarray([10.0, 20.0, 30.0]),
|
||||||
|
orientation_map_from_lidar_xyzw=np.asarray([0.0, 0.0, 0.0, 1.0]),
|
||||||
|
)
|
||||||
|
np.testing.assert_allclose(
|
||||||
|
result,
|
||||||
|
np.asarray([[1.0, 0.0, 0.0, 1.0], [0.0, 2.0, 0.0, 0.0]]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_visual_selection_prefers_vehicles_with_route_coverage() -> None:
|
||||||
|
selected = select_visual_frame_indices(
|
||||||
|
[0, 3, 1, 0, 0, 2],
|
||||||
|
[5, 3, 9, 0, 4, 2],
|
||||||
|
maximum_frames=3,
|
||||||
|
)
|
||||||
|
assert selected == (1, 2, 5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_visual_selection_rejects_impossible_class_count() -> None:
|
||||||
|
with pytest.raises(L31PointPillarsRavnovesError):
|
||||||
|
select_visual_frame_indices([2], [1])
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.routing import APIRoute
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
|
from k1link.web.l31_pointpillars_ravnoves_api import (
|
||||||
|
build_l31_pointpillars_ravnoves_router,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _endpoint(router: APIRouter, path: str) -> object:
|
||||||
|
for route in router.routes:
|
||||||
|
if (
|
||||||
|
isinstance(route, APIRoute)
|
||||||
|
and route.path == path
|
||||||
|
and route.methods is not None
|
||||||
|
and "GET" in route.methods
|
||||||
|
):
|
||||||
|
return route.endpoint
|
||||||
|
raise AssertionError(f"GET {path} route is missing")
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical(payload: object) -> bytes:
|
||||||
|
return json.dumps(
|
||||||
|
payload,
|
||||||
|
ensure_ascii=False,
|
||||||
|
separators=(",", ":"),
|
||||||
|
sort_keys=True,
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _write(path: Path, payload: object) -> dict[str, object]:
|
||||||
|
content = _canonical(payload)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes(content)
|
||||||
|
return {
|
||||||
|
"sha256": hashlib.sha256(content).hexdigest(),
|
||||||
|
"byte_length": len(content),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _result(root: Path) -> tuple[str, str]:
|
||||||
|
frame_id = "000075"
|
||||||
|
identity = {
|
||||||
|
"source_session_id": "20260720T065719Z_viewer_live",
|
||||||
|
"source_pack_id": f"lidar-replay-pack-{'1' * 64}",
|
||||||
|
"source_logical_content_sha256": "2" * 64,
|
||||||
|
"model": {"name": "pointpillars", "source_model_sha256": "3" * 64},
|
||||||
|
"execution": {
|
||||||
|
"worker_host_id": "worker-006",
|
||||||
|
"sequential": True,
|
||||||
|
"parallel_workers": 1,
|
||||||
|
},
|
||||||
|
"authority": {
|
||||||
|
"shadow_only": True,
|
||||||
|
"commands_enabled": False,
|
||||||
|
"navigation_or_safety_accepted": False,
|
||||||
|
"accuracy_accepted": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
identity_sha = hashlib.sha256(_canonical(identity)).hexdigest()
|
||||||
|
result_id = f"l31-pointpillars-ravnoves-{identity_sha}"
|
||||||
|
candidate = root / result_id
|
||||||
|
detail = {
|
||||||
|
"schema_version": "missioncore.l31-pointpillars-ravnoves-visual-frame/v1",
|
||||||
|
"frame_id": frame_id,
|
||||||
|
"points": {
|
||||||
|
"layout": "flat-xyzi",
|
||||||
|
"sampled_point_count": 1,
|
||||||
|
"values": [1.0, 2.0, 3.0, 0.5],
|
||||||
|
},
|
||||||
|
"prediction_boxes": [
|
||||||
|
{
|
||||||
|
"x_m": 1.0,
|
||||||
|
"y_m": 2.0,
|
||||||
|
"z_m": 0.5,
|
||||||
|
"length_m": 4.0,
|
||||||
|
"width_m": 2.0,
|
||||||
|
"height_m": 1.5,
|
||||||
|
"yaw_rad": 0.1,
|
||||||
|
"class_id": 0,
|
||||||
|
"model_class": "Vehicle",
|
||||||
|
"score": 0.8,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"interpretation": {
|
||||||
|
"ground_truth_available": False,
|
||||||
|
"boxes_are_model_hypotheses": True,
|
||||||
|
"accuracy_claim_allowed": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
detail_descriptor = _write(
|
||||||
|
candidate / "visual-frames" / f"{frame_id}.json",
|
||||||
|
detail,
|
||||||
|
)
|
||||||
|
frame = {
|
||||||
|
"frame_id": frame_id,
|
||||||
|
"frame_index": 75,
|
||||||
|
"session_seconds": 7.5,
|
||||||
|
"source_point_count": 2143,
|
||||||
|
"prediction_count": 1,
|
||||||
|
"class_counts": {"Vehicle": 1, "Pedestrian": 0, "Cyclist": 0},
|
||||||
|
"inference_ms": 26.3,
|
||||||
|
"detail_path": f"visual-frames/{frame_id}.json",
|
||||||
|
"detail_sha256": detail_descriptor["sha256"],
|
||||||
|
"detail_byte_length": detail_descriptor["byte_length"],
|
||||||
|
}
|
||||||
|
catalog = {
|
||||||
|
"schema_version": "missioncore.l31-pointpillars-ravnoves-catalog/v1",
|
||||||
|
"result_id": result_id,
|
||||||
|
"source_session_id": identity["source_session_id"],
|
||||||
|
"frame_count": 1,
|
||||||
|
"frames": [frame],
|
||||||
|
}
|
||||||
|
catalog_descriptor = _write(candidate / "catalog.json", catalog)
|
||||||
|
manifest = {
|
||||||
|
"schema_version": "missioncore.l31-pointpillars-ravnoves/v1",
|
||||||
|
"result_id": result_id,
|
||||||
|
"identity_sha256": identity_sha,
|
||||||
|
"identity": identity,
|
||||||
|
"created_at_utc": "2026-07-31T10:00:00Z",
|
||||||
|
"status": "k1-cross-domain-transfer-measured-visual-review-required",
|
||||||
|
"metrics": {
|
||||||
|
"frame_count": 4570,
|
||||||
|
"input_admission_fraction": 1.0,
|
||||||
|
"output_schema_valid_fraction": 1.0,
|
||||||
|
},
|
||||||
|
"catalog": {
|
||||||
|
"path": "catalog.json",
|
||||||
|
"kind": "visual-frame-catalog",
|
||||||
|
**catalog_descriptor,
|
||||||
|
},
|
||||||
|
"limitations": ["Independent 3D ground truth is unavailable."],
|
||||||
|
"authority": identity["authority"],
|
||||||
|
}
|
||||||
|
_write(candidate / "manifest.json", manifest)
|
||||||
|
return result_id, frame_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_l31_catalog_and_visual_frame_are_read_only(tmp_path: Path) -> None:
|
||||||
|
result_id, frame_id = _result(tmp_path)
|
||||||
|
router = build_l31_pointpillars_ravnoves_router(
|
||||||
|
root_provider=lambda: tmp_path,
|
||||||
|
)
|
||||||
|
catalog_route = _endpoint(
|
||||||
|
router,
|
||||||
|
"/api/v1/laboratory/l31/pointpillars-ravnoves/results",
|
||||||
|
)
|
||||||
|
frame_route = _endpoint(
|
||||||
|
router,
|
||||||
|
(
|
||||||
|
"/api/v1/laboratory/l31/pointpillars-ravnoves/"
|
||||||
|
"{result_id}/frames/{frame_id}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
catalog = catalog_route(limit=1) # type: ignore[operator]
|
||||||
|
assert catalog["configured"] is True
|
||||||
|
assert catalog["invalid_total"] == 0
|
||||||
|
assert catalog["items"][0]["result_id"] == result_id
|
||||||
|
assert catalog["items"][0]["status"] == (
|
||||||
|
"cross-domain-transfer-measured-visual-review-required"
|
||||||
|
)
|
||||||
|
assert catalog["items"][0]["frames"][0]["frame_id"] == frame_id
|
||||||
|
|
||||||
|
frame = frame_route( # type: ignore[operator]
|
||||||
|
result_id=result_id,
|
||||||
|
frame_id=frame_id,
|
||||||
|
)
|
||||||
|
assert frame["prediction_boxes"][0]["model_class"] == "Vehicle"
|
||||||
|
assert frame["interpretation"]["ground_truth_available"] is False
|
||||||
|
assert frame["access"] == "read-only"
|
||||||
|
|
||||||
|
|
||||||
|
def test_l31_visual_frame_fails_closed_after_mutation(tmp_path: Path) -> None:
|
||||||
|
result_id, frame_id = _result(tmp_path)
|
||||||
|
router = build_l31_pointpillars_ravnoves_router(
|
||||||
|
root_provider=lambda: tmp_path,
|
||||||
|
)
|
||||||
|
frame_route = _endpoint(
|
||||||
|
router,
|
||||||
|
(
|
||||||
|
"/api/v1/laboratory/l31/pointpillars-ravnoves/"
|
||||||
|
"{result_id}/frames/{frame_id}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
(tmp_path / result_id / "visual-frames" / f"{frame_id}.json").write_text(
|
||||||
|
"{}",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
with raises(Exception) as caught:
|
||||||
|
frame_route( # type: ignore[operator]
|
||||||
|
result_id=result_id,
|
||||||
|
frame_id=frame_id,
|
||||||
|
)
|
||||||
|
assert getattr(caught.value, "status_code", None) == 404
|
||||||
Reference in New Issue
Block a user