feat(lab): add recorded realtime spatial playback

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 23:17:48 +03:00
parent aacc6dc43b
commit 9df9f58ab8
23 changed files with 1443 additions and 517 deletions
+19
View File
@@ -257,6 +257,25 @@ class RecordedGeometryStore:
points.setflags(write=False)
return points
def current_points_for_frame(self, frame_index: int) -> FloatArray | None:
"""Expose one verified increment to a read-only recorded-evidence projector."""
if not isinstance(frame_index, int) or isinstance(frame_index, bool):
raise GeometryProviderError("replay evidence frame index is invalid")
if not 0 <= frame_index < self.profile.frame_count:
raise GeometryProviderError("replay evidence frame is outside the source profile")
if int(self._source["frame_indices"][frame_index]) != frame_index:
raise GeometryProviderError("source pack frame sequence changed")
if not bool(self._source["sample_available"][frame_index]) or not bool(
self._surface["frame_valid"][frame_index]
):
return None
offsets = self._source["cloud_offsets"]
start, end = int(offsets[frame_index]), int(offsets[frame_index + 1])
points = np.asarray(self._source["cloud_points_map"][start:end], dtype=np.float64)
points.setflags(write=False)
return points
def pose_values_for_frame(
self,
frame_id: str,
+118
View File
@@ -0,0 +1,118 @@
"""Deterministic projections shared by recorded spatial evidence producers."""
from __future__ import annotations
import math
from collections.abc import Mapping, Sequence
import numpy as np
import numpy.typing as npt
from .threat import ReplayBodyFrame
FloatArray = npt.NDArray[np.float64]
class SpatialEvidenceProjectionError(RuntimeError):
"""Recorded spatial evidence cannot be projected without changing meaning."""
def sample_points_in_body_frame(
points_map: FloatArray,
body_frame: ReplayBodyFrame,
*,
point_limit: int,
) -> tuple[list[list[float]], int]:
"""Project one immutable map-frame increment into the current body frame."""
if point_limit < 1:
raise SpatialEvidenceProjectionError("spatial evidence point limit must be positive")
points = np.asarray(points_map, dtype=np.float64)
if points.ndim != 2 or points.shape[1] != 3 or not np.isfinite(points).all():
raise SpatialEvidenceProjectionError("spatial evidence point array is invalid")
basis = np.asarray(body_frame.basis_map_from_body, dtype=np.float64)
origin = np.asarray(body_frame.origin_map_xyz_m, dtype=np.float64)
if basis.shape != (3, 3) or origin.shape != (3,):
raise SpatialEvidenceProjectionError("spatial evidence body frame is invalid")
points_body = (points - origin) @ basis
stride = max(1, math.ceil(points_body.shape[0] / point_limit))
sampled = points_body[::stride][:point_limit]
return np.round(sampled, 6).tolist(), int(points.shape[0])
def project_metric_obstacles_to_body(
metric_rows: Sequence[Mapping[str, object]],
body_frame: ReplayBodyFrame,
*,
occupied_voxel_size_m: float,
) -> list[dict[str, object]]:
"""Project ledger-owned metric components without recomputing their decision."""
if not math.isfinite(occupied_voxel_size_m) or occupied_voxel_size_m <= 0:
raise SpatialEvidenceProjectionError("occupied voxel size is invalid")
visuals: list[dict[str, object]] = []
for row in metric_rows:
centroid = row.get("centroid_map_xyz_m")
cells = row.get("cells")
if not isinstance(centroid, list) or len(centroid) != 3 or not isinstance(cells, list):
continue
centroid_map = _finite_vector3(centroid, "metric centroid")
centroid_body = body_frame.map_point_to_body(centroid_map)
cell_centers: list[list[float]] = []
for raw_cell in cells:
if not isinstance(raw_cell, dict):
raise SpatialEvidenceProjectionError("occupied cell is invalid")
indices = (
_signed_integer(raw_cell.get("x"), "cell x"),
_signed_integer(raw_cell.get("y"), "cell y"),
_signed_integer(raw_cell.get("z"), "cell z"),
)
point_map = (
(indices[0] + 0.5) * occupied_voxel_size_m,
(indices[1] + 0.5) * occupied_voxel_size_m,
(indices[2] + 0.5) * occupied_voxel_size_m,
)
cell_centers.append(list(body_frame.map_point_to_body(point_map)))
visuals.append(
{
"component_id": row.get("component_id"),
"state": row.get("state"),
"motion": row.get("motion"),
"centroid_body_xyz_m": list(centroid_body),
"cell_centers_body_xyz_m": cell_centers,
"assessment": row.get("assessment"),
}
)
return visuals
def _finite_vector3(value: Sequence[object], label: str) -> tuple[float, float, float]:
if len(value) != 3:
raise SpatialEvidenceProjectionError(f"{label} is invalid")
return (
_finite_float(value[0], label),
_finite_float(value[1], label),
_finite_float(value[2], label),
)
def _finite_float(value: object, label: str) -> float:
if not isinstance(value, (int, float)) or isinstance(value, bool):
raise SpatialEvidenceProjectionError(f"{label} is invalid")
parsed = float(value)
if not math.isfinite(parsed):
raise SpatialEvidenceProjectionError(f"{label} is invalid")
return parsed
def _signed_integer(value: object, label: str) -> int:
if not isinstance(value, int) or isinstance(value, bool):
raise SpatialEvidenceProjectionError(f"{label} is invalid")
return value
__all__ = [
"SpatialEvidenceProjectionError",
"project_metric_obstacles_to_body",
"sample_points_in_body_frame",
]
+17 -38
View File
@@ -38,6 +38,10 @@ from .geometry import RecordedGeometryStore
from .geometry_replay import GeometryReplayResult, read_geometry_replay_result
from .providers import SourcePacket
from .recorded_source import RecordedRavnoves00Source, ReplayPacing
from .spatial_evidence import (
project_metric_obstacles_to_body,
sample_points_in_body_frame,
)
from .temporal_replay import TemporalReplayResult, read_temporal_replay_result
from .threat import (
DEFAULT_REPLAY_THREAT_PROFILE_PATH,
@@ -654,49 +658,24 @@ def _visual_frame(
points = store.current_points(packet)
if points is None:
raise ThreatReplayError("visual frame has no current point cloud")
basis = np.asarray(body_frame.basis_map_from_body, dtype=np.float64)
origin = np.asarray(body_frame.origin_map_xyz_m, dtype=np.float64)
points_body = (points - origin) @ basis
stride = max(1, math.ceil(points_body.shape[0] / VISUAL_POINT_LIMIT))
sampled = points_body[::stride][:VISUAL_POINT_LIMIT]
metric_visuals = []
for row in metric_rows:
centroid = row.get("centroid_map_xyz_m")
cells = row.get("cells")
if not isinstance(centroid, list) or not isinstance(cells, list):
continue
centroid_body = body_frame.map_point_to_body(
(float(centroid[0]), float(centroid[1]), float(centroid[2]))
)
cell_centers = []
for raw_cell in cells:
cell = _object(raw_cell, "visual occupied cell")
point_map = tuple(
(_signed_integer(cell.get(key), f"cell {key}") + 0.5)
* profile.corridor.occupied_voxel_size_m
for key in ("x", "y", "z")
)
cell_centers.append(
list(body_frame.map_point_to_body((point_map[0], point_map[1], point_map[2])))
)
metric_visuals.append(
{
"component_id": row["component_id"],
"state": row["state"],
"motion": row["motion"],
"centroid_body_xyz_m": list(centroid_body),
"cell_centers_body_xyz_m": cell_centers,
"assessment": row["assessment"],
}
)
sampled, source_count = sample_points_in_body_frame(
points,
body_frame,
point_limit=VISUAL_POINT_LIMIT,
)
metric_visuals = project_metric_obstacles_to_body(
metric_rows,
body_frame,
occupied_voxel_size_m=profile.corridor.occupied_voxel_size_m,
)
return {
"schema_version": THREAT_REPLAY_VISUAL_SCHEMA_V2,
"sequence": packet.envelope.sequence,
"frame_id": packet.envelope.frame_id,
"source_time_ns": packet.envelope.timestamps.source_ns,
"point_cloud_body_xyz_m": np.round(sampled, 6).tolist(),
"point_cloud_source_count": int(points.shape[0]),
"point_cloud_sample_count": int(sampled.shape[0]),
"point_cloud_body_xyz_m": sampled,
"point_cloud_source_count": source_count,
"point_cloud_sample_count": len(sampled),
"point_cloud_layer": "current-increment",
"rolling_map_component_count": sum(
row.get("state") == TemporalState.RETAINED.value for row in metric_rows
+284
View File
@@ -0,0 +1,284 @@
"""Bounded recorded-realtime projection of a sealed replay threat ledger."""
from __future__ import annotations
import copy
import json
import math
import re
import statistics
from dataclasses import dataclass
from itertools import pairwise
from pathlib import Path
from threading import RLock
from typing import Final
from .geometry import RecordedGeometryStore
from .spatial_evidence import (
project_metric_obstacles_to_body,
sample_points_in_body_frame,
)
from .threat import (
DEFAULT_REPLAY_THREAT_PROFILE_PATH,
RecordedReplayBodyFrameResolver,
load_replay_threat_profile,
)
from .threat_replay import (
THREAT_REPLAY_FRAME_SCHEMA,
THREAT_REPLAY_FRAME_SCHEMA_V2,
ThreatReplayResult,
)
RECORDED_SPATIAL_TIMELINE_SCHEMA: Final = "missioncore.recorded-spatial-evidence-timeline/v1"
RECORDED_SPATIAL_CHUNK_SCHEMA: Final = "missioncore.recorded-spatial-evidence-chunk/v1"
RECORDED_SPATIAL_FRAME_SCHEMA: Final = "missioncore.recorded-spatial-evidence-frame/v1"
RECORDED_SPATIAL_POINT_LIMIT: Final = 2_000
RECORDED_SPATIAL_MAX_CHUNK_FRAMES: Final = 24
_EXPECTED_FRAME_COUNT: Final = 4_489
_SOURCE_TIME = re.compile(rb'"source_time_ns":([0-9]+)')
class RecordedThreatTimelineError(RuntimeError):
"""A bounded timeline projection escaped its sealed result or source."""
@dataclass(frozen=True, slots=True)
class RecordedThreatTimelineIndex:
offsets: tuple[int, ...]
source_times_ns: tuple[int, ...]
class RecordedThreatTimeline:
"""Read bounded spatial chunks without materializing the full ledger in memory."""
def __init__(self, *, repository_root: Path, result: ThreatReplayResult) -> None:
self.repository_root = repository_root.resolve(strict=True)
self.result = result
self.frames_path = (result.result_root / "frames.jsonl").resolve(strict=True)
if self.frames_path.is_symlink() or self.frames_path.parent != result.result_root:
raise RecordedThreatTimelineError("recorded timeline frame ledger is invalid")
self.profile = load_replay_threat_profile(
self.repository_root / DEFAULT_REPLAY_THREAT_PROFILE_PATH
)
identity = result.manifest.get("identity")
if not isinstance(identity, dict):
raise RecordedThreatTimelineError("recorded timeline identity is missing")
expected_identity = {
"profile_id": self.profile.profile_id,
"profile_sha256": self.profile.profile_sha256,
"source_id": self.profile.source_id,
"source_session_id": self.profile.session_id,
"source_pack_id": self.profile.source_pack_id,
"source_pack_sha256": self.profile.source_pack_sha256,
}
if any(identity.get(key) != value for key, value in expected_identity.items()):
raise RecordedThreatTimelineError("recorded timeline escaped the threat profile")
self.store = RecordedGeometryStore.from_repository(self.repository_root)
if (
self.store.profile.source_pack_id != self.profile.source_pack_id
or self.store.profile.source_pack_sha256 != self.profile.source_pack_sha256
or self.store.profile.frame_count != _EXPECTED_FRAME_COUNT
):
raise RecordedThreatTimelineError("recorded timeline geometry identity changed")
self.body_frames = RecordedReplayBodyFrameResolver(
self.store,
profile=self.profile.body_frame,
)
self.index = _index_frame_ledger(self.frames_path)
self._lock = RLock()
def metadata(self) -> dict[str, object]:
times = self.index.source_times_ns
intervals = [(current - previous) / 1_000_000_000 for previous, current in pairwise(times)]
nominal_interval = statistics.median(intervals)
if not math.isfinite(nominal_interval) or nominal_interval <= 0:
raise RecordedThreatTimelineError("recorded timeline cadence is invalid")
return {
"schema_version": RECORDED_SPATIAL_TIMELINE_SCHEMA,
"result_id": self.result.result_id,
"recorded_source": {
"session_id": self.profile.session_id,
"source_id": self.profile.source_id,
"synchronization": "host-arrival-best-effort",
},
"frame_count": len(times),
"frame_times_ns": list(times),
"timeline_start_seconds": times[0] / 1_000_000_000,
"timeline_end_seconds": times[-1] / 1_000_000_000,
"nominal_frame_interval_seconds": nominal_interval,
"nominal_rate_hz": 1 / nominal_interval,
"max_chunk_frames": RECORDED_SPATIAL_MAX_CHUNK_FRAMES,
"point_sample_limit": RECORDED_SPATIAL_POINT_LIMIT,
"image_width": 800,
"image_height": 600,
"rig": {
"length_m": self.profile.rig.body_length_m,
"width_m": self.profile.rig.body_width_m,
"nominal_sensor_height_m": self.profile.rig.nominal_sensor_height_m,
},
"corridor": {
"forward_length_m": self.profile.corridor.forward_length_m,
"rear_margin_m": self.profile.corridor.rear_margin_m,
"half_width_m": (
self.profile.rig.body_width_m / 2 + self.profile.corridor.lateral_clearance_m
),
"prediction_horizon_seconds": (self.profile.corridor.prediction_horizon_seconds),
},
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only-bounded-recorded-replay",
}
def chunk(self, *, start_sequence: int, frame_count: int) -> dict[str, object]:
if not 0 <= start_sequence < len(self.index.offsets):
raise RecordedThreatTimelineError("recorded timeline chunk start is invalid")
if not 1 <= frame_count <= RECORDED_SPATIAL_MAX_CHUNK_FRAMES:
raise RecordedThreatTimelineError("recorded timeline chunk size is invalid")
stop = min(len(self.index.offsets), start_sequence + frame_count)
with self._lock:
frames = [self._project_frame(sequence) for sequence in range(start_sequence, stop)]
return {
"schema_version": RECORDED_SPATIAL_CHUNK_SCHEMA,
"result_id": self.result.result_id,
"start_sequence": start_sequence,
"frame_count": len(frames),
"next_sequence": stop if stop < len(self.index.offsets) else None,
"frames": frames,
"ground_truth": False,
"authority": "replay-simulated",
"access": "read-only-bounded-recorded-replay",
}
def _project_frame(self, sequence: int) -> dict[str, object]:
row = _read_frame_at(self.frames_path, self.index, sequence)
frame_id = row.get("frame_id")
if not isinstance(frame_id, str) or not frame_id:
raise RecordedThreatTimelineError("recorded timeline frame identity is invalid")
body_frame = self.body_frames.body_frame_for_frame(frame_id)
body_frame_declared = row.get("body_frame_available")
if not isinstance(body_frame_declared, bool) or body_frame_declared is not (
body_frame is not None
):
raise RecordedThreatTimelineError("recorded timeline body-frame binding changed")
source_available = row.get("source_available")
if not isinstance(source_available, bool):
raise RecordedThreatTimelineError("recorded timeline source state is invalid")
point_cloud: list[list[float]] = []
point_source_count = 0
metric_visuals: list[dict[str, object]] = []
if body_frame is not None:
points = self.store.current_points_for_frame(sequence)
if points is None or not source_available:
raise RecordedThreatTimelineError(
"recorded timeline current increment binding changed"
)
point_cloud, point_source_count = sample_points_in_body_frame(
points,
body_frame,
point_limit=RECORDED_SPATIAL_POINT_LIMIT,
)
metric_visuals = project_metric_obstacles_to_body(
_mapping_array(row.get("metric_obstacles"), "metric obstacles"),
body_frame,
occupied_voxel_size_m=self.profile.corridor.occupied_voxel_size_m,
)
assessments = _mapping_array(row.get("assessments"), "threat assessments")
camera_proposals = row.get("camera_proposals")
if not isinstance(camera_proposals, list):
raise RecordedThreatTimelineError("recorded timeline camera proposals are invalid")
return {
"schema_version": RECORDED_SPATIAL_FRAME_SCHEMA,
"sequence": sequence,
"frame_id": frame_id,
"source_time_ns": self.index.source_times_ns[sequence],
"session_seconds": self.index.source_times_ns[sequence] / 1_000_000_000,
"source_available": source_available,
"spatial_available": body_frame is not None,
"point_cloud_body_xyz_m": point_cloud,
"point_cloud_source_count": point_source_count,
"point_cloud_sample_count": len(point_cloud),
"point_cloud_layer": "current-increment",
"rolling_map_component_count": sum(
item.get("state") == "retained" for item in metric_visuals
),
"metric_obstacles": metric_visuals,
"camera_proposals": copy.deepcopy(camera_proposals),
"decision_counts": _decision_counts(assessments),
"camera_url": (
f"/api/v1/laboratory/m4-threat/results/{self.result.result_id}"
f"/timeline/frames/{sequence}/camera"
),
"ground_truth": False,
"authority": "replay-simulated",
}
def _index_frame_ledger(path: Path) -> RecordedThreatTimelineIndex:
offsets: list[int] = []
source_times: list[int] = []
with path.open("rb") as handle:
while True:
offset = handle.tell()
line = handle.readline()
if not line:
break
match = _SOURCE_TIME.search(line)
if match is None:
raise RecordedThreatTimelineError("recorded timeline source time is missing")
offsets.append(offset)
source_times.append(int(match.group(1)))
if len(offsets) != _EXPECTED_FRAME_COUNT:
raise RecordedThreatTimelineError("recorded timeline frame count changed")
if any(current <= previous for previous, current in pairwise(source_times)):
raise RecordedThreatTimelineError("recorded timeline source time is not monotonic")
return RecordedThreatTimelineIndex(tuple(offsets), tuple(source_times))
def _read_frame_at(
path: Path,
index: RecordedThreatTimelineIndex,
sequence: int,
) -> dict[str, object]:
with path.open("rb") as handle:
handle.seek(index.offsets[sequence])
line = handle.readline()
try:
row = json.loads(line)
except (UnicodeDecodeError, json.JSONDecodeError) as error:
raise RecordedThreatTimelineError("recorded timeline frame JSON is invalid") from error
if (
not isinstance(row, dict)
or row.get("schema_version")
not in {THREAT_REPLAY_FRAME_SCHEMA, THREAT_REPLAY_FRAME_SCHEMA_V2}
or row.get("sequence") != sequence
or row.get("source_time_ns") != index.source_times_ns[sequence]
):
raise RecordedThreatTimelineError("recorded timeline frame binding changed")
return row
def _mapping_array(value: object, label: str) -> list[dict[str, object]]:
if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
raise RecordedThreatTimelineError(f"recorded timeline {label} are invalid")
return value
def _decision_counts(assessments: list[dict[str, object]]) -> dict[str, int]:
result = {"threat": 0, "not-threat": 0, "unknown": 0}
for item in assessments:
decision = item.get("decision")
if not isinstance(decision, str) or decision not in result:
raise RecordedThreatTimelineError("recorded timeline decision is invalid")
result[decision] += 1
return result
__all__ = [
"RECORDED_SPATIAL_CHUNK_SCHEMA",
"RECORDED_SPATIAL_FRAME_SCHEMA",
"RECORDED_SPATIAL_MAX_CHUNK_FRAMES",
"RECORDED_SPATIAL_POINT_LIMIT",
"RECORDED_SPATIAL_TIMELINE_SCHEMA",
"RecordedThreatTimeline",
"RecordedThreatTimelineError",
]