feat: qualify bounded K1 surface shadow
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# LAB E27 — bounded K1 local-surface shadow qualification
|
||||
|
||||
Date: 2026-07-26
|
||||
Status: **accepted for recorded-source-paced shadow diagnostic**
|
||||
Authority: diagnostic only; commands, navigation and safety acceptance disabled
|
||||
|
||||
## 1. Question
|
||||
|
||||
Can the accepted K1 rolling local-surface profile execute through a real
|
||||
bounded latest-wins worker loop at the recorded K1 source rate without queue
|
||||
growth, frame replacement or divergence from the immutable replay result?
|
||||
|
||||
This lab does not ask whether the derived surface is ground truth or
|
||||
planner-ready. It qualifies execution semantics only.
|
||||
|
||||
## 2. Fixed input
|
||||
|
||||
- session: `20260720T065719Z_viewer_live`;
|
||||
- source pack:
|
||||
`e10-lidar-pack-5da0396d32a27f9d1ca537cc2e8a371d386078d6f0dc71737b78620992af9625`;
|
||||
- source artifact SHA-256:
|
||||
`72aa73340b20fcfaa21b330ef5b93b975c14a70e2cd16a57752d9952ff05ad9a`;
|
||||
- replay reference:
|
||||
`k1-local-surface-628cd024775f02fea99765d1fb457efec2d5cc4d7371e56818dfe8e08c9b3b74`;
|
||||
- reference logical-content SHA-256:
|
||||
`b89a92887cedace9d3eab3e1490697fb6bc7fc16a2d1887f3fff7cb4547ceb14`;
|
||||
- selection: source frames `0–150`, `14.995 s`, 151 timeline entries and
|
||||
143 available LiDAR frames;
|
||||
- pace: recorded 1×;
|
||||
- work queue: latest-wins, capacity 2;
|
||||
- result ring: bounded to the 143-frame qualification selection.
|
||||
|
||||
The source pack and replay derivative were opened read-only. No scanner,
|
||||
firmware, MQTT command or persistent reconstruction was changed.
|
||||
|
||||
## 3. Implemented runtime boundary
|
||||
|
||||
`missioncore.k1-local-surface-shadow-runtime/v1` accepts only an already
|
||||
decoded map-frame point cloud and a compatible `T_map_from_sensor` pose. It:
|
||||
|
||||
1. copies and freezes the admitted point/pose pair;
|
||||
2. publishes work into a bounded latest-wins queue;
|
||||
3. runs the same robust rolling-cell and prior-only prediction profile used by
|
||||
replay;
|
||||
4. retains only a bounded diagnostic result ring;
|
||||
5. reports observed surface, observed occupied-above-surface, negative
|
||||
outliers, unverified step candidates, latency and freshness;
|
||||
6. explicitly keeps absence-of-points distinct from free space.
|
||||
|
||||
The runtime has no command method and every result carries
|
||||
`commands_enabled=false` and `navigation_or_safety_accepted=false`.
|
||||
|
||||
## 4. Result
|
||||
|
||||
Result:
|
||||
`k1-local-surface-shadow-04f14d8c580f74cbd5b0a452867563ebc6b3ef93d872129e4918680932253ab7`
|
||||
|
||||
| Metric | Result |
|
||||
| --- | ---: |
|
||||
| Published / consumed | 143 / 143 |
|
||||
| Latest-wins replacements | 0 |
|
||||
| Maximum queue depth | 1 / 2 |
|
||||
| Processing failures | 0 |
|
||||
| Processing p50 / p95 / max | 12.261 / 14.777 / 33.135 ms |
|
||||
| Result age p50 / p95 / max | 12.336 / 14.857 / 33.378 ms |
|
||||
| Replay state mismatches | 0 |
|
||||
| Point-class mismatches | 0 |
|
||||
| Step-candidate mismatches | 0 |
|
||||
| Maximum scalar delta | 0.0 |
|
||||
|
||||
All 143 results were valid for this selection. Queue accounting closed exactly:
|
||||
`consumed + dropped_overflow = published`, final depth was zero and the worker
|
||||
thread stopped cleanly.
|
||||
|
||||
## 5. Decision
|
||||
|
||||
The execution gate passes. The current CPU geometric profile is comfortably
|
||||
inside the observed roughly 10 Hz K1 publication interval on this 15-second
|
||||
slice, remains bounded and reproduces replay exactly when no work is replaced.
|
||||
|
||||
This result promotes the profile only from offline replay implementation to a
|
||||
recorded-source-paced shadow candidate. It does not promote:
|
||||
|
||||
- the surface estimate to ground truth;
|
||||
- observed occupancy to free-space evidence;
|
||||
- step candidates to semantic curbs;
|
||||
- the worker result to navigation or safety authority;
|
||||
- the replay transport to a physical-live K1 gate.
|
||||
|
||||
## 6. Next gate
|
||||
|
||||
Connect the runtime to the existing authenticated external worker stream using
|
||||
an explicit bounded LiDAR↔pose binder, then repeat at least 15 seconds against
|
||||
a physical K1 acquisition. Measure source sequence gaps, pose-binding misses,
|
||||
queue replacements, result age, memory slope and recovery across reconnect.
|
||||
React remains a read-only status/review surface; it does not execute the
|
||||
algorithm.
|
||||
@@ -0,0 +1,365 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
from k1link.compute import (
|
||||
DEFAULT_K1_LOCAL_SURFACE_PROFILE,
|
||||
E10LidarFieldSource,
|
||||
K1LocalSurfaceShadowInput,
|
||||
K1LocalSurfaceShadowResult,
|
||||
K1LocalSurfaceShadowRuntime,
|
||||
K1LocalSurfaceV1,
|
||||
)
|
||||
from k1link.compute.lidar_local_surface import (
|
||||
FRAME_FIT_FAILED,
|
||||
FRAME_INSUFFICIENT_SURFACE,
|
||||
FRAME_POSE_STALE,
|
||||
FRAME_VALID,
|
||||
)
|
||||
|
||||
QUALIFICATION_SCHEMA = "missioncore.k1-local-surface-shadow-qualification/v1"
|
||||
|
||||
|
||||
def _arguments() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Run the accepted K1 rolling-surface profile through a bounded "
|
||||
"latest-wins replay shadow and compare processed frames to the "
|
||||
"immutable replay derivative."
|
||||
)
|
||||
)
|
||||
parser.add_argument("source_pack", type=Path)
|
||||
parser.add_argument("reference_model", type=Path)
|
||||
parser.add_argument("output_root", type=Path)
|
||||
parser.add_argument("--start-frame", type=int, default=0)
|
||||
parser.add_argument("--duration-seconds", type=float, default=15.0)
|
||||
parser.add_argument("--pace-scale", type=float, default=1.0)
|
||||
parser.add_argument("--queue-capacity", type=int, default=2)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _distribution(values: list[float]) -> dict[str, float | int | None]:
|
||||
if not values:
|
||||
return {
|
||||
"sample_count": 0,
|
||||
"minimum": None,
|
||||
"mean": None,
|
||||
"p50": None,
|
||||
"p95": None,
|
||||
"maximum": None,
|
||||
}
|
||||
array = np.asarray(values, dtype=np.float64)
|
||||
if not np.isfinite(array).all():
|
||||
raise RuntimeError("shadow qualification latency is invalid")
|
||||
return {
|
||||
"sample_count": int(array.shape[0]),
|
||||
"minimum": float(np.min(array)),
|
||||
"mean": float(np.mean(array)),
|
||||
"p50": float(np.percentile(array, 50)),
|
||||
"p95": float(np.percentile(array, 95)),
|
||||
"maximum": float(np.max(array)),
|
||||
}
|
||||
|
||||
|
||||
def _expected_state(model: K1LocalSurfaceV1, frame_index: int) -> str:
|
||||
code = int(model.arrays["frame_failure_code"][frame_index])
|
||||
return {
|
||||
FRAME_VALID: "valid",
|
||||
FRAME_POSE_STALE: "pose-stale",
|
||||
FRAME_INSUFFICIENT_SURFACE: "insufficient-surface",
|
||||
FRAME_FIT_FAILED: "fit-failed",
|
||||
}[code]
|
||||
|
||||
|
||||
def _maximum_scalar_delta(
|
||||
result: K1LocalSurfaceShadowResult,
|
||||
model: K1LocalSurfaceV1,
|
||||
) -> float:
|
||||
frame_index = result.frame_index
|
||||
pairs = (
|
||||
(result.sensor_height_m, "sensor_height_m"),
|
||||
(result.slope_deg, "slope_deg"),
|
||||
(result.roughness_m, "roughness_m"),
|
||||
(result.confidence, "confidence"),
|
||||
(result.surface_max_age_ms, "surface_max_age_ms"),
|
||||
)
|
||||
deltas = [
|
||||
abs(float(value) - float(model.arrays[name][frame_index]))
|
||||
for value, name in pairs
|
||||
if value is not None
|
||||
]
|
||||
return max(deltas, default=0.0)
|
||||
|
||||
|
||||
def _qualification(
|
||||
source: E10LidarFieldSource,
|
||||
model: K1LocalSurfaceV1,
|
||||
*,
|
||||
start_frame: int,
|
||||
duration_seconds: float,
|
||||
pace_scale: float,
|
||||
queue_capacity: int,
|
||||
) -> dict[str, Any]:
|
||||
if (
|
||||
model.identity.get("source_pack_id") != source.pack_id
|
||||
or model.identity.get("source_pack_identity_sha256")
|
||||
!= source.manifest.get("identity_sha256")
|
||||
or model.identity.get("source_artifact_sha256")
|
||||
!= source.manifest.get("artifact", {}).get("sha256")
|
||||
or model.identity.get("profile") != DEFAULT_K1_LOCAL_SURFACE_PROFILE.to_dict()
|
||||
):
|
||||
raise RuntimeError("shadow qualification source/model binding is invalid")
|
||||
if (
|
||||
not 0 <= start_frame < source.frame_count
|
||||
or not math.isfinite(duration_seconds)
|
||||
or duration_seconds <= 0
|
||||
or not math.isfinite(pace_scale)
|
||||
or not 0 < pace_scale <= 10
|
||||
or not 1 <= queue_capacity <= 8
|
||||
):
|
||||
raise RuntimeError("shadow qualification selection is invalid")
|
||||
|
||||
arrays = source.arrays
|
||||
session_times = arrays["session_seconds"]
|
||||
start_seconds = float(session_times[start_frame])
|
||||
selected = [
|
||||
frame_index
|
||||
for frame_index in range(start_frame, source.frame_count)
|
||||
if float(session_times[frame_index]) - start_seconds <= duration_seconds
|
||||
]
|
||||
if len(selected) < 2:
|
||||
raise RuntimeError("shadow qualification selection is too short")
|
||||
expected_available = [
|
||||
frame_index for frame_index in selected if bool(arrays["sample_available"][frame_index])
|
||||
]
|
||||
runtime = K1LocalSurfaceShadowRuntime(
|
||||
f"{source.identity['session_id']}-qualification",
|
||||
queue_capacity=queue_capacity,
|
||||
result_capacity=min(256, max(1, len(expected_available))),
|
||||
)
|
||||
wall_started = time.perf_counter()
|
||||
offsets = arrays["cloud_offsets"]
|
||||
try:
|
||||
for frame_index in selected:
|
||||
release_at = (
|
||||
wall_started + (float(session_times[frame_index]) - start_seconds) * pace_scale
|
||||
)
|
||||
remaining = release_at - time.perf_counter()
|
||||
if remaining > 0:
|
||||
time.sleep(remaining)
|
||||
if not bool(arrays["sample_available"][frame_index]):
|
||||
continue
|
||||
start = int(offsets[frame_index])
|
||||
end = int(offsets[frame_index + 1])
|
||||
runtime.publish(
|
||||
K1LocalSurfaceShadowInput(
|
||||
frame_index=frame_index,
|
||||
source_frame_index=int(arrays["source_frame_indices"][frame_index]),
|
||||
session_seconds=float(session_times[frame_index]),
|
||||
pose_binding_age_ms=abs(float(arrays["pose_point_delta_ms"][frame_index])),
|
||||
points_map=np.asarray(
|
||||
arrays["cloud_points_map"][start:end],
|
||||
dtype=np.float64,
|
||||
),
|
||||
position_map=np.asarray(
|
||||
arrays["pose_positions_map"][frame_index],
|
||||
dtype=np.float64,
|
||||
),
|
||||
published_monotonic_ns=time.monotonic_ns(),
|
||||
)
|
||||
)
|
||||
runtime.close(timeout_seconds=30.0)
|
||||
results = runtime.results()
|
||||
runtime_snapshot = runtime.snapshot()
|
||||
finally:
|
||||
runtime.close()
|
||||
|
||||
state_mismatches = 0
|
||||
point_class_mismatches = 0
|
||||
step_candidate_mismatches = 0
|
||||
maximum_scalar_delta = 0.0
|
||||
for result in results:
|
||||
frame_index = result.frame_index
|
||||
state_mismatches += result.state != _expected_state(model, frame_index)
|
||||
if result.valid:
|
||||
start = int(offsets[frame_index])
|
||||
end = int(offsets[frame_index + 1])
|
||||
point_class_mismatches += int(
|
||||
np.count_nonzero(result.point_class != model.arrays["point_class"][start:end])
|
||||
)
|
||||
step_candidate_mismatches += int(
|
||||
np.count_nonzero(
|
||||
result.point_step_candidate != model.arrays["point_step_candidate"][start:end]
|
||||
)
|
||||
)
|
||||
maximum_scalar_delta = max(
|
||||
maximum_scalar_delta,
|
||||
_maximum_scalar_delta(result, model),
|
||||
)
|
||||
queue = runtime_snapshot["queue"]
|
||||
accepted = (
|
||||
int(queue["maximum_depth"]) <= queue_capacity
|
||||
and int(queue["dropped_overflow"]) == 0
|
||||
and int(queue["consumed"]) == len(expected_available)
|
||||
and len(results) == len(expected_available)
|
||||
and int(runtime_snapshot["results"]["failed"]) == 0
|
||||
and state_mismatches == 0
|
||||
and point_class_mismatches == 0
|
||||
and step_candidate_mismatches == 0
|
||||
and maximum_scalar_delta <= 1e-9
|
||||
)
|
||||
return {
|
||||
"schema_version": QUALIFICATION_SCHEMA,
|
||||
"identity": {
|
||||
"source_pack_id": source.pack_id,
|
||||
"source_pack_identity_sha256": source.manifest["identity_sha256"],
|
||||
"source_artifact_sha256": source.manifest["artifact"]["sha256"],
|
||||
"reference_model_id": model.model_id,
|
||||
"reference_logical_content_sha256": model.identity["logical_content_sha256"],
|
||||
"session_id": source.identity["session_id"],
|
||||
"profile": DEFAULT_K1_LOCAL_SURFACE_PROFILE.to_dict(),
|
||||
"selection": {
|
||||
"start_frame": start_frame,
|
||||
"end_frame": selected[-1],
|
||||
"source_duration_seconds": (float(session_times[selected[-1]]) - start_seconds),
|
||||
"selected_frames": len(selected),
|
||||
"available_frames": len(expected_available),
|
||||
},
|
||||
"runtime": {
|
||||
"queue_policy": "bounded-latest-wins",
|
||||
"queue_capacity": queue_capacity,
|
||||
"result_capacity": min(
|
||||
256,
|
||||
max(1, len(expected_available)),
|
||||
),
|
||||
"pace_scale": pace_scale,
|
||||
},
|
||||
"producer_sha256": _sha256(Path(__file__).resolve(strict=True)),
|
||||
},
|
||||
"state": "accepted" if accepted else "rejected",
|
||||
"accepted": accepted,
|
||||
"ground_truth": False,
|
||||
"metrics": {
|
||||
"wall_elapsed_seconds": time.perf_counter() - wall_started,
|
||||
"queue": queue,
|
||||
"result_states": runtime_snapshot["results"]["state_counts"],
|
||||
"processing_ms": _distribution([result.processing_ms for result in results]),
|
||||
"result_age_ms": _distribution([result.result_age_ms for result in results]),
|
||||
"replay_parity": {
|
||||
"compared_frames": len(results),
|
||||
"state_mismatches": state_mismatches,
|
||||
"point_class_mismatches": point_class_mismatches,
|
||||
"step_candidate_mismatches": step_candidate_mismatches,
|
||||
"maximum_scalar_delta": maximum_scalar_delta,
|
||||
},
|
||||
},
|
||||
"acceptance": {
|
||||
"queue_bounded": int(queue["maximum_depth"]) <= queue_capacity,
|
||||
"zero_latest_wins_replacements": int(queue["dropped_overflow"]) == 0,
|
||||
"zero_processing_failures": (int(runtime_snapshot["results"]["failed"]) == 0),
|
||||
"complete_processed_accounting": (
|
||||
int(queue["consumed"]) == len(expected_available)
|
||||
and len(results) == len(expected_available)
|
||||
),
|
||||
"replay_state_parity": state_mismatches == 0,
|
||||
"replay_point_class_parity": point_class_mismatches == 0,
|
||||
"replay_step_candidate_parity": step_candidate_mismatches == 0,
|
||||
"replay_scalar_parity": maximum_scalar_delta <= 1e-9,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
"occupancy_policy": {
|
||||
"absence_of_points_means_free": False,
|
||||
"unknown_is_traversable": False,
|
||||
},
|
||||
"authority": {
|
||||
"commands_enabled": False,
|
||||
"navigation_or_safety_accepted": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
arguments = _arguments()
|
||||
source = E10LidarFieldSource(arguments.source_pack)
|
||||
model = K1LocalSurfaceV1(arguments.reference_model)
|
||||
try:
|
||||
report = _qualification(
|
||||
source,
|
||||
model,
|
||||
start_frame=arguments.start_frame,
|
||||
duration_seconds=arguments.duration_seconds,
|
||||
pace_scale=arguments.pace_scale,
|
||||
queue_capacity=arguments.queue_capacity,
|
||||
)
|
||||
finally:
|
||||
model.close()
|
||||
source.close()
|
||||
report_sha256 = hashlib.sha256(_canonical_json(report)).hexdigest()
|
||||
result_id = f"k1-local-surface-shadow-{report_sha256}"
|
||||
report["result_id"] = result_id
|
||||
report["report_sha256"] = report_sha256
|
||||
report["created_at_utc"] = datetime.now(UTC).isoformat()
|
||||
output_root = arguments.output_root.expanduser().resolve()
|
||||
output_root.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
output = output_root / result_id
|
||||
if output.exists():
|
||||
raise RuntimeError("shadow qualification result already exists")
|
||||
staging = output_root / f".{result_id}.{os.getpid()}.incomplete"
|
||||
staging.mkdir(mode=0o700, exist_ok=False)
|
||||
try:
|
||||
report_path = staging / "report.json"
|
||||
report_path.write_bytes(_canonical_json(report))
|
||||
os.replace(staging, output)
|
||||
except BaseException:
|
||||
if staging.exists():
|
||||
for path in staging.iterdir():
|
||||
path.unlink()
|
||||
staging.rmdir()
|
||||
raise
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"result_id": result_id,
|
||||
"output": str(output),
|
||||
"state": report["state"],
|
||||
"metrics": report["metrics"],
|
||||
"authority": report["authority"],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return 0 if report["accepted"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user