test_flomni_cli_fovy_bounds_within_scan_bounds and
test_lamni_cli_piezo_range_bounds_within_scan_bounds opened the CLI
source files using a path hardcoded relative to the repo root, which
only works if pytest's cwd happens to equal the repo root. In CI the
job's cwd is one level above the checkout, so the same relative path
raised FileNotFoundError. Resolve the path from the already-imported
module's __file__ instead, which is independent of cwd.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Validated nine flomni_fermat_scan runs (S00391-399: single-frame,
frames_per_trigger=3 burst, and reduced-data-mode groups, two aborted
mid-scan) against rt_positions ground truth. All nine reproduce positions
with R^2 >= 0.9999999 on both axes and no anomalous residuals.
Resolves two open questions from sec 6:
- Burst-mode grouping: frame_counter increments once per sub-frame
trigger, not once per point -- point index is
frame_counter // frames_per_trigger, confirmed at R^2 = 1.0000000
against S00395/396/398.
- Reduced data mode: characterized empirically via S00399 -- keeps only
gate_detector_active==1 rows (~99.5% vs ~39% of rows in normal
continuous mode), roughly halving row count with no accuracy loss.
Also nails down the abort-into-restage stale frame_counter mechanism more
precisely: S00393 aborted with exactly 29 points recorded -> S00394
inherits a stale-29 prefix; S00397 (burst) aborted at 17 points x 3
sub-frames=51 -> S00398 inherits a stale-51 prefix. Both harmless
(gate_detector_active==0 throughout the stale rows in both cases), and
both an exact match between the aborted scan's last count and the next
scan's stale value.
Note: this batch does not exercise the arm_scan_allowlist fix (5590633)
-- the running device-server process predates that commit -- though it
wouldn't have changed anything here anyway since every scan in this batch
is flomni_fermat_scan.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Generic BEC staging (stage_all_devices/pre_scan_all_devices) has no
per-scan device opt-in -- every enabled device is staged/pre_scanned for
every scan type (grid_scan, line_scan, alignment moves, ...), not just the
ones that consume its data. deviceTags (ptycho_flomni) are never consulted
by that code path, they're purely a client-side grouping convenience. So
omny_panda_continuous was being Arm()ed and disarmed for every scan in a
session even though only flomni_fermat_scan reads its data -- unnecessary
hardware arm/disarm cycling, and it directly compounds the stale
frame_counter carryover bug documented in PANDA_POSITION_VALIDATION.md
sec 7 (more arm cycles means more abort->restage windows where that bug
can be triggered).
PandaBoxOMNY now takes an arm_scan_allowlist constructor kwarg (None by
default, arms for every scan -- unchanged behavior). When set, on_stage()
computes _should_arm_panda from scan_parameters.scan_name against the
list; on_pre_scan() skips sending Arm() entirely when it's False, and
on_complete() skips the wait-for-frame-count poll loop the same way, both
resolving immediately instead. The data-readout thread/TCP connection
startup in on_stage() is untouched, since it doesn't touch PandA's
hardware register state (COUNTER1) the way Arm() does.
ptycho_flomni.yaml's omny_panda_continuous now sets
arm_scan_allowlist: [flomni_fermat_scan].
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Add fheater userParameter (in: -11.5, enabled) and drive it to that
position at the end of ftransfer_flomni_stage_in(), gated on "enabled"
- fosa_in()/fosa_out() now refuse to move the OSA unless fheater is
verified fully up or fully down, instead of fosa_out() silently
forcing the heater up
- ensure_fheater_up() reuses the new _fheater_is_up() helper
- Reorder ftransfer_flomni_stage_out() to bring the eye in and optics
out before disabling feedback
- Tighten fosaz "out" clearance (6 -> 4mm)
- Re-enable omny_panda_continuous device config
- Update PandA position-validation doc with post-fix scan confirmation
and updated venv path notes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Write-up for continuing this testing in a future session without needing to
re-derive it: the required gate_detector_active/frame_counter grouping
(and why naive per-row comparison can't be fixed by any scale/offset
correction), the exact interferometer + capacitive-sensor conversion
formula, the stale-frame_counter carryover bug and its fix (3eb6e03), and
the validation method (R²/residuals against rt_positions' trusted
average_x_st_fzp/_y). Also records the two agreed next steps: burst
acquisition testing and reduced data mode (the latter still undefined,
to be scoped in that session).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPZ9gzN2rt3wgiMnTKyrug
Diagnosed against a real scan (S06313): grouping raw interf_st_fzp_x/_y
samples by frame_counter + gate_detector_active reproduces rt_positions'
trusted average_x_st_fzp/_y to R² > 0.9999, confirming that method -- but one
point (167) was off by ~116 nm because PandA's COUNTER1.OUT is not cleared by
Arm(). The first raw samples of an acquisition can briefly carry a stale
frame_counter value left over from the previous acquisition's tail (observed:
36 samples reading a stale 167 before dropping to 0), which silently
contaminates whichever real point later in the same scan happens to share
that counter value.
PandaBoxOMNY now drops raw rows captured before it observes frame_counter ==
0 for the first time in each acquisition -- on every arm, including
on_pre_scan retries, since each Arm() reopens the same stale-value window --
and drops leftover buffered rows from an abandoned attempt at the same point.
Verified against S06313: point 167's residual drops from ~116 nm to ~0.1 nm
after the fix, in line with the rest of the scan.
Write-up, including the diagnostic method and a possible PandA-layout-level
alternative fix, in docs/developer/panda_box_free_running_setup.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPZ9gzN2rt3wgiMnTKyrug
LamNI/omny never call enable_skip_button, so they only had a greyed-out
Skip button. Keep it hidden; the first enable_skip_button() call (flomni
does this at the start of the alignment) shows it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Flat samples are edge-on at 90 deg and cannot be centered there. The
XRayEye widget gets a Skip button (disabled unless the client enables it,
so LamNI/omny are unaffected) that signals omny_xray_gui.submit = -1.
XrayEyeAlign._align_impl leaves that step out of the fit: one of the five
angles may be skipped, at least 4 are needed. Skips of the FZP center, the
height marking, or a second angle are refused with a message and the run
continues. write_output() builds angles/offsets from the remaining steps
and raises if fewer than 4 are left; the h5 records the skipped angles.
Not yet tried on real hardware.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Idea only, not implemented. Includes the 4-point DAP sine-fit stability
study and the decision to send submit=-1 as the skip signal.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The global var was deleted on stage-in / new alignment and never created
at startup, so manual tweaks like fit[0][2] += 5.0 failed with an
undefined variable. reset_tomo_alignment_fit() now writes an all-zero
2x5 fit instead of deleting, and Flomni.__init__ creates it if missing
(an existing fit is kept so it survives client restarts).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The GUI "Motion Stop" button did not stop the stages when pressed during
a sample transfer:
- stop_all_axes was not in the Galil controllers' USER_ACCESS, so the
foptx stop over RPC could never work (only logged in the GUI server).
- fsamroy's controller (port 8084) was never stopped.
- an unresolvable device in _stop_device_controller returned silently.
The widget now takes hard_stop_device_names (flomni: ftransy, foptx,
fsamroy; transfer controller first) and sends stop_all_axes() to each
controller from its own thread, so a hung RPC cannot delay the others.
Devices are resolved freshly at click time with retries, every outcome
is logged and shown in the widget's message label, and positioning mode
is restored on the first controller only after all stops were sent. If
stop_all_axes is not yet exposed by the device server, the raw
XQ#STOP,1 is sent via socket_put_and_receive. The legacy
hard_stop_device_name/extra_hard_stop_device_name kwargs still work.
Not yet tested on hardware.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
List the full set of AGETI-CVME-MASTER-TMA:SR-Inj-Status-I enum
strings in the config comment, not just the top-up-relevant ones, so
the PV's possible states are documented in one place.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
Registers AGETI-CVME-MASTER-TMA:SR-Inj-Status-I (the injection-system
enum PV) as injection_status, alongside the other machine-status PVs
(curr, ofb, shutter). readoutPriority is 'monitored', matching curr,
so the value is captured continuously throughout each scan rather
than only once at scan start -- a top-up can start or stop mid-scan.
'0' / 'Filling Storage Ring' means the ring is being refilled.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
The per-call random sub-step offset of the Fermat lattice could not be
switched off from the scan (seed is only an argument of the static position
function, and a seed only makes the offset reproducible). Add an optional
randomize_offset: bool = True scan argument, passed through prepare_scan()
to get_flomni_fermat_spiral_pos(); False gives offset 0 (unshifted lattice).
Default keeps today's behaviour; tomo scans do not pass it.
Document it in the flomni user doc and add an omny AI_docs TODO describing
the port.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017szgTwuHG65YhjoBGiK2Gj
_fil_trans_report is a status report, so use readback.get(cached=True) (last
published readback from redis) instead of one RPC round trip per filter axis.
A missing value still ends in the existing "readback unavailable" error. The
post-umv position check in fil_trans stays live on purpose.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017szgTwuHG65YhjoBGiK2Gj
device.read() defaults to cached=False, i.e. one RPC round trip to the device
server per device (24 for the six slits). Use cached=True, which reads the last
published readback from redis; a missing value still shows as '---'.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017szgTwuHG65YhjoBGiK2Gj
The DAP start-value fix in the shared OMNY_XRayEye widget already applies to
LamNI/omny; 8-point full-period data was never affected. Records that
omny/x_ray_eye_align.py is still a LamNI copy that doesn't consume the GUI fit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017szgTwuHG65YhjoBGiK2Gj
PandaBoxOMNY inherited on_pre_scan, which waits forever for the PandA READY
event before sending Arm(). After rapid abort/retry cycles the readout thread
can be left parked, so READY never arrives and pre_scan_all_devices() hangs
with no error (flomni_fermat_scan 11998/12000).
on_pre_scan now runs a bounded arm task: wait pre_scan_timeout (default 5 s) for
READY; on failure force-reset the box, restart the readout thread (after the old
loop finished its cleanup) and try once more, printing and logging a warning with
diagnostics. If the second attempt fails too, the status fails with a
RuntimeError so the scan aborts. Arm() is sent from the task, not a status
callback, so the scan is only released after it went out. A stop is not retried.
Also records the outcome and the still-open ophyd_devices race in
AGENTS_mirko.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017szgTwuHG65YhjoBGiK2Gj
The xray eye alignment fit (A*sin(f*x+shift)+intercept, 5 points) relied on
lmfit's SineModel.guess() for the start values. Its FFT-based shift guess is
arbitrary for 5 points and often left the fit stuck at the shift bound (2*pi)
far from the data, so the plotted fit missed the submitted points and
wrong parameters were loaded into flomni.
Override all five parameters so the DAP server skips guess(): signed
amplitude (start 30), shift start 0 within +-pi, intercept start 0,
frequency and slope still fixed. Shared by the x and y waveforms.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017szgTwuHG65YhjoBGiK2Gj
Real hardware failure: flomni.fovy=100.0 was accepted by the property, GUI,
and CLI (all capped at an inclusive 100.0), but FlomniFermatScan's own
ScanArgument requires fovy strictly less than 100 -- so the scan rejected it
at runtime with ScanInputValidationError, only surfacing once actually run.
- flomni.fovy property setter: now raises at >=100 (was >100), matching
FlomniFermatScan's gt=0/lt=100. GUI/CLI max tightened 100.0 -> 99.9.
- lamni_piezo_range_x/y (become LamniFermatScan's fovx/fovy, gt=0/lt=80):
found the same bug class at BOTH ends while writing the regression test --
GUI/CLI min was an inclusive 0.0 (scan requires strictly >0) and max was
an inclusive 80.0 (scan requires strictly <80). Tightened to 0.1-79.9 in
both GUI and CLI. Property setter's large_range_scan bypass left
untouched per earlier explicit instruction -- this fixes the GUI/CLI
entry points, which is where the bound is actually enforced in practice.
- Added test_fov_bounds_match_scan_args.py: introspects the real
ScanArgument gt/ge/lt/le from FlomniFermatScan/LamniFermatScan and asserts
our configured GUI/CLI ranges are strictly inside them, so this class of
bug (passes every check except the one that actually runs the scan)
can't silently come back for these or future fields.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
tomo_countingtime is the exposure time of each individual frame within a
frames_per_trigger burst -- total dwell time at a position scales with
frames_per_trigger (see the scan's own timing model, flomni.py:2553:
normal_cadence = ... tomo_countingtime * frames_per_trigger). The GUI label
and both setups' tomo_parameters() CLI prompt/print text previously just
said "Counting time" with no qualifier, which reads as a per-point total.
Relabeled to "Counting time per frame" everywhere it appears.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
Not implemented for omny yet -- records the range tightening and _get_val()
min_/max_ enforcement just done for flomni/lamni (2026-09-19), and confirms
omny's parameter names mirror flomni's exactly. Also confirms tomo_params.py
has no "omny" SETUP_PROFILES entry -- omny doesn't use that shared GUI
widget at all today, so there's nothing to tighten there until its actual
parameter-entry UI (if any) is located.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
- tomo_circfov: GUI upper bound 200.0 -> 500.0 um (floor kept at 0.0 so
"0 = disabled" -- checked explicitly in lamni_fermat_scan.py:484 -- still
works)
- lamni_piezo_range_x/y: GUI upper bound 200.0 -> 80.0 um, matching the
"(max 80)" already printed in the CLI prompt. The property setter's
large_range_scan bypass is left untouched (uncertain hardware rationale) --
this only tightens the GUI/CLI entry points, which is where "<80" actually
becomes enforced in practice, since both now validate before ever calling
the (still-conditionally-permissive) property setter.
- LamNI._get_val() now supports min_/max_ (mirrors Flomni._get_val()
exactly, added previously) -- LamNI's tomo_parameters() CLI wizard
previously had zero range validation at all. Wired up for
tomo_countingtime (0.001-100.0), tomo_shellstep (0.025-20.0),
lamni_piezo_range_x/y (0.0-80.0), and tomo_circfov (0.0-500.0).
- tomo_stitch_overlap gets a brand new CLI prompt (0.0-10.0 um) -- it had no
prompt at all before (property-only, same gap Flomni still has).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
manual_shift_y is prompted by tomo_parameters() (has been all along) -- the
comment wrongly grouped it with tomo_stitch_overlap/corridor_size/
at_each_angle_hook as "only set directly as properties". Only those three
are actually property-only.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
tomo_shellstep: 0.001-1000.0 -> 0.025-20.0 um
stitch_x/stitch_y (flomni only): 0-50 -> 0-10
golden_ratio_bunch_size: 1-10000 -> 1-100
tomo_stitch_overlap: 0.0-50.0 -> 0.0-10.0 um
Applied in both the GUI (tomo_params.py) and the tomo_parameters() CLI
wizard (flomni.py), matching the ±50 um manual_shift_y precedent. Note:
tomo_shellstep, tomo_stitch_overlap, and golden_ratio_bunch_size are built
by shared (not per-profile) GUI code, so these ranges also apply to LamNI's
Tomo Parameters panel, not just flomni's -- stitch_x/stitch_y are the only
one of the four that's flomni-specific (lamni_stitch_x/y are a separate,
untouched profile entry). tomo_stitch_overlap has no CLI prompt to update
(property-only, not part of the tomo_parameters() wizard). All existing
defaults (tomo_shellstep=1, stitch_x/y=0, tomo_stitch_overlap=0.2,
golden_ratio_bunch_size=20) remain within the new ranges.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
_get_val() (the wizard's input prompt) previously did no range validation
at all -- only the Tomo Parameters GUI's spinboxes enforced limits, so the
CLI silently accepted any value of the right type (e.g. manual_shift_y=500
despite the GUI's ±50 µm range). _get_val() now takes optional min_/max_
and re-prompts until the value is in range; wired up for every
tomo_parameters() field that already has a GUI-defined range (tomo_countingtime,
tomo_shellstep, fermat_asymmetry, fovx, fovy, stitch_x/y, manual_shift_y,
frames_per_trigger, single_point_random_shift_max, total projections,
golden_ratio_bunch_size, golden_max_number_of_projections, projections/sub-tomo).
fermat_asymmetry reuses FlomniFermatScan's own MIN/MAX constants rather than
duplicating them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
manual_shift_y (the "additional offset y" that ultimately feeds
tomo_additional_offsety at the scan/RT-controller layer, where it's bounded
by rty's own ±100 hardware travel limit) had no meaningful upper bound of
its own in the Tomo Parameters GUI (-1000 to 1000 µm spinbox range) --
tighten it to ±50 µm, a sane bound at the parameter-entry level, well
inside rty's hardware limit which is left unchanged. GUI-only change
(manual_shift_y itself has no property-level range validation, matching
the existing convention for fovx/fovy/stitch_x/stitch_y). LamNI's own
manual_shift_x/y range is untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
Same gap as flomni.md (fixed previous commit): lamni.md had no mention of
stitching. Add the equivalent paragraph using lamni's own parameter names
(lamni_stitch_x/y, lamni_piezo_range_x/y) and cite tomo_scan_projection(),
which passes them straight through to scans.lamni_fermat_scan().
Also add an omny/AI_docs TODO note -- omny.md has the same gap and omny's
mechanism turns out to use identical parameter names to flomni's
(stitch_x/stitch_y/tomo_stitch_overlap), so the flomni.md paragraph should
carry over directly once written.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
flomni.md had no mention of stitching at all. Add a paragraph explaining
the tiled-Fermat-scan mechanism (flomni.py:3521-3527) and tomo_stitch_overlap,
matching the existing zero_deg_reference_at_each_subtomo callout's style.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
Not implemented for omny yet -- captures the same feature just added to
Flomni/LamNI so the intent isn't lost. Omny still needs its own retraction-
capability check and tomo_scan() entry point located before this can land.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
Take one real 0-degree projection at the very start of every new tomogram
(subtomo_number=1, included in reconstruction like any grid projection),
independent of zero_deg_reference_at_each_subtomo /
golden_projections_at_0_deg_for_damage_estimation -- gives an immediate
radiation-damage baseline even when those flags are off. Placed right after
Flomni's existing collect_empty_frames() step; LamNI gets the same 0-deg
shot but no flat-field step, since it has no way to retract the sample out
of the beam. Omny intentionally left as a follow-up (not implemented here).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
Design note only -- not implemented. Captures the idea (low-rate/~hourly
trend, not per-projection), plus a 2026-09-19 survey finding that flomni/
lamni/omny each have incompatible interferometer-read primitives (different
axis counts/semantics, no shared RT-controller base), and that omny has no
working webpage generator to hook into yet (its existing file is dead code,
never imported by the real factory).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
Record the exposure-active-gated PCAP.gate variant discussed for reducing
idle-time data volume in raw_stream_mode (currently PCAP free-runs via
CLOCK1 regardless of detector state). Documentation only -- not implemented
or tested against hardware.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
The 2026-09-11 fix capped alarm-text *count* per event (_ALARM_TEXTS_MAX),
but not text length, and _load() never re-caps an already-oversized event's
texts list -- only new appends are capped. Confirmed on x12sa: the file had
grown to 1,209,271 bytes, still tripping HTTP 413 on every upload cycle
(upload.php itself has no size check -- almost certainly nginx's default
client_max_body_size=1m). HistoryTracker.save() now shrinks a copy of the
payload to a ~250 KB budget (alarm texts, then ring trace, then oldest
events, then field length as a last resort) before writing/uploading, so
the file can no longer exceed the server's limit regardless of how the
underlying data grows. In-memory history is untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
The account-change check already offered to reset scan parameters to
defaults for a new experiment; extend it to also clear the tomo queue and
reset acquisition progress, so a new account doesn't inherit leftover
queued jobs or in-progress state from the previous account.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
tomo_scan()'s elapsed/idle timing breakdown only printed on normal
completion, so it was unreachable after a KeyboardInterrupt even though
the underlying data (self.progress, a BEC global var) survives the
interrupt. Factor the report construction into _build_timing_lines() and
expose it via tomo_print_timing_report() (console only) and
tomo_print_timing_report_and_scilog() on both Flomni and LamNI. Also
flags the same pre-existing gap on OMNY in a TODO note.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root cause isolated via a live A/B test (Mirko): the "camera image cut/
collapsed until the window is manually moved" symptom is specific to
OMNY_XRayEye -- flomnigui_show_cameras()'s Image/z_ConsoleButtonsWidget docks
never show it, even swapped in and out of the *same* persistent window
repeatedly (ruling out window-freshness/QtAds-first-dock theories tried and
reverted earlier this session). Further isolated to screen size: reproduces
reliably on a screen too small for the assumed geometry, not on a larger one.
OMNY_XRayEye.__init__() was the only widget in this GUI calling
self.resize(800, 600) on itself. This widget is normally embedded as a dock
(gui_tools.py's flomnigui_show_xeyealign()), where the dock manager (Qt
Advanced Docking System) owns its geometry -- an explicit self-resize call
fights that, and on an undersized screen corrupts the dock's layout until a
manual move/resize forces Qt to reflow within actual available space. The
standalone `python x_ray_eye.py` test harness at the bottom of this file
already does its own win.resize(1000, 800) after construction regardless, so
this call was never actually needed even there -- pure dead weight that only
caused harm once embedded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
Root cause finally isolated via a live A/B test: closing the whole "flomni"
window and reopening it (flomnigui_show_gui() takes the gui.new() branch,
creating a fresh top-level window) reproduces the "camera image squeezed/
missing until the window is moved" symptom; closing just the xeyegui dock
and reopening it (window already exists in self.gui.windows, reused as-is)
does not. Same OMNY_XRayEye construction either way -- the only variable is
whether the window itself is freshly created, which rules out the widget's
own layout code (already tried and reverted two fixes there this session).
The existing time.sleep(1) after gui.new() is itself evidence someone
already found the RPC response alone doesn't mean the window has actually
settled on screen. Bumping to 2s is a pragmatic, easily-reversible
experiment, not a confirmed fix -- flagging in case it needs to become a
poll-for-actual-geometry wait instead. LamNI's gui_tools.py has the
identical gui.new() + time.sleep(1) pattern (line 46-47) and is presumably
equally exposed, but untouched here -- not reported/tested there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
The previous fix (QTimer.singleShot(0, ...) from _init_ui()) was not
reliably late enough in practice -- still reproduced live over a remote
desktop session: window opens, image briefly visible, then the control
panel's wrong fixed width squeezes it down, recoverable only by moving
or resizing the window (which doesn't fix the wrong width itself, just
gives the image enough leftover space to look normal despite it).
showEvent() is the correct Qt idiom for "run once real geometry is
available" -- it fires every time the widget is actually made visible,
not just once at construction. Also more robust than a single deferred
call: it self-corrects on any later re-show too (e.g. a saved-workspace
restore re-applying stale geometry after the fix already ran once).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
_init_ui() locked the control panel to a fixed width computed from
sizeHint() called synchronously during construction -- before the
widget had ever been shown/laid out, with no real screen geometry and
fonts not fully resolved yet (worse over a remote desktop session).
Reported live: the widget opens correctly sized for a moment, then the
image area visibly collapses down to a sliver as the (wrong) fixed
width gets applied, recoverable only by manually resizing the window
(which doesn't fix the wrong width itself, just gives the image enough
leftover space to look normal again despite it).
Defer the actual sizeHint()/setFixedWidth() call via
QTimer.singleShot(0, ...) so it runs on the next event-loop iteration,
after the widget has actually been shown/laid out at least once --
same effect a manual resize was achieving, but automatic and correct
the first time.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
_SimIDSBackend's default exposure_time (10000.0, commented "ms") and its
get_exposure_range() ceiling formula (1_000_000.0 / pixel_clock_MHz) were
both scaled as if the unit were microseconds, inconsistent with the real
IDSCamera's documented exposure_time unit (ms; see its Cpt docstring and
exposure_time_max's own 1000.0 ms placeholder default). Net effect: every
fresh sim connect seeded exposure_time.put(10000), read by the real device
layer as a literal 10 second exposure -- ~50x a 5 Hz live-mode frame period
(200 ms), and get_exposure_range() reported a ~50 s ceiling at the default
20 MHz pixel clock.
New default (10 ms) and range formula (20_000.0 / pixel_clock_MHz, giving
~1000 ms at 20 MHz) keep the same pixel-clock-dependent shape but land in a
believable ms-scale range comfortably under a 5 Hz target.
Noticed live: simulated camera's exposure/gain readout looked nonsensical
after the manual-exposure work made this value load-bearing (previously
just an unused placeholder). Sim frame *content* is independent of
exposure/gain (no brightness modeling), so this alone doesn't explain a
reported "cut" first image, but the seeded value being wildly unrealistic
was worth fixing regardless.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
csaxs_bec/bec_widgets/widgets/client.py is a generated file (bw-generate-cli
--target csaxs_bec) that the BEC client uses to build its dynamic RPC proxy
classes -- it is the actual source of truth for what methods a client-side
widget reference exposes, NOT the live widget class itself. Adding
lock_vertical_center()/unlock_vertical_center() to OMNY_XRayEye without
regenerating this file meant every BEC client session (even a freshly
restarted one) built its xeyegui proxy without those two methods, causing
"AttributeError: 'OMNY_XRayEye' object has no attribute
'unlock_vertical_center'" live on a real session -- traced via
flomni.xeyegui._registry[...].__class__.__module__ == 'client', confirming
the proxy class comes from this file, not csaxs_bec.bec_widgets.widgets.xray_eye.x_ray_eye
directly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
The OMNY_TomoParams widget polled flomni_samples.sample_names.sample0
uncached every 2s to refresh a display label. Over a long interlock
lock this produced thousands of RPC calls, overflowing the client's
shared 100-entry request buffer (RequestStorage) and evicting a
pending scan request, which stalled the queue with no error. The
signal is auto-monitored and already live in Redis, so read it from
there instead of round-tripping to the device server.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCaaQQSYGCbkAsRnALWC3h
Both rows share one QGridLayout (for column alignment across rows of
different label widths) -- insert the separator as a full-width grid
row between them rather than a separate widget, and shift the
smear-integration and ROI-vertical-lock rows down accordingly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
Surfaced live on a simulated flomni session: tomo_scan_projection()
crashed with "DeviceConfigError: Device filter_array_1_x does not
exist" -- the real beamline's four filter_array_*_x devices
(bl_endstation.yaml) aren't part of any endstation simulation config,
by design (out of scope for an isolated endstation sim, same as
frontend/optics-hutch/detector devices).
filters_out_of_beam() already tried to skip a missing device via
getattr(dev, axis_name, None), but BEC's device container raises
DeviceConfigError -- a plain Exception, not AttributeError -- for a
missing device, so getattr's default never applied. Wrap the lookup
in try/except instead, which also preserves the pre-existing (and
apparently relied-upon, per several LamNI tests) behavior of `dev`
itself being None -- the usual state of this codebase's
`dev = builtins.__dict__.get("dev")` idiom before a real session sets
it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
Implements docs/plans/xrayeye-vertical-lock.md: once the height-centering
step fixes the sample's vertical position against the FZP crosshair, every
selection box drawn for the remaining angle-alignment steps (1-5) no longer
needs manual vertical placement -- it snaps to the crosshair's y position on
draw and again on every drag/resize.
- OMNY_XRayEye: new _vertical_lock_enabled state, lock_vertical_center()/
unlock_vertical_center() RPC methods, _snap_roi_vertical() helper wired
into _style_new_roi() (both roiAdded and sigRegionChangeFinished). Snap is
idempotent (no-op once already at the target y) so it can't loop back into
itself via its own set_position() call.
- Also added a "ROI vertical lock" toggle in the control panel so an
operator can release/re-engage the lock manually mid-run -- e.g. to freely
draw/measure an unrelated box without aborting the alignment. Both the
toggle and the RPC methods stay in sync (blockSignals pattern already used
elsewhere in this file for on_live_view_enabled).
- x_ray_eye_align.py: lock_vertical_center() right after _height_centered
flips to True; unlock_vertical_center() at the start of every fresh run
(so a run never inherits a stale lock) and in align()'s finally block
(so the lock never outlives a run, including on error/interruption).
FZP box (step 0) and the height-centering box itself remain fully free --
the lock isn't engaged yet at that point. LamNI/OMNY have their own
near-identical scripts but are explicitly out of scope (per the plan); the
widget change is inert by default.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
The earlier fermat_asymmetry commit added it to the wrong "Current
settings" printout -- the scilog/permanent-record summary block
further down in the file, not the interactive tomo_parameters()
display operators actually look at before confirming/editing
settings. Caught live: the prompt asked for it but the summary above
never showed it. Add the missing print line, column-aligned with the
existing ones (verified: '=' lands at the same column as every other
line in the block).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
Add an intuitive spacing table (x spacing stays at step, y spacing
becomes step/fermat_asymmetry) with a worked example, derived from
measuring actual generated positions (area-per-point vs. point count,
averaged over 20 randomized draws per ratio) rather than just stating
the formula. Mirrored into the OMNY port TODO so the same relationship
gets documented there once ported.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
Implements docs/plans/flomni-fermat-asymmetry.md as written: generate the
spiral against an inflated fovy half-width (length_axis2 * fermat_asymmetry),
then compress the accepted y-coordinate back down by the ratio before
storing -- keeps x spacing fixed at step, scales y spacing by the ratio,
preserves the requested FOV. Composes with the already-landed sub-step
randomization: the offset is subtracted at the raw (pre-compression) scale
on both axes, before the inflated-window check, so the offset's phase-shift
stays proportional to the local spacing on both axes.
Wired through the scan class (FlomniFermatScan), Flomni's global var +
queue/CLI plumbing (_TOMO_SCAN_PARAM_NAMES, scan_kwargs, tomo_parameters()
print/prompt, _set_default_tomo_params()), and the tomo_params Qt widget
(new field with a "?" help button, following the widget's existing
offsets-section help-button pattern).
Added tests verifying the inflate/compress math directly (against an
equivalent call with an inflated FOV and fermat_asymmetry=1.0, same seed)
rather than via an approximate spacing-ratio heuristic -- a first attempt
at the latter proved too noisy on a Fermat spiral to assert reliably.
Updated the flOMNI user docs and the OMNY port TODO (out of scope here,
explicitly excludes LamNI per the plan).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
Restores the legacy spec behavior of drawing a fresh random offset
(uniform in [0, step)) per call and shifting the sampling lattice by
it before the FOV crop check -- decorrelates position-tied
reconstruction errors that otherwise accumulate into ring artifacts
across a ptycho-tomography series. Applied identically to all three
generators (get_flomni_fermat_spiral_pos, get_omny_fermat_spiral_pos,
get_lamni_fermat_spiral_pos), same scalar offset for x and y, matching
the historical spec. New `seed` parameter (default None = fresh
randomization) allows deterministic/reproducible calls.
The four call sites that predict Fermat point counts ahead of a real
scan (flomni.py/lamni.py's _expected_fermat_position_count(),
tomo_params.py's _compute_fermat_positions_flomni/_lamni) now pin
seed=0 so their own estimates stay reproducible across repeated calls;
their docstrings note the real scan's count can differ by a point or
two near the threshold due to the randomization. Updated the affected
tests to compare against a matching seed=0 instead of an independent
random draw.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
Legacy spec code drew a fresh random offset per projection to shift the
Fermat lattice phase before FOV cropping (mitigates ptycho-tomo ring
artifacts). None of the current BEC Fermat spiral generators (flOMNI,
OMNY, LamNI) draw this offset -- all three are fully deterministic.
Plan only, not implemented; applies identically across all three setups.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
Update foptx/fopty/fosax/fosay in/out positions and detector_distance
for the 120 micron FZP (60 nm outermost zone), commenting out the
previous 150 micron settings for reference.
Introduces fermat_asymmetry, a ratio of x spacing to y spacing for the
Flomni Fermat-spiral scan: x spacing stays fixed at step while y spacing
scales by the ratio, with the requested field of view preserved (so the
total position count changes with the ratio). Covers the scan class,
global var/CLI, queuing, and the tomo_params Qt widget. Plan only, not
implemented -- written during a live beamtime experiment.
Also flags this for a future OMNY port (same Fermat-spiral algorithm),
explicitly excluding LamNI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WW873PFzsY4N1r65aQgfFn
bec.active_account is normally a p##### proposal account in
production, but _is_valid_eaccount() only matched e#####, causing
every real beamtime session to be silently rejected and fall back to
tomo ID 0 (skipping OMNY registration and PDF upload). Widen the
regex to accept both e- and p-accounts while still rejecting
test/gac-* accounts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpPtD7tCRwRZZnREhWcJi8
Once height-centering fixes the sample's vertical position during the
flomni alignment procedure, the selection box for the 0-180 deg rotation
steps should auto-snap to the FZP height instead of requiring manual
vertical placement each time. Plan only, not implemented.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpPtD7tCRwRZZnREhWcJi8
flomni and LamNI both have scilog_last_ptycho_scans plus its
projection-timing-log infrastructure; OMNY has neither, only the
older per-subtomo write_to_scilog/_write_subtomo_to_scilog. Flag it
as a TODO (same style as the existing filter/eye-check and
tomo_reconstruct TODOs in this folder) so it isn't lost, and note
that the step-size field just added to flomni/lamni's version should
be included from the start when this gets ported.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpPtD7tCRwRZZnREhWcJi8
Mirrors the same fix just applied to Flomni: tomo_shellstep was already
captured in every projection timing record but never surfaced in the
per-scan SciLog message. Add it next to FOV.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpPtD7tCRwRZZnREhWcJi8
tomo_shellstep was already captured in every projection timing record
but never surfaced in the per-scan SciLog message. Add it next to FOV,
matching the "step=...um" convention already used in
_describe_tomo_job_line.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpPtD7tCRwRZZnREhWcJi8
- flomni_fermat_scan.py's fovx bound was lt=220 (exclusive), so 220 itself
was rejected; change to le=220 to match flomni.py's fovx setter, which
was already inclusive (only raises for val > 220).
- tomo_params.py's flomni fov_fields spinbox max was still 200.0, an
independent limit that wasn't updated when fovx's cap was raised
elsewhere -- bring it to 220.0 for consistency.
- exp_time's real default is now 0.1 instead of 0, so the generated scan
doc's example (scans.flomni_fermat_scan?) shows a realistic exposure
time instead of 0.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197yD3hTxwvrgmYr7aaYiCD
get_flomni_fermat_spiral_pos/get_omny_fermat_spiral_pos/get_lamni_fermat_spiral_pos
sized their spiral loop's iteration cap (n_max) from the FOV's area alone,
but the spiral radius only grows as step*0.57*sqrt(ii). For an elongated
(non-square) FOV -- e.g. flomni's fovx up to 220 vs fovy capped at 100 --
this under-estimated the iterations needed, so the loop ran out before
reaching the long axis's outer region. The two explicitly-appended
bounding-box corner points masked this: they showed up regardless, while
genuine spiral fill points near the true edge silently went missing
(observed as the pattern being "cut" well short of the requested FOV).
Add a shared compute_fermat_spiral_n_max() helper that takes the max of
the old area-based estimate and a corner-radius-based estimate, so
square/near-square FOVs are numerically unaffected (verified
byte-for-byte identical) while elongated ones now reach the requested
edge (verified: flomni fovx=220,fovy=30 now reaches 109.98um vs the
82.78um it reached before, against a 110.0um target).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197yD3hTxwvrgmYr7aaYiCD
feye_in now widens sl1xs/sl1ys by +0.3 mm and sets filter transmission
to 5% before moving the eye in, so there is enough flux/visibility on
the camera. feye_out restores the pre-open slit position (persisted
via a BEC global var so it survives a kernel restart) and fully
removes filters (fil_trans(1)) before checking whether the direct
beam disappeared. If the slit was manually re-adjusted while the eye
was in, feye_out now asks whether to keep the current size instead of
restoring the pre-eye value.
csaxs (for fil_trans) is threaded into Flomni via constructor
injection, gated on the existing "fttrx1 in dev" real-beamline check,
rather than exposing it as a global.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpPtD7tCRwRZZnREhWcJi8
Raise the flOMNI fovx cap in the flomni.fovx property setter, the
tomo_parameters() summary/prompt text, and the flomni_fermat_scan
ScanArgument bound + docstring, so all four are consistent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197yD3hTxwvrgmYr7aaYiCD
The height-marking step's GUI message just said "Submit height. Use
arrows if far off.", which didn't tell users they first need to mark
the position with the image's selection (ROI) tool before submitting.
Spell that out explicitly. Verified against message_line_edit's actual
90px-tall QTextEdit box (across a 140-300px width range) that the
longer text still wraps within the fixed height, no scrollbar needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197yD3hTxwvrgmYr7aaYiCD
The PDF report's measured/started status markers used the ✓/◐ unicode
symbols, which the core Helvetica font in fpdf2 only supports up to
latin-1, so printing the report crashed with "Report failed: Character
outside the range of characters supported by the font". The on-screen
Qt label is unaffected, since it renders those symbols fine -- only
the PDF-only marker text needed to change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197yD3hTxwvrgmYr7aaYiCD
The generated `Examples:` section in scans.<name>? always replaces any
hand-written docstring example with placeholder values, using a generic
1.0 for required float args with no example hint. Set example= on the
required fovx/fovy/cenx/ceny/step/zshift/angle arguments of
flomni_fermat_scan, omny_fermat_scan, and lamni_fermat_scan so the
generated Minimum/Full calls show sensible values instead. Also drop the
now-fully-dead hand-written Examples: blocks in flomni/omny, since they
were always discarded before reaching users.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197yD3hTxwvrgmYr7aaYiCD
The Example: line was left out of sync with the parameter table fix
in 5141854 - argument order didn't match, and burst_at_each_point was
missing. Now mirrors the code docstring's example.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>