From b5655fd6eb6181c28f3cc141eaa766dc05f2301b Mon Sep 17 00:00:00 2001 From: x01dc Date: Mon, 27 Jul 2026 16:14:11 +0200 Subject: [PATCH] feat(LamNI): add ring-progress display for tomo_alignment_scan() tomo_alignment_scan() had no GUI feedback at all -- just console prints -- unlike the real tomogram, which already drives a RingProgressBar via lamnigui_show_progress(). Add the same kind of single-ring display for the 12-angle alignment loop (lamnigui_show_alignment_progress()/_lamnigui_update_alignment_progress()), backed by its own alignment_scan_progress global var rather than tomo_progress, so anything watching tomo_progress for the real tomogram (heartbeat/idle-time tracking) never sees alignment-scan writes mixed in. --- .../plugins/LamNI/gui_tools.py | 44 +++++++++++++++++++ .../bec_ipython_client/plugins/LamNI/lamni.py | 37 +++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/gui_tools.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/gui_tools.py index f625a26..ca5928b 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/gui_tools.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/gui_tools.py @@ -25,6 +25,7 @@ class LamniGuiTools: self.lamni_window = None self.text_box = None self.progressbar = None + self.alignment_progressbar = None self.xeyegui = None self.pdf_viewer = None self.idle_text_box = None @@ -72,6 +73,7 @@ class LamniGuiTools: if hasattr(self.gui, "lamni"): self.gui.lamni.delete_all(timeout=self.GUI_RPC_TIMEOUT) self.progressbar = None + self.alignment_progressbar = None self.text_box = None self.xeyegui = None self.pdf_viewer = None @@ -288,6 +290,48 @@ class LamniGuiTools: text += f"\n Hook: {hook_description}" self.progressbar.set_center_label(text) + # ------------------------------------------------------------------ + # Alignment scan progress bar + # ------------------------------------------------------------------ + + def lamnigui_show_alignment_progress(self): + """Open (or raise) a single-ring progress bar for tomo_alignment_scan(). + + A separate dock/widget from lamnigui_show_progress() (the real + tomogram's 3-ring bar) -- kept distinct since it's backed by its + own global var (see LamNI.alignment_scan_progress). + """ + self.lamnigui_show_gui() + if self._lamnigui_is_missing("alignment_progressbar"): + self.lamnigui_remove_all_docks() + self.alignment_progressbar = self.gui.lamni.new( + "RingProgressBar", timeout=self.GUI_RPC_TIMEOUT + ) + # Single ring: alignment-scan angle progress (manual update) + self.alignment_progressbar.add_ring().set_update("manual") + + self._lamnigui_update_alignment_progress() + + def _lamnigui_update_alignment_progress(self): + """Update the alignment-scan progress ring and centre label from + self.alignment_scan_progress (see LamNI.alignment_scan_progress).""" + if self.alignment_progressbar is None: + return + + ring = self.alignment_progressbar.rings[0] + total = self.alignment_scan_progress["total_angles"] + done = self.alignment_scan_progress["angle_index"] + progress = done / total * 100 if total else 0 + ring.set_value(progress) + + angle = self.alignment_scan_progress.get("angle", 0.0) + text = ( + f"Alignment scan progress:\n" + f" Angle {done}/{total}\n" + f" Current angle: {angle:.1f} deg" + ) + self.alignment_progressbar.set_center_label(text) + if __name__ == "__main__": from bec_lib.client import BECClient diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py index c584804..f2be986 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py @@ -109,6 +109,18 @@ class _ProgressProxy: return self._load() +class _AlignmentScanProgressProxy(_ProgressProxy): + """Same dict-proxy pattern as _ProgressProxy, but for tomo_alignment_scan()'s + own progress (angle N/12) -- kept in its own global var, deliberately + separate from tomo_progress, so anything watching tomo_progress for the + real tomogram (e.g. heartbeat/idle-time tracking) never sees alignment + scan writes mixed in. + """ + + _GLOBAL_VAR_KEY = "alignment_scan_progress" + _DEFAULTS: dict = {"angle_index": 0, "total_angles": 12, "angle": 0.0} + + class LamNIError(Exception): """A definite, non-transient tomo-scan failure (bad config, unmet precondition, ...) that should never be retried by @scan_repeat.""" @@ -224,6 +236,7 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools # tomo_scan()); use tomo_progress_reset() to explicitly clear stale # progress without starting a new scan. self._progress_proxy = _ProgressProxy(self.client) + self._alignment_scan_progress_proxy = _AlignmentScanProgressProxy(self.client) self._init_tomo_queue() from csaxs_bec.bec_ipython_client.plugins.LamNI.LamNI_webpage_generator import ( @@ -392,6 +405,21 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools self._progress_proxy.reset() print("Tomo progress reset.") + @property + def alignment_scan_progress(self) -> _AlignmentScanProgressProxy: + """Proxy dict backed by the BEC global variable ``alignment_scan_progress``. + + Tracks tomo_alignment_scan()'s own progress (angle N/total) -- + deliberately separate from ``progress``/``tomo_progress`` (the real + tomogram's state), so the two can never be confused by anything + watching one or the other. + + Readable from any BEC client session via:: + + client.get_global_var("alignment_scan_progress") + """ + return self._alignment_scan_progress_proxy + @staticmethod def _format_duration(seconds: float) -> str: """Format a duration in seconds as a human-readable string, e.g. '2h 03m 15s'.""" @@ -1222,7 +1250,11 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools angles = list(np.linspace(0, 360, num=12, endpoint=False)) alignment_scan_numbers = [] - for angle in angles: + self.alignment_scan_progress.reset() + self.alignment_scan_progress.update(total_angles=len(angles), angle_index=0, angle=angles[0]) + self.lamnigui_show_alignment_progress() + + for idx, angle in enumerate(angles): successful = False print(f"Starting LamNI scan for angle {angle}") while not successful: @@ -1242,6 +1274,9 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools successful = True + self.alignment_scan_progress.update(angle_index=idx + 1, angle=angle) + self._lamnigui_update_alignment_progress() + umv(dev.lsamrot, 0) self.OMNYTools.printgreenbold( "\n\nAlignment scan finished. Please run BEC_ptycho_align and load the new fit"