feat(perception): qualify lossless lidar observations
This commit is contained in:
@@ -40,6 +40,18 @@ from .e33_worker_shadow import (
|
||||
read_e33_worker_shadow_result,
|
||||
run_e33_worker_shadow,
|
||||
)
|
||||
from .e51_motion_semantic_qualification import (
|
||||
E51_FRAME_SCHEMA,
|
||||
E51_PROFILE_SCHEMA,
|
||||
E51_REPORT_SCHEMA,
|
||||
E51_RESULT_SCHEMA,
|
||||
E51_SIGNAL_SCHEMA,
|
||||
E51MotionSemanticError,
|
||||
E51MotionSemanticResult,
|
||||
build_e51_motion_semantic_qualification,
|
||||
derive_motion_semantic_signal,
|
||||
read_e51_motion_semantic_qualification,
|
||||
)
|
||||
from .evaluation_pack import (
|
||||
ANNOTATION_CONTRACT_SCHEMA,
|
||||
EVALUATION_PACK_SCHEMA,
|
||||
@@ -397,6 +409,16 @@ __all__ = [
|
||||
"E33WorkerShadowResult",
|
||||
"read_e33_worker_shadow_result",
|
||||
"run_e33_worker_shadow",
|
||||
"E51_FRAME_SCHEMA",
|
||||
"E51_PROFILE_SCHEMA",
|
||||
"E51_REPORT_SCHEMA",
|
||||
"E51_RESULT_SCHEMA",
|
||||
"E51_SIGNAL_SCHEMA",
|
||||
"E51MotionSemanticError",
|
||||
"E51MotionSemanticResult",
|
||||
"build_e51_motion_semantic_qualification",
|
||||
"derive_motion_semantic_signal",
|
||||
"read_e51_motion_semantic_qualification",
|
||||
"build_lidar_ground_annotation_template",
|
||||
"build_lidar_ground_benchmark",
|
||||
"build_k1_local_surface",
|
||||
|
||||
@@ -0,0 +1,940 @@
|
||||
"""Immutable E51 qualification of motion, proximity and semantic evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import resource
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from itertools import zip_longest
|
||||
from pathlib import Path
|
||||
from typing import Any, Final, TextIO, cast
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .e32_track_geometry_replay import read_e32_track_geometry_replay
|
||||
from .e34_temporal_occupied_replay import (
|
||||
E34TemporalOccupiedReplay,
|
||||
read_e34_temporal_occupied_replay,
|
||||
)
|
||||
from .lidar_field_review import E10LidarFieldSource
|
||||
|
||||
E51_PROFILE_SCHEMA: Final = "missioncore.e51-motion-semantic-profile/v1"
|
||||
E51_RESULT_SCHEMA: Final = "missioncore.e51-motion-semantic-result/v1"
|
||||
E51_FRAME_SCHEMA: Final = "missioncore.e51-motion-semantic-frame/v1"
|
||||
E51_SIGNAL_SCHEMA: Final = "missioncore.e51-motion-semantic-signal/v1"
|
||||
E51_REPORT_SCHEMA: Final = "missioncore.e51-motion-semantic-report/v1"
|
||||
|
||||
E51_FRAMES_NAME: Final = "motion-semantic-frames.jsonl"
|
||||
E51_REPORT_NAME: Final = "run-report.json"
|
||||
E51_MANIFEST_NAME: Final = "manifest.json"
|
||||
|
||||
_RESULT_ID = re.compile(r"^e51-motion-semantic-[a-f0-9]{64}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
|
||||
|
||||
class E51MotionSemanticError(RuntimeError):
|
||||
"""An E51 profile, source, replay or immutable result is invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class E51MotionSemanticResult:
|
||||
"""One validated immutable E51 diagnostic result."""
|
||||
|
||||
result_root: Path
|
||||
result_id: str
|
||||
manifest: dict[str, Any]
|
||||
report: dict[str, Any]
|
||||
|
||||
@property
|
||||
def accepted(self) -> bool:
|
||||
return bool(_object(self.report.get("acceptance"), "E51 acceptance")["accepted"])
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _Profile:
|
||||
raw: dict[str, Any]
|
||||
expected_e32_result_id: str
|
||||
expected_e34_result_id: str
|
||||
expected_source_pack_id: str
|
||||
minimum_motion_observations: int
|
||||
minimum_motion_span_seconds: float
|
||||
minimum_motion_displacement_m: float
|
||||
minimum_motion_speed_mps: float
|
||||
maximum_motion_speed_mps: float
|
||||
proximity_threshold_m: float
|
||||
maximum_signals_per_frame: int
|
||||
maximum_latency_p95_ms: float
|
||||
maximum_rss_growth_mib: float
|
||||
maximum_lidar_camera_age_p95_ms: float
|
||||
maximum_pose_age_p95_ms: float
|
||||
|
||||
|
||||
def build_e51_motion_semantic_qualification(
|
||||
*,
|
||||
e32_result_root: Path,
|
||||
e34_result_root: Path,
|
||||
e10_source_root: Path,
|
||||
profile_path: Path,
|
||||
output_root: Path,
|
||||
) -> E51MotionSemanticResult:
|
||||
"""Build or verify the bounded E51 diagnostic derivative."""
|
||||
|
||||
profile = _read_profile(profile_path)
|
||||
e32 = read_e32_track_geometry_replay(e32_result_root)
|
||||
e34 = read_e34_temporal_occupied_replay(e34_result_root)
|
||||
source = E10LidarFieldSource(e10_source_root)
|
||||
try:
|
||||
_validate_bindings(profile=profile, e32=e32, e34=e34, source=source)
|
||||
e32_artifacts = _verified_artifacts(
|
||||
e32.result_root,
|
||||
e32.manifest.get("artifacts"),
|
||||
key="role",
|
||||
)
|
||||
e34_artifacts = _verified_artifacts(
|
||||
e34.result_root,
|
||||
e34.manifest.get("artifacts"),
|
||||
key="kind",
|
||||
)
|
||||
source_artifact = _object(
|
||||
source.manifest.get("artifact"),
|
||||
"E51 E10 source artifact",
|
||||
)
|
||||
upstream_before = {
|
||||
"e32": _artifact_identity(e32_artifacts),
|
||||
"e34": _artifact_identity(e34_artifacts),
|
||||
"e10": {
|
||||
"lidar-pack": {
|
||||
"byte_length": source_artifact["byte_length"],
|
||||
"sha256": source_artifact["sha256"],
|
||||
}
|
||||
},
|
||||
}
|
||||
identity = {
|
||||
"schema_version": E51_RESULT_SCHEMA,
|
||||
"profile": profile.raw,
|
||||
"profile_sha256": _sha256(profile_path.resolve(strict=True)),
|
||||
"source_session_id": source.identity["session_id"],
|
||||
"frame_count": e32.manifest["identity"]["frame_count"],
|
||||
"e32_result_id": e32.result_id,
|
||||
"e32_identity_sha256": e32.manifest["identity_sha256"],
|
||||
"e34_result_id": e34.result_id,
|
||||
"e34_identity_sha256": e34.manifest["identity_sha256"],
|
||||
"source_pack_id": source.pack_id,
|
||||
"source_pack_identity_sha256": source.manifest["identity_sha256"],
|
||||
"upstream_artifacts": upstream_before,
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
"policy": {
|
||||
"dynamic_class_available": False,
|
||||
"collision_state_available": False,
|
||||
"free_space_available": False,
|
||||
"absence_of_points_means_free": False,
|
||||
"persistent_reconstruction_mutated": False,
|
||||
},
|
||||
"authority": _authority(),
|
||||
}
|
||||
identity_sha256 = hashlib.sha256(_canonical_json(identity)).hexdigest()
|
||||
result_id = f"e51-motion-semantic-{identity_sha256}"
|
||||
destination = output_root.expanduser().absolute()
|
||||
destination.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
result_root = destination / result_id
|
||||
if result_root.exists():
|
||||
return read_e51_motion_semantic_qualification(result_root)
|
||||
|
||||
staging = destination / f".{result_id}.{os.getpid()}.incomplete"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
report = _run_qualification(
|
||||
staging=staging,
|
||||
result_id=result_id,
|
||||
e32_frames=e32_artifacts["track-geometry-frames"],
|
||||
e34=e34,
|
||||
e34_frames=e34_artifacts["temporal-occupied-frames"],
|
||||
source=source,
|
||||
profile=profile,
|
||||
)
|
||||
upstream_after = {
|
||||
"e32": _artifact_identity(
|
||||
_verified_artifacts(
|
||||
e32.result_root,
|
||||
e32.manifest.get("artifacts"),
|
||||
key="role",
|
||||
)
|
||||
),
|
||||
"e34": _artifact_identity(
|
||||
_verified_artifacts(
|
||||
e34.result_root,
|
||||
e34.manifest.get("artifacts"),
|
||||
key="kind",
|
||||
)
|
||||
),
|
||||
"e10": {
|
||||
"lidar-pack": {
|
||||
"byte_length": source_artifact["byte_length"],
|
||||
"sha256": _sha256(source.root / str(source_artifact["path"])),
|
||||
}
|
||||
},
|
||||
}
|
||||
report["acceptance"]["upstream_unchanged"] = (
|
||||
upstream_after == upstream_before
|
||||
)
|
||||
checks = cast(dict[str, bool], report["acceptance"]["checks"])
|
||||
checks["upstream_unchanged"] = upstream_after == upstream_before
|
||||
report["acceptance"]["accepted"] = all(checks.values())
|
||||
_write_json(staging / E51_REPORT_NAME, report)
|
||||
artifacts = [
|
||||
_artifact(staging / E51_FRAMES_NAME, "motion-semantic-frames"),
|
||||
_artifact(staging / E51_REPORT_NAME, "run-report"),
|
||||
]
|
||||
manifest = {
|
||||
"schema_version": E51_RESULT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"identity_sha256": identity_sha256,
|
||||
"identity": identity,
|
||||
"created_at_utc": datetime.now(UTC)
|
||||
.isoformat(timespec="milliseconds")
|
||||
.replace("+00:00", "Z"),
|
||||
"classification": "private-diagnostic-derivative",
|
||||
"ground_truth": False,
|
||||
"artifacts": artifacts,
|
||||
}
|
||||
_write_json(staging / E51_MANIFEST_NAME, manifest)
|
||||
os.replace(staging, result_root)
|
||||
except BaseException:
|
||||
shutil.rmtree(staging, ignore_errors=True)
|
||||
raise
|
||||
return read_e51_motion_semantic_qualification(result_root)
|
||||
finally:
|
||||
source.close()
|
||||
|
||||
|
||||
def read_e51_motion_semantic_qualification(
|
||||
result_root: Path,
|
||||
) -> E51MotionSemanticResult:
|
||||
"""Read and verify one immutable E51 result."""
|
||||
|
||||
root = result_root.expanduser().absolute().resolve(strict=True)
|
||||
if not root.is_dir() or _RESULT_ID.fullmatch(root.name) is None:
|
||||
raise E51MotionSemanticError("E51 result id is invalid")
|
||||
manifest = _read_json(root / E51_MANIFEST_NAME)
|
||||
identity = _object(manifest.get("identity"), "E51 identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != E51_RESULT_SCHEMA
|
||||
or identity.get("schema_version") != E51_RESULT_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or _SHA256.fullmatch(identity_sha256) is None
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or root.name != f"e51-motion-semantic-{identity_sha256}"
|
||||
or manifest.get("result_id") != root.name
|
||||
):
|
||||
raise E51MotionSemanticError("E51 result identity is invalid")
|
||||
artifacts = _verified_artifacts(root, manifest.get("artifacts"), key="kind")
|
||||
required = {"motion-semantic-frames", "run-report"}
|
||||
if set(artifacts) != required:
|
||||
raise E51MotionSemanticError("E51 artifact set is invalid")
|
||||
report = _read_json(artifacts["run-report"])
|
||||
acceptance = _object(report.get("acceptance"), "E51 acceptance")
|
||||
checks = _object(acceptance.get("checks"), "E51 acceptance checks")
|
||||
if (
|
||||
report.get("schema_version") != E51_REPORT_SCHEMA
|
||||
or report.get("result_id") != root.name
|
||||
or acceptance.get("accepted") is not all(value is True for value in checks.values())
|
||||
or report.get("authority") != _authority()
|
||||
):
|
||||
raise E51MotionSemanticError("E51 report is invalid")
|
||||
return E51MotionSemanticResult(
|
||||
result_root=root,
|
||||
result_id=root.name,
|
||||
manifest=manifest,
|
||||
report=report,
|
||||
)
|
||||
|
||||
|
||||
def derive_motion_semantic_signal(
|
||||
component: dict[str, Any],
|
||||
geometry: dict[str, Any] | None,
|
||||
*,
|
||||
map_frame_jump_candidate: bool,
|
||||
profile: dict[str, float | int],
|
||||
) -> dict[str, Any] | None:
|
||||
"""Derive one conservative diagnostic signal from existing evidence."""
|
||||
|
||||
state = component.get("state")
|
||||
if state not in {"current", "held"}:
|
||||
raise E51MotionSemanticError("E51 component freshness is invalid")
|
||||
history = _list(component.get("history_tail"), "E51 component history")
|
||||
motion = _motion_metrics(
|
||||
history,
|
||||
map_frame_jump_candidate=map_frame_jump_candidate,
|
||||
minimum_observations=int(profile["minimum_motion_observations"]),
|
||||
minimum_span_seconds=float(profile["minimum_motion_span_seconds"]),
|
||||
minimum_displacement_m=float(profile["minimum_motion_displacement_m"]),
|
||||
minimum_speed_mps=float(profile["minimum_motion_speed_mps"]),
|
||||
maximum_speed_mps=float(profile["maximum_motion_speed_mps"]),
|
||||
)
|
||||
evidence_state = (
|
||||
str(geometry.get("evidence_state"))
|
||||
if geometry is not None
|
||||
else "held-temporal-evidence"
|
||||
)
|
||||
reason_codes = (
|
||||
[
|
||||
str(value)
|
||||
for value in _list(
|
||||
geometry.get("reason_codes"),
|
||||
"E51 geometry reason codes",
|
||||
)
|
||||
]
|
||||
if geometry is not None
|
||||
else []
|
||||
)
|
||||
conflict = evidence_state == "conflict" or any(
|
||||
"conflict" in value or "collision" in value for value in reason_codes
|
||||
)
|
||||
semantic = geometry.get("semantic") if geometry is not None else None
|
||||
if semantic is None:
|
||||
provenance = _object(
|
||||
component.get("semantic_provenance"),
|
||||
"E51 semantic provenance",
|
||||
)
|
||||
labels = _list(provenance.get("labels"), "E51 semantic labels")
|
||||
track_ids = _list(provenance.get("track_ids"), "E51 semantic track ids")
|
||||
if labels or track_ids or provenance.get("owner") is not None:
|
||||
semantic = {
|
||||
"owner": provenance.get("owner"),
|
||||
"labels": labels,
|
||||
"track_ids": track_ids,
|
||||
"source": "e34-held-semantic-provenance",
|
||||
}
|
||||
range_m = geometry.get("range_m") if geometry is not None else None
|
||||
range_value = (
|
||||
float(range_m)
|
||||
if isinstance(range_m, int | float) and math.isfinite(float(range_m))
|
||||
else None
|
||||
)
|
||||
proximity_candidate = (
|
||||
state == "current"
|
||||
and range_value is not None
|
||||
and range_value <= float(profile["proximity_threshold_m"])
|
||||
)
|
||||
if (
|
||||
not motion["candidate"]
|
||||
and not proximity_candidate
|
||||
and semantic is None
|
||||
and not conflict
|
||||
):
|
||||
return None
|
||||
return {
|
||||
"schema_version": E51_SIGNAL_SCHEMA,
|
||||
"temporal_id": component["temporal_id"],
|
||||
"source_owner_key": component["source_owner_key"],
|
||||
"owner_kind": component["owner_kind"],
|
||||
"freshness": {
|
||||
"state": state,
|
||||
"age_seconds": component["last_observed_age_seconds"],
|
||||
"current_hit_backed": state == "current",
|
||||
},
|
||||
"semantic": {
|
||||
"available": semantic is not None,
|
||||
"value": semantic,
|
||||
"confidence": {
|
||||
"available": False,
|
||||
"value": None,
|
||||
"reason": "upstream-contract-has-no-numeric-confidence",
|
||||
},
|
||||
},
|
||||
"evidence": {
|
||||
"state": evidence_state,
|
||||
"conflict": conflict,
|
||||
"reason_codes": reason_codes,
|
||||
},
|
||||
"motion": motion,
|
||||
"proximity": {
|
||||
"candidate": proximity_candidate,
|
||||
"range_m": range_value,
|
||||
"threshold_m": float(profile["proximity_threshold_m"]),
|
||||
"classification": "diagnostic-near-occupied-candidate",
|
||||
},
|
||||
"collision": {
|
||||
"state": "unavailable",
|
||||
"reason": "vehicle-body-and-lidar-mount-geometry-not-bound",
|
||||
},
|
||||
"authority": _authority(),
|
||||
}
|
||||
|
||||
|
||||
def _run_qualification(
|
||||
*,
|
||||
staging: Path,
|
||||
result_id: str,
|
||||
e32_frames: Path,
|
||||
e34: E34TemporalOccupiedReplay,
|
||||
e34_frames: Path,
|
||||
source: E10LidarFieldSource,
|
||||
profile: _Profile,
|
||||
) -> dict[str, Any]:
|
||||
started = time.perf_counter()
|
||||
rss_start = _process_peak_rss_mib()
|
||||
frame_latencies_ms: list[float] = []
|
||||
signal_counts = {
|
||||
"total": 0,
|
||||
"motion_candidates": 0,
|
||||
"proximity_candidates": 0,
|
||||
"semantic_available": 0,
|
||||
"conflicts": 0,
|
||||
"current": 0,
|
||||
"held": 0,
|
||||
}
|
||||
frame_count = 0
|
||||
accepted_current_point_rows = 0
|
||||
map_frame_jump_candidates = 0
|
||||
maximum_signals_observed = 0
|
||||
frames_path = staging / E51_FRAMES_NAME
|
||||
with (
|
||||
e32_frames.open("r", encoding="utf-8") as e32_stream,
|
||||
e34_frames.open("r", encoding="utf-8") as e34_stream,
|
||||
frames_path.open("x", encoding="utf-8") as output,
|
||||
):
|
||||
for e32_line, e34_line in zip_longest(e32_stream, e34_stream):
|
||||
frame_started = time.perf_counter()
|
||||
if e32_line is None or e34_line is None:
|
||||
raise E51MotionSemanticError("E51 upstream frame counts differ")
|
||||
e32_frame = _parse_json_line(e32_line, "E51 E32 frame")
|
||||
e34_frame = _parse_json_line(e34_line, "E51 E34 frame")
|
||||
_validate_frame_pair(e32_frame, e34_frame, frame_count)
|
||||
geometries = {
|
||||
str(geometry["owner_key"]): geometry
|
||||
for geometry in _object_list(
|
||||
e32_frame.get("geometries"),
|
||||
"E51 E32 geometries",
|
||||
)
|
||||
}
|
||||
jump = _object(
|
||||
e34_frame.get("map_frame_jump"),
|
||||
"E51 map-frame jump",
|
||||
)
|
||||
jump_candidate = jump.get("candidate") is True
|
||||
map_frame_jump_candidates += int(jump_candidate)
|
||||
signals: list[dict[str, Any]] = []
|
||||
for component in [
|
||||
*_object_list(e34_frame.get("current"), "E51 current components"),
|
||||
*_object_list(e34_frame.get("held"), "E51 held components"),
|
||||
]:
|
||||
owner_key = str(component.get("source_owner_key"))
|
||||
signal = derive_motion_semantic_signal(
|
||||
component,
|
||||
geometries.get(owner_key),
|
||||
map_frame_jump_candidate=jump_candidate,
|
||||
profile={
|
||||
"minimum_motion_observations": profile.minimum_motion_observations,
|
||||
"minimum_motion_span_seconds": profile.minimum_motion_span_seconds,
|
||||
"minimum_motion_displacement_m": profile.minimum_motion_displacement_m,
|
||||
"minimum_motion_speed_mps": profile.minimum_motion_speed_mps,
|
||||
"maximum_motion_speed_mps": profile.maximum_motion_speed_mps,
|
||||
"proximity_threshold_m": profile.proximity_threshold_m,
|
||||
},
|
||||
)
|
||||
if signal is not None:
|
||||
signals.append(signal)
|
||||
maximum_signals_observed = max(maximum_signals_observed, len(signals))
|
||||
if len(signals) > profile.maximum_signals_per_frame:
|
||||
raise E51MotionSemanticError(
|
||||
"E51 signal count exceeds the bounded profile"
|
||||
)
|
||||
input_summary = _object(e34_frame.get("input"), "E51 E34 input")
|
||||
current_point_rows = _nonnegative_int(
|
||||
input_summary.get("accepted_current_point_rows"),
|
||||
"E51 accepted current point rows",
|
||||
)
|
||||
accepted_current_point_rows += current_point_rows
|
||||
for signal in signals:
|
||||
_count_signal(signal_counts, signal)
|
||||
record = {
|
||||
"schema_version": E51_FRAME_SCHEMA,
|
||||
"frame_index": frame_count,
|
||||
"source_frame_index": e34_frame["source_frame_index"],
|
||||
"session_seconds": e34_frame["session_seconds"],
|
||||
"source_available": e34_frame["source_available"],
|
||||
"layer_state": e34_frame["layer_state"],
|
||||
"accepted_current_point_rows": current_point_rows,
|
||||
"signal_count": len(signals),
|
||||
"signals": signals,
|
||||
"policy": {
|
||||
"dynamic_class_available": False,
|
||||
"collision_state_available": False,
|
||||
"free_space_available": False,
|
||||
"absence_of_points_means_free": False,
|
||||
"persistent_reconstruction_mutated": False,
|
||||
},
|
||||
"authority": _authority(),
|
||||
}
|
||||
_write_json_line(output, record)
|
||||
frame_count += 1
|
||||
frame_latencies_ms.append(
|
||||
(time.perf_counter() - frame_started) * 1_000.0
|
||||
)
|
||||
|
||||
rss_end = _process_peak_rss_mib()
|
||||
lidar_age = _finite_abs(source.arrays["lidar_camera_delta_ms"])
|
||||
pose_age = _finite_abs(source.arrays["pose_point_delta_ms"])
|
||||
latency = _distribution(np.asarray(frame_latencies_ms, dtype=np.float64))
|
||||
lidar_age_report = _distribution(lidar_age)
|
||||
pose_age_report = _distribution(pose_age)
|
||||
e34_occupancy = _object(
|
||||
_object(e34.report.get("metrics"), "E51 E34 metrics").get("occupancy"),
|
||||
"E51 E34 occupancy metrics",
|
||||
)
|
||||
expected_point_rows = _nonnegative_int(
|
||||
e34_occupancy.get("e34_consumed_current_point_rows"),
|
||||
"E51 E34 consumed point rows",
|
||||
)
|
||||
rss_growth = max(0.0, rss_end - rss_start)
|
||||
checks = {
|
||||
"complete_frame_accounting": frame_count
|
||||
== _nonnegative_int(
|
||||
e34.manifest["identity"].get("frame_count"),
|
||||
"E51 E34 frame count",
|
||||
),
|
||||
"map_frame_jump_candidates_zero": map_frame_jump_candidates == 0,
|
||||
"current_obstacle_rows_preserved": (
|
||||
accepted_current_point_rows == expected_point_rows
|
||||
),
|
||||
"latency_p95_within_gate": _required_float(latency["p95"])
|
||||
<= profile.maximum_latency_p95_ms,
|
||||
"rss_growth_within_gate": rss_growth <= profile.maximum_rss_growth_mib,
|
||||
"lidar_camera_age_p95_within_gate": _required_float(
|
||||
lidar_age_report["p95"]
|
||||
)
|
||||
<= profile.maximum_lidar_camera_age_p95_ms,
|
||||
"pose_age_p95_within_gate": _required_float(pose_age_report["p95"])
|
||||
<= profile.maximum_pose_age_p95_ms,
|
||||
"bounded_signal_state": maximum_signals_observed
|
||||
<= profile.maximum_signals_per_frame,
|
||||
"free_space_not_published": True,
|
||||
"dynamic_class_not_invented": True,
|
||||
"collision_state_not_invented": True,
|
||||
"upstream_unchanged": False,
|
||||
}
|
||||
return {
|
||||
"schema_version": E51_REPORT_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"status": "diagnostic-only",
|
||||
"ground_truth": False,
|
||||
"metrics": {
|
||||
"frames": {
|
||||
"processed": frame_count,
|
||||
"map_frame_jump_candidates": map_frame_jump_candidates,
|
||||
},
|
||||
"signals": {
|
||||
**signal_counts,
|
||||
"maximum_per_frame": maximum_signals_observed,
|
||||
},
|
||||
"obstacle_preservation": {
|
||||
"e34_consumed_current_point_rows": expected_point_rows,
|
||||
"e51_observed_current_point_rows": accepted_current_point_rows,
|
||||
"exact": accepted_current_point_rows == expected_point_rows,
|
||||
"persistent_reconstruction_mutated": False,
|
||||
},
|
||||
"runtime": {
|
||||
"elapsed_ms": (time.perf_counter() - started) * 1_000.0,
|
||||
"frame_processing_ms": latency,
|
||||
"process_peak_rss_start_mib": rss_start,
|
||||
"process_peak_rss_end_mib": rss_end,
|
||||
"process_peak_rss_growth_mib": rss_growth,
|
||||
"rss_measurement": "process-peak-rss",
|
||||
},
|
||||
"point_age_ms": {
|
||||
"lidar_to_camera": lidar_age_report,
|
||||
"pose_to_lidar": pose_age_report,
|
||||
"basis": "accepted-e10-nearest-host-arrival-best-effort",
|
||||
},
|
||||
},
|
||||
"semantic_contract": {
|
||||
"camera_owns_semantics": True,
|
||||
"numeric_confidence_available": False,
|
||||
"freshness_explicit": True,
|
||||
"conflict_explicit": True,
|
||||
},
|
||||
"motion_contract": {
|
||||
"classification": "diagnostic-motion-candidate",
|
||||
"dynamic_class_available": False,
|
||||
"map_frame_jump_rejected": True,
|
||||
},
|
||||
"proximity_contract": {
|
||||
"classification": "diagnostic-near-occupied-candidate",
|
||||
"collision_state_available": False,
|
||||
"reason": "vehicle-body-and-lidar-mount-geometry-not-bound",
|
||||
},
|
||||
"acceptance": {
|
||||
"accepted": False,
|
||||
"upstream_unchanged": False,
|
||||
"checks": checks,
|
||||
},
|
||||
"authority": _authority(),
|
||||
}
|
||||
|
||||
|
||||
def _motion_metrics(
|
||||
history: list[object],
|
||||
*,
|
||||
map_frame_jump_candidate: bool,
|
||||
minimum_observations: int,
|
||||
minimum_span_seconds: float,
|
||||
minimum_displacement_m: float,
|
||||
minimum_speed_mps: float,
|
||||
maximum_speed_mps: float,
|
||||
) -> dict[str, Any]:
|
||||
points = [_object(value, "E51 history observation") for value in history]
|
||||
if len(points) < 2:
|
||||
span_seconds = 0.0
|
||||
displacement_m = 0.0
|
||||
speed_mps = 0.0
|
||||
else:
|
||||
first = points[0]
|
||||
last = points[-1]
|
||||
first_xyz = _xyz(first.get("centroid_map_xyz_m"))
|
||||
last_xyz = _xyz(last.get("centroid_map_xyz_m"))
|
||||
span_seconds = float(last["session_seconds"]) - float(first["session_seconds"])
|
||||
displacement_m = math.dist(first_xyz, last_xyz)
|
||||
speed_mps = displacement_m / span_seconds if span_seconds > 0.0 else 0.0
|
||||
candidate = (
|
||||
not map_frame_jump_candidate
|
||||
and len(points) >= minimum_observations
|
||||
and span_seconds >= minimum_span_seconds
|
||||
and displacement_m >= minimum_displacement_m
|
||||
and minimum_speed_mps <= speed_mps <= maximum_speed_mps
|
||||
)
|
||||
return {
|
||||
"candidate": candidate,
|
||||
"classification": "diagnostic-motion-candidate",
|
||||
"observation_count": len(points),
|
||||
"span_seconds": span_seconds,
|
||||
"displacement_m": displacement_m,
|
||||
"speed_mps": speed_mps,
|
||||
"map_frame_jump_rejected": map_frame_jump_candidate,
|
||||
"dynamic_class_available": False,
|
||||
}
|
||||
|
||||
|
||||
def _validate_bindings(
|
||||
*,
|
||||
profile: _Profile,
|
||||
e32: Any,
|
||||
e34: E34TemporalOccupiedReplay,
|
||||
source: E10LidarFieldSource,
|
||||
) -> None:
|
||||
e34_identity = _object(e34.manifest.get("identity"), "E51 E34 identity")
|
||||
if (
|
||||
e32.result_id != profile.expected_e32_result_id
|
||||
or e34.result_id != profile.expected_e34_result_id
|
||||
or source.pack_id != profile.expected_source_pack_id
|
||||
or e34_identity.get("e32_result_id") != e32.result_id
|
||||
or e34_identity.get("source_session_id") != source.identity.get("session_id")
|
||||
or not e34.accepted
|
||||
):
|
||||
raise E51MotionSemanticError("E51 upstream binding is invalid")
|
||||
|
||||
|
||||
def _validate_frame_pair(
|
||||
e32_frame: dict[str, Any],
|
||||
e34_frame: dict[str, Any],
|
||||
expected_index: int,
|
||||
) -> None:
|
||||
e32_seconds = e32_frame.get("session_seconds")
|
||||
e34_seconds = e34_frame.get("session_seconds")
|
||||
if (
|
||||
e32_frame.get("frame_index") != expected_index
|
||||
or e34_frame.get("frame_index") != expected_index
|
||||
or not isinstance(e32_seconds, int | float)
|
||||
or not isinstance(e34_seconds, int | float)
|
||||
or abs(float(e32_seconds) - float(e34_seconds)) > 1e-9
|
||||
or e32_frame.get("source_frame_index")
|
||||
!= e34_frame.get("source_frame_index")
|
||||
):
|
||||
raise E51MotionSemanticError("E51 upstream frame alignment is invalid")
|
||||
|
||||
|
||||
def _read_profile(path: Path) -> _Profile:
|
||||
raw = _read_json(path.expanduser().resolve(strict=True))
|
||||
motion = _object(raw.get("motion"), "E51 motion profile")
|
||||
proximity = _object(raw.get("proximity"), "E51 proximity profile")
|
||||
acceptance = _object(raw.get("acceptance"), "E51 acceptance profile")
|
||||
expected = _object(raw.get("expected"), "E51 expected sources")
|
||||
profile = _Profile(
|
||||
raw=raw,
|
||||
expected_e32_result_id=_required_string(expected.get("e32_result_id")),
|
||||
expected_e34_result_id=_required_string(expected.get("e34_result_id")),
|
||||
expected_source_pack_id=_required_string(expected.get("source_pack_id")),
|
||||
minimum_motion_observations=_positive_int(
|
||||
motion.get("minimum_observations")
|
||||
),
|
||||
minimum_motion_span_seconds=_positive_float(
|
||||
motion.get("minimum_span_seconds")
|
||||
),
|
||||
minimum_motion_displacement_m=_positive_float(
|
||||
motion.get("minimum_displacement_m")
|
||||
),
|
||||
minimum_motion_speed_mps=_positive_float(
|
||||
motion.get("minimum_speed_mps")
|
||||
),
|
||||
maximum_motion_speed_mps=_positive_float(
|
||||
motion.get("maximum_speed_mps")
|
||||
),
|
||||
proximity_threshold_m=_positive_float(
|
||||
proximity.get("threshold_m")
|
||||
),
|
||||
maximum_signals_per_frame=_positive_int(
|
||||
acceptance.get("maximum_signals_per_frame")
|
||||
),
|
||||
maximum_latency_p95_ms=_positive_float(
|
||||
acceptance.get("maximum_latency_p95_ms")
|
||||
),
|
||||
maximum_rss_growth_mib=_positive_float(
|
||||
acceptance.get("maximum_rss_growth_mib")
|
||||
),
|
||||
maximum_lidar_camera_age_p95_ms=_positive_float(
|
||||
acceptance.get("maximum_lidar_camera_age_p95_ms")
|
||||
),
|
||||
maximum_pose_age_p95_ms=_positive_float(
|
||||
acceptance.get("maximum_pose_age_p95_ms")
|
||||
),
|
||||
)
|
||||
if (
|
||||
raw.get("schema_version") != E51_PROFILE_SCHEMA
|
||||
or raw.get("profile_id")
|
||||
!= "e51-motion-proximity-semantic-qualification/v1"
|
||||
or profile.maximum_motion_speed_mps <= profile.minimum_motion_speed_mps
|
||||
):
|
||||
raise E51MotionSemanticError("E51 profile is invalid")
|
||||
return profile
|
||||
|
||||
|
||||
def _count_signal(counts: dict[str, int], signal: dict[str, Any]) -> None:
|
||||
counts["total"] += 1
|
||||
freshness = _object(signal["freshness"], "E51 signal freshness")
|
||||
counts[str(freshness["state"])] += 1
|
||||
if _object(signal["motion"], "E51 signal motion")["candidate"] is True:
|
||||
counts["motion_candidates"] += 1
|
||||
if _object(signal["proximity"], "E51 signal proximity")["candidate"] is True:
|
||||
counts["proximity_candidates"] += 1
|
||||
if _object(signal["semantic"], "E51 signal semantic")["available"] is True:
|
||||
counts["semantic_available"] += 1
|
||||
if _object(signal["evidence"], "E51 signal evidence")["conflict"] is True:
|
||||
counts["conflicts"] += 1
|
||||
|
||||
|
||||
def _finite_abs(value: Any) -> np.ndarray[Any, np.dtype[np.float64]]:
|
||||
array = np.abs(np.asarray(value, dtype=np.float64))
|
||||
return array[np.isfinite(array)]
|
||||
|
||||
|
||||
def _distribution(values: np.ndarray[Any, np.dtype[np.float64]]) -> dict[str, Any]:
|
||||
finite = values[np.isfinite(values)]
|
||||
if finite.size == 0:
|
||||
return {
|
||||
"sample_count": 0,
|
||||
"minimum": None,
|
||||
"mean": None,
|
||||
"p50": None,
|
||||
"p95": None,
|
||||
"maximum": None,
|
||||
}
|
||||
return {
|
||||
"sample_count": int(finite.size),
|
||||
"minimum": float(np.min(finite)),
|
||||
"mean": float(np.mean(finite)),
|
||||
"p50": float(np.percentile(finite, 50)),
|
||||
"p95": float(np.percentile(finite, 95)),
|
||||
"maximum": float(np.max(finite)),
|
||||
}
|
||||
|
||||
|
||||
def _process_peak_rss_mib() -> float:
|
||||
value = float(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)
|
||||
divisor = 1024.0 * 1024.0 if sys.platform == "darwin" else 1024.0
|
||||
return value / divisor
|
||||
|
||||
|
||||
def _verified_artifacts(
|
||||
root: Path,
|
||||
raw: object,
|
||||
*,
|
||||
key: str,
|
||||
) -> dict[str, Path]:
|
||||
result: dict[str, Path] = {}
|
||||
for value in _list(raw, "E51 artifacts"):
|
||||
artifact = _object(value, "E51 artifact")
|
||||
name = artifact.get(key)
|
||||
relative = artifact.get("path")
|
||||
sha256 = artifact.get("sha256")
|
||||
byte_length = artifact.get("byte_length")
|
||||
if (
|
||||
not isinstance(name, str)
|
||||
or not name
|
||||
or name in result
|
||||
or not isinstance(relative, str)
|
||||
or Path(relative).name != relative
|
||||
or not isinstance(sha256, str)
|
||||
or _SHA256.fullmatch(sha256) is None
|
||||
or not isinstance(byte_length, int)
|
||||
or isinstance(byte_length, bool)
|
||||
or byte_length < 0
|
||||
):
|
||||
raise E51MotionSemanticError("E51 artifact descriptor is invalid")
|
||||
path = (root / relative).resolve(strict=True)
|
||||
if (
|
||||
path.parent != root
|
||||
or not path.is_file()
|
||||
or path.stat().st_size != byte_length
|
||||
or _sha256(path) != sha256
|
||||
):
|
||||
raise E51MotionSemanticError("E51 artifact integrity failed")
|
||||
result[name] = path
|
||||
return result
|
||||
|
||||
|
||||
def _artifact_identity(artifacts: dict[str, Path]) -> dict[str, dict[str, Any]]:
|
||||
return {
|
||||
name: {
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
for name, path in sorted(artifacts.items())
|
||||
}
|
||||
|
||||
|
||||
def _artifact(path: Path, kind: str) -> dict[str, Any]:
|
||||
return {
|
||||
"kind": kind,
|
||||
"path": path.name,
|
||||
"media_type": (
|
||||
"application/x-ndjson"
|
||||
if path.suffix == ".jsonl"
|
||||
else "application/json"
|
||||
),
|
||||
"byte_length": path.stat().st_size,
|
||||
"sha256": _sha256(path),
|
||||
}
|
||||
|
||||
|
||||
def _parse_json_line(line: str, label: str) -> dict[str, Any]:
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise E51MotionSemanticError(f"{label} is invalid JSON") from exc
|
||||
return _object(value, label)
|
||||
|
||||
|
||||
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 E51MotionSemanticError(f"E51 JSON is invalid: {path}") from exc
|
||||
return _object(value, f"E51 JSON {path.name}")
|
||||
|
||||
|
||||
def _write_json(path: Path, value: object) -> None:
|
||||
path.write_bytes(_canonical_json(value) + b"\n")
|
||||
|
||||
|
||||
def _write_json_line(stream: TextIO, value: object) -> None:
|
||||
stream.write(_canonical_json(value).decode("utf-8"))
|
||||
stream.write("\n")
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode("utf-8")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _authority() -> dict[str, bool]:
|
||||
return {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
}
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise E51MotionSemanticError(f"{label} must be an object")
|
||||
return cast(dict[str, Any], value)
|
||||
|
||||
|
||||
def _list(value: object, label: str) -> list[object]:
|
||||
if not isinstance(value, list):
|
||||
raise E51MotionSemanticError(f"{label} must be a list")
|
||||
return value
|
||||
|
||||
|
||||
def _object_list(value: object, label: str) -> list[dict[str, Any]]:
|
||||
return [_object(item, label) for item in _list(value, label)]
|
||||
|
||||
|
||||
def _xyz(value: object) -> tuple[float, float, float]:
|
||||
values = _list(value, "E51 centroid")
|
||||
if (
|
||||
len(values) != 3
|
||||
or not all(
|
||||
isinstance(item, int | float) and math.isfinite(float(item))
|
||||
for item in values
|
||||
)
|
||||
):
|
||||
raise E51MotionSemanticError("E51 centroid is invalid")
|
||||
numeric = cast(list[int | float], values)
|
||||
return (float(numeric[0]), float(numeric[1]), float(numeric[2]))
|
||||
|
||||
|
||||
def _required_string(value: object) -> str:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise E51MotionSemanticError("E51 required string is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _positive_int(value: object) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
|
||||
raise E51MotionSemanticError("E51 positive integer is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _nonnegative_int(value: object, label: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
||||
raise E51MotionSemanticError(f"{label} is invalid")
|
||||
return value
|
||||
|
||||
|
||||
def _positive_float(value: object) -> float:
|
||||
if (
|
||||
not isinstance(value, int | float)
|
||||
or isinstance(value, bool)
|
||||
or not math.isfinite(float(value))
|
||||
or float(value) <= 0.0
|
||||
):
|
||||
raise E51MotionSemanticError("E51 positive number is invalid")
|
||||
return float(value)
|
||||
|
||||
|
||||
def _required_float(value: object) -> float:
|
||||
if not isinstance(value, int | float) or not math.isfinite(float(value)):
|
||||
raise E51MotionSemanticError("E51 required number is invalid")
|
||||
return float(value)
|
||||
@@ -24,6 +24,7 @@ from .lidar_field_review import (
|
||||
RAVNOVES00_CENTRAL_WINDOWS,
|
||||
E10LidarFieldSource,
|
||||
)
|
||||
from .lidar_replay import LIDAR_REPLAY_PACK_SCHEMA, LidarReplayPackV2
|
||||
|
||||
K1_LOCAL_SURFACE_SCHEMA: Final = "missioncore.k1-local-surface/v1"
|
||||
K1_LOCAL_SURFACE_REPORT_SCHEMA: Final = "missioncore.k1-local-surface-report/v1"
|
||||
@@ -53,6 +54,25 @@ _LOCAL_SURFACE_ID = re.compile(r"^k1-local-surface-[a-f0-9]{64}$")
|
||||
_E10_PACK_ID = re.compile(r"^e10-lidar-pack-[a-f0-9]{64}$")
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _LocalSurfaceSourceView:
|
||||
pack_id: str
|
||||
identity_sha256: str
|
||||
artifact_sha256: str
|
||||
session_id: str
|
||||
representation: str
|
||||
schema_version: str
|
||||
intensity_available: bool
|
||||
field_retention: dict[str, object] | None
|
||||
arrays: Mapping[str, npt.NDArray[Any]]
|
||||
frame_count: int
|
||||
point_count: int
|
||||
|
||||
|
||||
LocalSurfaceSource = (
|
||||
E10LidarFieldSource | LidarReplayPackV2 | _LocalSurfaceSourceView
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class K1LocalSurfaceProfile:
|
||||
@@ -511,9 +531,11 @@ class K1LocalSurfaceV1:
|
||||
|
||||
def frame_detail(
|
||||
self,
|
||||
source: E10LidarFieldSource,
|
||||
source: LocalSurfaceSource,
|
||||
frame_index: int,
|
||||
) -> dict[str, object]:
|
||||
source_view = _local_surface_source_view(source)
|
||||
source = source_view
|
||||
_validate_source_binding(self, source)
|
||||
if not 0 <= frame_index < source.frame_count:
|
||||
raise IndexError(frame_index)
|
||||
@@ -550,7 +572,7 @@ class K1LocalSurfaceV1:
|
||||
"schema_version": K1_LOCAL_SURFACE_FRAME_SCHEMA,
|
||||
"model_id": self.model_id,
|
||||
"source_pack_id": source.pack_id,
|
||||
"session_id": source.identity["session_id"],
|
||||
"session_id": source.session_id,
|
||||
"frame_index": frame_index,
|
||||
"frame_count": source.frame_count,
|
||||
"source_frame_index": int(source.arrays["source_frame_indices"][frame_index]),
|
||||
@@ -716,7 +738,9 @@ class K1LocalSurfaceV1:
|
||||
"ground_truth": False,
|
||||
}
|
||||
|
||||
def timeline_detail(self, source: E10LidarFieldSource) -> dict[str, object]:
|
||||
def timeline_detail(self, source: LocalSurfaceSource) -> dict[str, object]:
|
||||
source_view = _local_surface_source_view(source)
|
||||
source = source_view
|
||||
_validate_source_binding(self, source)
|
||||
frame_count = source.frame_count
|
||||
if self.has_temporal_qualification:
|
||||
@@ -745,7 +769,7 @@ class K1LocalSurfaceV1:
|
||||
"schema_version": K1_LOCAL_SURFACE_TIMELINE_SCHEMA,
|
||||
"model_id": self.model_id,
|
||||
"source_pack_id": source.pack_id,
|
||||
"session_id": source.identity["session_id"],
|
||||
"session_id": source.session_id,
|
||||
"frame_count": frame_count,
|
||||
"source_frame_index": source.arrays["source_frame_indices"]
|
||||
.astype(np.int64)
|
||||
@@ -772,7 +796,9 @@ class K1LocalSurfaceV1:
|
||||
"authority": self.report["authority"],
|
||||
}
|
||||
|
||||
def review_detail(self, source: E10LidarFieldSource) -> dict[str, object]:
|
||||
def review_detail(self, source: LocalSurfaceSource) -> dict[str, object]:
|
||||
source_view = _local_surface_source_view(source)
|
||||
source = source_view
|
||||
_validate_source_binding(self, source)
|
||||
criteria = self._review_criteria()
|
||||
reason_counts = {
|
||||
@@ -788,7 +814,7 @@ class K1LocalSurfaceV1:
|
||||
"review_profile_id": K1_LOCAL_SURFACE_REVIEW_PROFILE_ID,
|
||||
"model_id": self.model_id,
|
||||
"source_pack_id": source.pack_id,
|
||||
"session_id": source.identity["session_id"],
|
||||
"session_id": source.session_id,
|
||||
"available": False,
|
||||
"criteria": criteria,
|
||||
"summary": {
|
||||
@@ -918,7 +944,7 @@ class K1LocalSurfaceV1:
|
||||
"review_profile_id": K1_LOCAL_SURFACE_REVIEW_PROFILE_ID,
|
||||
"model_id": self.model_id,
|
||||
"source_pack_id": source.pack_id,
|
||||
"session_id": source.identity["session_id"],
|
||||
"session_id": source.session_id,
|
||||
"available": True,
|
||||
"criteria": criteria,
|
||||
"summary": {
|
||||
@@ -985,7 +1011,7 @@ class K1LocalSurfaceV1:
|
||||
|
||||
|
||||
def build_k1_local_surface(
|
||||
source: E10LidarFieldSource,
|
||||
source: LocalSurfaceSource,
|
||||
output_root: Path,
|
||||
*,
|
||||
profile: K1LocalSurfaceProfile = DEFAULT_K1_LOCAL_SURFACE_PROFILE,
|
||||
@@ -995,6 +1021,8 @@ def build_k1_local_surface(
|
||||
|
||||
if not display_name.strip() or len(display_name) > 200:
|
||||
raise LidarGroundError("K1 local-surface display name is invalid")
|
||||
source_view = _local_surface_source_view(source)
|
||||
source = source_view
|
||||
started = time.perf_counter()
|
||||
frame_count = source.frame_count
|
||||
point_count = source.point_count
|
||||
@@ -1174,9 +1202,11 @@ def build_k1_local_surface(
|
||||
identity = {
|
||||
"schema_version": K1_LOCAL_SURFACE_SCHEMA,
|
||||
"source_pack_id": source.pack_id,
|
||||
"source_pack_identity_sha256": source.manifest["identity_sha256"],
|
||||
"source_artifact_sha256": source.manifest["artifact"]["sha256"],
|
||||
"session_id": source.identity["session_id"],
|
||||
"source_pack_identity_sha256": source.identity_sha256,
|
||||
"source_artifact_sha256": source.artifact_sha256,
|
||||
"source_schema_version": source.schema_version,
|
||||
"source_representation": source.representation,
|
||||
"session_id": source.session_id,
|
||||
"display_name": display_name,
|
||||
"frame_count": frame_count,
|
||||
"valid_frame_count": int(np.count_nonzero(valid)),
|
||||
@@ -1204,12 +1234,15 @@ def build_k1_local_surface(
|
||||
"schema_version": K1_LOCAL_SURFACE_REPORT_SCHEMA,
|
||||
"model_id": model_id,
|
||||
"display_name": display_name,
|
||||
"session_id": source.identity["session_id"],
|
||||
"session_id": source.session_id,
|
||||
"source_pack_id": source.pack_id,
|
||||
"status": "diagnostic-only",
|
||||
"ground_truth": False,
|
||||
"source": {
|
||||
"representation": "legacy-e10-vendor-map-with-pose",
|
||||
"representation": source.representation,
|
||||
"schema_version": source.schema_version,
|
||||
"intensity_available": source.intensity_available,
|
||||
"field_retention": source.field_retention,
|
||||
"immutable": True,
|
||||
"passive_processing_only": True,
|
||||
"firmware_or_device_commands_used": False,
|
||||
@@ -1725,7 +1758,7 @@ def _height_above_plane(
|
||||
|
||||
|
||||
def _anchors(
|
||||
source: E10LidarFieldSource,
|
||||
source: _LocalSurfaceSourceView,
|
||||
valid: npt.NDArray[np.bool_],
|
||||
) -> list[dict[str, object]]:
|
||||
source_indices = source.arrays["source_frame_indices"]
|
||||
@@ -1737,6 +1770,18 @@ def _anchors(
|
||||
for window in RAVNOVES00_CENTRAL_WINDOWS:
|
||||
if candidates.size == 0:
|
||||
frame_index = 0
|
||||
elif source.representation == "lossless-lidar-replay-v2":
|
||||
midpoint_seconds = (window.start_seconds + window.end_seconds) / 2.0
|
||||
frame_index = int(
|
||||
candidates[
|
||||
np.argmin(
|
||||
np.abs(
|
||||
source.arrays["session_seconds"][candidates]
|
||||
- midpoint_seconds
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
frame_index = int(
|
||||
candidates[
|
||||
@@ -1763,20 +1808,185 @@ def _anchors(
|
||||
|
||||
def _validate_source_binding(
|
||||
model: K1LocalSurfaceV1,
|
||||
source: E10LidarFieldSource,
|
||||
source: _LocalSurfaceSourceView,
|
||||
) -> None:
|
||||
if (
|
||||
model.identity.get("source_pack_id") != source.pack_id
|
||||
or model.identity.get("source_pack_identity_sha256")
|
||||
!= source.manifest.get("identity_sha256")
|
||||
!= source.identity_sha256
|
||||
or model.identity.get("source_artifact_sha256")
|
||||
!= source.manifest.get("artifact", {}).get("sha256")
|
||||
!= source.artifact_sha256
|
||||
or model.identity.get("frame_count") != source.frame_count
|
||||
or model.identity.get("point_count") != source.point_count
|
||||
):
|
||||
raise LidarGroundError("K1 local-surface source binding is invalid")
|
||||
|
||||
|
||||
def _local_surface_source_view(
|
||||
source: LocalSurfaceSource | _LocalSurfaceSourceView,
|
||||
) -> _LocalSurfaceSourceView:
|
||||
if isinstance(source, _LocalSurfaceSourceView):
|
||||
return source
|
||||
if isinstance(source, E10LidarFieldSource):
|
||||
artifact = _object(source.manifest.get("artifact"), "E10 LiDAR artifact")
|
||||
identity_sha256 = source.manifest.get("identity_sha256")
|
||||
artifact_sha256 = artifact.get("sha256")
|
||||
session_id = source.identity.get("session_id")
|
||||
if (
|
||||
not isinstance(identity_sha256, str)
|
||||
or _SHA256.fullmatch(identity_sha256) is None
|
||||
or not isinstance(artifact_sha256, str)
|
||||
or _SHA256.fullmatch(artifact_sha256) is None
|
||||
or not isinstance(session_id, str)
|
||||
or not session_id
|
||||
):
|
||||
raise LidarGroundError("E10 local-surface source binding is invalid")
|
||||
arrays = {
|
||||
name: np.asarray(source.arrays[name])
|
||||
for name in (
|
||||
"source_frame_indices",
|
||||
"session_seconds",
|
||||
"sample_available",
|
||||
"cloud_offsets",
|
||||
"cloud_points_map",
|
||||
"pose_positions_map",
|
||||
"pose_quaternions_map_from_lidar",
|
||||
"pose_point_delta_ms",
|
||||
)
|
||||
}
|
||||
return _LocalSurfaceSourceView(
|
||||
pack_id=source.pack_id,
|
||||
identity_sha256=identity_sha256,
|
||||
artifact_sha256=artifact_sha256,
|
||||
session_id=session_id,
|
||||
representation="legacy-e10-vendor-map-with-pose",
|
||||
schema_version=str(source.identity["schema_version"]),
|
||||
intensity_available=False,
|
||||
field_retention=None,
|
||||
arrays=arrays,
|
||||
frame_count=source.frame_count,
|
||||
point_count=source.point_count,
|
||||
)
|
||||
if not isinstance(source, LidarReplayPackV2):
|
||||
raise LidarGroundError("K1 local-surface source type is unsupported")
|
||||
identity_sha256 = source.manifest.get("identity_sha256")
|
||||
session_id = source.identity.get("session_id")
|
||||
artifact_sha256 = _lidar_replay_arrays_sha256(source)
|
||||
if (
|
||||
source.identity.get("schema_version") != LIDAR_REPLAY_PACK_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or _SHA256.fullmatch(identity_sha256) is None
|
||||
or not isinstance(session_id, str)
|
||||
or not session_id
|
||||
):
|
||||
raise LidarGroundError("LiDAR replay v2 local-surface binding is invalid")
|
||||
point_times = np.asarray(
|
||||
source.arrays["point_received_monotonic_ns"],
|
||||
dtype="<i8",
|
||||
)
|
||||
pose_times = np.asarray(
|
||||
source.arrays["pose_received_monotonic_ns"],
|
||||
dtype="<i8",
|
||||
)
|
||||
if point_times.size < 1 or np.any(np.diff(point_times) <= 0):
|
||||
raise LidarGroundError("LiDAR replay v2 point time is not strictly increasing")
|
||||
pose_indices, pose_delta_ms = _nearest_pose_indices(point_times, pose_times)
|
||||
if pose_times.size:
|
||||
positions = np.asarray(
|
||||
source.arrays["pose_positions_map"][pose_indices],
|
||||
dtype="<f8",
|
||||
)
|
||||
quaternions = np.asarray(
|
||||
source.arrays["pose_quaternions_map_from_lidar"][pose_indices],
|
||||
dtype="<f8",
|
||||
)
|
||||
else:
|
||||
positions = np.zeros((source.point_frame_count, 3), dtype="<f8")
|
||||
quaternions = np.tile(
|
||||
np.asarray([0.0, 0.0, 0.0, 1.0], dtype="<f8"),
|
||||
(source.point_frame_count, 1),
|
||||
)
|
||||
origin_ns = int(
|
||||
min(
|
||||
int(point_times[0]),
|
||||
int(pose_times[0]) if pose_times.size else int(point_times[0]),
|
||||
)
|
||||
)
|
||||
session_seconds = (
|
||||
point_times.astype(np.float64) - float(origin_ns)
|
||||
) / 1_000_000_000.0
|
||||
arrays = {
|
||||
"source_frame_indices": np.asarray(
|
||||
source.arrays["point_capture_sequence"],
|
||||
dtype="<i8",
|
||||
),
|
||||
"session_seconds": np.asarray(session_seconds, dtype="<f8"),
|
||||
"sample_available": np.ones(source.point_frame_count, dtype="?"),
|
||||
"cloud_offsets": np.asarray(source.arrays["point_offsets"], dtype="<i8"),
|
||||
"cloud_points_map": np.asarray(
|
||||
source.arrays["point_xyz_map"],
|
||||
dtype="<f8",
|
||||
),
|
||||
"pose_positions_map": positions,
|
||||
"pose_quaternions_map_from_lidar": quaternions,
|
||||
"pose_point_delta_ms": pose_delta_ms,
|
||||
}
|
||||
field_retention_value = source.identity.get("field_retention")
|
||||
field_retention = (
|
||||
cast(dict[str, object], field_retention_value)
|
||||
if isinstance(field_retention_value, dict)
|
||||
else None
|
||||
)
|
||||
return _LocalSurfaceSourceView(
|
||||
pack_id=source.pack_id,
|
||||
identity_sha256=identity_sha256,
|
||||
artifact_sha256=artifact_sha256,
|
||||
session_id=session_id,
|
||||
representation="lossless-lidar-replay-v2",
|
||||
schema_version=LIDAR_REPLAY_PACK_SCHEMA,
|
||||
intensity_available=True,
|
||||
field_retention=field_retention,
|
||||
arrays=arrays,
|
||||
frame_count=source.point_frame_count,
|
||||
point_count=source.point_count,
|
||||
)
|
||||
|
||||
|
||||
def _lidar_replay_arrays_sha256(source: LidarReplayPackV2) -> str:
|
||||
artifacts = _list(source.manifest.get("artifacts"), "LiDAR replay artifacts")
|
||||
for value in artifacts:
|
||||
artifact = _object(value, "LiDAR replay artifact")
|
||||
if artifact.get("kind") == "lidar-arrays":
|
||||
sha256 = artifact.get("sha256")
|
||||
if isinstance(sha256, str) and _SHA256.fullmatch(sha256) is not None:
|
||||
return sha256
|
||||
raise LidarGroundError("LiDAR replay arrays artifact is missing")
|
||||
|
||||
|
||||
def _nearest_pose_indices(
|
||||
point_times: npt.NDArray[np.int64],
|
||||
pose_times: npt.NDArray[np.int64],
|
||||
) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float64]]:
|
||||
if pose_times.size == 0:
|
||||
return (
|
||||
np.zeros(point_times.shape[0], dtype="<i8"),
|
||||
np.full(point_times.shape[0], np.inf, dtype="<f8"),
|
||||
)
|
||||
if np.any(np.diff(pose_times) < 0):
|
||||
raise LidarGroundError("LiDAR replay v2 pose time is not monotonic")
|
||||
right = np.searchsorted(pose_times, point_times, side="left")
|
||||
right = np.clip(right, 0, pose_times.shape[0] - 1)
|
||||
left = np.maximum(right - 1, 0)
|
||||
right_delta = np.abs(pose_times[right] - point_times)
|
||||
left_delta = np.abs(pose_times[left] - point_times)
|
||||
indices = np.where(left_delta <= right_delta, left, right).astype("<i8")
|
||||
delta_ms = (
|
||||
np.abs(pose_times[indices] - point_times).astype(np.float64)
|
||||
/ 1_000_000.0
|
||||
)
|
||||
return indices, np.asarray(delta_ms, dtype="<f8")
|
||||
|
||||
|
||||
def _valid_distribution(
|
||||
values: npt.NDArray[np.float64],
|
||||
mask: npt.NDArray[np.bool_],
|
||||
@@ -1810,7 +2020,8 @@ def _logical_sha256(arrays: Mapping[str, npt.NDArray[Any]]) -> str:
|
||||
digest.update(name.encode())
|
||||
digest.update(array.dtype.str.encode())
|
||||
digest.update(_canonical_json(list(array.shape)))
|
||||
digest.update(memoryview(array).cast("B"))
|
||||
if array.nbytes:
|
||||
digest.update(memoryview(array).cast("B"))
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
|
||||
@@ -21,12 +21,9 @@ from k1link.data_plane import (
|
||||
DecodedPoseView,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.protocol.streams import (
|
||||
LioPointCloudFrame,
|
||||
LioPoseFrame,
|
||||
decode_lio_pcl,
|
||||
decode_lio_pose,
|
||||
)
|
||||
from k1link.device_plugins.xgrids_k1.viewer.messages import StreamMessage
|
||||
from k1link.device_plugins.xgrids_k1.viewer.replay import iter_replay_messages
|
||||
|
||||
from .lidar_contract import (
|
||||
@@ -86,6 +83,27 @@ class LidarReplayError(ValueError):
|
||||
"""A replay pack or its source evidence violates the v2 contract."""
|
||||
|
||||
|
||||
class _MaterializedNpz:
|
||||
"""One-time decompression wrapper for bounded repeated array access."""
|
||||
|
||||
def __init__(self, path: Path) -> None:
|
||||
archive = np.load(path, allow_pickle=False)
|
||||
try:
|
||||
self.files = list(archive.files)
|
||||
self._arrays = {
|
||||
name: np.asarray(archive[name])
|
||||
for name in self.files
|
||||
}
|
||||
finally:
|
||||
archive.close()
|
||||
|
||||
def __getitem__(self, name: str) -> npt.NDArray[Any]:
|
||||
return self._arrays[name]
|
||||
|
||||
def close(self) -> None:
|
||||
self._arrays.clear()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LidarReplayPointFrame:
|
||||
capture_sequence: int
|
||||
@@ -180,7 +198,7 @@ class LidarReplayPackV2:
|
||||
self.arrays_path = artifacts["lidar-arrays"]
|
||||
self.quality_path = artifacts["lidar-quality"]
|
||||
self.equivalence_path = artifacts["live-replay-equivalence"]
|
||||
self.arrays = np.load(self.arrays_path, allow_pickle=False)
|
||||
self.arrays = _MaterializedNpz(self.arrays_path)
|
||||
if set(self.arrays.files) != set(_ARRAY_DTYPES):
|
||||
self.close()
|
||||
raise LidarReplayError("LiDAR replay array set is incompatible")
|
||||
@@ -413,6 +431,7 @@ def build_lidar_replay_pack_v2(
|
||||
"artifacts": artifacts,
|
||||
}
|
||||
_write_json(staging / LIDAR_MANIFEST_NAME, manifest)
|
||||
del arrays
|
||||
os.replace(staging, output)
|
||||
try:
|
||||
validation = LidarReplayPackV2(output)
|
||||
@@ -486,127 +505,159 @@ def lidar_pack_detail(pack: LidarReplayPackV2) -> dict[str, object]:
|
||||
|
||||
|
||||
def _capture_arrays(path: Path) -> dict[str, npt.NDArray[Any]]:
|
||||
point_messages: list[tuple[StreamMessage, LioPointCloudFrame]] = []
|
||||
pose_messages: list[tuple[StreamMessage, LioPoseFrame]] = []
|
||||
point_capture_sequence: list[int] = []
|
||||
point_payload_bytes: list[int] = []
|
||||
point_received_at_epoch_ns: list[int] = []
|
||||
point_received_monotonic_ns: list[int] = []
|
||||
point_header_seq: list[int] = []
|
||||
point_header_stamp: list[int] = []
|
||||
point_scaler: list[int] = []
|
||||
point_counts: list[int] = []
|
||||
pose_capture_sequence: list[int] = []
|
||||
pose_payload_bytes: list[int] = []
|
||||
pose_received_at_epoch_ns: list[int] = []
|
||||
pose_received_monotonic_ns: list[int] = []
|
||||
pose_header_seq: list[int] = []
|
||||
pose_header_stamp: list[int] = []
|
||||
pose_header_scaler: list[int] = []
|
||||
pose_stamp: list[int] = []
|
||||
pose_positions_map: list[tuple[float, float, float]] = []
|
||||
pose_quaternions_map_from_lidar: list[tuple[float, float, float, float]] = []
|
||||
pose_distance: list[float] = []
|
||||
pose_accuracy: list[float] = []
|
||||
|
||||
for message in iter_replay_messages(path):
|
||||
if message.source != "k1mqtt" or message.received_monotonic_ns is None:
|
||||
raise LidarReplayError("LiDAR v2 requires native capture with exact host time")
|
||||
if message.topic.endswith(_POINT_TOPIC_SUFFIX):
|
||||
point_messages.append((message, decode_lio_pcl(message.payload)))
|
||||
point_frame = decode_lio_pcl(message.payload)
|
||||
point_capture_sequence.append(message.sequence)
|
||||
point_payload_bytes.append(len(message.payload))
|
||||
point_received_at_epoch_ns.append(message.received_at_epoch_ns)
|
||||
point_received_monotonic_ns.append(message.received_monotonic_ns)
|
||||
point_header_seq.append(point_frame.header.seq)
|
||||
point_header_stamp.append(point_frame.header.stamp)
|
||||
point_scaler.append(point_frame.header.scaler)
|
||||
point_counts.append(len(point_frame.points))
|
||||
elif message.topic.endswith(_POSE_TOPIC_SUFFIX):
|
||||
pose_messages.append((message, decode_lio_pose(message.payload)))
|
||||
if not point_messages:
|
||||
pose_frame = decode_lio_pose(message.payload)
|
||||
pose_capture_sequence.append(message.sequence)
|
||||
pose_payload_bytes.append(len(message.payload))
|
||||
pose_received_at_epoch_ns.append(message.received_at_epoch_ns)
|
||||
pose_received_monotonic_ns.append(message.received_monotonic_ns)
|
||||
pose_header_seq.append(pose_frame.header.seq)
|
||||
pose_header_stamp.append(pose_frame.header.stamp)
|
||||
pose_header_scaler.append(pose_frame.header.scaler)
|
||||
pose_stamp.append(pose_frame.pose_stamp)
|
||||
pose_positions_map.append(pose_frame.position_xyz)
|
||||
pose_quaternions_map_from_lidar.append(pose_frame.orientation_xyzw)
|
||||
pose_distance.append(pose_frame.distance)
|
||||
pose_accuracy.append(pose_frame.pose_accuracy)
|
||||
if not point_capture_sequence:
|
||||
raise LidarReplayError("LiDAR replay source contains no lio_pcl frames")
|
||||
if any(count <= 0 for count in point_counts):
|
||||
raise LidarReplayError("LiDAR replay contains an empty point frame")
|
||||
|
||||
point_offsets = [0]
|
||||
point_raw: list[npt.NDArray[np.int64]] = []
|
||||
point_xyz: list[npt.NDArray[np.float64]] = []
|
||||
point_rgbi: list[npt.NDArray[np.uint32]] = []
|
||||
point_intensity: list[npt.NDArray[np.uint8]] = []
|
||||
for _, frame in point_messages:
|
||||
point_offsets = np.empty(len(point_counts) + 1, dtype="<i8")
|
||||
point_offsets[0] = 0
|
||||
np.cumsum(np.asarray(point_counts, dtype="<i8"), out=point_offsets[1:])
|
||||
point_count = int(point_offsets[-1])
|
||||
point_raw = np.empty((point_count, 3), dtype="<i8")
|
||||
point_xyz = np.empty((point_count, 3), dtype="<f8")
|
||||
point_rgbi = np.empty(point_count, dtype="<u4")
|
||||
point_intensity = np.empty(point_count, dtype="u1")
|
||||
point_index = 0
|
||||
for message in iter_replay_messages(path):
|
||||
if not message.topic.endswith(_POINT_TOPIC_SUFFIX):
|
||||
continue
|
||||
if point_index >= len(point_counts):
|
||||
raise LidarReplayError("LiDAR source changed between bounded passes")
|
||||
point_frame = decode_lio_pcl(message.payload)
|
||||
if (
|
||||
message.source != "k1mqtt"
|
||||
or message.received_monotonic_ns is None
|
||||
or message.sequence != point_capture_sequence[point_index]
|
||||
or message.received_at_epoch_ns
|
||||
!= point_received_at_epoch_ns[point_index]
|
||||
or message.received_monotonic_ns
|
||||
!= point_received_monotonic_ns[point_index]
|
||||
or len(message.payload) != point_payload_bytes[point_index]
|
||||
or point_frame.header.seq != point_header_seq[point_index]
|
||||
or point_frame.header.stamp != point_header_stamp[point_index]
|
||||
or point_frame.header.scaler != point_scaler[point_index]
|
||||
or len(point_frame.points) != point_counts[point_index]
|
||||
):
|
||||
raise LidarReplayError("LiDAR source changed between bounded passes")
|
||||
start = int(point_offsets[point_index])
|
||||
end = int(point_offsets[point_index + 1])
|
||||
raw = np.asarray(
|
||||
[(point.x_raw, point.y_raw, point.z_raw) for point in frame.points],
|
||||
[
|
||||
(point.x_raw, point.y_raw, point.z_raw)
|
||||
for point in point_frame.points
|
||||
],
|
||||
dtype="<i8",
|
||||
).reshape((-1, 3))
|
||||
rgbi = np.asarray([point.rgbi for point in frame.points], dtype="<u4")
|
||||
intensity = (rgbi & np.uint32(0xFF)).astype(np.uint8)
|
||||
xyz = raw.astype(np.float64) / float(frame.header.scaler)
|
||||
point_raw.append(raw)
|
||||
point_xyz.append(xyz)
|
||||
point_rgbi.append(rgbi)
|
||||
point_intensity.append(intensity)
|
||||
point_offsets.append(point_offsets[-1] + raw.shape[0])
|
||||
rgbi = np.asarray(
|
||||
[point.rgbi for point in point_frame.points],
|
||||
dtype="<u4",
|
||||
)
|
||||
point_raw[start:end] = raw
|
||||
point_xyz[start:end] = raw.astype(np.float64) / float(
|
||||
point_frame.header.scaler
|
||||
)
|
||||
point_rgbi[start:end] = rgbi
|
||||
point_intensity[start:end] = (rgbi & np.uint32(0xFF)).astype(np.uint8)
|
||||
point_index += 1
|
||||
if point_index != len(point_counts):
|
||||
raise LidarReplayError("LiDAR source changed between bounded passes")
|
||||
|
||||
arrays: dict[str, npt.NDArray[Any]] = {
|
||||
"point_capture_sequence": _message_int_array(point_messages, "sequence"),
|
||||
"point_payload_bytes": np.asarray(
|
||||
[len(message.payload) for message, _ in point_messages],
|
||||
"point_capture_sequence": np.asarray(point_capture_sequence, dtype="<i8"),
|
||||
"point_payload_bytes": np.asarray(point_payload_bytes, dtype="<i8"),
|
||||
"point_received_at_epoch_ns": np.asarray(
|
||||
point_received_at_epoch_ns,
|
||||
dtype="<i8",
|
||||
),
|
||||
"point_received_at_epoch_ns": _message_int_array(
|
||||
point_messages,
|
||||
"received_at_epoch_ns",
|
||||
),
|
||||
"point_received_monotonic_ns": np.asarray(
|
||||
[message.received_monotonic_ns for message, _ in point_messages],
|
||||
point_received_monotonic_ns,
|
||||
dtype="<i8",
|
||||
),
|
||||
"point_header_seq": np.asarray(
|
||||
[frame.header.seq for _, frame in point_messages],
|
||||
dtype="<u8",
|
||||
),
|
||||
"point_header_stamp": np.asarray(
|
||||
[frame.header.stamp for _, frame in point_messages],
|
||||
"point_header_seq": np.asarray(point_header_seq, dtype="<u8"),
|
||||
"point_header_stamp": np.asarray(point_header_stamp, dtype="<i8"),
|
||||
"point_scaler": np.asarray(point_scaler, dtype="<i8"),
|
||||
"point_offsets": point_offsets,
|
||||
"point_raw_xyz": point_raw,
|
||||
"point_xyz_map": point_xyz,
|
||||
"point_rgbi": point_rgbi,
|
||||
"point_intensity": point_intensity,
|
||||
"pose_capture_sequence": np.asarray(pose_capture_sequence, dtype="<i8"),
|
||||
"pose_payload_bytes": np.asarray(pose_payload_bytes, dtype="<i8"),
|
||||
"pose_received_at_epoch_ns": np.asarray(
|
||||
pose_received_at_epoch_ns,
|
||||
dtype="<i8",
|
||||
),
|
||||
"point_scaler": np.asarray(
|
||||
[frame.header.scaler for _, frame in point_messages],
|
||||
dtype="<i8",
|
||||
),
|
||||
"point_offsets": np.asarray(point_offsets, dtype="<i8"),
|
||||
"point_raw_xyz": np.concatenate(point_raw),
|
||||
"point_xyz_map": np.concatenate(point_xyz),
|
||||
"point_rgbi": np.concatenate(point_rgbi),
|
||||
"point_intensity": np.concatenate(point_intensity),
|
||||
"pose_capture_sequence": _message_int_array(pose_messages, "sequence"),
|
||||
"pose_payload_bytes": np.asarray(
|
||||
[len(message.payload) for message, _ in pose_messages],
|
||||
dtype="<i8",
|
||||
),
|
||||
"pose_received_at_epoch_ns": _message_int_array(
|
||||
pose_messages,
|
||||
"received_at_epoch_ns",
|
||||
),
|
||||
"pose_received_monotonic_ns": np.asarray(
|
||||
[message.received_monotonic_ns for message, _ in pose_messages],
|
||||
dtype="<i8",
|
||||
),
|
||||
"pose_header_seq": np.asarray(
|
||||
[frame.header.seq for _, frame in pose_messages],
|
||||
dtype="<u8",
|
||||
),
|
||||
"pose_header_stamp": np.asarray(
|
||||
[frame.header.stamp for _, frame in pose_messages],
|
||||
dtype="<i8",
|
||||
),
|
||||
"pose_header_scaler": np.asarray(
|
||||
[frame.header.scaler for _, frame in pose_messages],
|
||||
dtype="<i8",
|
||||
),
|
||||
"pose_stamp": np.asarray(
|
||||
[frame.pose_stamp for _, frame in pose_messages],
|
||||
pose_received_monotonic_ns,
|
||||
dtype="<i8",
|
||||
),
|
||||
"pose_header_seq": np.asarray(pose_header_seq, dtype="<u8"),
|
||||
"pose_header_stamp": np.asarray(pose_header_stamp, dtype="<i8"),
|
||||
"pose_header_scaler": np.asarray(pose_header_scaler, dtype="<i8"),
|
||||
"pose_stamp": np.asarray(pose_stamp, dtype="<i8"),
|
||||
"pose_positions_map": np.asarray(
|
||||
[frame.position_xyz for _, frame in pose_messages],
|
||||
pose_positions_map,
|
||||
dtype="<f8",
|
||||
).reshape((-1, 3)),
|
||||
"pose_quaternions_map_from_lidar": np.asarray(
|
||||
[frame.orientation_xyzw for _, frame in pose_messages],
|
||||
pose_quaternions_map_from_lidar,
|
||||
dtype="<f8",
|
||||
).reshape((-1, 4)),
|
||||
"pose_distance": np.asarray(
|
||||
[frame.distance for _, frame in pose_messages],
|
||||
dtype="<f8",
|
||||
),
|
||||
"pose_accuracy": np.asarray(
|
||||
[frame.pose_accuracy for _, frame in pose_messages],
|
||||
dtype="<f8",
|
||||
),
|
||||
"pose_distance": np.asarray(pose_distance, dtype="<f8"),
|
||||
"pose_accuracy": np.asarray(pose_accuracy, dtype="<f8"),
|
||||
}
|
||||
return arrays
|
||||
|
||||
|
||||
def _message_int_array(
|
||||
messages: list[tuple[StreamMessage, Any]],
|
||||
attribute: str,
|
||||
) -> npt.NDArray[np.int64]:
|
||||
return np.asarray(
|
||||
[getattr(message, attribute) for message, _ in messages],
|
||||
dtype="<i8",
|
||||
)
|
||||
|
||||
|
||||
def _validate_arrays(arrays: Any, identity: dict[str, Any]) -> None:
|
||||
for name, dtype in _ARRAY_DTYPES.items():
|
||||
if arrays[name].dtype != dtype:
|
||||
|
||||
Reference in New Issue
Block a user