feat(perception): measure M48S load envelope
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user