fix(lamni): tomo_scan_resume resumes at the interrupted angle, not a shifted sweep

_subtomo_angle_plan() used an explicit start_angle as the origin of a new
360-degree sweep. Resuming subtomo 2 at 85 deg generated 435..85; the >360
artifacts were silently skipped by _tomo_scan_at_angle's range gate, so the
resume began at 355 deg, re-measured done angles, never reached the rest and
reported wrong progress indices.

start_angle is now the literal resume point on the sub-tomogram's own grid:
the plan returns the remaining tail of the normal sweep plus a subtomo_offset
used for progress numbering (mirrors Flomni). Off-grid angles raise ValueError.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 13:07:46 +02:00
committed by holler
co-authored by Claude Sonnet 5
parent 00ddc5f560
commit fd4e8a2de1
2 changed files with 97 additions and 21 deletions
@@ -129,6 +129,53 @@ def test_all_eight_subtomos_are_equally_spaced_when_combined(stepsize):
_assert_equally_spaced(combined, "full (all 8)")
# ── resuming mid-sub-tomogram (start_angle) ─────────────────────────────────
#
# start_angle used to be treated as the ORIGIN of a fresh 360-degree sweep,
# so resuming e.g. subtomo 2 at 85 deg generated 435..85 (flipped): the >360
# artifacts were silently skipped, the resume started at 355 deg instead of
# 85, re-measured done angles and never reached the remaining ones.
def _resume(stepsize: float, subtomo_number: int, start_angle: float):
lamni = make_lamni(stepsize)
recorded = []
def _record(angle, subtomo):
recorded.append((float(angle), int(lamni.progress["subtomo_projection"])))
lamni._tomo_scan_at_angle = _record
lamni.sub_tomo_scan(subtomo_number, start_angle=start_angle)
return recorded
@pytest.mark.parametrize("stepsize", STEPSIZES)
@pytest.mark.parametrize("subtomo_number", range(1, 9))
def test_resume_yields_tail_of_normal_sweep(stepsize, subtomo_number):
full = make_lamni(stepsize)
full_recorded = []
full._tomo_scan_at_angle = lambda angle, subtomo: full_recorded.append(float(angle))
full.sub_tomo_scan(subtomo_number)
for k, start in enumerate(full_recorded):
resumed = _resume(stepsize, subtomo_number, start)
assert [a for a, _ in resumed] == pytest.approx(full_recorded[k:])
assert [i for _, i in resumed] == list(range(k, len(full_recorded)))
assert all(0 <= a < 360.05 for a, _ in resumed)
def test_resume_reported_case_subtomo2_at_85_deg():
resumed = _resume(10.0, 2, 85.0)
assert resumed[0] == (pytest.approx(85.0), 27)
assert resumed[-1][0] == pytest.approx(5.0)
assert len(resumed) == 36 - 27
def test_resume_off_grid_start_angle_raises():
with pytest.raises(ValueError):
_resume(10.0, 2, 80.0)
# ── tomo_scan()'s new-scan account handling ─────────────────────────────────
#
# bec.active_account used to be passed through .decode() (as if it were