From 68ea8235e6028afd889c5f615288d7a49b3b9bb1 Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Mon, 26 Oct 2026 10:59:15 +0100 Subject: [PATCH] feat(flomni): rt_positions prints the average of the per-point standard deviations (sample vs FZP during the exposures) to the BEC CLI at the end of each scan; no ZeroDivisionError when no point was read Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QnSJvqxh9oDbshx8qXgeiX --- csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py | 25 ++++++++++++++++---- tests/tests_devices/test_rt_flomni.py | 17 +++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py b/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py index 3ef464b1..eca5bff9 100644 --- a/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py +++ b/csaxs_bec/devices/omny/rt/rt_flomni_ophyd.py @@ -877,11 +877,28 @@ class RtFlomniFlyer(Device): status.set_finished() self.progress.put(value=read_counter, max_value=number_of_positions_planned, done=True) - logger.info( - "Flomni statistics: Average of all standard deviations: x" - f" {self.average_stdeviations_x_st_fzp/read_counter*1000:.1f}, y" - f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}" + self._report_statistics(read_counter) + + def _report_statistics(self, read_counter: int): + """At the end of a scan: mean of the per-point standard deviations (sample vs FZP during each exposure, + RT_sampler) to the device-server log and to the BEC command line (Mirko 2026-10-26).""" + if read_counter == 0: + logger.info("Flomni statistics: no scan points read, no average of the standard deviations") + return + msg = ( + "flOMNI scan statistics: average of the standard deviations during the exposures" + f" ({read_counter} points, sample vs FZP): x" + f" {self.average_stdeviations_x_st_fzp/read_counter*1000:.1f} nm, y" + f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f} nm" ) + logger.info(msg) + try: + self.controller.device_manager.connector.send_client_info( + msg, scope="flomni_scan_statistics", show_asap=True + ) + except Exception as exc: # pylint: disable=broad-except + # the statistics are information only: never fail the readout for them + logger.warning(f"Flomni statistics not sent to the client: {exc}") def _get_signals_from_table(self, return_table) -> dict: self.average_stdeviations_x_st_fzp += float(return_table[4]) diff --git a/tests/tests_devices/test_rt_flomni.py b/tests/tests_devices/test_rt_flomni.py index 811d6571..9d860184 100644 --- a/tests/tests_devices/test_rt_flomni.py +++ b/tests/tests_devices/test_rt_flomni.py @@ -4,6 +4,7 @@ import pytest from ophyd_devices.tests.utils import SocketMock from csaxs_bec.devices.omny.rt import RtFlomniController, RtFlomniMotor +from csaxs_bec.devices.omny.rt.rt_flomni_ophyd import RtFlomniFlyer from csaxs_bec.devices.omny.rt.rt_ophyd import RtError @@ -98,3 +99,19 @@ def test_feedback_enable_without_reset_raises(rt_flomni): rt_flomni.feedback_enable_without_reset() laser_tracker_on.assert_called_once() assert mock.call(b"l3\n") in rt_flomni.sock.put.mock_calls + + +def test_flyer_reports_average_stdev_to_the_client(rt_flomni): + flyer = RtFlomniFlyer(name="rt_positions", socket_cls=SocketMock, device_manager=rt_flomni.device_manager) + flyer.controller = rt_flomni + for row in (["0", "0", "1", "1", "0.002", "2", "2", "0.001", "0", "0"], + ["1", "0", "1", "1", "0.004", "2", "2", "0.003", "0", "0"]): + flyer._get_signals_from_table(row) + flyer._report_statistics(2) + send = rt_flomni.device_manager.connector.send_client_info + msg = send.call_args.args[0] + assert "x 3.0 nm, y 2.0 nm" in msg and "2 points" in msg + assert send.call_args.kwargs["show_asap"] is True + send.reset_mock() + flyer._report_statistics(0) # no points: nothing sent, no ZeroDivisionError + send.assert_not_called()