From 15c720e89e8917ee0d78d46993ba42cd7a93540b Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Fri, 25 Sep 2026 14:49:31 +0200 Subject: [PATCH] fix(lamni): fully reset tomo progress when a new tomo scan starts tomo_scan() only reset the timing/ETA fields, so after a finished scan the progress GUI kept showing the old subtomo, angle and projection == total (rings at 100 %) while the initial 0-degree reference shot ran, with an ETA computed from the old projection count against the new start time. New _reset_progress_for_new_scan() resets every field (incl. the right totals for the tomo type) in one global-var update and refreshes the GUI. Resumes are unaffected. Co-Authored-By: Claude Sonnet 5 --- .../bec_ipython_client/plugins/LamNI/lamni.py | 59 +++++++++++++++--- .../test_lamni_tomo_angles.py | 60 +++++++++++++++++++ 2 files changed, 111 insertions(+), 8 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py index 1a3b816e..90cfcecc 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py @@ -1478,6 +1478,56 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools print(f"Estimated finish time: ........... {finish_str}\x1b[0m") self._lamnigui_update_progress() + def _reset_progress_for_new_scan(self) -> None: + """Reset ``tomo_progress`` (and refresh the progress GUI) for a brand-new + tomogram, in one global-var round-trip. + + Every field must be reset, not just the timing/ETA ones: a finished + previous scan leaves projection == total_projections (100 %), its last + subtomo and angle behind, and the new scan's very first + _print_progress() -- fired by the 0-degree reference shot, before the + grid loop has written a single field -- would otherwise show those + stale values (rings at 100 %, old subtomo/angle, and an ETA of "0 s" + computed from the old projection count against the NEW start time). + + subtomo is set to 1 (not 0) so a scan interrupted during the 0-degree + shot is still resumable by tomo_scan_resume(). + """ + if self.tomo_type == 1: + tomo_type_name = "Equally spaced sub-tomograms" + subtomo_total, _, total = self._tomo_type1_actual_grid() + elif self.tomo_type == 2: + tomo_type_name = "Golden ratio tomography" + subtomo_total = self.golden_ratio_bunch_size + total = self.golden_max_number_of_projections + elif self.tomo_type == 3: + tomo_type_name = "Equally spaced, golden ratio starting angles" + subtomo_total = 360 / self.tomo_angle_stepsize + total = self.golden_max_number_of_projections + else: + raise ValueError(f"Unknown tomo_type: {self.tomo_type}.") + + self.progress.update( + { + "tomo_type": tomo_type_name, + "subtomo": 1, + "subtomo_projection": 0, + # totals are used as divisors by the GUI rings -- never 0 + "subtomo_total_projections": subtomo_total or 1, + "projection": 0, + "total_projections": total if total and total > 0 else 1, + "angle": 0, + "tomo_start_time": datetime.datetime.now().isoformat(), + # no leftover ETA from a previous scan until this one has + # accumulated enough projections to compute a fresh one + "estimated_remaining_time": None, + "estimated_finish_time": None, + "heartbeat": None, + "accumulated_idle_time": 0.0, + } + ) + self._lamnigui_update_progress() + # ------------------------------------------------------------------ # Tomo scan orchestration # ------------------------------------------------------------------ @@ -1857,14 +1907,7 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools "BEC", ) self.write_pdf_report() - self.progress["tomo_start_time"] = datetime.datetime.now().isoformat() - # reset stale estimates from any previous scan, otherwise the GUI - # would keep showing a leftover ETA from before this scan has - # accumulated enough projections to compute a fresh one - self.progress["estimated_remaining_time"] = None - self.progress["estimated_finish_time"] = None - self.progress["accumulated_idle_time"] = 0.0 - self.progress["heartbeat"] = None + self._reset_progress_for_new_scan() # Unconditional single 0-degree reference projection at the very # start of a new tomogram, independent of # zero_deg_reference_at_each_subtomo / diff --git a/tests/tests_bec_ipython_client/test_lamni_tomo_angles.py b/tests/tests_bec_ipython_client/test_lamni_tomo_angles.py index 0342f389..7c3dad41 100644 --- a/tests/tests_bec_ipython_client/test_lamni_tomo_angles.py +++ b/tests/tests_bec_ipython_client/test_lamni_tomo_angles.py @@ -129,6 +129,65 @@ def test_all_eight_subtomos_are_equally_spaced_when_combined(stepsize): _assert_equally_spaced(combined, "full (all 8)") +# ── progress reset at the start of a new scan ─────────────────────────────── +# +# A finished scan leaves projection == total (100 %), its last subtomo and +# angle in tomo_progress. The new scan's first _print_progress() (from the +# 0-degree reference shot) used to render those stale values in the progress +# GUI, because only the timing/ETA fields were reset. + + +def test_new_scan_resets_stale_progress_before_first_projection(monkeypatch): + lamni = make_lamni_for_tomo_scan(monkeypatch, 10.0, active_account="") + lamni.progress.update( + { + "tomo_type": "Golden ratio tomography", + "subtomo": 8, + "subtomo_projection": 36, + "subtomo_total_projections": 36, + "projection": 288, + "total_projections": 288, + "angle": 355.0, + "estimated_remaining_time": 0.0, + "estimated_finish_time": "2020-01-01T00:00:00", + "accumulated_idle_time": 99.0, + "heartbeat": "2020-01-01T00:00:00", + } + ) + lamni.add_sample_database = lambda *a, **k: 0 + seen = [] + lamni._tomo_scan_at_angle = lambda angle, subtomo_number: seen.append( + (angle, subtomo_number, dict(lamni.progress.as_dict())) + ) + + lamni.tomo_scan() + + angle, subtomo_number, prog = seen[0] + assert (angle, subtomo_number) == (0, 1) + assert prog["tomo_type"] == "Equally spaced sub-tomograms" + assert prog["subtomo"] == 1 + assert prog["projection"] == 0 + assert prog["subtomo_projection"] == 0 + assert prog["angle"] == 0 + assert prog["total_projections"] == 36 * 8 + assert prog["subtomo_total_projections"] == 36 + assert prog["estimated_remaining_time"] is None + assert prog["estimated_finish_time"] is None + assert prog["accumulated_idle_time"] == 0.0 + assert prog["heartbeat"] is None + assert prog["tomo_start_time"] is not None + + +def test_resume_does_not_reset_progress(monkeypatch): + lamni = make_lamni_for_tomo_scan(monkeypatch, 10.0, active_account="") + resets = [] + lamni._reset_progress_for_new_scan = lambda: resets.append(1) + + lamni.tomo_scan(subtomo_start=2, start_angle=85.0) + + assert resets == [] + + # ── resuming mid-sub-tomogram (start_angle) ───────────────────────────────── # # start_angle used to be treated as the ORIGIN of a fresh 360-degree sweep, @@ -218,6 +277,7 @@ def make_lamni_for_tomo_scan( obj._print_progress = lambda: None obj._format_duration = lambda seconds: "0s" obj.lamnigui_show_progress = lambda: None + obj._lamnigui_update_progress = lambda: None obj.at_each_angle_hook = None obj.OMNYTools = types.SimpleNamespace(printgreenbold=lambda msg: None) # These tests exercise tomo_scan()'s account-handling/heartbeat/progress-GUI