feat(LamNI): measure camera live-mode FPS and smear-sweep capture FPS
CI for csaxs_bec / test (pull_request) Successful in 3m42s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 1s
CI for csaxs_bec / test (push) Successful in 2m31s

Neither rate was previously measured anywhere, making it hard to tell a
real acquisition bottleneck from a display/overhead one.

- IDSCamera.get_live_fps() (new USER_ACCESS entry): rolling-buffer
  measurement of the live-mode push rate in _live_mode_loop(), readable as
  dev.cam_xeye.get_live_fps(). SimIDSCamera inherits it unchanged, so it
  works against the simulated deployment too.
- XrayEyeAlign._smear_sweep() now tracks timestamps of the first and most
  recent distinct frame it captures, storing the computed rate as
  self.last_smear_fps and printing it in the sweep-done summary line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit was merged in pull request #277.
This commit is contained in:
x01dc
2026-07-27 10:35:38 +02:00
co-authored by Claude Sonnet 5
parent 83f9a6bb59
commit e299775c44
4 changed files with 114 additions and 1 deletions
@@ -424,6 +424,53 @@ def test_smear_sweep_max_projection(bec_client_mock):
assert [c.args[0] for c in align.gui.set_smear_active.call_args_list] == [True, False]
def test_smear_sweep_computes_capture_fps(bec_client_mock):
client = bec_client_mock
align, dev_mock = _make_calibration_align(client)
frame_a = np.array([[1, 5], [3, 2]], dtype=np.uint8)
frame_b = np.array([[4, 2], [3, 6]], dtype=np.uint8)
frame_c = np.array([[0, 1], [9, 0]], dtype=np.uint8)
dev_mock.cam_xeye.get_last_image.side_effect = [frame_a, frame_b, frame_c]
report = _report_with_status_sequence("RUNNING", "RUNNING", "RUNNING", "COMPLETED")
# Deterministic clock: each time.time() call advances by 0.05s. With
# frame_a (first frame, uncounted per the existing off-by-one) then
# frame_b and frame_c as the 2 distinct frames that follow, the elapsed
# time between the first frame and the last distinct one spans exactly
# 4 of these ticks (0.20s) -- see the call trace in _smear_sweep().
clock = {"t": 0.0}
def fake_time():
clock["t"] += 0.05
return clock["t"]
with mock.patch(f"{XRAY_EYE_ALIGN}.dev", dev_mock):
with mock.patch(f"{XRAY_EYE_ALIGN}.mv", return_value=report):
with mock.patch(f"{XRAY_EYE_ALIGN}.time.sleep"):
with mock.patch(f"{XRAY_EYE_ALIGN}.time.time", side_effect=fake_time):
align._smear_sweep()
assert align.last_smear_fps == pytest.approx(10.0)
def test_smear_sweep_fps_none_with_fewer_than_two_distinct_frames(bec_client_mock):
client = bec_client_mock
align, dev_mock = _make_calibration_align(client)
frame_a = np.array([[1, 5], [3, 2]], dtype=np.uint8)
dev_mock.cam_xeye.get_last_image.return_value = frame_a
report = _report_with_status_sequence("RUNNING", "COMPLETED")
with mock.patch(f"{XRAY_EYE_ALIGN}.dev", dev_mock):
with mock.patch(f"{XRAY_EYE_ALIGN}.mv", return_value=report):
with mock.patch(f"{XRAY_EYE_ALIGN}.time.sleep"):
align._smear_sweep()
# only the first (uncounted) frame was seen -- frames_seen stays 0
assert align.last_smear_fps is None
def test_smear_sweep_issues_single_continuous_move(bec_client_mock):
client = bec_client_mock
align, dev_mock = _make_calibration_align(client)