From 0a12d4892f80097c9049404f602f7fa082500a3e Mon Sep 17 00:00:00 2001 From: x01dc Date: Sat, 19 Sep 2026 10:06:49 +0200 Subject: [PATCH] feat(flomni,lamni): unconditional 0-deg reference projection at scan start Take one real 0-degree projection at the very start of every new tomogram (subtomo_number=1, included in reconstruction like any grid projection), independent of zero_deg_reference_at_each_subtomo / golden_projections_at_0_deg_for_damage_estimation -- gives an immediate radiation-damage baseline even when those flags are off. Placed right after Flomni's existing collect_empty_frames() step; LamNI gets the same 0-deg shot but no flat-field step, since it has no way to retract the sample out of the beam. Omny intentionally left as a follow-up (not implemented here). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw --- .../bec_ipython_client/plugins/LamNI/lamni.py | 12 ++++++++++++ .../bec_ipython_client/plugins/flomni/flomni.py | 10 ++++++++++ .../test_lamni_tomo_angles.py | 17 +++++++++++++---- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py index 785c02e4..506cd8e5 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py @@ -1836,6 +1836,18 @@ class LamNI(TomoQueueMixin, LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools self.progress["estimated_finish_time"] = None self.progress["accumulated_idle_time"] = 0.0 self.progress["heartbeat"] = None + # Unconditional single 0-degree reference projection at the very + # start of a new tomogram, independent of + # zero_deg_reference_at_each_subtomo / + # golden_projections_at_0_deg_for_damage_estimation -- gives an + # immediate radiation-damage baseline even when those flags are + # off. Real data (uses _tomo_scan_at_angle like any grid + # projection), included in reconstruction like any other + # projection. Logged under subtomo_number=1. Mirrors Flomni's + # equivalent addition -- no flat-field/empty-frame step here + # (unlike Flomni's collect_empty_frames()), since LamNI has no + # way to retract the sample out of the beam. + self._tomo_scan_at_angle(0, 1) try: with scans.dataset_id_on_hold: diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py index 519d1bf7..cfd575a9 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py @@ -2731,6 +2731,16 @@ class Flomni( self.progress["accumulated_idle_time"] = 0.0 self.progress["heartbeat"] = None self.collect_empty_frames() + # Unconditional single 0-degree reference projection at the very + # start of a new tomogram, independent of + # zero_deg_reference_at_each_subtomo / + # golden_projections_at_0_deg_for_damage_estimation -- gives an + # immediate radiation-damage baseline even when those flags are + # off. Real data (uses _tomo_scan_at_angle like any grid + # projection), included in reconstruction like any other + # projection -- not a flat-field/reference-only frame like + # collect_empty_frames() above. Logged under subtomo_number=1. + self._tomo_scan_at_angle(0, 1) try: with scans.dataset_id_on_hold: 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 f9a63fe9..36e9e220 100644 --- a/tests/tests_bec_ipython_client/test_lamni_tomo_angles.py +++ b/tests/tests_bec_ipython_client/test_lamni_tomo_angles.py @@ -164,6 +164,10 @@ def make_lamni_for_tomo_scan( obj.special_angles = [] obj.write_pdf_report = lambda: None obj.sub_tomo_scan = lambda subtomo_number, start_angle=None: None + # Default no-op for the unconditional 0-deg reference projection tomo_scan() + # now fires at the start of every new scan (see lamni.py) -- tests that + # care about it override this after calling make_lamni_for_tomo_scan(). + obj._tomo_scan_at_angle = lambda angle, subtomo_number: None obj._print_progress = lambda: None obj._format_duration = lambda seconds: "0s" obj.lamnigui_show_progress = lambda: None @@ -301,7 +305,9 @@ def test_subtomo_starts_near_zero(subtomo_number, expected): def test_zero_deg_reference_disabled_by_default(monkeypatch): """zero_deg_reference_at_each_subtomo defaults to False -- a fresh - tomo_scan() must not fire any extra 0-deg shots.""" + tomo_scan() must not fire any *flag-gated* extra 0-deg shots, but the + unconditional single 0-deg reference projection at scan start (see + lamni.py) always fires regardless of the flag.""" lamni = make_lamni_for_tomo_scan(monkeypatch, 45.0, active_account="") lamni.add_sample_database = lambda *a, **k: 0 recorded = [] @@ -312,13 +318,16 @@ def test_zero_deg_reference_disabled_by_default(monkeypatch): lamni.tomo_scan() - assert recorded == [] + assert recorded == [(0, 1)] def test_zero_deg_reference_fires_for_odd_subtomos_and_final_shot(monkeypatch): """With the flag on, a fresh tomo_scan() must fire an extra angle-0 shot before each odd sub-tomogram (1, 3, 5, 7) plus one final shot after - sub-tomogram 8 completes -- mirrors Flomni.tomo_scan()'s equivalent.""" + sub-tomogram 8 completes -- mirrors Flomni.tomo_scan()'s equivalent. The + leading (0, 1) is the unconditional scan-start reference projection + (fires regardless of the flag), followed by the flag's own (0, 1) for + sub-tomogram 1 itself -- the two are independent and both fire here.""" lamni = make_lamni_for_tomo_scan(monkeypatch, 45.0, active_account="") lamni.add_sample_database = lambda *a, **k: 0 lamni.zero_deg_reference_at_each_subtomo = True @@ -330,7 +339,7 @@ def test_zero_deg_reference_fires_for_odd_subtomos_and_final_shot(monkeypatch): lamni.tomo_scan() - assert recorded == [(0, 1), (0, 3), (0, 5), (0, 7), (0, 8)] + assert recorded == [(0, 1), (0, 1), (0, 3), (0, 5), (0, 7), (0, 8)] def test_zero_deg_reference_skipped_when_resuming_mid_subtomo(monkeypatch):