fix(tomo): no initial 0-deg projection for tomo_type 4 (angle file) #354

Merged
holler merged 1 commits from fix_tomotype_4 into main 2026-11-09 09:33:37 +01:00
5 changed files with 48 additions and 27 deletions

No files matched your search

@@ -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:
@@ -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"
@@ -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:
@@ -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):
@@ -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)