feat(perception): add persistent lidar motion evidence

This commit is contained in:
DCCONSTRUCTIONS 2026-07-24 12:49:11 +03:00
parent e6dd8c2cf1
commit 230cba4b21
10 changed files with 2496 additions and 4 deletions

View File

@ -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.421857292484.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.32.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 7090 s;
- parked vehicles at 132148 s;
- parked vehicles at 161166 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:
- 7090 s: coverage 0.4619, classified fraction 0.77665,
306 static, 88 unknown, false-dynamic fraction 0.0;
- 132148 s: coverage 0.5683, classified fraction 0.63948,
298 static, 168 unknown, false-dynamic fraction 0.0;
- 161166 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.84.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:
<https://autowarefoundation.github.io/autoware_universe/pr-10143/perception/autoware_multi_object_tracker/>
- Autoware detection-by-tracker:
<https://autowarefoundation.github.io/autoware_universe/main/perception/autoware_detection_by_tracker/>
- Autoware probabilistic occupancy grid:
<https://autowarefoundation.github.io/autoware_universe/pr-10077/perception/autoware_probabilistic_occupancy_grid_map/>
- Autoware obstacle point-cloud validator:
<https://autowarefoundation.github.io/autoware_universe/pr-10075/perception/autoware_detected_object_validation/obstacle-pointcloud-based-validator/>
- Autoware perception architecture:
<https://autowarefoundation.github.io/autoware-documentation/main/design/autoware-architecture-v1/components/perception/>

View File

@ -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
}
]
}

View File

@ -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
}
}

View File

@ -36,12 +36,14 @@ from .jobs import (
) )
from .lab_instances import ( from .lab_instances import (
PublishedIntegratedLabInstance, PublishedIntegratedLabInstance,
PublishedPersistentSupportLabInstance,
PublishedTemporalLabInstance, PublishedTemporalLabInstance,
PublishedWorldMotionLabInstance, PublishedWorldMotionLabInstance,
publish_e21_lab_instance, publish_e21_lab_instance,
publish_e22_lab_instance, publish_e22_lab_instance,
publish_e23_lab_instance, publish_e23_lab_instance,
publish_e24_lab_instance, publish_e24_lab_instance,
publish_e25_lab_instance,
publish_integrated_lab_instance, publish_integrated_lab_instance,
) )
from .live_perception import ( from .live_perception import (
@ -120,6 +122,7 @@ __all__ = [
"LivePerceptionIngress", "LivePerceptionIngress",
"IntegratedPerceptionOverlayStore", "IntegratedPerceptionOverlayStore",
"PublishedIntegratedLabInstance", "PublishedIntegratedLabInstance",
"PublishedPersistentSupportLabInstance",
"PublishedTemporalLabInstance", "PublishedTemporalLabInstance",
"PublishedWorldMotionLabInstance", "PublishedWorldMotionLabInstance",
"IntegratedPerceptionResult", "IntegratedPerceptionResult",
@ -154,6 +157,7 @@ __all__ = [
"publish_e22_lab_instance", "publish_e22_lab_instance",
"publish_e23_lab_instance", "publish_e23_lab_instance",
"publish_e24_lab_instance", "publish_e24_lab_instance",
"publish_e25_lab_instance",
"publish_integrated_lab_instance", "publish_integrated_lab_instance",
"validate_multirate_perception_qualification_result", "validate_multirate_perception_qualification_result",
"prepare_recorded_qualification_slice", "prepare_recorded_qualification_slice",

View File

@ -73,7 +73,7 @@ class _PresentedCuboid:
track_id: int track_id: int
label: str label: str
association_group: str association_group: str
distance_m: float distance_m: float | None
support_points: int support_points: int
center: np.ndarray center: np.ndarray
half_size: np.ndarray half_size: np.ndarray
@ -126,12 +126,20 @@ class _CuboidPresentationState:
raise RecordedPerceptionOverlayError( raise RecordedPerceptionOverlayError(
"integrated perception cuboid track identity is invalid" "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( self._latest[track_id] = _PresentedCuboid(
observed_ns=timestamp_ns, observed_ns=timestamp_ns,
track_id=track_id, track_id=track_id,
label=str(item.get("label", "object")), label=str(item.get("label", "object")),
association_group=str(item.get("association_group", "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"]), support_points=int(item["clustered_points"]),
center=np.asarray(center).copy(), center=np.asarray(center).copy(),
half_size=np.asarray(half_size).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))) color[3] = max(24, round(float(color[3]) * (1.0 - 0.55 * fade)))
presented_colors.append(color) presented_colors.append(color)
age_label = "" if age_ns == 0 else f" · hold {age_ns / 1_000_000:.0f} ms" 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( labels.append(
f"{cuboid.association_group} #{cuboid.track_id} {cuboid.label} · " f"{cuboid.association_group} #{cuboid.track_id} {cuboid.label}"
f"{cuboid.distance_m:.1f} m · {cuboid.support_points} pts{age_label}" f"{distance_label} · {cuboid.support_points} pts{age_label}"
) )
return ( return (
np.stack([cuboid.center for cuboid in presented]), np.stack([cuboid.center for cuboid in presented]),

View File

@ -28,6 +28,10 @@ from .integrated_perception import (
validate_integrated_perception_result, validate_integrated_perception_result,
) )
from .jobs import CameraComputeJob, validate_camera_compute_job from .jobs import CameraComputeJob, validate_camera_compute_job
from .occupancy_motion import (
PersistentSupportBuild,
build_persistent_support_result,
)
from .temporal_stability import ( from .temporal_stability import (
TemporalStabilityBuild, TemporalStabilityBuild,
_quality_metrics, _quality_metrics,
@ -59,6 +63,14 @@ class PublishedWorldMotionLabInstance:
build: WorldMotionBuild build: WorldMotionBuild
@dataclass(frozen=True, slots=True)
class PublishedPersistentSupportLabInstance:
binding: LabSessionBinding
job: CameraComputeJob
result: IntegratedPerceptionResult
build: PersistentSupportBuild
def publish_integrated_lab_instance( def publish_integrated_lab_instance(
*, *,
repository_root: Path, 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( def _validate_e23_inputs(
worker_root: Path, worker_root: Path,
source_report_path: Path, source_report_path: Path,

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@ from k1link.compute import (
publish_e22_lab_instance, publish_e22_lab_instance,
publish_e23_lab_instance, publish_e23_lab_instance,
publish_e24_lab_instance, publish_e24_lab_instance,
publish_e25_lab_instance,
publish_integrated_lab_instance, publish_integrated_lab_instance,
) )
from k1link.device_plugins.xgrids_k1.analyze import ( 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") @app.command("serve")
def serve_console( def serve_console(
port: Annotated[ port: Annotated[

View File

@ -267,6 +267,30 @@ def test_recorded_cuboid_presentation_holds_one_failed_association_then_expires(
assert expired is None 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: def test_e13_completes_a_visible_car_face_away_from_the_sensor() -> None:
fusion, runner = _worker_modules() fusion, runner = _worker_modules()
profile, _digest = runner.read_profile( profile, _digest = runner.read_profile(

View File

@ -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)