fix(perception): separate preroll history from current sensor binding
Preserve rolling points and original clocks; expose per-modality ages, held pose and rejection reasons. Saved-lineage audit changes only the first admission (75 to 76 of 128). No new model/performance claim. 384 focused Python and 62 frontend tests passed.
This commit is contained in:
@@ -6,6 +6,7 @@ import json
|
||||
import threading
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
@@ -214,3 +215,120 @@ def test_completed_gpu_and_ingress_share_two_pending_slots(pilot):
|
||||
assert mailbox.bytes == 0
|
||||
with pytest.raises(ValueError, match="accounting"):
|
||||
mailbox.take_completed()
|
||||
|
||||
|
||||
def _sensor(pilot, channel, milliseconds, sequence=0):
|
||||
return pilot("pilot_source").SensorEvent(
|
||||
milliseconds * 1_000_000, channel, sequence, (np.zeros((1, 3)), None)
|
||||
)
|
||||
|
||||
|
||||
def test_preroll_history_cannot_poison_the_first_fresh_pair(pilot):
|
||||
pose = _sensor(pilot, "pose", 975)
|
||||
points = tuple(_sensor(pilot, "points", t, i) for i, t in enumerate((590, 738, 808, 910, 980)))
|
||||
binding = pilot("pilot_sensor_binding").bind_sensors(1_000_000_000, pose, points)
|
||||
assert binding.available
|
||||
assert binding.increments == points[-2:]
|
||||
assert binding.history_only == points[:3]
|
||||
assert binding.pose_age_ns == 25_000_000
|
||||
assert binding.oldest_point_age_ns == 90_000_000
|
||||
assert binding.binding_age_ns == 65_000_000
|
||||
assert binding.document()["preroll_history_only"][0] == {
|
||||
"sequence": 0,
|
||||
"host_monotonic_ns": 590_000_000,
|
||||
"points": 1,
|
||||
}
|
||||
|
||||
|
||||
def test_after_preroll_admission_does_not_silently_filter_bad_increments(pilot):
|
||||
pose = _sensor(pilot, "pose", 975)
|
||||
points = (_sensor(pilot, "points", 730), _sensor(pilot, "points", 980, 1))
|
||||
binding = pilot("pilot_sensor_binding").bind_sensors(
|
||||
1_000_000_000, pose, points, previous_camera_time_ns=700_000_000
|
||||
)
|
||||
assert binding.increments == points and not binding.history_only
|
||||
assert not binding.available
|
||||
assert set(binding.reasons) == {"oldest-points-too-old", "point-pose-skew"}
|
||||
|
||||
|
||||
def test_pose_reuse_keeps_age_and_empty_increment_is_not_new_geometry(pilot):
|
||||
function = pilot("pilot_sensor_binding").bind_sensors
|
||||
pose = _sensor(pilot, "pose", 975)
|
||||
binding = function(1_000_000_000, pose, (), previous_camera_time_ns=980_000_000)
|
||||
assert binding.pose_state == "held" and binding.pose_age_ns == 25_000_000
|
||||
assert binding.points_state == "unavailable" and not binding.available
|
||||
assert binding.reasons == ("point-increment-unavailable",)
|
||||
expired = function(1_080_000_000, pose, (), previous_camera_time_ns=1_000_000_000)
|
||||
assert expired.pose_state == "stale" and "pose-too-old" in expired.reasons
|
||||
missing = function(1_000_000_000, None, ())
|
||||
assert missing.pose_state == "unavailable" and not missing.available
|
||||
|
||||
|
||||
def test_sensor_binding_preserves_exact_limits_and_rejects_lookahead(pilot):
|
||||
function = pilot("pilot_sensor_binding").bind_sensors
|
||||
pose = _sensor(pilot, "pose", 900)
|
||||
points = (_sensor(pilot, "points", 800), _sensor(pilot, "points", 900, 1))
|
||||
assert function(1_000_000_000, pose, points, previous_camera_time_ns=700_000_000).available
|
||||
assert not function(1_000_000_001, pose, points, previous_camera_time_ns=700_000_000).available
|
||||
with pytest.raises(ValueError, match="future pose"):
|
||||
function(1_000_000_000, _sensor(pilot, "pose", 1001), ())
|
||||
with pytest.raises(ValueError, match="future points"):
|
||||
function(1_000_000_000, pose, (_sensor(pilot, "points", 1001),))
|
||||
with pytest.raises(ValueError, match="backwards"):
|
||||
function(1_000_000_000, pose, points[::-1])
|
||||
|
||||
|
||||
def test_producer_keeps_preroll_history_for_tgs_and_publishes_sensor_ages(pilot, monkeypatch):
|
||||
runner = pilot("run_joint_pilot")
|
||||
events = [
|
||||
_sensor(pilot, "points", 590),
|
||||
_sensor(pilot, "pose", 975),
|
||||
_sensor(pilot, "points", 980, 1),
|
||||
pilot("pilot_source").SensorEvent(
|
||||
1_000_000_000, "camera", 0, {"sequence": 1, "host_epoch_ns": 2_000_000_000}
|
||||
),
|
||||
]
|
||||
|
||||
class Archive:
|
||||
def __init__(self, path):
|
||||
pass
|
||||
|
||||
def counters(self):
|
||||
return {}
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
class NoWait:
|
||||
def is_set(self):
|
||||
return False
|
||||
|
||||
def wait(self, seconds):
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(runner, "SensorArchive", Archive)
|
||||
monkeypatch.setattr(runner, "camera_events", lambda *args: iter(events[-1:]))
|
||||
monkeypatch.setattr(runner, "merged_events", lambda *args: iter(events))
|
||||
raw = io.BytesIO()
|
||||
pilot("pilot_ipc").send(raw, {"decode_ms": 0.0}, bytes(600 * 800 * 3))
|
||||
raw.seek(0)
|
||||
decoder = SimpleNamespace(stdin=io.BytesIO(), stdout=raw)
|
||||
mailbox = pilot("pilot_queue").Mailbox()
|
||||
report = {}
|
||||
runner.produce(
|
||||
SimpleNamespace(sensor_archive="unused", camera_index="unused", frames=1),
|
||||
decoder,
|
||||
mailbox,
|
||||
NoWait(),
|
||||
report,
|
||||
)
|
||||
assert mailbox.error is None
|
||||
bundle = mailbox.take()
|
||||
assert bundle["available"] and len(bundle["points"]) == 1
|
||||
assert len(bundle["rolling_points"]) == 2
|
||||
np.testing.assert_array_equal(bundle["rolling_times"], [590_000_000, 980_000_000])
|
||||
assert bundle["lineage"]["point_increments"][0]["host_monotonic_ns"] == 980_000_000
|
||||
assert bundle["sensor_binding"]["preroll_history_only"][0]["host_monotonic_ns"] == 590_000_000
|
||||
assert report["preroll_history_only_points"] == 1
|
||||
mailbox.release(bundle)
|
||||
assert mailbox.bytes == 0 and mailbox.take() is None
|
||||
|
||||
Reference in New Issue
Block a user