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
|
||||
|
||||
> Контур размещения: это отдельный публичный benchmark на KITTI. Он сохранён
|
||||
> для проверки измерителя, box-truth и cross-domain ideal/reference path, но не
|
||||
> является основной лабораторной работой нашего сканера. Реальный прогон
|
||||
> RAVNOVES00 оформлен отдельно как `L3.1`; см.
|
||||
> `L31_POINTPILLARS_RAVNOVES_REPORT_2026-07-31.md`.
|
||||
|
||||
## Decision
|
||||
|
||||
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
|
||||
through the already proven canonical Triton seam.
|
||||
5. Require a useful public box-truth result before K1 accuracy language.
|
||||
6. Run `RAVNOVES01` as a K1 representation/stability transfer after the public
|
||||
candidate is useful. Because that session has no camera and no independent
|
||||
3D cuboids, it can prove deterministic replay, output stability, latency,
|
||||
queue/drop behavior and visual plausibility, but not detector accuracy.
|
||||
6. Keep the public KITTI benchmark outside the primary scanner profile.
|
||||
RAVNOVES transfer is now the separate `L3.1` laboratory work; its completed
|
||||
baseline and limitations are recorded in
|
||||
`L31_POINTPILLARS_RAVNOVES_REPORT_2026-07-31.md`.
|
||||
|
||||
The adaptation task is a new admission. This report does not authorize a
|
||||
training container, a second serving stack, K1 quality claims or navigation
|
||||
|
||||
@@ -26,6 +26,9 @@ _SOURCE_FILES = {
|
||||
"runtime/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": (
|
||||
"src/k1link/datasets/kitti_3d_admission.py"
|
||||
),
|
||||
@@ -41,6 +44,9 @@ _SOURCE_FILES = {
|
||||
"runtime/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": (
|
||||
"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 k1link.web.l3_pointpillars_visual_api import latest_l3_visual_identity
|
||||
|
||||
from k1link.compute.e31_source_qualification import (
|
||||
E31SourceQualification,
|
||||
E31SourceQualificationError,
|
||||
@@ -57,6 +55,8 @@ from k1link.compute.e40_perception_product_gate import (
|
||||
E40PerceptionProductGateError,
|
||||
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 = (
|
||||
"missioncore.laboratory-advanced-catalog/v1"
|
||||
@@ -828,6 +828,7 @@ def build_advanced_laboratory_router(
|
||||
e39_root_provider: RootProvider = lambda: None,
|
||||
e40_root_provider: RootProvider = lambda: None,
|
||||
l3_visual_root_provider: RootProvider = lambda: None,
|
||||
l31_ravnoves_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
|
||||
|
||||
@@ -909,6 +910,15 @@ def build_advanced_laboratory_router(
|
||||
"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
|
||||
|
||||
@router.get("/e31/results")
|
||||
|
||||
+24
-3
@@ -34,9 +34,6 @@ from k1link.sessions import (
|
||||
SessionStore,
|
||||
)
|
||||
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.compute_contour_api import build_compute_contour_router
|
||||
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.e40_case_review_api import build_e40_case_review_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.lidar_api import build_lidar_router
|
||||
from k1link.web.lidar_local_surface_service import K1LocalSurfaceReadService
|
||||
@@ -609,6 +612,13 @@ app.include_router(
|
||||
/ "l3"
|
||||
/ "visual-audits"
|
||||
),
|
||||
l31_ravnoves_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "pointpillars-ravnoves"
|
||||
),
|
||||
)
|
||||
)
|
||||
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(
|
||||
build_e30_review_router(
|
||||
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(
|
||||
tmp_path: Path,
|
||||
) -> 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