fix(flomni): clear busy heartbeat on scan end, fix duplicated tomo math

Two independent fixes:

- GUI "beamline busy" indicators (TomoParamsWidget's banner,
  FlomniWebpageGenerator's status page) purely wait for
  progress["heartbeat"] to go stale (120s / 90s respectively) - there was
  no explicit "scan finished" signal to react to instead, since
  tomo_scan() wrote a heartbeat at the start of every projection but
  never cleared it on exit. Wrapped the scan loop in try/finally so
  heartbeat is cleared on every exit path (normal completion, exception,
  or interrupt/abort), not just at the next scan's start - both
  busy-detectors now see this on their very next poll instead of waiting
  out the timeout. Live-verified: heartbeat is None immediately after
  tomo_scan() returns.

- Found while investigating a related report ("angular step of the final
  combined tomogram shown different between 180 and 360 mode, although
  it has to be identical" + "GUI shows projection count doubling when
  switching 180->360"): TomoParamsWidget's _compute_type1()/
  _requested_to_stepsize() (tomo_params.py) and the generated status
  webpage's calcProjections() JS (flomni_webpage_generator.py) are both
  independent, un-synced duplicates of the exact old N=int(angle_range/
  stepsize) formula fixed in flomni.py's own _tomo_type1_actual_grid()
  two commits ago - they were never updated when that fix landed, so the
  GUI and webpage kept reporting double the projection count for 360
  mode. Fixed both to match flomni.py: N/step/total are always computed
  against a fixed 180 degrees, independent of angle_range. Also fixed
  the "angular step of the final (combined) tomogram" CLI/wizard lines
  in flomni.py itself, which used tomo_angle_range/total_projections
  (correct for 180 mode by coincidence, wrong for 360 mode - the true
  combined resolution is always 180/total_projections, identical
  between modes). Added a regression test asserting the widget's
  formulas match flomni.py's for both modes, to catch this exact kind of
  drift if it recurs. Live-verified against the running sim: both modes
  now report identical total_projections and combined-tomogram step for
  the same stepsize.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
x01dc
2026-07-15 10:18:38 +02:00
co-authored by Claude Sonnet 5
parent 278364e650
commit aebfccc5f3
4 changed files with 227 additions and 132 deletions
@@ -0,0 +1,48 @@
"""Regression test for TomoParamsWidget's independent (duplicated)
projection-count math: it must stay in sync with Flomni's own
_tomo_type1_actual_grid()/_subtomo_angle_plan() -- this exact drift (the
widget kept using the old, angle-range-dependent formula after flomni.py
was fixed to be mode-independent) is what caused the GUI to show the
projection count doubling when switching from 180 to 360 mode.
"""
import pytest
from csaxs_bec.bec_ipython_client.plugins.flomni.flomni import Flomni
from csaxs_bec.bec_widgets.widgets.tomo_params.tomo_params import (
_compute_type1,
_requested_to_stepsize,
)
STEPSIZES = [10.0, 7.0, 25.0, 12.5]
@pytest.mark.parametrize("stepsize", STEPSIZES)
@pytest.mark.parametrize("angle_range", [180, 360])
def test_compute_type1_matches_flomni(stepsize, angle_range):
total, step, N = _flomni_reference(stepsize)
actual_total, achievable_step, _ = _compute_type1(angle_range, stepsize)
assert actual_total == total
assert achievable_step == pytest.approx(step)
@pytest.mark.parametrize("requested_total", [24, 48, 96, 144, 200])
@pytest.mark.parametrize("angle_range", [180, 360])
def test_requested_to_stepsize_matches_flomni_wizard_formula(requested_total, angle_range):
"""Mirrors the exact line in Flomni.tomo_parameters()'s wizard:
self.tomo_angle_stepsize = (180.0 / tomo_numberofprojections) * 8"""
stepsize = _requested_to_stepsize(angle_range, requested_total)
assert stepsize == pytest.approx((180.0 / requested_total) * 8)
def test_total_projections_identical_between_modes_for_same_stepsize():
for stepsize in STEPSIZES:
total_180, _, _ = _compute_type1(180, stepsize)
total_360, _, _ = _compute_type1(360, stepsize)
assert total_180 == total_360
def _flomni_reference(stepsize):
N = int(180.0 / stepsize)
step = 180.0 / N
return N * 8, step, N