diff --git a/experiments/perception/LAB_E25_REPORT_2026-07-24.md b/experiments/perception/LAB_E25_REPORT_2026-07-24.md
new file mode 100644
index 0000000..ea1393d
--- /dev/null
+++ b/experiments/perception/LAB_E25_REPORT_2026-07-24.md
@@ -0,0 +1,273 @@
+# LAB E25 — persistent LiDAR support and honest motion evidence
+
+Date: 2026-07-24
+Status: diagnostic artifact accepted; motion benchmark not accepted
+Best immutable replay:
+`LAB E25.3 · Persistent support · full RAVNOVES00`
+
+## Objective
+
+Test a different motion measurement after E24 proved that the center of a
+completed 3D cuboid is not a stable world-space observation. E25 tracks the
+LiDAR cells that actually support an object in `k1-map`, compares their
+occupancy through time and emits `static`, `dynamic`, or `unknown`.
+
+This is not a box beautification experiment. The target is a bounded,
+streaming-equivalent evidence branch for an unmanned vehicle. 3D boxes remain
+an operator-facing presentation. Missing sensor evidence must remain
+`unknown/occupied`; it must not be converted into an invented motion estimate
+or free space.
+
+## Source data and immutable inputs
+
+- Source session: `20260720T065719Z_viewer_live` (`RAVNOVES00`).
+- Camera source: `sensor.camera.right`, factory slot `camera_1`.
+- Source frames: 4,489.
+- Source timeline: 35.421857292–484.044857292 session seconds.
+- Full map-frame LiDAR pack: 9,207,270 points.
+- Semantic masks: 898 frames at 600×800.
+- Factory camera model: KB4.
+- Calibration SHA-256:
+ `05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9`.
+- Source integrated result:
+ `e10-integrated-perception-34ade557b5636717aa497fc00355b84df7063e483a175f2f5d3f04c03df1c898`.
+- Source detector: YOLOX-S, COCO-80, 640×640.
+- Source semantic model:
+ `tue-mps/cityscapes_semantic_eomt_large_1024`, FP16 autocast.
+
+E25 does not repeat GPU inference. It reuses the accepted detector and semantic
+outputs and recomputes object support from the immutable full LiDAR pack,
+factory projection and rig poses. Raw LiDAR, ground returns, camera media,
+calibration, detector output and semantic masks were not modified.
+
+## Why E25 was required
+
+E24 classified motion from the temporal behavior of fitted cuboid centers.
+That measurement is biased by visibility: while the rig passes a parked car,
+different portions of the car become visible, and an amodal box fitter can move
+its completed center even though the physical object is static.
+
+This creates a structurally false velocity. More smoothing only delays it.
+Looser association hides missing observations but can join neighboring cars.
+Stricter association preserves identity but exposes gaps. E25 therefore changes
+the measurement rather than tuning another center filter.
+
+## Implemented pipeline
+
+For every source detector object, E25:
+
+1. takes the synchronized full LiDAR scan in `k1-map`;
+2. projects candidate points through the factory KB4 camera calibration;
+3. applies detector-box inset and compatible semantic labels;
+4. performs depth and spatial clustering to reject unrelated projected points;
+5. excludes ground only from the object-proposal branch while preserving the
+ complete ground and raw point cloud for visualization and downstream use;
+6. quantizes object support into 0.25 m map-frame occupancy cells;
+7. stores a bounded history of occupied cells and support points;
+8. compares the current support with a reference 0.3–2.5 s in the past;
+9. classifies static evidence from persistent overlap;
+10. classifies dynamic evidence only when overlap is low and displacement,
+ robust speed, direction consistency and repeated current observations agree.
+
+The E25 branch is deliberately bounded:
+
+- maximum 192 live tracks;
+- maximum 32 support snapshots per track;
+- maximum 256 points and 256 cells per object snapshot;
+- no lookahead;
+- no commands, navigation authority or safety authority.
+
+The final pass also enforces an important evidence invariant: one current LiDAR
+measurement can belong only to the current `e24-observed` object. An E24
+`held-hypothesis` may preserve a short-lived track state, but it receives
+`current=false` and cannot duplicate the same present-time support.
+
+## Benchmark contract
+
+Benchmark v2 is bound to source detector IDs and time windows rather than to
+any nearby object of the expected class. This prevents a parked car from
+accidentally satisfying a moving-car event.
+
+Dynamic operator anchors:
+
+- 58 s: woman with dog, left;
+- 65 s: person loading a car, right;
+- 115 s: adult and child, left;
+- 127 s: pedestrian on road, right;
+- 155 s: woman with stroller and child, left;
+- 170 s: oncoming vehicle, right;
+- 173 s: vulnerable-road-user group, left;
+- 183 s: vehicle moving with the rig direction, right.
+
+Static controls:
+
+- parked vehicles at 70–90 s;
+- parked vehicles at 132–148 s;
+- parked vehicles at 161–166 s.
+
+Frame review corrected the 183 s anchor from source track 391, which is parked,
+to source track 373, which is the moving vehicle. Static controls report
+coverage, classified fraction and false-dynamic fraction separately. A lack of
+classification is therefore not counted as a correct static result.
+
+## Run series
+
+### E25.1 — first persistent-support pass
+
+- Immutable session: `lab-e25-1-persistent-support`.
+- Result:
+ `e10-integrated-perception-fac9421996ce5d8622c534f66af1a9bfaea25c7ea8a2bfd1ec954330d12ce9ed`.
+- Benchmark: 7/11.
+- Processing p95: 3.966 ms/frame.
+
+An initial CPU-bound attempt was stopped because the NPZ container was
+decompressing LiDAR members repeatedly for every frame. This was a laboratory
+loader defect, not tracker cost. Materializing the immutable arrays once in
+memory reduced the full pass to about 20 seconds.
+
+### E25.2 — benchmark identity correction
+
+- Immutable session: `lab-e25-2-persistent-support`.
+- Result:
+ `e10-integrated-perception-e24715571eb5c40d6f0a853bb0551737b2935e0ccbb65e2b90061c0f422a49e3`.
+- Benchmark: 7/11.
+- Processing p95: 3.970 ms/frame.
+
+This pass binds the 183 s event to moving source track 373 and adds defensive
+deduplication for source/frame evidence in the evaluator.
+
+### E25.3 — single-owner current evidence
+
+- Immutable session: `lab-e25-3-persistent-support`.
+- Result:
+ `e10-integrated-perception-9034a5cf306b379cc248c1da17639f140fc896bb55e412bef43f89974de892a3`.
+- Benchmark: 6/11.
+- Processing mean/p50/p95:
+ 2.114/2.078/4.322 ms per frame.
+- Processing maximum: 84.791 ms/frame.
+- Peak live tracker states: 16 of 192.
+- Current support observations: 7,204.
+- Explicit held states: 3,073.
+- Full laboratory packaging: approximately 20.5 s.
+
+The score decreased from 7/11 to 6/11 because held hypotheses no longer reuse
+current LiDAR evidence. This is the accepted result: it is less flattering and
+more correct.
+
+## E25.3 event results
+
+Passed:
+
+- woman/dog at 58 s;
+- adult/child at 115 s;
+- vulnerable-road-user group at 173 s;
+- all three parked-vehicle control windows.
+
+Static control details:
+
+- 70–90 s: coverage 0.4619, classified fraction 0.77665,
+ 306 static, 88 unknown, false-dynamic fraction 0.0;
+- 132–148 s: coverage 0.5683, classified fraction 0.63948,
+ 298 static, 168 unknown, false-dynamic fraction 0.0;
+- 161–166 s: coverage 0.1722, classified fraction 0.61538,
+ 16 static, 10 unknown, false-dynamic fraction 0.0.
+
+Failed:
+
+- person loading car at 65 s: two current dynamic hits, minimum is three;
+- right-side person at 127 s: only 3/21 frames contain usable LiDAR support;
+- stroller group at 155 s: support exists, but association is ambiguous and
+ forks across source tracks;
+- oncoming car at 170 s: only 2/15 current LiDAR-support observations, so no
+ reliable temporal history can be formed;
+- same-direction car at 183 s, source track 373: the available evidence is
+ strong (`overlap=0`, approximately 4.8–4.9 m/s), but only two current dynamic
+ hits remain after held-evidence duplication is removed.
+
+## Performance and replay
+
+The tracker timing is CPU diagnostic timing without repeated detector or
+semantic inference. Its p95 of 4.322 ms/frame is compatible with a future
+near-real-time evidence branch, but it does not prove end-to-end real-time
+performance.
+
+The first cold Rerun publication exposed a fail-soft viewer bug: a provisional
+cuboid can legitimately have `distance_smoothed_m=null`, while the label
+renderer called `float(None)`. The renderer now keeps the cuboid and simply
+omits an unconfirmed range from its label.
+
+The final E25.3 overlay:
+
+- Rerun magic: `RRF2`;
+- payload: 69,809,981 bytes;
+- saved session: `ready`;
+- replayable: `true`;
+- warm endpoint response on the existing localhost server: HTTP 200 in 0.11 s.
+
+The server on `127.0.0.1:8000` was not stopped or restarted.
+
+## Main conclusion
+
+E25 fixes an important E24 failure: persistent map-frame LiDAR support no longer
+turns cuboid-center drift on parked cars into false motion. All static control
+windows have a false-dynamic fraction of 0.0.
+
+E25 also establishes the sensor limit. A LiDAR-only support branch cannot infer
+motion when the object receives too few current returns. Threshold changes
+cannot create absent observations. Treating those gaps as a confident velocity
+would make the output visually smoother but less safe.
+
+E25.3 is therefore accepted as a diagnostic and evidence-quality result, not as
+a navigation or safety motion classifier.
+
+## Change of emphasis for E26
+
+E26 must add information from an independent source instead of further tuning
+the LiDAR branch:
+
+1. maintain a temporally stable camera track in image space;
+2. remove apparent image motion caused by rig ego-motion;
+3. turn the factory KB4 pixel ray, rig pose/extrinsic and a bounded
+ ground-contact/range hypothesis into world-motion evidence;
+4. fuse that evidence conservatively with the E25 LiDAR branch;
+5. emit explicit `agree`, `single-source/unknown` and `conflict` outcomes;
+6. keep unknown objects occupied for planning;
+7. expose occupancy footprint, velocity and uncertainty to planner-facing
+ consumers while retaining boxes for the operator UI.
+
+Acceptance must be split into independent gates:
+
+- camera-track association integrity and ID-switch rate;
+- current LiDAR-support coverage;
+- false dynamic on parked vehicles;
+- recall on the known moving people and two moving vehicles;
+- camera/LiDAR conflict rate;
+- end-to-end stage latency and bounded memory.
+
+This follows the established autonomous-driving separation between detection,
+tracking, occupancy and validation. Foxglove and Rerun visualize those
+products; they do not supply the motion-estimation algorithm.
+
+## Validation
+
+- 30 targeted E10/E24/E25 tests pass after the viewer fail-soft fix.
+- Ruff passes for the changed modules.
+- Targeted strict mypy passes for the changed compute modules.
+- E25.3 passes the integrated-perception validator.
+- The immutable saved session is ready and replayable on
+ `http://127.0.0.1:8000/`.
+- Raw evidence was not modified.
+- Navigation and safety acceptance remain explicitly false.
+
+## External implementation references
+
+- Autoware multi-object tracker:
+
+- Autoware detection-by-tracker:
+
+- Autoware probabilistic occupancy grid:
+
+- Autoware obstacle point-cloud validator:
+
+- Autoware perception architecture:
+
diff --git a/experiments/perception/e25_motion_benchmark.json b/experiments/perception/e25_motion_benchmark.json
new file mode 100644
index 0000000..842fc6e
--- /dev/null
+++ b/experiments/perception/e25_motion_benchmark.json
@@ -0,0 +1,131 @@
+{
+ "schema_version": "missioncore.e25-motion-benchmark/v1",
+ "benchmark_id": "ravnoves00-spatially-bound-operator-anchors-v2",
+ "source_session_id": "20260720T065719Z_viewer_live",
+ "timeline": "session_seconds",
+ "annotation_status": "operator-approximate-with-reviewed-detector-track-binding",
+ "events": [
+ {
+ "id": "dynamic-person-woman-dog-left-58s",
+ "kind": "target-motion",
+ "window_seconds": [54.0, 62.0],
+ "class_group": "person",
+ "target_source_track_ids": [83],
+ "expected_motion": "dynamic",
+ "minimum_hits": 3,
+ "minimum_span_seconds": 0.2,
+ "minimum_coverage_fraction": 0.08
+ },
+ {
+ "id": "dynamic-person-loading-car-right-65s",
+ "kind": "target-motion",
+ "window_seconds": [62.0, 69.0],
+ "class_group": "person",
+ "target_source_track_ids": [112],
+ "expected_motion": "dynamic",
+ "minimum_hits": 3,
+ "minimum_span_seconds": 0.2,
+ "minimum_coverage_fraction": 0.1
+ },
+ {
+ "id": "static-vehicles-before-moving-70-90s",
+ "kind": "static-control",
+ "window_seconds": [70.0, 90.0],
+ "class_group": "vehicle",
+ "expected_motion": "static",
+ "minimum_source_observations": 80,
+ "minimum_coverage_fraction": 0.2,
+ "minimum_classified_fraction": 0.2,
+ "maximum_false_dynamic_fraction": 0.1
+ },
+ {
+ "id": "dynamic-person-adult-child-left-115s",
+ "kind": "target-motion",
+ "window_seconds": [111.0, 120.0],
+ "class_group": "person",
+ "target_source_track_ids": [229],
+ "expected_motion": "dynamic",
+ "minimum_hits": 3,
+ "minimum_span_seconds": 0.2,
+ "minimum_coverage_fraction": 0.08
+ },
+ {
+ "id": "dynamic-person-road-right-127s",
+ "kind": "target-motion",
+ "window_seconds": [124.0, 132.0],
+ "class_group": "person",
+ "target_source_track_ids": [253],
+ "expected_motion": "dynamic",
+ "minimum_hits": 2,
+ "minimum_span_seconds": 0.15,
+ "minimum_coverage_fraction": 0.08
+ },
+ {
+ "id": "static-vehicles-mid-run-132-148s",
+ "kind": "static-control",
+ "window_seconds": [132.0, 148.0],
+ "class_group": "vehicle",
+ "expected_motion": "static",
+ "minimum_source_observations": 80,
+ "minimum_coverage_fraction": 0.2,
+ "minimum_classified_fraction": 0.2,
+ "maximum_false_dynamic_fraction": 0.1
+ },
+ {
+ "id": "dynamic-person-stroller-group-left-155s",
+ "kind": "target-motion",
+ "window_seconds": [151.0, 161.0],
+ "class_group": "person",
+ "target_source_track_ids": [322, 328],
+ "expected_motion": "dynamic",
+ "minimum_hits": 3,
+ "minimum_span_seconds": 0.2,
+ "minimum_coverage_fraction": 0.08
+ },
+ {
+ "id": "static-vehicles-before-oncoming-161-166s",
+ "kind": "static-control",
+ "window_seconds": [161.0, 166.0],
+ "class_group": "vehicle",
+ "expected_motion": "static",
+ "minimum_source_observations": 25,
+ "minimum_coverage_fraction": 0.15,
+ "minimum_classified_fraction": 0.15,
+ "maximum_false_dynamic_fraction": 0.1
+ },
+ {
+ "id": "dynamic-vehicle-oncoming-right-170s",
+ "kind": "target-motion",
+ "window_seconds": [167.0, 173.0],
+ "class_group": "vehicle",
+ "target_source_track_ids": [365],
+ "expected_motion": "dynamic",
+ "minimum_hits": 3,
+ "minimum_span_seconds": 0.15,
+ "minimum_coverage_fraction": 0.1
+ },
+ {
+ "id": "dynamic-person-bike-group-left-173s",
+ "kind": "target-motion",
+ "window_seconds": [170.0, 179.0],
+ "class_group": "vulnerable_road_user",
+ "target_source_track_ids": [379, 386, 388],
+ "expected_motion": "dynamic",
+ "minimum_hits": 3,
+ "minimum_span_seconds": 0.2,
+ "minimum_coverage_fraction": 0.08
+ },
+ {
+ "id": "dynamic-vehicle-same-direction-right-183s",
+ "kind": "target-motion",
+ "window_seconds": [180.0, 188.0],
+ "class_group": "vehicle",
+ "target_source_track_ids": [373],
+ "binding_review": "frame-and-2d-track-reviewed; 391 is a parked vehicle",
+ "expected_motion": "dynamic",
+ "minimum_hits": 3,
+ "minimum_span_seconds": 0.2,
+ "minimum_coverage_fraction": 0.1
+ }
+ ]
+}
diff --git a/experiments/perception/e25_persistent_support_profile.json b/experiments/perception/e25_persistent_support_profile.json
new file mode 100644
index 0000000..8b31cf0
--- /dev/null
+++ b/experiments/perception/e25_persistent_support_profile.json
@@ -0,0 +1,77 @@
+{
+ "schema_version": "missioncore.e25-persistent-support-profile/v1",
+ "profile_id": "lab-e25-map-frame-persistent-support-v1",
+ "mode": "recorded-streaming-qualification",
+ "source": {
+ "source_id": "sensor.camera.right",
+ "coordinate_frame": "k1-map",
+ "calibration_slot": "camera_1",
+ "calibration_sha256": "05f3ad9b38b3a4fc95388a8ec83da83c745e217709e51787b3d5aad0969f6fa9"
+ },
+ "support": {
+ "minimum_evidence_points": {
+ "person": 2,
+ "bicycle": 2,
+ "motorcycle": 2,
+ "vehicle": 2,
+ "default": 2
+ },
+ "maximum_points_per_object": 256
+ },
+ "occupancy": {
+ "voxel_size_m": 0.25,
+ "neighbor_radius_cells": 1,
+ "reference_minimum_age_seconds": {
+ "person": 0.3,
+ "bicycle": 0.3,
+ "motorcycle": 0.3,
+ "vehicle": 0.55,
+ "default": 0.55
+ },
+ "reference_maximum_age_seconds": 2.5,
+ "history_seconds": 2.0,
+ "static_overlap_enter": 0.55,
+ "static_overlap_exit": 0.35,
+ "dynamic_overlap_enter": 0.25,
+ "dynamic_overlap_exit": 0.4,
+ "dynamic_minimum_speed_mps": {
+ "person": 0.4,
+ "bicycle": 0.5,
+ "motorcycle": 0.6,
+ "vehicle": 0.7,
+ "default": 0.7
+ },
+ "dynamic_minimum_displacement_m": {
+ "person": 0.4,
+ "bicycle": 0.45,
+ "motorcycle": 0.55,
+ "vehicle": 0.65,
+ "default": 0.65
+ },
+ "minimum_direction_consistency": 0.6,
+ "dynamic_confirmation_frames": {
+ "person": 2,
+ "bicycle": 2,
+ "motorcycle": 2,
+ "vehicle": 5,
+ "default": 5
+ },
+ "static_confirmation_frames": 4,
+ "contradiction_frames": 2,
+ "maximum_support_hold_seconds": 0.45,
+ "maximum_snapshots_per_track": 32,
+ "maximum_cells_per_snapshot": 256
+ },
+ "bounds": {
+ "maximum_tracks": 192,
+ "maximum_track_idle_seconds": 1.5
+ },
+ "acceptance": {
+ "maximum_processing_p95_ms": 12.0,
+ "maximum_tracks_observed": 192
+ },
+ "authority": {
+ "commands_enabled": false,
+ "navigation_or_safety_accepted": false
+ }
+}
diff --git a/src/k1link/compute/__init__.py b/src/k1link/compute/__init__.py
index 108d62f..dbfbd4f 100644
--- a/src/k1link/compute/__init__.py
+++ b/src/k1link/compute/__init__.py
@@ -36,12 +36,14 @@ from .jobs import (
)
from .lab_instances import (
PublishedIntegratedLabInstance,
+ PublishedPersistentSupportLabInstance,
PublishedTemporalLabInstance,
PublishedWorldMotionLabInstance,
publish_e21_lab_instance,
publish_e22_lab_instance,
publish_e23_lab_instance,
publish_e24_lab_instance,
+ publish_e25_lab_instance,
publish_integrated_lab_instance,
)
from .live_perception import (
@@ -120,6 +122,7 @@ __all__ = [
"LivePerceptionIngress",
"IntegratedPerceptionOverlayStore",
"PublishedIntegratedLabInstance",
+ "PublishedPersistentSupportLabInstance",
"PublishedTemporalLabInstance",
"PublishedWorldMotionLabInstance",
"IntegratedPerceptionResult",
@@ -154,6 +157,7 @@ __all__ = [
"publish_e22_lab_instance",
"publish_e23_lab_instance",
"publish_e24_lab_instance",
+ "publish_e25_lab_instance",
"publish_integrated_lab_instance",
"validate_multirate_perception_qualification_result",
"prepare_recorded_qualification_slice",
diff --git a/src/k1link/compute/integrated_perception.py b/src/k1link/compute/integrated_perception.py
index 8756663..69e8bc1 100644
--- a/src/k1link/compute/integrated_perception.py
+++ b/src/k1link/compute/integrated_perception.py
@@ -73,7 +73,7 @@ class _PresentedCuboid:
track_id: int
label: str
association_group: str
- distance_m: float
+ distance_m: float | None
support_points: int
center: np.ndarray
half_size: np.ndarray
@@ -126,12 +126,20 @@ class _CuboidPresentationState:
raise RecordedPerceptionOverlayError(
"integrated perception cuboid track identity is invalid"
)
+ distance_value = item.get("distance_smoothed_m")
+ distance_m = (
+ float(distance_value)
+ if isinstance(distance_value, int | float)
+ and not isinstance(distance_value, bool)
+ and np.isfinite(float(distance_value))
+ else None
+ )
self._latest[track_id] = _PresentedCuboid(
observed_ns=timestamp_ns,
track_id=track_id,
label=str(item.get("label", "object")),
association_group=str(item.get("association_group", "object")),
- distance_m=float(item["distance_smoothed_m"]),
+ distance_m=distance_m,
support_points=int(item["clustered_points"]),
center=np.asarray(center).copy(),
half_size=np.asarray(half_size).copy(),
@@ -160,9 +168,10 @@ class _CuboidPresentationState:
color[3] = max(24, round(float(color[3]) * (1.0 - 0.55 * fade)))
presented_colors.append(color)
age_label = "" if age_ns == 0 else f" · hold {age_ns / 1_000_000:.0f} ms"
+ distance_label = "" if cuboid.distance_m is None else f" · {cuboid.distance_m:.1f} m"
labels.append(
- f"{cuboid.association_group} #{cuboid.track_id} {cuboid.label} · "
- f"{cuboid.distance_m:.1f} m · {cuboid.support_points} pts{age_label}"
+ f"{cuboid.association_group} #{cuboid.track_id} {cuboid.label}"
+ f"{distance_label} · {cuboid.support_points} pts{age_label}"
)
return (
np.stack([cuboid.center for cuboid in presented]),
diff --git a/src/k1link/compute/lab_instances.py b/src/k1link/compute/lab_instances.py
index fe37871..5184d9d 100644
--- a/src/k1link/compute/lab_instances.py
+++ b/src/k1link/compute/lab_instances.py
@@ -28,6 +28,10 @@ from .integrated_perception import (
validate_integrated_perception_result,
)
from .jobs import CameraComputeJob, validate_camera_compute_job
+from .occupancy_motion import (
+ PersistentSupportBuild,
+ build_persistent_support_result,
+)
from .temporal_stability import (
TemporalStabilityBuild,
_quality_metrics,
@@ -59,6 +63,14 @@ class PublishedWorldMotionLabInstance:
build: WorldMotionBuild
+@dataclass(frozen=True, slots=True)
+class PublishedPersistentSupportLabInstance:
+ binding: LabSessionBinding
+ job: CameraComputeJob
+ result: IntegratedPerceptionResult
+ build: PersistentSupportBuild
+
+
def publish_integrated_lab_instance(
*,
repository_root: Path,
@@ -590,6 +602,113 @@ def publish_e24_lab_instance(
)
+def publish_e25_lab_instance(
+ *,
+ repository_root: Path,
+ source_result_root: Path,
+ world_motion_profile_path: Path,
+ profile_path: Path,
+ benchmark_path: Path,
+ lab_session_id: str,
+ lab_id: str,
+ display_name: str,
+) -> PublishedPersistentSupportLabInstance:
+ """Derive and publish one bounded persistent-support motion LAB run."""
+
+ root = repository_root.expanduser().resolve(strict=True)
+ jobs_root = root / ".runtime" / "compute-jobs"
+ results_root = root / ".runtime" / "compute-experiments" / "e10" / "worker-results"
+ packs_root = root / ".runtime" / "compute-experiments" / "e10" / "lidar-packs"
+ source_path = source_result_root.expanduser().resolve(strict=True)
+ source_document = _read_object(source_path / "result.json", source_path)
+ identity = source_document.get("identity")
+ if not isinstance(identity, dict) or not isinstance(identity.get("job_id"), str):
+ raise SessionIntegrityError("E25 source has no job identity")
+ source = validate_integrated_perception_result(
+ jobs_root / identity["job_id"],
+ source_path,
+ packs_root,
+ )
+ if not source.accepted:
+ raise SessionIntegrityError("E25 source result is not accepted")
+
+ lab_job = _publish_lab_job(source.job, jobs_root, lab_session_id)
+ lab_pack = _publish_lab_pack(source, lab_job, packs_root, lab_session_id)
+ build = build_persistent_support_result(
+ source=source,
+ lab_job=lab_job,
+ lab_pack=lab_pack,
+ results_root=results_root,
+ world_motion_profile_path=world_motion_profile_path,
+ profile_path=profile_path,
+ benchmark_path=benchmark_path,
+ )
+ validated = validate_integrated_perception_result(
+ lab_job.job_root,
+ build.result_root,
+ packs_root,
+ )
+ if not validated.accepted:
+ failed = [
+ name for name, accepted in build.report["acceptance"]["checks"].items() if not accepted
+ ]
+ raise SessionIntegrityError(
+ f"E25 persistent-support artifact acceptance failed: {', '.join(failed)}"
+ )
+
+ store = SessionStore(root)
+ source_lab = store.get_lab_instance(source.job.session_id)
+ source_session_id = (
+ source.job.session_id if source_lab is None else source_lab.source_session_id
+ )
+ publish_lab_replay_cache(
+ store.data_dir,
+ source_session_id=source_session_id,
+ lab_session_id=lab_session_id,
+ timeline_start_ns=round(validated.timeline_start_seconds * 1_000_000_000),
+ timeline_end_ns=round(validated.timeline_end_seconds * 1_000_000_000),
+ )
+ metrics = build.report["metrics"]
+ binding = store.publish_lab_instance(
+ session_id=lab_session_id,
+ source_session_id=source_session_id,
+ display_name=display_name,
+ lab_id=lab_id,
+ result_kind="e25-persistent-support-motion",
+ result_id=validated.result_id,
+ source_result_id=source.result_id,
+ config_sha256=build.profile_sha256,
+ run_created_at_utc=validated.created_at_utc,
+ duration_seconds=(validated.timeline_end_seconds - validated.timeline_start_seconds),
+ include_recorded_media=False,
+ provenance={
+ "schema_version": "missioncore.e25-lab-publication/v1",
+ "storage_mode": "bounded-persistent-support-and-immutable-source-replay",
+ "source_result_id": source.result_id,
+ "source_lab_session_id": (None if source_lab is None else source_lab.session_id),
+ "source_payloads_mutated": False,
+ "coordinate_frame": "k1-map",
+ "measurement": "persistent-object-support-occupancy",
+ "lookahead_frames": 0,
+ "benchmark_sha256": build.benchmark_sha256,
+ "benchmark_passed": metrics["benchmark"]["passed"],
+ "benchmark_passed_events": metrics["benchmark"]["passed_events"],
+ "benchmark_total_events": metrics["benchmark"]["total_events"],
+ "persistent_support_processing_p95_ms": metrics["runtime"][
+ "persistent_support_frame_processing_ms"
+ ]["p95"],
+ "peak_tracks": metrics["runtime"]["peak_tracks"],
+ "navigation_or_safety_accepted": False,
+ },
+ )
+ return PublishedPersistentSupportLabInstance(
+ binding=binding,
+ job=lab_job,
+ result=validated,
+ build=build,
+ )
+
+
def _validate_e23_inputs(
worker_root: Path,
source_report_path: Path,
diff --git a/src/k1link/compute/occupancy_motion.py b/src/k1link/compute/occupancy_motion.py
new file mode 100644
index 0000000..61e0136
--- /dev/null
+++ b/src/k1link/compute/occupancy_motion.py
@@ -0,0 +1,1466 @@
+"""Persistent map-frame support evidence for conservative object motion.
+
+LAB E25 keeps the detector, semantic masks, full LiDAR cloud, calibration and
+source cuboids immutable. It replays the already recorded inputs once, rebuilds
+per-object LiDAR support, and compares current occupied cells with older cells
+in ``k1-map``. A 3D box remains presentation geometry; motion is not inferred
+from the completed box center alone.
+"""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import math
+import os
+import secrets
+import shutil
+import time
+from collections import Counter, defaultdict, deque
+from collections.abc import Mapping, Sequence
+from dataclasses import dataclass, field
+from datetime import UTC, datetime
+from pathlib import Path
+from typing import Any
+
+import numpy as np
+import numpy.typing as npt
+
+from k1link.artifacts import write_json_atomic
+from k1link.device_plugins.xgrids_k1.analyze.calibrated_projection import (
+ Kb4ProjectionProfile,
+ map_points_to_lidar,
+ project_map_points_kb4,
+)
+from k1link.sessions import SessionIntegrityError
+
+from .integrated_perception import IntegratedPerceptionResult
+from .jobs import CameraComputeJob
+from .world_motion import (
+ WorldMotionTracker,
+ _artifact,
+ _box_arrays,
+ _canonical,
+ _percentiles,
+ _read_jsonl,
+ _read_object,
+ _rewrite_npz_boxes,
+ _robust_motion,
+ _sha256,
+ _write_jsonl,
+ read_world_motion_profile,
+)
+
+PROFILE_SCHEMA = "missioncore.e25-persistent-support-profile/v1"
+BENCHMARK_SCHEMA = "missioncore.e25-motion-benchmark/v1"
+PIPELINE_ID = "map-frame-persistent-support-dual-hypothesis/v1"
+
+FloatArray = npt.NDArray[np.float64]
+IntArray = npt.NDArray[np.int64]
+Cell = tuple[int, int]
+
+
+@dataclass(frozen=True, slots=True)
+class SupportMeasurement:
+ source_track_id: int
+ label: str
+ group: str
+ score: float
+ points_map: FloatArray
+ source_indices: IntArray
+
+
+@dataclass(slots=True)
+class _SupportSnapshot:
+ session_seconds: float
+ cells: frozenset[Cell]
+ center_map: np.ndarray
+
+
+@dataclass(slots=True)
+class _OccupancyTrack:
+ track_id: int
+ group: str
+ aliases: set[int]
+ snapshots: deque[_SupportSnapshot] = field(default_factory=deque)
+ motion_state: str = "unknown"
+ static_evidence: int = 0
+ dynamic_evidence: int = 0
+ contradictions: int = 0
+ overlap_fraction: float | None = None
+ speed_mps: float | None = None
+ direction_consistency: float = 0.0
+ displacement_m: float | None = None
+ confidence: float = 0.0
+ velocity_map_mps: np.ndarray | None = None
+ last_observed_seconds: float = 0.0
+ support_observations: int = 0
+ current_cells: frozenset[Cell] = frozenset()
+
+
+@dataclass(frozen=True, slots=True)
+class PersistentSupportBuild:
+ result_root: Path
+ profile_sha256: str
+ benchmark_sha256: str
+ report: dict[str, Any]
+
+
+class PersistentSupportTracker:
+ """Bounded occupancy-evidence state keyed by the E24 world track."""
+
+ def __init__(self, profile: dict[str, Any]) -> None:
+ self.profile = profile
+ self.tracks: dict[int, _OccupancyTrack] = {}
+ self.peak_tracks = 0
+ self.created_tracks = 0
+ self.support_observations = 0
+ self.held_states = 0
+ self.published_states: Counter[str] = Counter()
+
+ def observe(
+ self,
+ *,
+ track_id: int,
+ source_track_id: int,
+ group: str,
+ session_seconds: float,
+ points_map: FloatArray,
+ ) -> dict[str, Any]:
+ track = self.tracks.get(track_id)
+ if track is None:
+ track = _OccupancyTrack(
+ track_id=track_id,
+ group=group,
+ aliases={source_track_id},
+ last_observed_seconds=session_seconds,
+ )
+ self.tracks[track_id] = track
+ self.created_tracks += 1
+ else:
+ track.aliases.add(source_track_id)
+ cells = _voxel_cells(
+ points_map,
+ voxel_size=float(self.profile["occupancy"]["voxel_size_m"]),
+ maximum=int(self.profile["occupancy"]["maximum_cells_per_snapshot"]),
+ )
+ center = np.median(points_map, axis=0).astype(np.float64, copy=False)
+ self._classify(track, session_seconds, cells, center)
+ track.last_observed_seconds = session_seconds
+ track.support_observations += 1
+ track.current_cells = cells
+ self.support_observations += 1
+ self._prune(session_seconds)
+ self.peak_tracks = max(self.peak_tracks, len(self.tracks))
+ self.published_states[track.motion_state] += 1
+ return self._evidence(track, session_seconds, current=True)
+
+ def hold(self, track_id: int, session_seconds: float) -> dict[str, Any] | None:
+ track = self.tracks.get(track_id)
+ if track is None:
+ return None
+ age = session_seconds - track.last_observed_seconds
+ if age > float(self.profile["occupancy"]["maximum_support_hold_seconds"]):
+ return None
+ self.held_states += 1
+ self.published_states[track.motion_state] += 1
+ return self._evidence(track, session_seconds, current=False)
+
+ def snapshot(self) -> dict[str, Any]:
+ active = Counter(track.motion_state for track in self.tracks.values())
+ return {
+ "active_tracks": len(self.tracks),
+ "peak_tracks": self.peak_tracks,
+ "created_tracks": self.created_tracks,
+ "support_observations": self.support_observations,
+ "held_states": self.held_states,
+ "active_motion_states": dict(active),
+ "published_motion_states": dict(self.published_states),
+ }
+
+ def _classify(
+ self,
+ track: _OccupancyTrack,
+ now: float,
+ cells: frozenset[Cell],
+ center: np.ndarray,
+ ) -> None:
+ config = self.profile["occupancy"]
+ minimum_age = _group_value(
+ config["reference_minimum_age_seconds"],
+ track.group,
+ )
+ maximum_age = float(config["reference_maximum_age_seconds"])
+ reference: set[Cell] = set()
+ for snapshot in track.snapshots:
+ age = now - snapshot.session_seconds
+ if minimum_age <= age <= maximum_age:
+ reference.update(snapshot.cells)
+ overlap = (
+ None
+ if not reference
+ else _cell_overlap(
+ cells,
+ reference,
+ radius=int(config["neighbor_radius_cells"]),
+ )
+ )
+ track.overlap_fraction = overlap
+ track.snapshots.append(
+ _SupportSnapshot(
+ session_seconds=now,
+ cells=cells,
+ center_map=center.copy(),
+ )
+ )
+ history_seconds = float(config["history_seconds"])
+ while track.snapshots and now - track.snapshots[0].session_seconds > max(
+ maximum_age,
+ history_seconds,
+ ):
+ track.snapshots.popleft()
+ maximum_snapshots = int(config["maximum_snapshots_per_track"])
+ while len(track.snapshots) > maximum_snapshots:
+ track.snapshots.popleft()
+ history: deque[tuple[float, np.ndarray]] = deque(
+ (
+ snapshot.session_seconds,
+ snapshot.center_map,
+ )
+ for snapshot in track.snapshots
+ if now - snapshot.session_seconds <= history_seconds
+ )
+ speed, direction = _robust_motion(history)
+ track.speed_mps = speed
+ track.direction_consistency = direction
+ track.displacement_m = (
+ None
+ if len(history) < 2
+ else float(np.linalg.norm(history[-1][1][:2] - history[0][1][:2]))
+ )
+ track.velocity_map_mps = _median_velocity(history)
+ if overlap is None or speed is None or track.displacement_m is None:
+ track.motion_state = "unknown"
+ track.confidence = 0.0
+ return
+ dynamic = (
+ overlap <= float(config["dynamic_overlap_enter"])
+ and speed >= _group_value(config["dynamic_minimum_speed_mps"], track.group)
+ and track.displacement_m
+ >= _group_value(config["dynamic_minimum_displacement_m"], track.group)
+ and direction >= float(config["minimum_direction_consistency"])
+ )
+ static = overlap >= float(config["static_overlap_enter"])
+ track.dynamic_evidence = track.dynamic_evidence + 1 if dynamic else 0
+ track.static_evidence = track.static_evidence + 1 if static else 0
+ raw_contradiction = not dynamic and not static
+ track.contradictions = track.contradictions + 1 if raw_contradiction else 0
+ dynamic_frames = int(_group_value(config["dynamic_confirmation_frames"], track.group))
+ static_frames = int(config["static_confirmation_frames"])
+ if track.dynamic_evidence >= dynamic_frames:
+ track.motion_state = "dynamic"
+ elif track.static_evidence >= static_frames:
+ track.motion_state = "static"
+ elif (
+ track.motion_state == "dynamic" and overlap >= float(config["dynamic_overlap_exit"])
+ ) or (track.motion_state == "static" and overlap < float(config["static_overlap_exit"])):
+ track.motion_state = "unknown"
+ if track.contradictions >= int(config["contradiction_frames"]):
+ track.motion_state = "unknown"
+ if track.motion_state == "dynamic":
+ overlap_confidence = max(
+ 0.0,
+ 1.0 - overlap / max(float(config["dynamic_overlap_exit"]), 1e-6),
+ )
+ track.confidence = min(
+ 1.0,
+ 0.5 * overlap_confidence + 0.5 * direction,
+ )
+ elif track.motion_state == "static":
+ track.confidence = min(
+ 1.0,
+ overlap / max(float(config["static_overlap_enter"]), 1e-6),
+ )
+ else:
+ track.confidence = 0.0
+
+ def _evidence(
+ self,
+ track: _OccupancyTrack,
+ now: float,
+ *,
+ current: bool,
+ ) -> dict[str, Any]:
+ voxel_size = float(self.profile["occupancy"]["voxel_size_m"])
+ footprint = _footprint(track.current_cells, voxel_size)
+ velocity = (
+ None
+ if track.motion_state == "unknown" or track.velocity_map_mps is None
+ else track.velocity_map_mps.tolist()
+ )
+ if track.motion_state == "static":
+ velocity = [0.0, 0.0, 0.0]
+ return {
+ "motion_state": track.motion_state,
+ "motion_confidence": track.confidence,
+ "motion_status": ("e25-current-map-support" if current else "e25-held-map-support"),
+ "speed_mps": track.speed_mps,
+ "velocity_map_mps": velocity,
+ "occupancy_overlap_fraction": track.overlap_fraction,
+ "occupancy_displacement_m": track.displacement_m,
+ "occupancy_direction_consistency": track.direction_consistency,
+ "occupancy_support_observations": track.support_observations,
+ "occupancy_cell_count": len(track.current_cells),
+ "occupancy_resolution_m": voxel_size,
+ "occupancy_footprint_map_xy": footprint,
+ "occupancy_evidence_current": current,
+ "occupancy_observation_age_ms": max(
+ 0.0,
+ (now - track.last_observed_seconds) * 1000.0,
+ ),
+ "source_track_aliases": sorted(track.aliases),
+ }
+
+ def _prune(self, now: float) -> None:
+ maximum_idle = float(self.profile["bounds"]["maximum_track_idle_seconds"])
+ stale = [
+ track_id
+ for track_id, track in self.tracks.items()
+ if now - track.last_observed_seconds > maximum_idle
+ ]
+ for track_id in stale:
+ self.tracks.pop(track_id, None)
+ maximum_tracks = int(self.profile["bounds"]["maximum_tracks"])
+ if len(self.tracks) <= maximum_tracks:
+ return
+ ordered = sorted(
+ self.tracks.values(),
+ key=lambda track: track.last_observed_seconds,
+ )
+ for track in ordered[: len(self.tracks) - maximum_tracks]:
+ self.tracks.pop(track.track_id, None)
+
+
+class RecordedSupportExtractor:
+ """Rebuild sparse per-object support from immutable recorded inputs."""
+
+ def __init__(
+ self,
+ *,
+ source: IntegratedPerceptionResult,
+ lidar_pack: Path,
+ association: dict[str, Any],
+ profile: dict[str, Any],
+ ) -> None:
+ self.source = source
+ self.association = association
+ self.profile = profile
+ semantic_rows = _read_jsonl(source.semantic_path)
+ with np.load(source.arrays_path, allow_pickle=False) as source_arrays:
+ semantic_indices = source_arrays["semantic_frame_indices"]
+ self.semantic_masks = source_arrays["semantic_masks"]
+ if not np.array_equal(
+ semantic_indices,
+ np.asarray([row["frame_index"] for row in semantic_rows], dtype=np.int64),
+ ):
+ raise SessionIntegrityError("LAB E25 semantic frame binding changed")
+ self.semantic_by_source = {
+ int(row["source_frame_index"]): index for index, row in enumerate(semantic_rows)
+ }
+ required = {
+ "frame_indices",
+ "source_frame_indices",
+ "session_seconds",
+ "sample_available",
+ "cloud_offsets",
+ "cloud_points_map",
+ "pose_positions_map",
+ "pose_quaternions_map_from_lidar",
+ "lidar_camera_delta_ms",
+ "pose_point_delta_ms",
+ "intrinsic_fx_fy_cx_cy",
+ "distortion_kb4",
+ "t_camera_from_lidar",
+ }
+ with np.load(lidar_pack / "lidar-pack.npz", allow_pickle=False) as lidar_arrays:
+ if set(lidar_arrays.files) != required:
+ raise SessionIntegrityError("LAB E25 LiDAR replay array set changed")
+ if lidar_arrays["frame_indices"].shape != (source.frame_count,):
+ raise SessionIntegrityError("LAB E25 LiDAR replay frame count changed")
+ self.sample_available = lidar_arrays["sample_available"]
+ self.cloud_offsets = lidar_arrays["cloud_offsets"]
+ self.cloud_points_map = lidar_arrays["cloud_points_map"]
+ self.pose_positions_map = lidar_arrays["pose_positions_map"]
+ self.pose_quaternions_map_from_lidar = lidar_arrays["pose_quaternions_map_from_lidar"]
+ intrinsic_values = np.asarray(
+ lidar_arrays["intrinsic_fx_fy_cx_cy"],
+ dtype=np.float64,
+ )
+ distortion_values = np.asarray(
+ lidar_arrays["distortion_kb4"],
+ dtype=np.float64,
+ )
+ t_camera_from_lidar = np.asarray(
+ lidar_arrays["t_camera_from_lidar"],
+ dtype=np.float64,
+ )
+ configuration = _read_object(source.result_root / "result.json")["identity"][
+ "configuration"
+ ]
+ source_profile = configuration["profile"]["source"]
+ if intrinsic_values.shape != (4,) or distortion_values.shape != (4,):
+ raise SessionIntegrityError("LAB E25 calibration vector shape changed")
+ intrinsic = (
+ float(intrinsic_values[0]),
+ float(intrinsic_values[1]),
+ float(intrinsic_values[2]),
+ float(intrinsic_values[3]),
+ )
+ distortion = (
+ float(distortion_values[0]),
+ float(distortion_values[1]),
+ float(distortion_values[2]),
+ float(distortion_values[3]),
+ )
+ self.projection = Kb4ProjectionProfile(
+ source_id=str(source_profile["source_id"]),
+ calibration_slot=str(source_profile["calibration_slot"]),
+ width=int(source_profile["resolution"][0]),
+ height=int(source_profile["resolution"][1]),
+ intrinsic_fx_fy_cx_cy=intrinsic,
+ distortion_kb4=distortion,
+ t_camera_from_lidar=t_camera_from_lidar,
+ )
+ self.frames_with_support = 0
+ self.measurements = 0
+ self.points = 0
+
+ def close(self) -> None:
+ return None
+
+ def frame(
+ self,
+ frame_index: int,
+ fusion: dict[str, Any],
+ ) -> dict[int, SupportMeasurement]:
+ if not bool(self.sample_available[frame_index]):
+ return {}
+ semantic_source = fusion.get("semantic_source_frame_index")
+ if not isinstance(semantic_source, int):
+ return {}
+ semantic_index = self.semantic_by_source.get(semantic_source)
+ if semantic_index is None:
+ return {}
+ start = int(self.cloud_offsets[frame_index])
+ end = int(self.cloud_offsets[frame_index + 1])
+ points_map = np.asarray(
+ self.cloud_points_map[start:end],
+ dtype=np.float64,
+ )
+ position_values = np.asarray(
+ self.pose_positions_map[frame_index],
+ dtype=np.float64,
+ )
+ quaternion_values = np.asarray(
+ self.pose_quaternions_map_from_lidar[frame_index],
+ dtype=np.float64,
+ )
+ if position_values.shape != (3,) or quaternion_values.shape != (4,):
+ raise SessionIntegrityError("LAB E25 pose vector shape changed")
+ position = (
+ float(position_values[0]),
+ float(position_values[1]),
+ float(position_values[2]),
+ )
+ quaternion = (
+ float(quaternion_values[0]),
+ float(quaternion_values[1]),
+ float(quaternion_values[2]),
+ float(quaternion_values[3]),
+ )
+ projected = project_map_points_kb4(
+ points_map,
+ position_map_xyz=position,
+ orientation_map_from_lidar_xyzw=quaternion,
+ profile=self.projection,
+ )
+ visible = _visible_projection_indices(
+ projected.pixels_xy,
+ projected.depths_m,
+ self.projection.width,
+ )
+ pixels = projected.pixels_xy[visible]
+ depths = projected.depths_m[visible]
+ source_indices = projected.source_indices[visible]
+ points_lidar = map_points_to_lidar(
+ points_map,
+ position_map_xyz=position,
+ orientation_map_from_lidar_xyzw=quaternion,
+ )
+ semantic = self.semantic_masks[semantic_index]
+ pixel_x = np.clip(
+ np.rint(pixels[:, 0]).astype(np.int64),
+ 0,
+ self.projection.width - 1,
+ )
+ pixel_y = np.clip(
+ np.rint(pixels[:, 1]).astype(np.int64),
+ 0,
+ self.projection.height - 1,
+ )
+ sampled_semantic = semantic[pixel_y, pixel_x]
+ candidates: list[SupportMeasurement] = []
+ for source_object in fusion["objects"]:
+ measurement = self._object_support(
+ source_object,
+ pixels=pixels,
+ depths=depths,
+ source_indices=source_indices,
+ sampled_semantic=sampled_semantic,
+ points_map=points_map,
+ points_lidar=points_lidar,
+ )
+ if measurement is not None:
+ candidates.append(measurement)
+ selected = _suppress_duplicate_measurements(
+ candidates,
+ overlap_threshold=float(self.association["support_duplicate_overlap_threshold"]),
+ )
+ if selected:
+ self.frames_with_support += 1
+ self.measurements += len(selected)
+ self.points += sum(item.points_map.shape[0] for item in selected)
+ return {item.source_track_id: item for item in selected}
+
+ def snapshot(self) -> dict[str, int]:
+ return {
+ "frames_with_support": self.frames_with_support,
+ "support_measurements": self.measurements,
+ "support_points": self.points,
+ }
+
+ def _object_support(
+ self,
+ source: dict[str, Any],
+ *,
+ pixels: FloatArray,
+ depths: FloatArray,
+ source_indices: IntArray,
+ sampled_semantic: npt.NDArray[np.uint8],
+ points_map: FloatArray,
+ points_lidar: FloatArray,
+ ) -> SupportMeasurement | None:
+ group = str(source.get("association_group", source.get("label", "object")))
+ if group not in self.association["semantic_ids"]:
+ return None
+ x1, y1, x2, y2 = (float(value) for value in source["bbox_xyxy"])
+ width, height = x2 - x1, y2 - y1
+ inset = self.association["box_inset"]
+ inside = (
+ (pixels[:, 0] >= x1 + width * float(inset["horizontal_fraction"]))
+ & (pixels[:, 0] < x2 - width * float(inset["horizontal_fraction"]))
+ & (pixels[:, 1] >= y1 + height * float(inset["top_fraction"]))
+ & (pixels[:, 1] < y2 - height * float(inset["bottom_fraction"]))
+ )
+ candidates = np.flatnonzero(inside).astype(np.int64)
+ allowed = np.asarray(
+ self.association["semantic_ids"][group],
+ dtype=np.uint8,
+ )
+ compatible = candidates[np.isin(sampled_semantic[candidates], allowed)]
+ clustered = _depth_cluster(compatible, depths, self.association)
+ selected = _spatial_cluster(
+ source_indices[clustered],
+ points_lidar,
+ float(self.association["spatial_cluster_radius_m"][group]),
+ )
+ selected = _filter_support_by_ground(
+ selected,
+ points_map,
+ group=group,
+ profile=self.association["object_support_ground_filter"],
+ )
+ minimum = int(
+ _group_value(
+ self.profile["support"]["minimum_evidence_points"],
+ group,
+ )
+ )
+ if selected.size < minimum:
+ return None
+ maximum_points = int(self.profile["support"]["maximum_points_per_object"])
+ if selected.size > maximum_points:
+ step = selected.size / maximum_points
+ selected = selected[np.floor(np.arange(maximum_points) * step).astype(np.int64)]
+ return SupportMeasurement(
+ source_track_id=int(source.get("source_track_id", source["track_id"])),
+ label=str(source.get("label", "object")),
+ group=group,
+ score=float(source.get("score", 0.0)),
+ points_map=points_map[selected],
+ source_indices=selected,
+ )
+
+
+def read_persistent_support_profile(path: Path) -> tuple[dict[str, Any], str]:
+ resolved = path.expanduser().resolve(strict=True)
+ profile = _read_object(resolved)
+ occupancy = profile.get("occupancy")
+ if (
+ profile.get("schema_version") != PROFILE_SCHEMA
+ or profile.get("mode") != "recorded-streaming-qualification"
+ or profile.get("authority")
+ != {"commands_enabled": False, "navigation_or_safety_accepted": False}
+ or not isinstance(occupancy, dict)
+ or not 0.05 <= float(occupancy.get("voxel_size_m", 0.0)) <= 1.0
+ or not 1 <= int(profile.get("bounds", {}).get("maximum_tracks", 0)) <= 512
+ or float(occupancy.get("dynamic_overlap_enter", 1.0))
+ >= float(occupancy.get("static_overlap_enter", 0.0))
+ ):
+ raise SessionIntegrityError("LAB E25 persistent-support profile is invalid")
+ return profile, _sha256(resolved)
+
+
+def read_persistent_support_benchmark(
+ path: Path,
+) -> tuple[dict[str, Any], str]:
+ resolved = path.expanduser().resolve(strict=True)
+ benchmark = _read_object(resolved)
+ events = benchmark.get("events")
+ if (
+ benchmark.get("schema_version") != BENCHMARK_SCHEMA
+ or benchmark.get("source_session_id") != "20260720T065719Z_viewer_live"
+ or not isinstance(events, list)
+ or not events
+ ):
+ raise SessionIntegrityError("LAB E25 motion benchmark is invalid")
+ identifiers: set[str] = set()
+ for event in events:
+ if (
+ not isinstance(event, dict)
+ or not isinstance(event.get("id"), str)
+ or event["id"] in identifiers
+ or event.get("kind") not in {"target-motion", "static-control"}
+ or event.get("expected_motion") not in {"static", "dynamic"}
+ or event.get("class_group")
+ not in {"person", "vehicle", "bicycle", "vulnerable_road_user"}
+ or not isinstance(event.get("window_seconds"), list)
+ or len(event["window_seconds"]) != 2
+ or float(event["window_seconds"][0]) >= float(event["window_seconds"][1])
+ ):
+ raise SessionIntegrityError("LAB E25 benchmark event is invalid")
+ if event["kind"] == "target-motion":
+ targets = event.get("target_source_track_ids")
+ if (
+ not isinstance(targets, list)
+ or not targets
+ or not all(isinstance(value, int) and value > 0 for value in targets)
+ ):
+ raise SessionIntegrityError("LAB E25 target binding is invalid")
+ identifiers.add(event["id"])
+ return benchmark, _sha256(resolved)
+
+
+def evaluate_persistent_support_benchmark(
+ source_rows: list[dict[str, Any]],
+ fusion_rows: list[dict[str, Any]],
+ benchmark: dict[str, Any],
+) -> dict[str, Any]:
+ outcomes: list[dict[str, Any]] = []
+ for event in benchmark["events"]:
+ start, end = (float(value) for value in event["window_seconds"])
+ targets = set(int(value) for value in event.get("target_source_track_ids", []))
+ source_keys: set[tuple[int, int]] = set()
+ for row in source_rows:
+ timestamp = float(row["session_seconds"])
+ if not start <= timestamp <= end:
+ continue
+ for item in row["objects"]:
+ source_id = int(item.get("source_track_id", item["track_id"]))
+ if _event_class_matches(item, event) and (
+ event["kind"] == "static-control" or source_id in targets
+ ):
+ source_keys.add((_benchmark_frame_key(row, timestamp), source_id))
+ source_observations = len(source_keys)
+ evidence_by_key: dict[
+ tuple[int, int],
+ tuple[float, dict[str, Any]],
+ ] = {}
+ duplicate_evidence_observations = 0
+ for row in fusion_rows:
+ timestamp = float(row["session_seconds"])
+ if not start <= timestamp <= end:
+ continue
+ for item in row["objects"]:
+ source_id = item.get("source_track_id")
+ if (
+ isinstance(source_id, int)
+ and _event_class_matches(item, event)
+ and (event["kind"] == "static-control" or source_id in targets)
+ and item.get("occupancy_evidence_current") is True
+ ):
+ key = (_benchmark_frame_key(row, timestamp), source_id)
+ previous = evidence_by_key.get(key)
+ if previous is not None:
+ duplicate_evidence_observations += 1
+ if previous is None or _benchmark_evidence_rank(
+ item
+ ) > _benchmark_evidence_rank(previous[1]):
+ evidence_by_key[key] = (timestamp, item)
+ matched = sorted(evidence_by_key.values(), key=lambda value: value[0])
+ coverage = len(matched) / max(1, source_observations)
+ if event["kind"] == "static-control":
+ state_counts = Counter(str(item["motion_state"]) for _, item in matched)
+ classified = state_counts["static"] + state_counts["dynamic"]
+ classified_fraction = classified / max(1, len(matched))
+ false_dynamic_fraction = state_counts["dynamic"] / max(1, len(matched))
+ passed = (
+ source_observations >= int(event["minimum_source_observations"])
+ and coverage >= float(event["minimum_coverage_fraction"])
+ and classified_fraction >= float(event["minimum_classified_fraction"])
+ and false_dynamic_fraction <= float(event["maximum_false_dynamic_fraction"])
+ )
+ outcomes.append(
+ {
+ "id": event["id"],
+ "kind": event["kind"],
+ "expected_motion": event["expected_motion"],
+ "class_group": event["class_group"],
+ "window_seconds": [start, end],
+ "passed": passed,
+ "source_observations": source_observations,
+ "evidence_observations": len(matched),
+ "duplicate_evidence_observations": (duplicate_evidence_observations),
+ "coverage_fraction": coverage,
+ "classified_fraction": classified_fraction,
+ "false_dynamic_fraction": false_dynamic_fraction,
+ "state_counts": dict(state_counts),
+ }
+ )
+ continue
+ by_track: dict[int, list[tuple[float, dict[str, Any]]]] = defaultdict(list)
+ for timestamp, item in matched:
+ if item.get("motion_state") == event["expected_motion"]:
+ by_track[int(item["track_id"])].append((timestamp, item))
+ best_track: int | None = None
+ best_hits = 0
+ best_span = 0.0
+ for track_id, observations in by_track.items():
+ timestamps = [value[0] for value in observations]
+ span = max(timestamps) - min(timestamps)
+ if (len(observations), span) > (best_hits, best_span):
+ best_track = track_id
+ best_hits = len(observations)
+ best_span = span
+ passed = (
+ coverage >= float(event["minimum_coverage_fraction"])
+ and best_hits >= int(event["minimum_hits"])
+ and best_span >= float(event["minimum_span_seconds"])
+ )
+ outcomes.append(
+ {
+ "id": event["id"],
+ "kind": event["kind"],
+ "expected_motion": event["expected_motion"],
+ "class_group": event["class_group"],
+ "target_source_track_ids": sorted(targets),
+ "window_seconds": [start, end],
+ "passed": passed,
+ "source_observations": source_observations,
+ "evidence_observations": len(matched),
+ "duplicate_evidence_observations": duplicate_evidence_observations,
+ "coverage_fraction": coverage,
+ "best_track_id": best_track,
+ "best_hits": best_hits,
+ "best_span_seconds": best_span,
+ }
+ )
+ return {
+ "schema_version": "missioncore.e25-motion-benchmark-result/v1",
+ "passed": all(item["passed"] for item in outcomes),
+ "passed_events": sum(item["passed"] for item in outcomes),
+ "total_events": len(outcomes),
+ "events": outcomes,
+ }
+
+
+def build_persistent_support_result(
+ *,
+ source: IntegratedPerceptionResult,
+ lab_job: CameraComputeJob,
+ lab_pack: Path,
+ results_root: Path,
+ world_motion_profile_path: Path,
+ profile_path: Path,
+ benchmark_path: Path,
+) -> PersistentSupportBuild:
+ """Build one immutable E25 result from the accepted E19 source."""
+
+ if not source.accepted:
+ raise SessionIntegrityError("LAB E25 source result is not accepted")
+ world_profile, world_profile_sha256 = read_world_motion_profile(world_motion_profile_path)
+ profile, profile_sha256 = read_persistent_support_profile(profile_path)
+ benchmark, benchmark_sha256 = read_persistent_support_benchmark(benchmark_path)
+ if source.job.session_id != benchmark["source_session_id"]:
+ raise SessionIntegrityError("LAB E25 benchmark source session differs")
+ source_document = _read_object(source.result_root / "result.json")
+ source_report = _read_object(source.report_path)
+ source_fusion = _read_jsonl(source.fusion_path)
+ source_world = _read_jsonl(source.world_path)
+ if len(source_fusion) != source.frame_count or len(source_world) != source.frame_count:
+ raise SessionIntegrityError("LAB E25 source timelines are incomplete")
+ configuration = source_document["identity"].get("configuration")
+ association = (
+ configuration.get("profile", {}).get("association")
+ if isinstance(configuration, dict)
+ else None
+ )
+ if not isinstance(association, dict):
+ raise SessionIntegrityError("LAB E25 source association profile is unavailable")
+
+ world_tracker = WorldMotionTracker(world_profile)
+ occupancy_tracker = PersistentSupportTracker(profile)
+ extractor = RecordedSupportExtractor(
+ source=source,
+ lidar_pack=lab_pack,
+ association=association,
+ profile=profile,
+ )
+ fusion_rows: list[dict[str, Any]] = []
+ world_rows: list[dict[str, Any]] = []
+ frame_ms: list[float] = []
+ support_offsets = [0]
+ support_points: list[np.ndarray] = []
+ support_colors: list[np.ndarray] = []
+ try:
+ for index, (source_fusion_row, source_world_row) in enumerate(
+ zip(source_fusion, source_world, strict=True)
+ ):
+ started = time.perf_counter()
+ measurements = extractor.frame(index, source_fusion_row)
+ augmented = [
+ _augment_source_object(item, measurements) for item in source_fusion_row["objects"]
+ ]
+ session_seconds = float(source_fusion_row["session_seconds"])
+ fusion_objects, world_objects = world_tracker.update(
+ frame_index=int(source_fusion_row["frame_index"]),
+ session_seconds=session_seconds,
+ objects=augmented,
+ )
+ normalized_objects = _apply_occupancy_evidence(
+ fusion_objects,
+ measurements,
+ occupancy_tracker,
+ session_seconds,
+ )
+ normalized_fusion = json.loads(json.dumps(source_fusion_row))
+ normalized_fusion["objects"] = normalized_objects
+ normalized_fusion["world_motion"] = {
+ "schema_version": "missioncore.e25-persistent-support-frame/v1",
+ "coordinate_frame": "k1-map",
+ "lookahead_frames": 0,
+ "measurement": "persistent-object-support-occupancy",
+ }
+ normalized_world = json.loads(json.dumps(source_world_row))
+ normalized_world["objects"] = _update_world_objects(
+ world_objects,
+ normalized_objects,
+ )
+ normalized_world["object_count"] = len(normalized_world["objects"])
+ delivery = normalized_world.setdefault("delivery", {})
+ delivery["world_motion"] = "e25-persistent-support-dual-hypothesis"
+ elapsed = (time.perf_counter() - started) * 1000.0
+ delivery["world_motion_processing_ms"] = elapsed
+ fusion_rows.append(normalized_fusion)
+ world_rows.append(normalized_world)
+ frame_ms.append(elapsed)
+ frame_points, frame_colors = _support_arrays(measurements)
+ if frame_points.size:
+ support_points.append(frame_points)
+ support_colors.append(frame_colors)
+ support_offsets.append(support_offsets[-1] + frame_points.shape[0])
+ finally:
+ extractor.close()
+
+ benchmark_result = evaluate_persistent_support_benchmark(
+ source_fusion,
+ fusion_rows,
+ benchmark,
+ )
+ runtime = _percentiles(frame_ms)
+ tracker_metrics = occupancy_tracker.snapshot()
+ checks = {
+ "source_payloads_immutable": True,
+ "diagnostic_authority_only": True,
+ "single_pass_no_lookahead": True,
+ "track_state_bound": tracker_metrics["peak_tracks"]
+ <= int(profile["acceptance"]["maximum_tracks_observed"]),
+ "processing_p95_ms": runtime["p95"]
+ <= float(profile["acceptance"]["maximum_processing_p95_ms"]),
+ }
+ accepted = all(checks.values())
+ source_identity = source_document["identity"]
+ result_configuration = {
+ "pipeline": PIPELINE_ID,
+ "world_motion_profile_sha256": world_profile_sha256,
+ "world_motion_profile": world_profile,
+ "profile_sha256": profile_sha256,
+ "profile": profile,
+ "implementation_sha256": _sha256(Path(__file__).resolve()),
+ "benchmark_sha256": benchmark_sha256,
+ "benchmark": benchmark,
+ "source_result_id": source.result_id,
+ "source_result_sha256": _sha256(source.result_root / "result.json"),
+ "source_session_id": source.job.session_id,
+ "processing": "single-pass-bounded-state-no-lookahead",
+ }
+ identity = {
+ "schema_version": "missioncore.e10-integrated-perception-identity/v1",
+ "job_id": lab_job.job_id,
+ "input_sha256": lab_job.input_sha256,
+ "session_id": lab_job.session_id,
+ "source_id": lab_job.source_id,
+ "lidar_pack_id": lab_pack.name,
+ "selection": json.loads(json.dumps(source_identity["selection"])),
+ "configuration": result_configuration,
+ "models": source_identity["models"],
+ }
+ identity_sha256 = hashlib.sha256(_canonical(identity)).hexdigest()
+ result_id = f"e10-integrated-perception-{identity_sha256}"
+ destination = results_root / result_id
+ created_at = (
+ datetime.now(UTC)
+ .isoformat(timespec="milliseconds")
+ .replace(
+ "+00:00",
+ "Z",
+ )
+ )
+ report = {
+ "schema_version": "missioncore.e10-integrated-perception-report/v1",
+ "result_id": result_id,
+ "created_at_utc": created_at,
+ "state": "accepted" if accepted else "rejected",
+ "ground_truth": False,
+ "identity": identity,
+ "acceptance": {
+ "accepted": accepted,
+ "navigation_or_safety_accepted": False,
+ "checks": checks,
+ },
+ "metrics": {
+ "runtime": {
+ "persistent_support_frame_processing_ms": runtime,
+ **tracker_metrics,
+ **extractor.snapshot(),
+ },
+ "benchmark": benchmark_result,
+ "source_realtime_envelope": source_report.get("metrics", {}),
+ },
+ "runtime": {
+ "executor": "local-deterministic-postprocessor",
+ "numpy": np.__version__,
+ "streaming_equivalent": True,
+ "lookahead_frames": 0,
+ },
+ "limitations": [
+ "Recorded diagnostic qualification only; no navigation or command authority.",
+ (
+ "Operator timestamps and detector-track bindings are reviewed annotations, "
+ "not ground truth."
+ ),
+ (
+ "Unknown remains an occupied obstacle and is intentional when motion "
+ "evidence is insufficient."
+ ),
+ (
+ "Motion uses persistent map-frame support overlap; the completed cuboid "
+ "center is not the sole measurement."
+ ),
+ "Raw LiDAR, ground returns, camera media, calibration and model outputs are unchanged.",
+ ],
+ }
+ if destination.exists():
+ existing = _read_object(destination / "result.json")
+ if existing.get("identity") != identity:
+ raise SessionIntegrityError("LAB E25 result id collides")
+ return PersistentSupportBuild(
+ destination,
+ profile_sha256,
+ benchmark_sha256,
+ _read_object(destination / "run-report.json"),
+ )
+
+ results_root.mkdir(mode=0o700, parents=True, exist_ok=True)
+ staging = results_root / f".{result_id}.{secrets.token_hex(12)}.tmp"
+ staging.mkdir(mode=0o700)
+ try:
+ semantic_path = staging / "semantic-frames.jsonl"
+ fusion_path = staging / "fusion-frames.jsonl"
+ world_path = staging / "world-state.jsonl"
+ arrays_path = staging / "transient-perception.npz"
+ gpu_path = staging / "gpu-telemetry.jsonl"
+ report_path = staging / "run-report.json"
+ os.link(source.semantic_path, semantic_path, follow_symlinks=False)
+ _write_jsonl(fusion_path, fusion_rows)
+ _write_jsonl(world_path, world_rows)
+ box_offsets, centers, half_sizes, quaternions, box_colors = _box_arrays(fusion_rows)
+ all_support = (
+ np.concatenate(support_points).astype(np.float32, copy=False)
+ if support_points
+ else np.empty((0, 3), dtype=np.float32)
+ )
+ all_support_colors = (
+ np.concatenate(support_colors).astype(np.uint8, copy=False)
+ if support_colors
+ else np.empty((0, 3), dtype=np.uint8)
+ )
+ _rewrite_npz_boxes(
+ source.arrays_path,
+ arrays_path,
+ {
+ "support_offsets": np.asarray(support_offsets, dtype=np.int64),
+ "support_points": all_support,
+ "support_colors": all_support_colors,
+ "box_offsets": box_offsets,
+ "box_centers": centers,
+ "box_half_sizes": half_sizes,
+ "box_quaternions": quaternions,
+ "box_colors": box_colors,
+ },
+ )
+ os.link(
+ source.result_root / "gpu-telemetry.jsonl",
+ gpu_path,
+ follow_symlinks=False,
+ )
+ write_json_atomic(report_path, report)
+ artifacts = [
+ _artifact(
+ "e10-semantic-frames",
+ semantic_path,
+ "missioncore.e10-semantic-frame/v1",
+ ),
+ _artifact(
+ "e10-fusion-frames",
+ fusion_path,
+ "missioncore.e10-fusion-frame/v1",
+ ),
+ _artifact(
+ "e10-world-state",
+ world_path,
+ "missioncore.live-perception-world-state/v1",
+ ),
+ _artifact("e10-transient-perception", arrays_path, None),
+ _artifact("worker-gpu-telemetry", gpu_path, None),
+ _artifact(
+ "e10-run-report",
+ report_path,
+ "missioncore.e10-integrated-perception-report/v1",
+ ),
+ ]
+ write_json_atomic(
+ staging / "result.json",
+ {
+ "schema_version": "missioncore.e10-integrated-perception-result/v1",
+ "result_id": result_id,
+ "identity_sha256": identity_sha256,
+ "identity": identity,
+ "created_at_utc": created_at,
+ "ground_truth": False,
+ "publication_scope": "recorded-integrated-realtime-qualification-only",
+ "acceptance_state": "accepted" if accepted else "rejected",
+ "frames_processed": source.frame_count,
+ "artifacts": artifacts,
+ },
+ )
+ os.replace(staging, destination)
+ finally:
+ if staging.exists():
+ shutil.rmtree(staging)
+ return PersistentSupportBuild(
+ destination,
+ profile_sha256,
+ benchmark_sha256,
+ report,
+ )
+
+
+def _augment_source_object(
+ source: dict[str, Any],
+ measurements: dict[int, SupportMeasurement],
+) -> dict[str, Any]:
+ normalized: dict[str, Any] = json.loads(json.dumps(source))
+ source_track_id = int(source.get("source_track_id", source["track_id"]))
+ measurement = measurements.get(source_track_id)
+ if measurement is None:
+ return normalized
+ normalized["e25_source_clustered_points"] = normalized.get("clustered_points", 0)
+ normalized["clustered_points"] = int(measurement.points_map.shape[0])
+ if not _valid_vector(normalized.get("observed_cuboid_center_map"), 3):
+ normalized["observed_cuboid_center_map"] = np.median(
+ measurement.points_map,
+ axis=0,
+ ).tolist()
+ normalized["observed_cuboid_quaternion_xyzw"] = [0.0, 0.0, 0.0, 1.0]
+ normalized["e25_support_recovered"] = True
+ return normalized
+
+
+def _apply_occupancy_evidence(
+ objects: list[dict[str, Any]],
+ measurements: dict[int, SupportMeasurement],
+ tracker: PersistentSupportTracker,
+ session_seconds: float,
+) -> list[dict[str, Any]]:
+ result: list[dict[str, Any]] = []
+ for source in objects:
+ item = json.loads(json.dumps(source))
+ source_track_id = item.get("source_track_id")
+ track_id = item.get("track_id")
+ evidence: dict[str, Any] | None = None
+ if isinstance(source_track_id, int) and isinstance(track_id, int):
+ measurement = measurements.get(source_track_id)
+ if measurement is not None and item.get("temporal_status") == "e24-observed":
+ evidence = tracker.observe(
+ track_id=track_id,
+ source_track_id=source_track_id,
+ group=measurement.group,
+ session_seconds=session_seconds,
+ points_map=measurement.points_map,
+ )
+ else:
+ evidence = tracker.hold(track_id, session_seconds)
+ if evidence is None:
+ item["motion_state"] = "unknown"
+ item["motion_confidence"] = 0.0
+ item["motion_status"] = "e25-unavailable-no-persistent-support"
+ item["occupancy_evidence_current"] = False
+ else:
+ item.update(evidence)
+ item["temporal_status"] = (
+ "e25-support-observed"
+ if evidence["occupancy_evidence_current"]
+ else "e25-support-held"
+ )
+ result.append(item)
+ return result
+
+
+def _update_world_objects(
+ world_objects: list[dict[str, Any]],
+ fusion_objects: list[dict[str, Any]],
+) -> list[dict[str, Any]]:
+ by_track = {
+ int(item["track_id"]): item
+ for item in fusion_objects
+ if isinstance(item.get("track_id"), int) and isinstance(item.get("source_track_id"), int)
+ }
+ result: list[dict[str, Any]] = []
+ for source in world_objects:
+ item = json.loads(json.dumps(source))
+ fusion = by_track.get(int(item["track_id"]))
+ if fusion is None:
+ item["motion_state"] = "unknown"
+ item["motion_confidence"] = 0.0
+ item["velocity_status"] = "e25-unavailable-no-persistent-support"
+ result.append(item)
+ continue
+ for key in (
+ "motion_state",
+ "motion_confidence",
+ "speed_mps",
+ "velocity_map_mps",
+ "occupancy_overlap_fraction",
+ "occupancy_displacement_m",
+ "occupancy_direction_consistency",
+ "occupancy_support_observations",
+ "occupancy_cell_count",
+ "occupancy_resolution_m",
+ "occupancy_footprint_map_xy",
+ "occupancy_evidence_current",
+ "occupancy_observation_age_ms",
+ "source_track_aliases",
+ ):
+ item[key] = fusion.get(key)
+ item["velocity_status"] = fusion["motion_status"]
+ item["temporal_status"] = fusion["temporal_status"]
+ result.append(item)
+ return result
+
+
+def _support_arrays(
+ measurements: dict[int, SupportMeasurement],
+) -> tuple[np.ndarray, np.ndarray]:
+ points: list[np.ndarray] = []
+ colors: list[np.ndarray] = []
+ for source_track_id, measurement in sorted(measurements.items()):
+ values = measurement.points_map.astype(np.float32, copy=False)
+ seed = hashlib.sha256(f"e25:{measurement.group}:{source_track_id}".encode()).digest()
+ color = np.asarray(
+ [[64 + value % 176 for value in seed[:3]]],
+ dtype=np.uint8,
+ )
+ points.append(values)
+ colors.append(np.tile(color, (values.shape[0], 1)))
+ if not points:
+ return (
+ np.empty((0, 3), dtype=np.float32),
+ np.empty((0, 3), dtype=np.uint8),
+ )
+ return np.concatenate(points), np.concatenate(colors)
+
+
+def _event_class_matches(
+ item: dict[str, Any],
+ event: dict[str, Any],
+) -> bool:
+ group = str(
+ item.get(
+ "association_group",
+ item.get("class", item.get("label", "object")),
+ )
+ )
+ if event["class_group"] == "vulnerable_road_user":
+ return group in {"person", "bicycle", "motorcycle"}
+ return group == str(event["class_group"])
+
+
+def _benchmark_frame_key(row: dict[str, Any], timestamp: float) -> int:
+ frame_index = row.get("frame_index")
+ if isinstance(frame_index, int):
+ return frame_index
+ return round(timestamp * 1_000_000)
+
+
+def _benchmark_evidence_rank(item: dict[str, Any]) -> tuple[int, int, float, int]:
+ return (
+ int(item.get("occupancy_cell_count", 0)),
+ int(item.get("occupancy_support_observations", 0)),
+ float(item.get("motion_confidence", 0.0)),
+ -int(item.get("track_id", 0)),
+ )
+
+
+def _visible_projection_indices(
+ pixels: FloatArray,
+ depths: FloatArray,
+ width: int,
+) -> IntArray:
+ if depths.size == 0:
+ return np.empty((0,), dtype=np.int64)
+ px = np.rint(pixels[:, 0]).astype(np.int64)
+ py = np.rint(pixels[:, 1]).astype(np.int64)
+ keys = py * width + px
+ order = np.lexsort((depths, keys))
+ ordered_keys = keys[order]
+ first = np.concatenate(([True], ordered_keys[1:] != ordered_keys[:-1]))
+ return np.sort(order[first]).astype(np.int64, copy=False)
+
+
+def _depth_cluster(
+ indices: IntArray,
+ depths: FloatArray,
+ profile: Mapping[str, Any],
+) -> IntArray:
+ if indices.size < 2:
+ return indices
+ ordered = indices[np.argsort(depths[indices])]
+ groups: list[IntArray] = []
+ start = 0
+ for offset, gap in enumerate(np.diff(depths[ordered]), start=1):
+ threshold = max(
+ float(profile["depth_cluster_minimum_gap_m"]),
+ float(profile["depth_cluster_gap_fraction"]) * float(depths[ordered[offset - 1]]),
+ )
+ if float(gap) > threshold:
+ groups.append(ordered[start:offset])
+ start = offset
+ groups.append(ordered[start:])
+ return min(
+ groups,
+ key=lambda group: (-int(group.size), float(np.median(depths[group]))),
+ )
+
+
+def _spatial_cluster(
+ indices: IntArray,
+ points_lidar: FloatArray,
+ radius: float,
+) -> IntArray:
+ if indices.size < 2:
+ return indices
+ points = points_lidar[indices]
+ adjacent = np.sum((points[:, None, :] - points[None, :, :]) ** 2, axis=2) <= radius * radius
+ unseen = set(range(indices.size))
+ groups: list[list[int]] = []
+ while unseen:
+ seed = unseen.pop()
+ group = [seed]
+ pending = [seed]
+ while pending:
+ current = pending.pop()
+ for neighbor in [value for value in tuple(unseen) if adjacent[current, value]]:
+ unseen.remove(neighbor)
+ pending.append(neighbor)
+ group.append(neighbor)
+ groups.append(group)
+ selected = min(
+ groups,
+ key=lambda group: (
+ -len(group),
+ float(np.median(np.linalg.norm(points[group], axis=1))),
+ ),
+ )
+ return indices[np.asarray(selected, dtype=np.int64)]
+
+
+def _filter_support_by_ground(
+ indices: IntArray,
+ points_map: FloatArray,
+ *,
+ group: str,
+ profile: Mapping[str, Any],
+) -> IntArray:
+ if indices.size == 0:
+ return indices
+ support = points_map[indices]
+ support_lower = float(np.percentile(support[:, 2], 5.0))
+ center = np.median(support[:, :2], axis=0)
+ distances = np.linalg.norm(points_map[:, :2] - center, axis=1)
+ candidates = points_map[distances <= float(profile["local_radius_m"]), 2]
+ ground = (
+ float(np.percentile(candidates, float(profile["lower_percentile"])))
+ if candidates.size
+ else math.nan
+ )
+ if (
+ not math.isfinite(ground)
+ or ground < support_lower - float(profile["maximum_below_support_m"])
+ or ground > support_lower + float(profile["maximum_above_support_m"])
+ ):
+ ground = support_lower - float(profile["fallback_below_support_m"])
+ minimum = ground + float(profile["minimum_height_above_ground_m"][group])
+ maximum_height = ground + float(profile["maximum_height_above_ground_m"][group])
+ keep = (support[:, 2] >= minimum) & (support[:, 2] <= maximum_height)
+ return indices[keep]
+
+
+def _suppress_duplicate_measurements(
+ measurements: Sequence[SupportMeasurement],
+ *,
+ overlap_threshold: float,
+) -> list[SupportMeasurement]:
+ accepted: list[SupportMeasurement] = []
+ for item in sorted(
+ measurements,
+ key=lambda value: (-value.score, value.source_track_id),
+ ):
+ if any(
+ other.group == item.group
+ and _index_overlap(other.source_indices, item.source_indices) >= overlap_threshold
+ for other in accepted
+ ):
+ continue
+ accepted.append(item)
+ return accepted
+
+
+def _index_overlap(one: IntArray, two: IntArray) -> float:
+ if one.size == 0 or two.size == 0:
+ return 0.0
+ shared = np.intersect1d(one, two, assume_unique=False).size
+ return float(shared / min(one.size, two.size))
+
+
+def _voxel_cells(
+ points: FloatArray,
+ *,
+ voxel_size: float,
+ maximum: int,
+) -> frozenset[Cell]:
+ values = sorted(
+ {
+ (int(value[0]), int(value[1]))
+ for value in np.floor(points[:, :2] / voxel_size).astype(np.int64)
+ }
+ )
+ if len(values) > maximum:
+ step = len(values) / maximum
+ values = [values[math.floor(index * step)] for index in range(maximum)]
+ return frozenset(values)
+
+
+def _cell_overlap(
+ current: frozenset[Cell],
+ reference: set[Cell],
+ *,
+ radius: int,
+) -> float:
+ if not current or not reference:
+ return 0.0
+ expanded = {
+ (x + delta_x, y + delta_y)
+ for x, y in reference
+ for delta_x in range(-radius, radius + 1)
+ for delta_y in range(-radius, radius + 1)
+ }
+ return sum(cell in expanded for cell in current) / len(current)
+
+
+def _footprint(
+ cells: frozenset[Cell],
+ voxel_size: float,
+) -> list[list[float]] | None:
+ if not cells:
+ return None
+ x_values = [value[0] for value in cells]
+ y_values = [value[1] for value in cells]
+ minimum_x = min(x_values) * voxel_size
+ minimum_y = min(y_values) * voxel_size
+ maximum_x = (max(x_values) + 1) * voxel_size
+ maximum_y = (max(y_values) + 1) * voxel_size
+ return [
+ [minimum_x, minimum_y],
+ [maximum_x, minimum_y],
+ [maximum_x, maximum_y],
+ [minimum_x, maximum_y],
+ ]
+
+
+def _median_velocity(
+ history: deque[tuple[float, np.ndarray]],
+) -> np.ndarray | None:
+ if len(history) < 3:
+ return None
+ values = list(history)
+ velocities: list[np.ndarray] = []
+ for start in range(len(values) - 1):
+ for end in range(start + 1, len(values)):
+ delta = values[end][0] - values[start][0]
+ if delta < 0.35:
+ continue
+ velocities.append((values[end][1] - values[start][1]) / delta)
+ if not velocities:
+ return None
+ return np.asarray(
+ np.median(np.stack(velocities), axis=0),
+ dtype=np.float64,
+ )
+
+
+def _group_value(values: Mapping[str, Any], group: str) -> float:
+ return float(values.get(group, values["default"]))
+
+
+def _valid_vector(value: Any, length: int) -> bool:
+ return (
+ isinstance(value, list)
+ and len(value) == length
+ and all(
+ isinstance(item, int | float)
+ and not isinstance(item, bool)
+ and math.isfinite(float(item))
+ for item in value
+ )
+ )
diff --git a/src/k1link/device_plugins/xgrids_k1/cli.py b/src/k1link/device_plugins/xgrids_k1/cli.py
index dbcc591..3bea462 100644
--- a/src/k1link/device_plugins/xgrids_k1/cli.py
+++ b/src/k1link/device_plugins/xgrids_k1/cli.py
@@ -23,6 +23,7 @@ from k1link.compute import (
publish_e22_lab_instance,
publish_e23_lab_instance,
publish_e24_lab_instance,
+ publish_e25_lab_instance,
publish_integrated_lab_instance,
)
from k1link.device_plugins.xgrids_k1.analyze import (
@@ -656,6 +657,92 @@ def publish_e24_lab(
)
+@lab_app.command("publish-e25")
+def publish_e25_lab(
+ result: Annotated[
+ Path,
+ typer.Option(
+ exists=True,
+ file_okay=False,
+ readable=True,
+ resolve_path=True,
+ help="Accepted full-session integrated result used as the E25 source.",
+ ),
+ ],
+ world_profile: Annotated[
+ Path,
+ typer.Option(
+ "--world-profile",
+ exists=True,
+ dir_okay=False,
+ readable=True,
+ resolve_path=True,
+ help="Bounded E24 world-association profile used before E25 evidence.",
+ ),
+ ],
+ profile: Annotated[
+ Path,
+ typer.Option(
+ exists=True,
+ dir_okay=False,
+ readable=True,
+ resolve_path=True,
+ help="Bounded E25 persistent-support profile.",
+ ),
+ ],
+ benchmark: Annotated[
+ Path,
+ typer.Option(
+ exists=True,
+ dir_okay=False,
+ readable=True,
+ resolve_path=True,
+ help="Spatially bound E25 motion benchmark.",
+ ),
+ ],
+ session_id: Annotated[
+ str,
+ typer.Option("--session-id", help="New immutable LAB session id."),
+ ],
+ lab_id: Annotated[
+ str,
+ typer.Option("--lab-id", help="LAB marker, for example 'LAB E25.1'."),
+ ],
+ display_name: Annotated[
+ str,
+ typer.Option("--display-name", help="Operator-facing saved-session title."),
+ ],
+) -> None:
+ """Build and publish map-frame persistent-support motion evidence."""
+
+ repository_root = Path(__file__).resolve().parents[4]
+ try:
+ published = publish_e25_lab_instance(
+ repository_root=repository_root,
+ source_result_root=result,
+ world_motion_profile_path=world_profile,
+ profile_path=profile,
+ benchmark_path=benchmark,
+ lab_session_id=session_id,
+ lab_id=lab_id,
+ display_name=display_name,
+ )
+ except (OSError, SessionIntegrityError, RuntimeError, ValueError) as exc:
+ console.print(f"[red]E25 LAB publication failed:[/red] {exc}")
+ raise typer.Exit(code=2) from exc
+ metrics = published.build.report["metrics"]
+ console.print(
+ "[green]E25 LAB instance published.[/green] "
+ f"session={published.binding.session_id}; "
+ f"source={published.binding.source_session_id}; "
+ f"result={published.binding.result_id}; "
+ f"benchmark={metrics['benchmark']['passed_events']}/"
+ f"{metrics['benchmark']['total_events']}; "
+ f"p95={metrics['runtime']['persistent_support_frame_processing_ms']['p95']:.3f}ms; "
+ "source_payloads_mutated=false"
+ )
+
+
@app.command("serve")
def serve_console(
port: Annotated[
diff --git a/tests/test_e10_integrated_perception.py b/tests/test_e10_integrated_perception.py
index 9204563..07420a6 100644
--- a/tests/test_e10_integrated_perception.py
+++ b/tests/test_e10_integrated_perception.py
@@ -267,6 +267,30 @@ def test_recorded_cuboid_presentation_holds_one_failed_association_then_expires(
assert expired is None
+def test_recorded_cuboid_presentation_omits_unavailable_distance() -> None:
+ state = _CuboidPresentationState(hold_ns=500_000_000)
+ accepted = {
+ "association_group": "person",
+ "clustered_points": 3,
+ "cuboid_status": "accepted-world-track-provisional-e24-v1",
+ "distance_smoothed_m": None,
+ "label": "person",
+ "track_id": 9,
+ }
+
+ presented = state.update(
+ 1_000_000_000,
+ [accepted],
+ np.asarray([[1.0, 2.0, 3.0]], dtype=np.float32),
+ np.asarray([[0.35, 0.35, 0.9]], dtype=np.float32),
+ np.asarray([[0.0, 0.0, 0.0, 1.0]], dtype=np.float32),
+ np.asarray([[118, 204, 132, 88]], dtype=np.uint8),
+ )
+
+ assert presented is not None
+ assert presented[4] == ["person #9 person · 3 pts"]
+
+
def test_e13_completes_a_visible_car_face_away_from_the_sensor() -> None:
fusion, runner = _worker_modules()
profile, _digest = runner.read_profile(
diff --git a/tests/test_e25_occupancy_motion.py b/tests/test_e25_occupancy_motion.py
new file mode 100644
index 0000000..402ceba
--- /dev/null
+++ b/tests/test_e25_occupancy_motion.py
@@ -0,0 +1,302 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+import numpy as np
+
+from k1link.compute.occupancy_motion import (
+ PersistentSupportTracker,
+ SupportMeasurement,
+ _apply_occupancy_evidence,
+ evaluate_persistent_support_benchmark,
+ read_persistent_support_benchmark,
+ read_persistent_support_profile,
+)
+
+
+def _profile() -> dict[str, object]:
+ root = Path(__file__).resolve().parents[1]
+ profile, digest = read_persistent_support_profile(
+ root / "experiments" / "perception" / "e25_persistent_support_profile.json"
+ )
+ assert len(digest) == 64
+ return profile
+
+
+def _support(x: float, y: float = 2.0) -> np.ndarray:
+ return np.asarray(
+ [
+ [x - 0.2, y - 0.2, 0.4],
+ [x + 0.2, y - 0.2, 0.5],
+ [x - 0.2, y + 0.2, 0.6],
+ [x + 0.2, y + 0.2, 0.7],
+ ],
+ dtype=np.float64,
+ )
+
+
+def test_e25_profile_is_bounded_and_has_no_control_authority() -> None:
+ profile = _profile()
+
+ assert profile["source"]["coordinate_frame"] == "k1-map"
+ assert profile["bounds"]["maximum_tracks"] == 192
+ assert profile["occupancy"]["maximum_snapshots_per_track"] == 32
+ assert profile["authority"] == {
+ "commands_enabled": False,
+ "navigation_or_safety_accepted": False,
+ }
+
+
+def test_e25_static_support_survives_visible_surface_center_jitter() -> None:
+ tracker = PersistentSupportTracker(_profile())
+ evidence = {}
+ for frame in range(30):
+ jitter = 0.18 if frame % 2 else -0.18
+ evidence = tracker.observe(
+ track_id=250001,
+ source_track_id=10,
+ group="vehicle",
+ session_seconds=frame * 0.1,
+ points_map=_support(10.0 + jitter),
+ )
+
+ assert evidence["motion_state"] == "static"
+ assert evidence["occupancy_overlap_fraction"] is not None
+ assert evidence["occupancy_overlap_fraction"] >= 0.55
+
+
+def test_e25_coherent_translation_becomes_dynamic() -> None:
+ tracker = PersistentSupportTracker(_profile())
+ evidence = {}
+ for frame in range(20):
+ evidence = tracker.observe(
+ track_id=250002,
+ source_track_id=20,
+ group="person",
+ session_seconds=frame * 0.1,
+ points_map=_support(4.0 + frame * 0.5),
+ )
+
+ assert evidence["motion_state"] == "dynamic"
+ assert evidence["speed_mps"] is not None
+ assert evidence["speed_mps"] > 1.0
+ assert evidence["occupancy_overlap_fraction"] <= 0.25
+
+
+def test_e25_does_not_claim_motion_without_older_support() -> None:
+ tracker = PersistentSupportTracker(_profile())
+
+ evidence = tracker.observe(
+ track_id=250003,
+ source_track_id=30,
+ group="vehicle",
+ session_seconds=0.0,
+ points_map=_support(8.0),
+ )
+
+ assert evidence["motion_state"] == "unknown"
+ assert evidence["motion_confidence"] == 0.0
+
+
+def test_e25_current_support_is_not_attached_to_an_e24_held_hypothesis() -> None:
+ tracker = PersistentSupportTracker(_profile())
+ measurement = SupportMeasurement(
+ source_track_id=40,
+ label="car",
+ group="vehicle",
+ score=0.9,
+ points_map=_support(10.0),
+ source_indices=np.asarray([1, 2, 3, 4], dtype=np.int64),
+ )
+ _apply_occupancy_evidence(
+ [
+ {
+ "track_id": 250040,
+ "source_track_id": 40,
+ "temporal_status": "e24-observed",
+ }
+ ],
+ {40: measurement},
+ tracker,
+ 0.0,
+ )
+ result = _apply_occupancy_evidence(
+ [
+ {
+ "track_id": 250041,
+ "source_track_id": 40,
+ "temporal_status": "e24-observed",
+ },
+ {
+ "track_id": 250040,
+ "source_track_id": 40,
+ "temporal_status": "e24-held-prediction",
+ },
+ ],
+ {40: measurement},
+ tracker,
+ 0.1,
+ )
+
+ assert result[0]["occupancy_evidence_current"] is True
+ assert result[1]["occupancy_evidence_current"] is False
+ assert tracker.snapshot()["support_observations"] == 2
+
+
+def test_e25_target_benchmark_cannot_be_satisfied_by_another_object() -> None:
+ event = {
+ "id": "target-five",
+ "kind": "target-motion",
+ "window_seconds": [10.0, 12.0],
+ "class_group": "vehicle",
+ "target_source_track_ids": [5],
+ "expected_motion": "dynamic",
+ "minimum_hits": 2,
+ "minimum_span_seconds": 0.1,
+ "minimum_coverage_fraction": 0.5,
+ }
+ source_rows = [
+ {
+ "session_seconds": 10.0 + index * 0.2,
+ "objects": [
+ {
+ "track_id": 5,
+ "association_group": "vehicle",
+ }
+ ],
+ }
+ for index in range(3)
+ ]
+ wrong_fusion_rows = [
+ {
+ "session_seconds": 10.0 + index * 0.2,
+ "objects": [
+ {
+ "track_id": 250099,
+ "source_track_id": 99,
+ "association_group": "vehicle",
+ "motion_state": "dynamic",
+ "occupancy_evidence_current": True,
+ }
+ ],
+ }
+ for index in range(3)
+ ]
+
+ result = evaluate_persistent_support_benchmark(
+ source_rows,
+ wrong_fusion_rows,
+ {"events": [event]},
+ )
+
+ assert result["passed"] is False
+ assert result["events"][0]["evidence_observations"] == 0
+
+
+def test_e25_benchmark_deduplicates_world_hypotheses_for_one_source_object() -> None:
+ event = {
+ "id": "target-five",
+ "kind": "target-motion",
+ "window_seconds": [10.0, 12.0],
+ "class_group": "vehicle",
+ "target_source_track_ids": [5],
+ "expected_motion": "dynamic",
+ "minimum_hits": 2,
+ "minimum_span_seconds": 0.1,
+ "minimum_coverage_fraction": 0.5,
+ }
+ source_rows = [
+ {
+ "frame_index": index,
+ "session_seconds": 10.0 + index * 0.2,
+ "objects": [{"track_id": 5, "association_group": "vehicle"}],
+ }
+ for index in range(3)
+ ]
+ fusion_rows = [
+ {
+ "frame_index": index,
+ "session_seconds": 10.0 + index * 0.2,
+ "objects": [
+ {
+ "track_id": world_track,
+ "source_track_id": 5,
+ "association_group": "vehicle",
+ "motion_state": "dynamic",
+ "motion_confidence": 0.8,
+ "occupancy_cell_count": 4,
+ "occupancy_support_observations": 6,
+ "occupancy_evidence_current": True,
+ }
+ for world_track in (250005, 250006)
+ ],
+ }
+ for index in range(3)
+ ]
+
+ result = evaluate_persistent_support_benchmark(
+ source_rows,
+ fusion_rows,
+ {"events": [event]},
+ )
+
+ assert result["events"][0]["coverage_fraction"] == 1.0
+ assert result["events"][0]["evidence_observations"] == 3
+ assert result["events"][0]["duplicate_evidence_observations"] == 3
+
+
+def test_e25_static_control_rejects_excess_false_dynamic_evidence() -> None:
+ event = {
+ "id": "parked-vehicles",
+ "kind": "static-control",
+ "window_seconds": [20.0, 22.0],
+ "class_group": "vehicle",
+ "expected_motion": "static",
+ "minimum_source_observations": 3,
+ "minimum_coverage_fraction": 0.5,
+ "minimum_classified_fraction": 0.5,
+ "maximum_false_dynamic_fraction": 0.1,
+ }
+ source_rows = [
+ {
+ "session_seconds": 20.0 + index * 0.2,
+ "objects": [{"track_id": 7, "association_group": "vehicle"}],
+ }
+ for index in range(3)
+ ]
+ fusion_rows = [
+ {
+ "session_seconds": 20.0 + index * 0.2,
+ "objects": [
+ {
+ "track_id": 250007,
+ "source_track_id": 7,
+ "association_group": "vehicle",
+ "motion_state": "dynamic",
+ "occupancy_evidence_current": True,
+ }
+ ],
+ }
+ for index in range(3)
+ ]
+
+ result = evaluate_persistent_support_benchmark(
+ source_rows,
+ fusion_rows,
+ {"events": [event]},
+ )
+
+ assert result["passed"] is False
+ assert result["events"][0]["false_dynamic_fraction"] == 1.0
+
+
+def test_e25_benchmark_contract_uses_reviewed_source_track_bindings() -> None:
+ root = Path(__file__).resolve().parents[1]
+ benchmark, digest = read_persistent_support_benchmark(
+ root / "experiments" / "perception" / "e25_motion_benchmark.json"
+ )
+ target_events = [event for event in benchmark["events"] if event["kind"] == "target-motion"]
+
+ assert len(digest) == 64
+ assert target_events
+ assert all(event["target_source_track_ids"] for event in target_events)