Ports flomni's per-projection/tomogram JSONL timing log
(~/data/raw/logs/timing_statistics/*.jsonl, feeding a future scan-time
prediction model) and the scilog_last_ptycho_scans(n) user command to
lamni. Direct port adapted only for lamni's own param names (no
fovx/fovy/stitch_x/stitch_y/single_point_instead_of_fermat_scan --
uses tomo_circfov/lamni_stitch_x/y/lamni_piezo_range_x/y instead, no
single-point concept at all).
_log_projection_timing() is only called on a clean (non-error_caught)
completion of _tomo_scan_at_angle()'s retry loop, so an
AlarmBase/TimeoutError attempt (no reliable duration measurement)
doesn't pollute the log.
Item 1 of csaxs_bec/bec_ipython_client/plugins/LamNI/AI_docs/
FLOMNI_LAMNI_FEATURE_GAPS_2026-07.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports flomni's zero_deg_reference_at_each_subtomo to lamni's "8 equally
spaced sub-tomograms" mode: an extra dedicated projection at exactly 0
degrees before every sub-tomogram where the rotation naturally passes
back through 0, plus one final shot once the tomogram completes, for
tracking radiation damage over a long acquisition. lamni's tomo_type
2/3 already had the equivalent (golden_projections_at_0_deg_for_damage_estimation,
byte-for-byte identical to flomni's) -- only tomo_type 1 was missing.
_subtomo_starts_near_zero() uses subtomo_number % 2 (every odd
sub-tomogram) rather than flomni's 360-mode subtomo_number % 4 == 1 --
confirmed against lamni's actual rotation behavior rather than derived
from the position-array math, which doesn't cleanly split odd/even.
The GUI (tomo_params.py) needed no new UI code: _build_type1_section()
already gated this control on a per-setup has_zero_deg_reference flag,
so enabling it for lamni was just flipping that flag plus adding the
param name/default, mirroring flomni's entries exactly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FlomniFermatScan/LamNIFermatScan already refuse to run with fewer than
20 positions (_check_min_positions(), raises ScanAbortion), but only
once the scan actually starts -- too late for an unattended tomo-queue
run, where the job just aborts mid-run and pauses the queue. Surface
the same prediction earlier instead, as a non-blocking warning:
- Extracted the position-generating methods on both scan classes
(get_flomni_fermat_spiral_pos, and lamni's chain --
_lamni_compute_scan_center, _lamni_compute_stitch_center,
_compute_total_shift, _lamni_check_pos_in_fov_range_and_circ_fov,
get_lamni_fermat_spiral_pos) into pure @staticmethods, so the exact
same algorithm the scan server runs can also be called from
client-side code without a live scan session. Behavior-preserving --
verified against the existing exact-position/instruction assertion
tests for both classes. Lifted the hardcoded "20" into a _MIN_POSITIONS
class attribute on each, so client code references the same threshold.
- lamni.py/flomni.py: new _expected_fermat_position_count() calls the
real scan-class algorithm with the live tomo parameters and prints a
warning line in tomo_parameters() when below the minimum.
- tomo_params.py: a new live-updating "Estimated Fermat scan points"
field (mirroring the existing achievable-step preview's styling),
wired to the fov/step/stitch/piezo-range fields, flagged orange below
the minimum -- never blocks Submit/Add-to-queue.
lamni's circular-FOV crop is angle/stitch-dependent, so the estimate is
representative (center tile, angle 0 for the CLI; the live stitch tile
for the GUI, which has those fields right there) rather than an exact
per-projection guarantee -- documented as such in both places.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add tomo_queue_reacquire(job_index, projection_number) to the shared
TomoQueueMixin: reopens any queue job (even an already-"done" one) at
an earlier projection than its own recorded progress, and resets every
job queued after it -- tomo and command jobs alike -- to "pending", so
a following tomo_queue_resume() (a new alias for tomo_queue_execute(),
added for naming symmetry with tomo_scan_resume()) re-runs everything
from there forward in order. Reuses the existing tomo_scan_resume()
machinery by writing the requested resume point into the shared
progress global var, rather than adding new parameters to
tomo_scan()/tomo_queue_execute(). Guards against clobbering another
job's progress if one is already "incomplete"/"running" elsewhere in
the queue.
A flat projection number is used as the resume-point unit for all
three tomo_types, including type 1 (8 equally-spaced sub-tomograms),
via a new per-setup _resolve_type1_projection() that maps it to
(subtomo_start, start_angle). flomni's version reuses its existing
_subtomo_angle_plan(); lamni's inline angle math in sub_tomo_scan() was
extracted into an analogous _subtomo_angle_plan() static method first
(behavior-preserving -- verified against the existing angle-math test
suite) so both setups share the same pattern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Port two flomni tomo_scan() behaviors lamni was missing: an end-of-scan
timing overview (total time, time excluding gaps, time lost to gaps,
sample/hook info) printed and written to scilog, and automatic repeat
of an entire projection when the beamline interlock trips mid-scan.
The latter needed two pieces together: enabling
bec.builtin_actors.scan_interlock at tomo_scan() start (previously
never enabled for lamni), and wrapping _tomo_scan_at_angle in
@scan_repeat so the resulting ScanRestart (or any other transient
exception) redoes the whole projection instead of aborting the scan.
A new LamNIError marks the one non-retryable case (unregistered
at_each_angle_hook), replacing a plain ValueError there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mirrors two fixes already made to Flomni.tomo_scan(): LamNI.tomo_scan()
never opened the progress GUI automatically, and left a stale "busy"
heartbeat for up to 120s after a scan had already finished (or crashed),
since it was only ever reset at the start of the next scan.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bec.active_account.decode() crashed on every real tomo_scan() call --
active_account is a plain str, not bytes. Flomni's equivalent never had
the .decode() call and already guards an empty active_account (e.g. a
dev/sim session) by skipping sample-database registration instead of
crashing. Same bug fixed in DataDrivenLamNI.tomo_scan() (extra_tomo.py),
which had an identical inline copy.
write_pdf_report() read a nonexistent "mokev" device; the real device is
ccm_energy. Reads it for real now, wrapped in a broad try/except falling
back to "N/A" if unavailable, rather than a hardcoded placeholder.
write_pdf_report()'s logbook/scilog write crashed when scilog isn't
configured -- lamni.py already had this exact tolerance pattern one
method over, in write_to_scilog(); applied the same try/except here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Generalize TomoParamsWidget to lamni via a SETUP_PROFILES mechanism
(setup detection, per-setup field lists/order, a new Offsets section,
a setup-agnostic "Duplicate job" queue button), fixing two latent
TomoQueueDialog bugs that silently mishandled lamni jobs along the way.
While verifying the GUI's projection-count preview against the CLI,
found two real, pre-existing bugs in LamNI.sub_tomo_scan() unrelated
to the GUI itself: a duplicate closing angle every sub-tomogram
(360=0 degrees), and a phase offset computed from the raw stepsize
instead of the achievable one, breaking the equally-spaced-when-
combined guarantee for sub-tomogram pairs/quads/the full set. Both
fixed to mirror Flomni's existing, correct equivalents.
Also fills in lamni's user documentation with the queue/command-job/
at-each-angle-hook system, which it previously lacked entirely,
mirroring flomni.md's coverage.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract the tomo-queue/at-each-angle-hook backend (job proxy, hook
registry, command-job dispatch) out of flomni.py into a new shared
TomoQueueMixin (OMNY_shared/tomo_queue_mixin.py) so it can be reused
instead of duplicated, avoiding the same param-name-mirror drift
already flagged for tomo_params.py. Flomni's refactor is behavior-
preserving; LamNI is the new consumer, with its own param names
(tomo_circfov, lamni_stitch_x/y, ...) and no 180-degree/single-point
concepts, since lamni doesn't have them. Also adds LamNI.tomo_scan_resume(),
required for tomo_queue_execute()'s resume-after-crash path but not
previously present.
Verified via a new unit test suite (test_lamni_tomo_queue.py) plus live
checks against the running simulated flomni and lamni deployments,
documented in LamNI/AI_docs/TOMO_QUEUE_PORT.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Supersedes the complementary even/odd-eighths tables from 900c810 with
a single shared phase_eighths table used in both 180 and 360 mode, so
subtomo N carries the same phase-tier role regardless of
tomo_angle_range. The complementary property of the low/high phase
sets now falls out as a consequence of the original table's structure
instead of needing a separately derived table.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
The previous commit's 360-degree phase assignment gave the low and high
halves the SAME phase per pair (1,2)/(3,4)/(5,6)/(7,8), so each pair
concatenated into one continuous evenly-spaced range -- but that means
every low-half angle is exactly 180deg from a high-half angle, which is
exactly the redundant-measurement bug this was supposed to fix, just
reintroduced between subtomos instead of within one.
Corrected: the low half (subtomos 1,4,5,8) now uses the even eighths of
the original 8-way phase_eighths table, and the high half (2,3,6,7) uses
the odd eighths -- complementary, not shared. Folded mod 180, the two
halves interleave into exactly the same 8-way, step/8 grid that 180-mode
itself produces: every position is measured exactly once, using its full
0-360 physical range, with zero redundant measurements anywhere. Total
projection count is unchanged (still identical to 180 mode for a given
tomo_angle_stepsize).
Verified: pure-math unit tests confirm the 360-mode combined set, folded
mod 180, is an exact set match (same count, no repeated residue) against
180-mode's own set. Live-verified against the running flomni sim: real
motor motion for both modes, folding the observed 360-mode angles mod
180 exactly reproduces the observed 180-mode angles.
Also updated the two "angular step of the final combined tomogram"
CLI/parameter-wizard print statements to additionally report the
effective mod-180 reconstruction resolution for 360 mode, since it's
now finer than the raw per-half spacing they already printed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three independent fixes:
- sub_tomo_scan(): in 360-degree tomo_type-1 mode, every sub-tomogram used
to sweep the full 0-360 range, so each one's own fine grid contained
angle pairs exactly 180deg apart -- redundant tomographic information.
Sub-tomograms now each cover a 180deg span, split into low/high halves
by subtomo_number % 4 with a bit-reversal-of-4 phase table, so adjacent
pairs (1,2)/(3,4)/(5,6)/(7,8), either quartet, and all 8 combined each
independently form a complete, evenly-spaced 360deg tomogram at
successively finer spacing. Total projection count for a given
tomo_angle_stepsize is now identical between 180 and 360 mode (same N,
no longer doubled). Updated the 4 other consumers of the old N/step
formula (zero-deg reference gating, _tomo_type1_actual_grid, the
parameter wizard, the PDF report) to match. 180-degree mode is
unchanged. Live-verified against the running flomni sim: real motor
motion traces the expected boustrophedon path with no duplicate or
180deg-apart angles.
- Sample storage: added an owner field, packed into the same EPICS DESC
field as the sample name ("name | owner", via new sample_desc_codec.py)
since there's no separate PV for it. Wired through
FlomniSampleStorage, the CLI (flomni_modify_storage_non_interactive,
ftransfer_modify_storage), the two transfer routines that forward a
raw DESC value across a gripper move (now unpacked/repacked so owner
survives the move instead of being dropped or double-packed), and
SampleStorageWidget. Scoped to flomni only this session; OMNY's
storage/transfer mixin is unchanged.
- ConsoleButtonsWidget's ABORT button used to send SIGINT then, 500ms
later, a stop_devices() broadcast to ALL devices with no stop_id --
an un-suppressed error from that broadcast landing on a queue-tracked
instruction could kill the scan worker thread outright, requiring a
full BEC restart. Replaced with: queue.request_scan_abortion() (safe
no-op if idle, but registers a stop_id so expected errors are
suppressed), then SIGINT, then a direct, immediate Galil hard stop via
new GalilController.hard_abort_and_restore_positioning_mode() -- the
same method ftransfer_abort() now delegates to, so the CLI and GUI
paths can't drift apart again. SIGINT is sent before the hard stop:
live testing showed that if the hard stop's mntprgs-clearing side
effect lands first, a same-session polling loop can mistake it for
normal completion and fall through into ensure_gripper_up(), which
must not happen mid-transfer -- sending SIGINT first (near-instant)
gives that loop's own KeyboardInterrupt handler a head start before
the hard stop's own multi-step sequence completes. The button is
labeled per beamline (e.g. "Flomni Motion Stop") and disabled rather
than silently inert when no hard-stop device is configured/enabled.
Scoped to flomni only this session (OMNY/LamNI wiring deferred).
Live-verified against the running sim, including the exact
stop-lands-mid-queued-instruction scenario that previously crashed
the scan worker.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
XrayEyeAlign.gui is a property delegating to lamni.xeyegui and has no
setter. Mock lamni.xeyegui in the update_frame tests; drop the redundant
alignment_images reset, which __init__ already handles.
Drop test_save_frame (method no longer exists). Rewrite test_update_frame
against dev.fsh/dev.cam_xeye instead of the removed LamNI.alignment module.
Patch the module-level umv in test_tomo_rotate rather than builtins.