Commit Graph
64 Commits
Author SHA1 Message Date
x01dcandClaude Sonnet 5 78f0c8fa3b feat(omny): port zero_deg_reference_at_each_subtomo to types 1/4/5
CI for csaxs_bec / test (push) Successful in 2m35s
flomni already has this (type-1-only: a dedicated projection at exactly
0 degrees before every odd/forward sub-tomogram, plus once more at the
end, for tracking radiation damage over a tomogram). Deliberately
excluded from OMNY during the Phase 1 port as flomni-specific; Mirko
asked for it to be extended to OMNY's generalized equally-spaced types
(1/4/5, 2/4/8 sub-tomograms), and confirmed the pre-shot should fire
even for sub-tomogram 1 (whose own first projection already lands on 0)
to get two consecutive 0-degree shots as an immediate baseline pair.

Also fixes a second hardcoded `tomo_type != 1` bug in the widget's
_irrelevant_params_for_type() (same class as the earlier _validate()
fix) that would have hidden this setting from OMNY type-4/5 job
tooltips even when enabled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-03 16:25:07 +02:00
x01dcandClaude Sonnet 5 70696756f1 fix(omny): stop polling sample name via a device_rpc queue round-trip
CI for csaxs_bec / test (push) Successful in 1m51s
OMNY_TomoParams's poll timer called sample_name_getter every tick,
which for OMNY was dev.omny_samples.get_sample_name_in_samplestage()
-- a device method, dispatched by BEC as a device_rpc request through
the scan queue rather than a local read. With the queue busy or
paused this blocked the widget's Qt thread (GUI freeze while a scan
ran) and piled up duplicate PENDING device_rpc entries in BEC's
primary queue (confirmed live via bec.queue.queue_storage
.describe_queue()). The method was just a one-line wrapper around an
already-readable EpicsSignal, so read it directly with cached=True
instead, matching flomni's fast-getter pattern and avoiding even the
signal round-trip. Also applied cached=True to flomni's own getter,
which didn't have the device_rpc bug but is polled the same way.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-03 15:52:52 +02:00
x01dcandClaude Sonnet 5 0bdd20013b fix(omny): catch KeyboardInterrupt around frozen-GUI raise_window() calls
CI for csaxs_bec / test (push) Successful in 5m0s
A Ctrl+C landing during raise_window()'s RPC wait (e.g. when the GUI
process is frozen/unresponsive) was escaping two `except Exception`
handlers that don't catch KeyboardInterrupt (a BaseException sibling,
not an Exception subclass): omnygui_show_progress() aborted the whole
tomo_scan()/tomo_scan_resume() call instead of just skipping the
window-raise, and tomo_queue_execute() left the job silently stuck at
status="running" instead of the usual clean "incomplete" + explanatory
message. Reproduced live by Mirko via the tomo queue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-03 15:28:27 +02:00
x01dcandClaude Sonnet 5 28eac31ab0 feat(omny): add OMNY support to OMNY_TomoParams widget (Phase 2 of 2)
CI for csaxs_bec / test (push) Failing after 5s
Mirko, once Phase 1 (tomo-queue backend) was live-confirmed working: "is
the way to move to the widget phase" -- this adds OMNY as a third setup to
the tomo params GUI widget, alongside flomni/lamni.

Key design decision: replaced the module-level TOMO_TYPES dict (shared
across setups, couldn't express "type 1 means 2 sub-tomograms here but 8
there") with a per-profile "tomo_types" dict:
{tomo_type_int: {"label": str, "n_subtomos": int | None}}. n_subtomos is
not None became the single, uniform test for "is this an equally-spaced
type" everywhere (combo box, section visibility, validation, queue-table
projections display), since OMNY has three such flavors (types 1/4/5 --
2/4/8 sub-tomograms) instead of flomni's/lamni's one (always 8).

Critical bug caught by verifying the design against the live file, not by
the initial design itself: _validate() hardcoded
`tomo_type not in (1, 2, 3)` -- would have made the rest of Phase 2 look
like it worked (types 4/5 selectable, sections visible, math correct)
while silently rejecting every actual submit/queue attempt for an OMNY
type-4/5 job. Fixed to check profile membership instead. Also fixed three
UI strings that hardcoded "8 equal sub-tomograms" to read the active
type's real count.

New _compute_type1_omny/_requested_to_stepsize_omny (parameterized by
tomo_type via {1:2,4:4,5:8}, matching
OMNY._equally_spaced_subtomo_angles()'s own mapping exactly) and
_compute_fermat_positions_omny (calls OmnyFermatScan's real static
method, same pattern as flomni's). _format_projections() gained a 3-way
setup duck-type (OMNY's param set is a strict subset of flomni's -- no
positively-unique OMNY-only key exists, so a negative-then-positive check
is necessary) that resolves into SETUP_PROFILES rather than duplicating
the type-count mapping a third time.

Confirmed needing zero changes (verified, not assumed): _job_tooltip(),
_build_type23_section(), compute_fermat_positions call sites,
_detect_setup() (osamroy as OMNY's discriminator), the busy-banner
detection methods (already generic via the shared tomo_progress global
var from Phase 1), and the Qt Designer plugin registration files.

New test_omny_tomo_params_widget_math.py (29 pure-function tests, no
QApplication/QWidget, same style as the existing flomni/lamni test
files). Full suite: 761 passed. Not live-tested (needs a real display,
same as every GUI change this branch) -- next step is Mirko trying it
against a real OMNY session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-03 03:28:08 +02:00
x01dcandClaude Sonnet 5 68801cd901 fix(omny): stop re-raising the progress window on every projection
CI for csaxs_bec / test (push) Failing after 5s
Mirko, on macOS: the progress window "seems to close and recreate each
update, which is highly annoying" -- caused by _tomo_scan_at_angle()
calling self.omnygui_show_progress() (the full show-window-and-raise-to-
front routine) once per angle, on top of _print_progress(). flomni's own
_tomo_scan_at_angle() never does this: it only ever calls _print_progress(),
which calls self._flomnigui_update_progress() (values/text only) at its own
end -- the window is shown/raised exactly once, by tomo_scan()'s
unconditional call at scan start.

Mirrored that structure: _print_progress() now calls
self._omnygui_update_progress() itself, and the redundant per-angle
omnygui_show_progress() call was removed from _tomo_scan_at_angle().

1 new regression test. Full suite: 732 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-03 02:52:49 +02:00
x01dcandClaude Sonnet 5 afc13df24d feat(omny): add third progress ring, idle-time tracking, timing summary
CI for csaxs_bec / test (push) Failing after 5s
Mirko: "the flomni progress bar has three rings. here i am seing only
two... at the end of a tomogram in flomni a summary of the measurement
time and time lost is displayed. omny seems not to have that, maybe not
even measuring the time."

Both traced back to the same root gap left by the Phase-1 tomo-queue port:
nothing computed estimated_remaining_time/estimated_finish_time, and
nothing refreshed progress["heartbeat"] *during* a running scan (only
cleared it to None on completion) -- so accumulated_idle_time stayed 0.0
forever and there was no ETA data to show or summarize.

- _tomo_scan_at_angle() now refreshes the heartbeat every angle and
  attributes any gap beyond a normal-cadence heuristic to
  accumulated_idle_time (mirrors flomni's identical formula, minus its
  frames_per_trigger factor which OMNY has no property for).
- _print_progress() computes/stores/prints estimated_remaining_time and
  estimated_finish_time once the scan rate has stabilized.
- tomo_scan() prints an end-of-scan "Total measurement time"/"...excluding
  detected gaps"/"...lost to detected gaps" summary and sends it to scilog,
  direct port of flomni's block (minus its measured_log call, which OMNY
  has no equivalent of).
- gui_tools.py: omnygui_show_progress() adds flomni's third, scan-linked
  auto-updating ring; _omnygui_update_progress()'s center label now shows
  start time / ETA / estimated finish / active hook, matching flomni's
  _flomnigui_update_progress().

_describe_active_hook()/_active_hook_source() needed no porting -- already
provided by TomoQueueMixin since Phase 1. Not ported: flomni's separate
persistent timing-statistics-log subsystem (_log_tomogram_timing()) --
out of scope for what was actually asked (the printed summary + ring
count).

9 new tests (test_omny_tomo_scan.py, test_omny_gui_tools.py). Full suite:
731 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-03 02:45:59 +02:00
x01dcandClaude Sonnet 5 f9a1f4a7d2 fix(omny): warn about too-few Fermat points; fix live-testing crashes
CI for csaxs_bec / test (push) Failing after 5s
Live-testing findings from Mirko's own session, four real bugs:

- tomo_parameters() now warns when the current fovx/fovy/tomo_shellstep
  would yield fewer than OmnyFermatScan.MIN_POSITIONS (20) Fermat scan
  points, matching Flomni.tomo_parameters()'s identical warning -- the scan
  server already aborted (ScanAbortion) on too few points, but nothing told
  the user ahead of time. Needed converting
  OmnyFermatScan.get_omny_fermat_spiral_pos() to a @staticmethod (was an
  instance method reading self.cenx/ceny/zshift) so it's callable from
  tomo_parameters() without a full scan instance, matching flomni's
  already-static version. Also promoted the hardcoded "20" in
  prepare_scan()'s abort check to a MIN_POSITIONS class constant.

- OMNY.tomo_scan() didn't accept interactive=, so every
  tomo_queue_execute() run (which always calls tomo_scan(interactive=False)
  for unattended queued runs) failed immediately with TypeError. Added the
  parameter, accepted but unused, matching flomni's own docstring reasoning
  (no fine-alignment gate on either setup).

- write_to_scilog()/write_pdf_report() used the old bec.logbook API with no
  "is scilog configured" check, so a session without scilog credentials
  hung indefinitely instead of failing fast (confirmed live: 29+ minutes).
  Migrated both to bec.messaging.scilog, gated on _enabled, matching
  flomni's actual current mechanism.

- Stale ~/Data10/specES1/ paths (predating the current ~/data/raw/ deployment
  layout) crashed _write_tomo_scan_number() with FileNotFoundError on a
  fresh session. Fixed that plus write_pdf_report()'s PDF target and
  tomo_reconstruct()'s default base_path, all three sharing the same stale
  root -- now also explicitly create their target directory first, since
  flomni's own equivalents assume it already exists.

16 new tests across test_omny_fermat_scan.py, test_omny_tomo_angles.py, and
test_omny_tomo_scan.py. Full suite: 725 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-02 18:37:37 +02:00
x01dcandClaude Sonnet 5 f6fc8476fe feat(sample_storage): add OMNY support to OMNY_SampleStorage widget
CI for csaxs_bec / test (push) Failing after 5s
Extends the sample-storage bookkeeping widget, previously FlOMNI-only, to
also work against dev.omny_samples: shuttles A/B/C (6 slots each), the
sparse fixed-O positions, stage, gripper, and a per-shuttle installed/active
parking status (mirrors the oparkz-driven "active parking slot" concept
from omny_sample_transfer_mixin.py). Setup detection follows the
SETUP_PROFILES/_detect_setup() convention already used by tomo_params.py.

Also fixes SimOMNYSampleStorage, which never cleared the fixed-O/shuttle-B-C
/parking slots, so they read back as the mock-PV default "0" instead of the
real empty sentinels ("-"/"none") -- SimFlomniSampleStorage already does
this correctly for the same reason.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Mupip8x7Hean6E3FFvpbj
2026-09-02 17:48:08 +02:00
x01dcandClaude Sonnet 5 2eaa609d29 fix(omny): migrate scilog writes to bec.messaging.scilog, checking _enabled
CI for csaxs_bec / test (push) Failing after 5s
write_to_scilog()/write_pdf_report() used the old bec.logbook/client.logbook.
send_logbook_message() API, which has no "is scilog even configured" check.
Confirmed live: on a session without scilog credentials (this dev deployment
has no /etc/bec/secrets/.scilog.env at all), send() blocks indefinitely
instead of failing fast -- a real tomo_scan() hung inside
_write_subtomo_to_scilog() for 29+ minutes during Phase 1 live testing.

flomni already migrated off this API to bec.messaging.scilog, which gates
every send behind scilog._enabled (published from the deployment's actual
scilog config) -- per Mirko's direction, ported the same mechanism to both
OMNY call sites, matching flomni's write_pdf_report()/_scilog_write()
exactly rather than merely adding an active_account check.

Added 3 unit tests locking in the _enabled gate (skip when disabled, send
when enabled, tolerate a send() exception).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-02 17:46:14 +02:00
x01dcandClaude Sonnet 5 906e9f5318 feat(omny): port tomo-queue backend from flomni/lamni (Phase 1 of 2)
CI for csaxs_bec / test (push) Failing after 5s
Mirko wants the tomo params GUI widget available for OMNY before running
real tomo scans. That widget (Phase 2, separate follow-up) needs a working
job queue and scan progress visible outside the CLI session that started
it -- neither existed for OMNY. OMNY now inherits TomoQueueMixin
(OMNY_shared/tomo_queue_mixin.py), the same shared backend flomni/lamni
already use, giving it tomo_queue_add/delete/move/clear/show/execute/
reacquire, at_each_angle_hook registration, and tomo_scan_resume() for the
first time.

self.progress is now a global-var-backed proxy (_ProgressProxy, byte-for-
byte port of flomni's/lamni's own), not a plain in-memory dict -- the
load-bearing change the queue mixin and the eventual widget's busy-banner
both depend on. tomo_queue_mixin.py's tomo_queue_reacquire() hardcoded
tomo_type == 1; generalized via a new _TOMO_TYPE_1_LIKE class attribute
(default frozenset({1}), flomni/lamni unaffected) since OMNY has three
"equally spaced sub-tomograms" flavors (types 1/4/5), not just one.

Along the way, fixed several real bugs in tomo_scan()/write_pdf_report()
that predate this session and would have crashed on a real account/
real energy read (bec.active_account.decode() -- active_account is str
not bytes; dev.mokev doesn't exist for OMNY, only dev.ccm_energy; an
unguarded logbook send; a stale "LamNI" logo/tag copy-paste leftover) --
all direct ports of fixes flomni/lamni already needed and got. Also added
the missing unconditional omnygui_show_progress() call and heartbeat-
clearing try/finally in tomo_scan(), matching both.

Verification: 23 new tests (test_omny_tomo_queue.py, test_omny_tomo_scan.py)
covering the mixin wiring, hook dispatch/resolution, tomo_scan_resume()/
_resolve_type1_projection()/reacquire() across all three sub-tomogram
flavors, account handling, and heartbeat clearing on both normal completion
and a mid-scan exception. Full suite: 702 passed. Not yet live-tested
against a running session (the local bec-server may be in active use by
Mirko's own parallel session) -- next step is his own pull-and-test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-02 14:42:35 +02:00
x01dcandClaude Sonnet 5 ea9519ff20 test(omny): check combined sub-tomogram spacing adds up to 180 degrees
CI for csaxs_bec / test (push) Failing after 5s
Mirko recalled that flomni/lamni check the combined angular spacing adds up
to 180 (360 for lamni) -- found the precise existing pattern in lamni's own
test suite (test_lamni_tomo_angles.py's _assert_equally_spaced(): sort the
combined angles, append a wraparound point at first_angle + range, and
assert every gap including that seam is uniform). This is strictly stronger
than checking the total span alone -- it also catches a double-wide or
missing gap right at the seam, which is exactly the shape of bug flomni's
historical fixes (e.g. 6acaadb, "fix angular start in even subtomos")
addressed.

Ported the same wraparound-inclusive check into OMNY's tests: the existing
full-combination test now asserts it directly, and the intermediate-pairing
helper (_assert_evenly_spaced, used by the type-4 and type-5 pairwise/
progressive combination tests added previously) now includes it too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-02 13:48:51 +02:00
x01dcandClaude Sonnet 5 789c0dbf72 test(omny): lock in equal spacing at every sub-tomogram combination stage
CI for csaxs_bec / test (push) Failing after 9s
Mirko flagged that flomni previously had real, painful bugs where combined
sub-tomograms weren't actually equally spaced -- given that history, verify
(and permanently regression-guard) the same property for OMNY's tomo types
1/4/5, at the specific combination granularity he described: type 4's 1+2
and 3+4 pairings individually, not just the full 1-2-3-4 combination already
covered; and type 5's progressive 1+2 / 1+2+3+4 / 1..8 doubling stages,
matching flomni's own documented interlacing design.

Verified offline against the exact production function
(_equally_spaced_subtomo_angles) across several step sizes including ones
that don't divide 180 evenly; for n_subtomos=8 its bit-reversal phase index
was also cross-checked to reproduce flomni's hardcoded, already-proven-
correct phase_eighths table exactly (already covered by the existing
test_bit_reversal_matches_flomnis_hardcoded_phase_eighths_table, unchanged).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-02 13:43:08 +02:00
x01dcandClaude Sonnet 5 2fbaa6679f feat(omny): drop redundant tomo auth code, cap type-3 step at 2.5 deg
CI for csaxs_bec / test (push) Successful in 1m45s
The "Enter authorization code" (x12sa) prompt gating advanced tomo modes
(types 2-5) predates the account-based _SUPERUSER_ACCOUNTS gate added
earlier this branch, and is now fully redundant with it -- Mirko asked to
remove it. Each mode still prints the existing "significant wear" warning
unconditionally instead of only after entering the code.

Also added a hard 2.5 degree ceiling on tomo type 3's angular step (golden
ratio starting angle mode), replacing its old minimum-100-projections floor
with an explicit wear-motivated cap; if the requested projection count
implies a larger step, it's clamped down to 2.5 degrees. Type 2's
golden_ratio_bunch_size minimum stays at 100 per Mirko's call -- wear
avoidance there is handled by the warning message, not a lower floor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-02 13:32:19 +02:00
x01dcandClaude Sonnet 5 c802104f6d fix(omny): fix stale widget-API bugs in gui_tools.py cameras/xray-eye, add cam_xeye
CI for csaxs_bec / test (push) Successful in 1m51s
Live testing of the previous gui_tools.py port (49ade7c) hit two further crashes:

1. omnygui_show_xeyealign() -> ValueError: Device 'cam_xeye' not found in current
   BEC session. The OMNY_XRayEye widget (shared verbatim with flomni) hardcodes
   CAMERA = ("cam_xeye", "image") as a module constant -- it isn't configurable
   per-beamline, so omny needs its own cam_xeye device or the widget can never be
   constructed. Added cam_xeye to simulated_omny.yaml (hand-added, duplicating
   flomni's real cam_xeye's camera_id/pixel_calibration as placeholders; SimIDSCamera
   already accepts both camera_id and the legacy camera_ID key omny's other cameras
   use). ptycho_omny.yaml intentionally left untouched -- the real hardware camera ID
   and pixel calibration for OMNY's x-ray-eye camera are unknown; noted as a followup
   in OPEN_ISSUES.md, including that a future regen of simulated_omny.yaml from
   ptycho_omny.yaml will drop this hand-added block until the real config has one too.

2. omnygui_show_omnycam_parking() -> ValueError: Unknown widget type: BECImageWidget.
   That string was carried over unmodified from omny's pre-modernization code, not
   from flomni's current gui_tools.py (whose current class name is "Image"). Beyond
   the widget name, the per-camera calls inside were also stale: fig.set_rotation(
   deg_90=3) doesn't exist on the current Image/ImageBase widget at all (replaced by
   the num_rotation_90 property), and self.figN.lock_aspect_ratio(True) called a
   property as a function. Ported omnygui_show_omnycam_parking()/
   omnygui_show_omnycam_samplestage() onto flomni's current per-camera pattern
   (image(device=, signal="preview"), num_rotation_90/lock_aspect_ratio as
   properties, start_live_mode()), preserving the existing 3x90 rotation for
   cam200/cam203 (physical mount) and no rotation for cam201/cam202, unchanged from
   before.

Also removed omnygui_show_cameras() (added last pass, modeled on flomni's single
generic camera view): redundant now that both existing parking/samplestage views got
the current API, since omny -- unlike flomni -- already has two purpose-built camera
views. Its hard-stop console (z_ConsoleButtonsWidget, targeting otransy) now lives
directly in both omnygui_show_omnycam_parking() and omnygui_show_omnycam_samplestage()
instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-01 22:19:46 +02:00
x01dcandClaude Sonnet 5 49ade7ce98 feat(omny): modernize gui_tools.py to flomni's current dock/window API
CI for csaxs_bec / test (push) Successful in 1m52s
OMNYGuiTools still used pre-modernization code: a gui_window handle that
was never actually populated (self.gui.windows["main"].widget always
raised/returned None), the old two-level add_dock().add_widget() API, and
bare `if self.figN is None` checks instead of an _is_deleted()-aware
guard. omnygui_show_progress() crashed with AttributeError as soon as it
was exercised live.

Ported OMNYGuiTools onto flomni's current single-level .new() pattern:
split __init__()/set_client(), a window-reuse check keyed on
self.gui.windows, _omnygui_is_missing() for dock-reuse (verbatim port of
flomni's _is_deleted()-based guard), the current RingProgressBar API for
omnygui_show_progress(), and new omnygui_show_xeyealign()/
omnygui_show_xeyealign_fittab()/omnygui_show_cameras() methods. cam_xeye
is not part of omny's device config (unlike flomni's), so the xeyealign
live-view toggle is guarded with an existence check instead of copied
unconditionally. omnygui_show_cameras() adds a hard-stop console targeting
otransy, the closest omny analog to flomni's ftransy.

Also fixes two independently-broken call sites in x_ray_eye_align.py
(self.lamni.lamnigui_show_xeyealign[_fittab]() -> the omny-side method
names actually defined on OMNYGuiTools -- self.lamni is really the OMNY
instance) that were raising AttributeError before the GUI code was even
reached. The rest of that file's LamNI-derived content is left alone; a
full rewrite is tracked separately.

omny.py's __init__ now calls OMNYGuiTools.__init__(self) + set_client(),
matching the split-constructor pattern instead of the old single-call
OMNYGuiTools.__init__(self, self.client).

Deliberately deferred: flomni's ETA/heartbeat progress fields
(tomo_start_time, heartbeat, estimated_remaining_time, etc.) are not
ported -- omny's self.progress dict only has 7 basic fields. Noted in
omny/AI_docs/OPEN_ISSUES.md so it isn't forgotten; not a crash risk since
omny_webpage_generator.py already reads these defensively.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLrD7sVYGLAzsQjLVJpCgt
2026-09-01 21:51:46 +02:00
x01dc e08ac1a44b fix(omny): make correction data global-var-backed, fix default file path
CI for csaxs_bec / test (push) Successful in 2m33s
Two real bugs found while investigating a recurring AttributeError:
'OMNY' object has no attribute 'corr_pos_x' from tomo_scan_projection().

1. default_correction_file/default_correction_file_x were bare relative
   filenames ("correction_omny_202204.txt"), unlike flomni's equivalent
   which resolves an absolute path via Path(csaxs_bec.__file__). open()
   would only ever succeed if bec were launched from this exact plugin
   directory -- silently swallowed everywhere else by the existing
   FileNotFoundError handler in reset_correction(). The real correction
   files genuinely exist on disk next to the plugin code and were very
   likely never actually being loaded.

2. corr_pos_x/corr_angle_x/corr_pos_y/corr_angle_y/corr_pos_y_2/
   corr_angle_y_2 were plain instance attributes, only ever assigned
   inside reset_correction() (itself only called from
   otransfer_put_sample() during a sample transfer) -- hence the
   AttributeError when calling tomo_scan_projection() beforehand.
   Converted to global-var-backed properties defaulting to [] when unset,
   mirroring flomni's corr_pos_y/corr_angle_y/etc. exactly. This
   supersedes a shallower getattr()-based band-aid from an earlier,
   uncommitted attempt at this fix. As a bonus, correction data now
   persists across BEC client restarts, same as flomni's.

Note flomni has no x-correction concept at all (only corr_pos_y/
corr_pos_y_2) -- omny's x-correction is its own thing, not something
copied from flomni.

17 new/updated tests in test_omny_alignment_mixin.py, including an
end-to-end check that reset_correction(use_default_correction=True) now
actually loads the real correction_omny_202204*.txt files.
2026-09-01 20:15:25 +02:00
x01dc 62b4b8d8fa feat(omny): skip OSA/FZP moves when already at the target position
CI for csaxs_bec / test (push) Successful in 2m58s
oosa_in(), oosa_out(), ofzp_in(), ofzp_out() used to unconditionally
re-move every axis (and, for FZP, unconditionally disable rt feedback)
even when already at the target -- visible as repeated no-op progress
bars on back-to-back calls. Ported the same "skip if already there"
optimization flomni's fosa_in()/ffzp_in() already have (and that lamni
already got too, see test_lamni_optics_skip_if_in_position.py).

For oosa_in()/oosa_out() the "already there" check has to run *before*
_oosa_check_y(), since that call unconditionally parks oosay at 0.9 --
never the real in/out position for either near- or far-field mode -- so
checking afterward would always miss the match. oosa_out()'s "out"
targets are hardcoded literals in the existing code (no user_parameter
for out, unlike oosa_in's far_field_in/near_field_in), so the skip
check compares against those same literals.

New tests/tests_bec_ipython_client/test_omny_optics_skip_if_in_position.py
(9 tests, mirroring the lamni test's structure): skip-vs-move for both
near/far field on oosa_in/oosa_out, and skip-vs-move-and-disable-feedback
for ofzp_in/ofzp_out.
2026-09-01 19:52:36 +02:00
x01dc da0121216b feat(omny): fall back to OS login for superuser check when no pgroup is set
CI for csaxs_bec / test (push) Successful in 2m3s
bec.active_account is a Redis-backed pgroup, only set via bec-set-account
-- a fresh dev/test session normally has it unset. _is_superuser_account()
now falls back to os.getenv("USER") in that case, matching the
account/system-user comparison convention already used in
OMNY_shared/omny_general_tools.py's _accounts_match(). Lets gac-x01dc's
superuser grant take effect without requiring bec-set-account first.
2026-09-01 19:12:03 +02:00
x01dc e6928f295e feat(omny): add 4/8 sub-tomogram tomo modes, hide advanced modes from regular users
CI for csaxs_bec / test (push) Successful in 2m26s
- New shared static helper _equally_spaced_subtomo_angles(), ported from
  flomni's _subtomo_angle_plan() with the 180/360-degree-range branch
  removed (OMNY only ever scans 180 degrees). Generalizes the bit-reversal
  sub-tomogram interleaving to any power-of-two sub-tomogram count; for
  n_subtomos=2 it reduces to OMNY's original type-1 scheme.
- sub_tomo_scan() refactored onto the new helper (gains an n_subtomos
  param); tomo_type 1 (2 sub-tomograms) is otherwise unchanged.
- Adds tomo_type 4 (4 sub-tomograms) and 5 (8 sub-tomograms), gated by the
  same "wear in OMNY" authorization-code prompt already used for tomo_type
  2/3.
- Advanced tomo modes (2, 3, 4, 5) are now hidden from tomo_parameters()'s
  menu unless bec.active_account is in the new _SUPERUSER_ACCOUNTS
  allowlist; a non-superuser landing on an advanced type is forced back to
  type 1. _SUPERUSER_ACCOUNTS is left empty -- needs real account codes
  before anyone can use the advanced modes.
- As a side effect of reusing flomni's formula, also fixes a latent bug:
  the old code derived the inter-sub-tomogram phase offset from the raw
  configured tomo_angle_stepsize rather than the achievable (int-truncated)
  step, producing an unevenly-spaced combined grid whenever stepsize didn't
  divide 180 evenly.
- New tests/tests_bec_ipython_client/test_omny_tomo_angles.py: angle-count
  and no-duplicate-angle checks for n_subtomos in (2, 4, 8), a regression
  check against the old type-1 formula, an explicit test of the
  achievable-step fix, a cross-check against flomni's hardcoded N=8
  bit-reversal table, and superuser-gating tests (including the full
  tomo_parameters() interactive flow forcing a non-superuser back to
  type 1, and a superuser successfully unlocking type 5).
2026-09-01 14:26:10 +02:00
x12saandClaude Sonnet 5 a20be61251 fix(tests): stub measured_log in flomni tomo-angle test helper
CI for csaxs_bec / test (pull_request) Successful in 1m40s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 1m39s
make_flomni_for_golden_resume() builds a bare Flomni via object.__new__,
bypassing __init__, so it never got measured_log or a working
sample_get_measured_log_key -- both added by the measured-sample-tracking
commit for the P-touch label printer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 15:34:58 +02:00
x01dcandClaude Sonnet 5 67769d5ff8 fix(flomni,lamni): use .get() in tomo_queue_show()'s job-line formatter
CI for csaxs_bec / test (pull_request) Successful in 2m14s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 1m49s
A job's params dict only has whatever _TOMO_SCAN_PARAM_NAMES held when it
was snapshotted by tomo_queue_add(); a job persisted before a key was
added (e.g. fovx/fovy, tomo_circfov) raised KeyError on the hard p['key']
lookup, making the whole queue un-inspectable instead of just missing a
field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 15:47:51 +02:00
x01dcandClaude Sonnet 5 abb933e10d fix(tests): patch input() where yesno() actually calls it
CI for csaxs_bec / test (pull_request) Successful in 1m52s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 1m48s
find_rotation_center()'s y/n prompts now go through OMNYTools.yesno()
(omny_general_tools.py) instead of a raw input() in x_ray_eye_align.py,
but these tests were still patching input() on the old module -- so
the real input() got called under pytest's captured stdin and raised
"reading from stdin while output is captured".

Patch input() on omny_general_tools instead, where yesno() actually
calls it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 10:39:12 +02:00
x01dcandClaude Sonnet 5 48b9f893ab fix(lamni,flomni): suppress spurious 0-deg damage-estimation shot on golden-ratio resume
CI for csaxs_bec / test (push) Successful in 1m48s
previous_subtomo_number resets to -1 on every tomo_scan() call in the
golden-ratio branches (tomo_type 2/3), including a resume. That makes
the first loop iteration look like a genuine sub-tomogram transition,
firing the 0-deg reference shot before the actual resume angle even
though the rotation isn't passing through 0 deg at that moment.

Mirrors tomo_type 1's existing start_angle-is-None resume guard:
suppress the shot only for the first, possibly-spurious transition
right after a resume, then let later genuine transitions in the same
call fire normally. Same fix applied to both lamni.py and flomni.py,
which share byte-for-byte identical golden-ratio logic here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 12:43:30 +02:00
x01dcandClaude Sonnet 5 7caf57cff6 feat(lamni): check measurement configuration before tomo_scan()
Flomni's tomo_scan() warns and prompts if the X-ray eye isn't out and
the FZP/OSA optics aren't in before scanning; LamNI had no equivalent,
letting a tomogram start with the eye still in the beam or the optics
retracted. Ports the same check, wired into the existing
force/interactive gating so it never blocks on input() during
unattended/queued runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-31 12:42:49 +02:00
wakonig_k cd13e7ed9e feat: migrate to v4 scans 2026-07-29 14:48:35 +02:00
x01dc 13e9cb21e4 fix(LamNI): update tests for prompts/kwargs added earlier this branch
CI for csaxs_bec / test (pull_request) Successful in 1m45s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 1m59s
Several tomo_scan()/tomo_alignment_scan()/find_rotation_center() tests
started failing (CI) after recent commits on this branch introduced new
input()-backed confirmation/sample-name prompts, an
alignment_scan_progress GUI proxy, and tomo_queue_execute()'s
interactive=False kwarg -- none of which the existing test fixtures or
assertions were updated for. Mock/bypass the new prompts and GUI calls,
add the missing _alignment_scan_progress_proxy to the bare-object test
fixture, and update two assertions (tomo_queue interactive kwarg,
account-less sample registration) to match the intended new behavior.
2026-07-28 09:53:59 +02:00
x01dcandClaude Sonnet 5 e299775c44 feat(LamNI): measure camera live-mode FPS and smear-sweep capture FPS
CI for csaxs_bec / test (pull_request) Successful in 3m42s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 1s
CI for csaxs_bec / test (push) Successful in 2m31s
Neither rate was previously measured anywhere, making it hard to tell a
real acquisition bottleneck from a display/overhead one.

- IDSCamera.get_live_fps() (new USER_ACCESS entry): rolling-buffer
  measurement of the live-mode push rate in _live_mode_loop(), readable as
  dev.cam_xeye.get_live_fps(). SimIDSCamera inherits it unchanged, so it
  works against the simulated deployment too.
- XrayEyeAlign._smear_sweep() now tracks timestamps of the first and most
  recent distinct frame it captures, storing the computed rate as
  self.last_smear_fps and printing it in the sweep-done summary line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 10:35:38 +02:00
x01dcandClaude Sonnet 5 83f9a6bb59 fix(LamNI): show cached composite on manual channel switch, align toggle grid, archive smear runs
CI for csaxs_bec / test (pull_request) Successful in 2m42s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 1s
CI for csaxs_bec / test (push) Successful in 1m45s
Three follow-ups from real-hardware testing of the smear aid:

1. Switching the "Smear preview channel" toggle off then back on showed
   the live camera feed, not the previous composite. Root cause:
   Image.image()'s connect_slot subscription only delivers stream
   entries published *after* connecting (bec_lib's default
   register(from_start=False, newest_only=False) bookmarks the
   then-current last entry and only forwards newer ones) -- so nothing
   re-renders on reconnect until the next sweep pushes a new frame.
   Added a permanent, independent subscription (newest_only=True) that
   keeps the last smear_preview payload cached regardless of which
   channel is currently displayed; set_live_view_signal() now force-
   renders that cache via main_image.set_data() when switching back to
   "smear_preview", instead of waiting for a push that may never come.
   Naturally "resets" once a new sweep starts pushing fresh composites.

2. The new switch row wasn't column-aligned with the existing
   shutter/camera-running row (each used an independent QHBoxLayout
   sized by its own labels' widths). Merged both rows into one
   QGridLayout with right-aligned label columns, so all four toggles
   line up regardless of label text length. Shutter now pairs with
   Smear integrating, Camera running with Smear preview channel.

3. find_rotation_center_smear_experimental() now archives each run to
   a timestamped HDF5 file (~/data/raw/logs/xrayeye_smear_calibration/),
   mirroring align()'s existing _save_alignment_data() pattern: the FZP
   reference, one composite image + one clicked centre per iteration,
   and the final result -- collecting data towards eventually
   automating center detection from the composite offline.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 39e81130af fix(LamNI): reset stale live-view channel, add GUI status/toggle switches
A Ctrl-C interrupting an in-flight RPC call (e.g. mid
set_live_view_signal) could leave the XRayEye widget stuck showing the
smear-preview channel afterward -- and since lamnigui_show_xeyealign()
reuses the existing xeyegui widget across calls rather than recreating
it, that stale state would silently carry into the *next* alignment
run too (including production align()/find_rotation_center(), since
on_live_view_enabled() now respects whichever channel is selected).

Fix: lamnigui_show_xeyealign() unconditionally resets both
set_live_view_signal("image") and set_smear_active(False) every time
it's called -- both are cheap no-ops if already in the default state,
and this is the single shared entry point for every alignment
procedure, so one fix covers all of them.

Also add two GUI-facing controls to XRayEye, in a new row below the
shutter/camera-running toggles:
- "Smear preview channel" -- an interactive toggle mirroring
  set_live_view_signal(), so the operator has a GUI-only recovery path
  independent of the ipython client (e.g. if it's hung/disconnected).
- "Smear integrating" -- a read-only status indicator (disabled
  ToggleSwitch) reflecting whether _smear_sweep() is currently
  accumulating a composite, driven entirely by the automation.

set_live_view_signal() keeps the interactive toggle's visual state in
sync regardless of who changed the channel (automation or operator) --
single source of truth. _smear_sweep() brackets accumulation with
set_smear_active(True)/(False), turning it off as soon as the sweep
ends (independent of the channel switch, which stays on the composite
until the operator's click is collected). Both new XRayEye methods
required regenerating csaxs_bec/bec_widgets/widgets/client.py via
bw-generate-cli --target csaxs_bec.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 bcdfc12a88 fix(LamNI): decouple smear composite display from live acquisition channel
The "camera running" toggle (IDSCamera.live_mode_enabled) was flapping
on/off during the smear sweep, and on real hardware that's genuinely
slow (starting/stopping the live acquisition thread has real driver
overhead, unlike the simulator). Root cause: composite pushes shared
the same device_preview channel the live thread continuously publishes
to, so live_mode_enabled had to be toggled off around every push to
keep the composite from being instantly overwritten.

Give the composite its own channel instead:
- IDSCamera gains a smear_preview PreviewSignal (no rotation_90/
  transpose configured -- composite data is already display-oriented)
  and push_smear_preview(), mirroring push_preview_image() but fully
  decoupled from the live channel.
- XRayEye gains set_live_view_signal() to switch which camera signal
  the live Image view displays, defaulting to the normal channel for
  every fresh GUI instance; on_live_view_enabled() now uses whichever
  signal is currently selected instead of a hardcoded constant.
- _smear_sweep() switches the GUI to "smear_preview" for the sweep and
  pushes composite updates there -- live_mode_enabled is never touched
  at all (just ensured on at the start, exactly like _live_sweep()).
  The GUI is deliberately left on the composite after a successful
  sweep until the caller has collected the operator's click, then
  switched back to "image"; on KeyboardInterrupt it's switched back
  immediately.

This removes the resume_live/hold_display_s toggle-avoidance mechanism
added in def9bf4, which is no longer needed with a dedicated channel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 6d65a87ae4 fix(LamNI): push smear composite via new IDSCamera RPC method
dev.cam_xeye.image.put(composite) failed on real hardware --
PreviewSignal is a BECMessageSignal subclass, and BEC hardcodes
rpc_access=False for that signal_info, so the client-side device proxy
never exposes `image` as an attribute at all (confirmed: this is a
structural BEC constraint, not specific to this device -- no existing
RPC surface, device or widget, offers a raw-array setter either).

Add IDSCamera.push_preview_image(data), a small device-server-side
method (alongside the existing get_last_image()) that does
self.image.put(data) from within the device-server process, where
PreviewSignal is a normal ophyd attribute. _push_smear_composite now
calls dev.cam_xeye.push_preview_image(composite) instead of writing to
the signal directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 d0d3602ed1 feat(LamNI): experimental continuous-rotation smear aid for rotation-center calibration
Adds an EXPERIMENTAL, operator-only alternative to the extended-sample
rotation-center click: instead of judging the center from a single
instant of live rotation, accumulate a max-projection composite while
lsamrot rotates continuously through a (tunable, default 360 deg)
sweep. Off-axis features smear into circular arcs; the common center
of curvature is the rotation axis, an easier target to click than one
live frame. No automatic circle fitting -- purely a visual aid feeding
the same _collect_click mechanism the production feature already uses.

Rotation is issued non-blocking via scans.mv() (new local wrapper,
sibling of this file's existing blocking umv()) and frames are grabbed
via get_last_image() in a single-threaded loop polling ScanReport.status
-- the same thing ScanReport.wait() does internally -- so no background
thread or device-server change is needed to grab frames while the motor
is actively moving. The composite is pushed to the GUI through the
camera's existing PreviewSignal (dev.cam_xeye.image), duty-cycling
live_mode_enabled off/on around each push so the background 5 Hz live
thread can't immediately overwrite it.

New entry point: lamni.xrayeye_rotation_center_calibration_smear_experimental().
Purely additive -- does not modify find_rotation_center()/_live_sweep()
or the two production entry points.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 16:08:01 +02:00
x01dcandClaude Sonnet 5 b7b5b9282a fix(LamNI): accumulate rotation-center shift across calibration iterations
lamni_move_to_scan_center's shift_x/shift_y are absolute offsets from the
currently-configured lsamx_center/lsamy_center (unchanged until the operator
confirms at the end of the procedure), not incremental deltas from wherever
the stage currently sits. The extended-sample iteration loop was passing
only the latest click's delta each time, so each new iteration partially
undid the previous one's correction instead of adding to it. Fixed by
accumulating the total shift across iterations and always applying the
running total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 13:02:01 +02:00
x01dcandClaude Sonnet 5 7014760175 fix(LamNI): remove redundant rotate, simplify sweep, add center precondition
Three refinements found while testing find_rotation_center() against the
simulated LamNI:

- Drop the trailing tomo_rotate(0) at the end of find_rotation_center():
  both the isolated path (explicit rotate-back before applying the
  correction) and the extended path (verification sweep ends at 0) already
  leave lsamrot at 0 by then, so it was a pure duplicate move.

- Simplify _live_sweep() to rotate directly to each waypoint instead of
  stepping through artificial step_deg-sized sub-moves with settle pauses
  in between -- a single tomo_rotate() already produces continuous physical
  rotation, so the stepping added nothing but jerkier motion and extra
  queue overhead.

- Add _ensure_at_configured_center(), called at the start of
  find_rotation_center() before lfzp_in(). lamni_move_to_scan_center's
  interferometer-drift safety check (LamNIFermatScan.py) assumes lsamx/
  lsamy start near their configured center and silently skips the entire
  corrective move if that drift exceeds 150um -- exactly the situation
  find_rotation_center() is meant to handle, since its purpose is
  recalibrating a potentially-stale center. Confirmed this was a real gap:
  a run where the stage hadn't started at its configured center produced a
  correct absolute target (verified via the interferometer reading) but a
  confusingly small apparent motor delta.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 13:02:01 +02:00
x01dcandClaude Sonnet 5 26e7fddfc6 fix(LamNI): ensure OSA is out before requesting the FZP-centre click
Both align() and find_rotation_center() moved the FZP in and immediately
asked the operator to click its centre, but only confirmed OSA was out
afterwards (via loptics_out(), which runs after that click). If OSA was
left in from a previous scan setup, it would still be in the beam path
during the FZP reference click. Add an explicit losa_out() call right
before lfzp_in() in both procedures; it's a cheap, skip-if-already-out
no-op otherwise.

Found while testing find_rotation_center() against the simulated LamNI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 13:02:01 +02:00
x01dcandClaude Sonnet 5 bd2e87ebdd feat(LamNI): add automated rotation-center calibration via X-ray eye
lsamx_center/lsamy_center have to be re-measured by hand for nearly every
new sample (the rotation stage is tilted on top of lsamx/lsamy, so the
effective axis position shifts with sample thickness/mounting). Add
XrayEyeAlign.find_rotation_center()/lamni.rotation_center_calibration_start()
to automate this using the existing X-ray eye GUI and lamni_move_to_scan_center
interferometer-feedback move:

- "isolated" sample: click particle centre at 0 and 180 deg, use the
  midpoint (angle-tilt-independent) as the rotation axis position.
- "extended" sample: live 0->180->0 sweep for visual identification, single
  click, then a verification sweep and an accept/iterate prompt.

Both write the result to lsamx/lsamy's "center" userParameter after
confirmation. Heavily verbose logging throughout since this is fundamentally
an interactive, hardware-in-the-loop procedure that can't be fully exercised
by automated tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 13:02:01 +02:00
x01dcandClaude Sonnet 5 10ef93557b fix/stop lamni tests from leaking a fake bec global across the suite
CI for csaxs_bec / test (pull_request) Successful in 3m9s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 3m13s
test_lamni_account_change_reset.py and test_lamni_tomo_angles.py set
builtins.__dict__["bec"] directly with no cleanup, so a leaked fake
account ("e22222") from an earlier test file was still in place when
test_x_ray_eye_align.py later constructed a real LamNI(), tripping
_maybe_reset_params_on_account_change()'s interactive yesno() prompt
and failing under pytest's captured stdin. Use monkeypatch.setitem so
pytest reverts the global after each test, matching the pattern already
used in test_lamni_tomo_alignment_scan.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 16:57:06 +02:00
x01dcandClaude Sonnet 5 636dd0ec88 fix/stop LamNI per-sub-tomogram scilog entries
LamNI's tomo_scan() wrote a separate scilog entry ("Starting subtomo:
N...") every time the sub-tomogram number changed -- 8 extra entries
per tomo_type-1 scan, and one per sub-tomogram traversed for types 2/3
-- leftover behavior carried over from omny.py during the flomni->lamni
port. Flomni itself never does this: it only writes at scan start (PDF
report) and scan end (timing summary). Removed the three
_write_subtomo_to_scilog() call sites and the now-unused method so
LamNI's scilog behavior matches Flomni's.

Also fixed a stale test (test_tomo_queue_reacquire_rejects_when_another_job_is_incomplete)
that encoded the old, buggy tomo_queue_reacquire() invariant (rejecting
on ANY other job being incomplete/running) rather than the fixed one
from the prior commit (only an EARLIER job is a real conflict) --
split it into two tests covering both directions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 15:59:02 +02:00
x01dcandClaude Sonnet 5 d963cd6dfd feat/auto-refresh stale at_each_angle hooks on redefinition
register_at_each_angle_hook() stores a function object by reference, so
editing a hook's def and forgetting to re-register left the old version
running silently -- easy to miss mid-beamtime. Detect it via
func.__globals__ (a redefined top-level def, or a reloaded module,
mutates the same namespace dict in place) and auto-adopt the new
definition with a printed note instead of failing or staying stale.

Centralizes the shared lookup/error logic (previously duplicated in
lamni.py and flomni.py) into TomoQueueMixin._resolve_at_each_angle_hook().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 15:08:11 +02:00
x01dcandClaude Sonnet 5 12b2538333 feat/add lamni.tomo_alignment_scan() -- dedicated fine-alignment scan
Replaces the awkward documented workaround (configure a full tomo_type
1 setup with 96 projections, then launch just sub_tomo_scan(1, 0)) with
a dedicated command, ported from flomni's tomo_alignment_scan(): adjust
tomo_parameters() (FOV/step/counting time), then call
lamni.tomo_alignment_scan() directly -- no tomo_type/sub-tomogram
bookkeeping involved, matching flomni's clean two-step workflow.

Runs 12 points evenly spaced across the full 360 degrees (lamni has no
180-degree symmetry the way flomni does, so unlike flomni's 5-point/
180-degree scan, this covers the full circle -- point count matches
what the old workaround's docs defaulted to, endpoint=False since
360==0 degrees). Aborts if x-ray-eye alignment hasn't been done yet
(tomo_fit_xray_eye unset), mirroring flomni's equivalent guard.
write_alignment_scan_numbers() writes the same 4-line scan-number/
angle/offset log flomni's version does, to
~/data/raw/logs/ptychotomoalign_scannum.txt for SPEC_ptycho_align.m,
also printed at the console (flomni's own console-print equivalent is
dead/commented-out code; lamni's actually prints).

Scope note: flomni's version conditionally skips its eye-out/optics-in
transition when already in measurement condition with feedback
running, to avoid an unneeded interferometer reset -- lamni has no
equivalent helpers for that check, so this calls leye_out()
unconditionally instead. Left as a possible follow-up, not in scope
here.

Item 6 of csaxs_bec/bec_ipython_client/plugins/LamNI/AI_docs/
FLOMNI_LAMNI_FEATURE_GAPS_2026-07.md. Documented in
docs/user/ptychography/lamni.md's "Fine alignment" section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 13:22:01 +02:00
x01dcandClaude Sonnet 5 db18929742 feat/add OSA collision-clearance warning to lamni lfzp_info()
Ports flomni's ffzp_info() OSA collision-clearance section (compares
live losaz against nominal "in" position and a collision-boundary
constant, warns if closer to collision than nominal or reports
clearance at IN) -- same formula shape and sign convention as flomni
(motion directions confirmed identical between the two setups).

lamni's collision-boundary constant has never been measured, so it's
read from loptz's device config (userParameter.collision_offset) via
a new _get_user_param_optional() helper -- unlike the existing
_get_user_param_safe(), it returns None instead of raising when the
parameter is undefined (as it currently is for every lamni device
config, including the simulated one), since absence here means
"not yet commissioned" rather than a config error. lfzp_info() prints
a clear warning and skips the numeric estimate entirely until someone
adds the real measured value to the config -- deliberately not a
CLI-settable/global-var parameter, since this is deployment-level
hardware calibration.

Item 7 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>
2026-07-21 12:58:24 +02:00
x01dcandClaude Sonnet 5 7863b8fd37 feat/skip lfzp_in/losa_in/losa_out moves when already in position
Ports flomni's ffzp_in()/fosa_in() "skip the move (and, for the FZP,
the expensive feedback-disable + reset-enable cycle) if already there"
optimization to lamni's lfzp_in()/losa_in(). Also adds the same
skip-if-already-there check to losa_out(), which has no flomni
equivalent to mirror (fosa_out() always moves unconditionally there)
-- added as a lamni-side enhancement per discussion.

Checked every existing caller of lfzp_in()/loptics_in() before making
this change, since a skip changes behavior for callers that used to
get an unconditional reset. Found one real risk:
x_ray_eye_align.py's "Step 0: FZP centre" (the start of a fresh
alignment run) had its manual feedback disable/enable calls already
commented out, meaning it relied entirely on lfzp_in()'s old
unconditional reset to establish a correct interferometer zero.
flomni's equivalent call site doesn't have this problem because
flomni's alignment flow does its own explicit
feedback_enable_with_reset() independently beforehand -- lamni's
doesn't. Fixed by passing force_feedback_reset=True at that one call
site so it keeps its current always-reset behavior; no other lamni
caller needed this.

Item 4 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>
2026-07-21 12:40:45 +02:00
x01dcandClaude Sonnet 5 b4c262d41b feat/offer tomo-param reset on lamni experiment-account change
Ports flomni's _maybe_reset_params_on_account_change()/
_set_default_tomo_params() to lamni: at LamNI.__init__, if the active
BEC account differs from the one defaults were last applied for
(persisted via defaults_applied_for_account), interactively offer to
reset tomo params to defaults -- preventing a new experiment silently
inheriting the previous one's tuned FOV/stitch/etc. Same account is a
silent no-op, so restarting a client mid-experiment never disturbs
tuned parameters.

_set_default_tomo_params() is a direct port adapted to lamni's own
param names (tomo_circfov/lamni_stitch_x/y/lamni_piezo_range_x/y
instead of fovx/fovy/stitch_x/y, no tomo_angle_range/
single_point_random_shift_max -- lamni has neither concept). Per-
sample alignment state is deliberately not touched, matching flomni.

Item 2 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>
2026-07-21 11:37:37 +02:00
x01dcandClaude Sonnet 5 e59a42f978 feat/port timing-statistics log + scilog_last_ptycho_scans() to lamni
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>
2026-07-21 10:50:08 +02:00
x01dcandClaude Sonnet 5 c3877ecdef feat/add zero-deg radiation-damage reference shot for lamni tomo_type 1
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>
2026-07-21 04:18:23 +02:00
x01dcandClaude Sonnet 5 8a7a350280 feat/warn early about Fermat scans below the minimum point count
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>
2026-07-21 03:52:44 +02:00
x01dcandClaude Sonnet 5 abc2a3d041 feat/tomo queue reacquire from an earlier projection
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>
2026-07-20 22:00:22 +02:00
x01dcandClaude Sonnet 5 6c3d60306d feat/lamni tomogram timing overview + repeat-on-interruption
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>
2026-07-20 21:13:21 +02:00
x01dcandClaude Sonnet 5 46cadc8894 fix(lamni): show progress GUI on scan start, clear busy heartbeat on exit
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>
2026-07-16 14:21:45 +02:00
x01dcandClaude Sonnet 5 f3c90c5b91 fix(lamni): fix three real tomo_scan() crashes, mirroring flomni's fixes
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>
2026-07-16 13:59:12 +02:00