feat(perception): measure M48S load envelope

This commit is contained in:
DCCONSTRUCTIONS
2026-08-25 20:36:21 +03:00
parent 078b8421a6
commit e21332b9e1
6 changed files with 257 additions and 10 deletions
@@ -92,6 +92,7 @@ def build_m48s_reference_graph_runtime(
decode_timing_observer: DecodeTimingObserver | None = None,
detector_timing_observer: DetectorTimingObserver | None = None,
maximum_frames: int | None = None,
source_rate_hz: float | None = None,
) -> M48sReferenceGraphRuntime:
"""Instantiate the complete graph with only its detector pin replaced."""
@@ -125,6 +126,11 @@ def build_m48s_reference_graph_runtime(
if run_mode is GraphRunMode.SOURCE_PACED_LATEST_WINS
else ReplayPacing.UNCAPPED
),
target_rate_hz=(
source_rate_hz
if run_mode is GraphRunMode.SOURCE_PACED_LATEST_WINS
else None
),
),
decoder=PyAvRecordedImageDecoder(paths.video),
timing_observer=decode_timing_observer,
+33 -2
View File
@@ -99,7 +99,11 @@ DecodeTimingObserver = Callable[[DecodedFrameTiming], None]
class RecordedRavnoves00Source:
"""Emit the admitted synchronized source timeline at 1.0x or uncapped speed."""
"""Emit the admitted timeline at its recorded or an explicit replay rate.
A target rate changes wall-clock pacing only. Immutable sensor timestamps stay
untouched so accelerated load measurements cannot silently change scene motion.
"""
provider_id: str = RECORDED_SOURCE_PROVIDER_ID
@@ -111,16 +115,24 @@ class RecordedRavnoves00Source:
pacing: ReplayPacing = ReplayPacing.UNCAPPED,
expected_frame_count: int = DEFAULT_FRAME_COUNT,
expected_source_pack_sha256: str | None = RECORDED_SOURCE_PACK_SHA256,
target_rate_hz: float | None = None,
clock_ns: Callable[[], int] = time.monotonic_ns,
wait: WaitFunction | None = None,
) -> None:
if expected_frame_count < 1:
raise RecordedSourceError("expected frame count must be positive")
if target_rate_hz is not None and (
not np.isfinite(target_rate_hz) or target_rate_hz <= 0
):
raise RecordedSourceError("target replay rate must be positive and finite")
if target_rate_hz is not None and pacing is not ReplayPacing.ONE_X:
raise RecordedSourceError("target replay rate requires paced replay")
self.camera_index_path = camera_index_path.resolve()
self.source_pack_path = source_pack_path.resolve()
self.pacing = pacing
self.expected_frame_count = expected_frame_count
self.expected_source_pack_sha256 = expected_source_pack_sha256
self.target_rate_hz = target_rate_hz
self._clock_ns = clock_ns
self._wait = wait or _event_wait
@@ -130,6 +142,7 @@ class RecordedRavnoves00Source:
repository_root: Path,
*,
pacing: ReplayPacing = ReplayPacing.UNCAPPED,
target_rate_hz: float | None = None,
) -> RecordedRavnoves00Source:
root = repository_root.resolve()
camera_index = (
@@ -148,6 +161,7 @@ class RecordedRavnoves00Source:
camera_index_path=camera_index,
source_pack_path=source_pack,
pacing=pacing,
target_rate_hz=target_rate_hz,
)
def packets(self, stop_event: Event) -> Iterator[SourcePacket]:
@@ -163,6 +177,7 @@ class RecordedRavnoves00Source:
started_ns = int(self._clock_ns())
source_origin_ns = _source_time_ns(timeline_rows[0])
pacing_scale = _pacing_scale(timeline_rows, self.target_rate_hz)
for frame_index, (camera, timeline) in enumerate(
zip(camera_rows, timeline_rows, strict=True)
):
@@ -170,7 +185,8 @@ class RecordedRavnoves00Source:
return
packet = _packet(frame_index, camera, timeline)
if self.pacing is ReplayPacing.ONE_X:
target_ns = started_ns + packet.envelope.timestamps.source_ns - source_origin_ns
source_elapsed_ns = packet.envelope.timestamps.source_ns - source_origin_ns
target_ns = started_ns + round(source_elapsed_ns * pacing_scale)
if not self._pace_until(stop_event, target_ns):
return
yield packet
@@ -349,6 +365,21 @@ def _source_time_ns(document: _SourceTimelineRow) -> int:
return round(value * 1_000_000_000)
def _pacing_scale(
timeline: tuple[_SourceTimelineRow, ...],
target_rate_hz: float | None,
) -> float:
if target_rate_hz is None:
return 1.0
if len(timeline) < 2:
raise RecordedSourceError("target-rate replay requires at least two frames")
duration_seconds = timeline[-1].session_seconds - timeline[0].session_seconds
if not np.isfinite(duration_seconds) or duration_seconds <= 0:
raise RecordedSourceError("source timeline duration is invalid")
recorded_rate_hz = (len(timeline) - 1) / duration_seconds
return recorded_rate_hz / target_rate_hz
def _integer(document: dict[str, object], key: str) -> int:
value = document.get(key)
if not isinstance(value, int) or isinstance(value, bool) or value < 0: