diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py index df8db921..8bbe8f8e 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/lamni.py @@ -2426,7 +2426,15 @@ class LamNI( # 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) + # + # Skipped for tomo_type 4 (angle file): the file may override + # the scan parameters per angle, so a 0-degree shot with the + # normal tomo parameters could end up as e.g. a low-resolution + # projection inside a high-resolution tomogram. The angle file + # is the sole source of projections there -- put 0 in the file + # if one is wanted. Mirrors Flomni. + if self.tomo_type != 4: + self._tomo_scan_at_angle(0, 1) try: with scans.dataset_id_on_hold: diff --git a/csaxs_bec/bec_ipython_client/plugins/OMNY_shared/tomo_angle_file_mixin.py b/csaxs_bec/bec_ipython_client/plugins/OMNY_shared/tomo_angle_file_mixin.py index 4a655e9d..5293ce05 100644 --- a/csaxs_bec/bec_ipython_client/plugins/OMNY_shared/tomo_angle_file_mixin.py +++ b/csaxs_bec/bec_ipython_client/plugins/OMNY_shared/tomo_angle_file_mixin.py @@ -187,8 +187,7 @@ class TomoAngleFileMixin: def _init_angle_file_progress(self, file_angles: AngleFileResult, projection_number) -> None: """Fill the progress fields that are already known from the file, so the GUI shows the real tomo type/total (and sub-tomogram 1) during the - preparation steps and the 0-deg reference shot that precede the loop, - instead of the blank reset defaults (tomo type 0, projection 0).""" + preparation steps (e.g. empty frames) that precede the loop, instead of the blank reset defaults (tomo type 0, projection 0).""" self.progress["tomo_type"] = "Angles from file" self.progress["total_projections"] = ( len(file_angles.angles) if file_angles.is_complete else 0 @@ -197,9 +196,12 @@ class TomoAngleFileMixin: self.progress["subtomo"] = 1 def _run_angle_file_loop(self, file_angles: AngleFileResult, projection_number) -> None: - """Acquire the angles of tomo_type 4 in file order (no extra 0-deg - damage-estimation shots: sub-tomogram numbers in a user file are just - labels, put 0-deg lines in the file instead). Waits for lines to be + """Acquire the angles of tomo_type 4 in file order. The file is the + sole source of projections: no initial 0-deg shot at the start of a + new tomogram (it would use the normal tomo parameters, not the + file's per-angle ones) and no extra 0-deg damage-estimation shots + (sub-tomogram numbers in a user file are just labels) -- put 0-deg + lines in the file instead. Waits for lines to be appended while the file has no 'END' marker.""" error = self._TOMO_PARAMS_ERROR self.progress["tomo_type"] = "Angles from file" diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py index e561a3c7..d8788a67 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py @@ -2827,7 +2827,16 @@ class Flomni( # 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) + # + # Skipped for tomo_type 4 (angle file): the file may override + # the scan parameters (step, exp_time, frames_per_trigger, ...) + # per angle, so a 0-degree shot taken with the normal tomo + # parameters could be e.g. a low-resolution projection inside a + # high-resolution tomogram, which breaks loading the projections + # for further processing. The angle file is the sole source of + # projections there -- put 0 in the file if one is wanted. + if self.tomo_type != 4: + self._tomo_scan_at_angle(0, 1) try: with scans.dataset_id_on_hold: diff --git a/tests/tests_bec_ipython_client/test_flomni_tomo_angles.py b/tests/tests_bec_ipython_client/test_flomni_tomo_angles.py index 414ff838..13912379 100644 --- a/tests/tests_bec_ipython_client/test_flomni_tomo_angles.py +++ b/tests/tests_bec_ipython_client/test_flomni_tomo_angles.py @@ -355,8 +355,8 @@ def test_angle_file_scan_follows_file_order_and_subtomos(monkeypatch, tmp_path): flomni.tomo_scan() - # (0, 1): unconditional 0-deg reference shot at the start of a new tomogram - assert recorded == [(0, 1), (10.0, 1), (170.0, 1), (270.0, 2), (5.5, 3)] + # no initial 0-deg shot: for tomo_type 4 the file is the only source of projections + assert recorded == [(10.0, 1), (170.0, 1), (270.0, 2), (5.5, 3)] assert flomni.progress["total_projections"] == 4 assert flomni.progress["tomo_type"] == "Angles from file" @@ -407,8 +407,9 @@ def test_angle_file_missing_raises_before_any_acquisition(monkeypatch, tmp_path) def test_angle_file_ignores_golden_zero_deg_damage_flag(monkeypatch, tmp_path): - """No extra 0-deg shots at sub-tomogram transitions for type 4 -- only - the unconditional reference shot at the start of a new tomogram.""" + """No 0-deg shots at all for type 4 -- neither at sub-tomogram + transitions nor the initial reference shot at the start of a new + tomogram (it would use the normal tomo parameters, not the file's).""" flomni = make_flomni_for_angle_file(monkeypatch, tmp_path, "10 1\n20 2\n30 3\n40 5\nEND\n") flomni.golden_projections_at_0_deg_for_damage_estimation = 1 recorded = [] @@ -416,7 +417,7 @@ def test_angle_file_ignores_golden_zero_deg_damage_flag(monkeypatch, tmp_path): flomni.tomo_scan() - assert recorded == [(0, 1), (10.0, 1), (20.0, 2), (30.0, 3), (40.0, 5)] + assert recorded == [(10.0, 1), (20.0, 2), (30.0, 3), (40.0, 5)] def test_max_scan_angle_is_362_only_for_type_4(): @@ -452,8 +453,7 @@ def test_angle_file_accepts_physical_overtravel_range(monkeypatch, tmp_path): flomni.tomo_scan() # exercises the real _tomo_scan_at_angle(), not a stub - # 0: unconditional 0-deg reference shot at the start of a new tomogram - assert recorded == [0, -2.0, -0.1, 361.0, 362.0] + assert recorded == [-2.0, -0.1, 361.0, 362.0] # ── tomo_type 4: live append -- wait at EOF without 'END', poll, pick up ──── @@ -475,8 +475,7 @@ def test_angle_file_waits_then_picks_up_appended_lines(monkeypatch, tmp_path): flomni.tomo_scan() - # (0, 1): unconditional 0-deg reference shot at the start of a new tomogram - assert recorded == [(0, 1), (5.0, 1), (10.0, 1), (20.0, 1)] + assert recorded == [(5.0, 1), (10.0, 1), (20.0, 1)] assert len(sleep_calls) == 1 assert flomni.progress["total_projections"] == 3 @@ -529,12 +528,11 @@ def test_angle_file_heartbeat_refreshed_while_waiting(monkeypatch, tmp_path): flomni.tomo_scan() - # (0, 1) reference shot, then (5.0, 1) -- both before any wait tick, so no - # heartbeat has been written yet (the stub above never sets one itself). + # (5.0, 1) comes before any wait tick, so no heartbeat has been written + # yet (the stub above never sets one itself). assert recorded_heartbeats[0] is None - assert recorded_heartbeats[1] is None # (10.0, 1) is only reached after the wait tick refreshed the heartbeat. - assert recorded_heartbeats[2] is not None + assert recorded_heartbeats[1] is not None # ── tomo_type 4: per-line acquisition overrides (step=/exptime=/frames=) ─── @@ -554,7 +552,6 @@ def test_angle_file_overrides_reach_tomo_scan_at_angle_and_are_sticky(monkeypatc flomni.tomo_scan() assert recorded == [ - (0, 1, {}), # unconditional 0-deg reference shot at tomogram start (10.0, 1, {}), (20.0, 1, {"step": 0.5}), (30.0, 1, {"step": 0.5}), # sticky: step carries forward @@ -727,7 +724,7 @@ def test_resume_does_not_reset_progress(monkeypatch, tmp_path): assert flomni.progress["estimated_remaining_time"] == 42.0 -def test_angle_file_progress_shows_type_and_total_before_the_zero_deg_shot(monkeypatch, tmp_path): +def test_angle_file_progress_shows_type_and_total_and_skips_zero_deg_shot(monkeypatch, tmp_path): flomni = make_flomni_for_angle_file(monkeypatch, tmp_path, "10\n20\nEND\n") seen = [] flomni._tomo_scan_at_angle = lambda angle, subtomo, **kw: seen.append( @@ -736,7 +733,10 @@ def test_angle_file_progress_shows_type_and_total_before_the_zero_deg_shot(monke flomni.tomo_scan() - assert seen[0] == (0, "Angles from file", 2) # the initial 0-deg shot + # no initial 0-deg shot for tomo_type 4: the file is the only source of + # projections (a 0-deg shot would use the normal tomo parameters) + assert [a for a, _, _ in seen] == [10, 20] + assert seen[0] == (10, "Angles from file", 2) def test_flomni_gui_progress_survives_zero_total(monkeypatch): diff --git a/tests/tests_bec_ipython_client/test_lamni_tomo_angle_file.py b/tests/tests_bec_ipython_client/test_lamni_tomo_angle_file.py index a601c7a3..a5104a63 100644 --- a/tests/tests_bec_ipython_client/test_lamni_tomo_angle_file.py +++ b/tests/tests_bec_ipython_client/test_lamni_tomo_angle_file.py @@ -107,7 +107,7 @@ def test_scan_follows_file_order_and_reports_progress(monkeypatch, tmp_path): lamni.tomo_scan(force=True) - assert recorded == [(0, 1), (10.0, 1), (170.0, 1), (270.0, 2), (362.0, 3)] + assert recorded == [(10.0, 1), (170.0, 1), (270.0, 2), (362.0, 3)] assert lamni.progress["total_projections"] == 4 assert lamni.progress["tomo_type"] == "Angles from file" @@ -175,7 +175,6 @@ def test_per_line_overrides_are_sticky_and_passed_to_scan_at_angle(monkeypatch, lamni.tomo_scan(force=True) assert calls == [ - (0, {}), (10.0, {}), (20.0, {"step": 0.5, "exp_time": 2.0, "frames_per_trigger": 3}), (30.0, {"step": 0.7, "exp_time": 2.0, "frames_per_trigger": 3}), @@ -259,7 +258,7 @@ def test_queue_job_line_shows_angle_file_for_type_4(): assert "angles=" not in line -def test_progress_shows_type_and_total_before_the_zero_deg_shot(monkeypatch, tmp_path): +def test_progress_shows_type_and_total_and_skips_zero_deg_shot(monkeypatch, tmp_path): lamni = make_lamni_for_angle_file(monkeypatch, tmp_path, "10\n20\nEND\n") seen = [] lamni._tomo_scan_at_angle = lambda angle, subtomo: seen.append( @@ -268,4 +267,7 @@ def test_progress_shows_type_and_total_before_the_zero_deg_shot(monkeypatch, tmp lamni.tomo_scan(force=True) - assert seen[0] == (0, "Angles from file", 2) # the initial 0-deg shot + # no initial 0-deg shot for tomo_type 4: the file is the only source of + # projections (a 0-deg shot would use the normal tomo parameters) + assert [a for a, _, _ in seen] == [10, 20] + assert seen[0] == (10, "Angles from file", 2)