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
8.1 KiB
Plan: Auto-lock vertical center of the sample selection box to the FZP height (flomni X-ray eye alignment)
Status: not implemented (written during live beamtime operation on request; no code changes made — see conversation this plan came from).
Context
The flomni X-ray eye alignment procedure (XrayEyeAlign._align_impl in
csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py) works in two phases:
- Step 0 — the user draws a selection box around the FZP and submits it
(
xval_x_0/yval_y_0). This position is stored and a fixed crosshair (TargetCrosshair, drawn viaself.gui.set_crosshair_position()/show_crosshair()) is placed on the live view at that FZP center — the horizontal line of that crosshair marks the FZP's vertical (y) position. - Height centering — the user draws one more box around the sample and
submits it (still logically "step 1"). The script computes
delta_y_mm = HEIGHT_CENTERING_SIGN * (fzp_center_y - mark_y) * pixel_calibrationand movesfsamyso the sample lines up with the FZP height on camera (x_ray_eye_align.py:307-340).self._height_centeredflips toTrueright after this move. - Steps 1-5 (angles 0°/45°/90°/135°/180°) — for each rotation angle, the
user draws a new box around the sample and submits it; only the box's
xcenter is used (alignment_values[k], used later to compute per-angle fovx offsets). The vertical (y) position of these boxes is not used for anything, but the user currently still has to eyeball/re-draw it near the right height each time.
Once _height_centered becomes True, the sample's vertical position is
physically fixed for the rest of the run — it should equal the FZP's y
(the same value the crosshair's horizontal line is drawn at). The goal is
for the selection box in steps 1-5 to no longer require manual vertical
placement: draw the box anywhere, and its vertical center snaps to the FZP
height automatically, staying correct even if the user drags or resizes it
further, right up until they hit submit.
This must not affect the box drawn during the height-centering step
itself (the one submission where k == 1 and not self._height_centered) —
that box's vertical position is the actual measurement the height
correction is computed from, so it must remain fully free.
Design
Add an explicit widget-side "vertical lock" flag, toggled by the flomni
script via a new RPC method pair on OMNY_XRayEye, mirroring the existing
show_crosshair()/hide_crosshair() pattern (x_ray_eye.py:238-261 for the
USER_ACCESS declarations, x_ray_eye.py:833-874 for implementations). This
avoids adding a new EPICS/ophyd Signal and avoids ambiguity from
overloading the existing step counter (confirmed by reading
x_ray_eye_align.py: step reads 1 for both the height-centering box
and the first real angle-0° box, so step alone can't distinguish "lock"
from "don't lock").
- New widget state:
self._vertical_lock_enabled: bool = False, initialized alongsideself.target_crosshair(x_ray_eye.py:299). - New RPC methods on
OMNY_XRayEye(USER_ACCESS, same section asshow_crosshair/hide_crosshair):lock_vertical_center()/unlock_vertical_center(). They just setself._vertical_lock_enabled. - New helper
_snap_roi_vertical(self, roi):- No-op if
not self._vertical_lock_enabledor the crosshair isn't visible. - Otherwise reads the crosshair's fixed
y(the same value used bycrosshair_position(),x_ray_eye.py:868-874), reads the roi's currentxorigin and height viaroi.pos()/roi.get_coordinates(typed=True), computesnew_origin_y = fzp_y - height / 2(ROIs are positioned by bottom-left origin + size, not by center — confirmed viabec_widgets/widgets/plots/roi/image_roi.py), and callsroi.set_position(current_x, new_origin_y). Guard against signal re-entrancy (block signals or a re-entrancy flag) while callingset_position, since this will itself be triggered from a region-changed signal.
- No-op if
- Wire this helper into two places:
_style_new_roi(x_ray_eye.py:630-636, connected toself.roi_manager.controller.roiAdded) — call it right after the ROI is styled, so a freshly-drawn box snaps immediately (roiAddedalready fires only after the user finishes drawing/releases the mouse, so this naturally happens post-draw, not mid-drag).- Each new ROI's
sigRegionChangeFinished(pyqtgraph's inherited "drag/resize finished" signal) — connect it in the same placeroiAddedis handled, so any subsequent manual drag or resize snaps back to the correct vertical center as soon as the user releases the mouse, without fighting the live drag. (No extra visual affordance beyond this — the box stays draggable/resizable in both axes, it's just always corrected back to the right vertical center on release.)
Script-side wiring (x_ray_eye_align.py)
- Call
self.gui.lock_vertical_center()right afterself._height_centered = Trueis set (line 334), before the "Step 1/5" prompt — so every box from here through step 5 is auto-centered vertically. The height-centering box itself (processed just before this line) is unaffected since the lock isn't enabled yet at that point. - Call
self.gui.unlock_vertical_center():- At the start of a fresh run, alongside
self._reset_init_values()(x_ray_eye_align.py:196) or where the crosshair is implicitly reset, so a new alignment run doesn't inherit a stale lock. - In
align()'sfinallyblock (x_ray_eye_align.py:172), alongside the existingself.gui.hide_crosshair()cleanup call, so the lock never outlives a run (including on error/interruption).
- At the start of a fresh run, alongside
LamNI and OMNY have their own near-identical x_ray_eye_align.py scripts
but different height/rotation procedures — out of scope here. The widget
change is inert by default (_vertical_lock_enabled starts False and is
only ever set by a script that explicitly opts in), so it's safe to leave
their scripts untouched.
Files to change
csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py- Add
self._vertical_lock_enabledstate nearself.target_crosshairinit (~line 299). - Add
lock_vertical_center()/unlock_vertical_center()USER_ACCESS RPC methods (declare near ~238-261, implement near ~833-874, next to the crosshair show/hide methods). - Add
_snap_roi_vertical(self, roi)helper. - In
_style_new_roi(~630-636): call_snap_roi_vertical(roi)after existing styling, and connectroi.sigRegionChangeFinishedtolambda: self._snap_roi_vertical(roi).
- Add
csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py- Add
self.gui.lock_vertical_center()call after line 334 (self._height_centered = True). - Add
self.gui.unlock_vertical_center()calls at run start (near line 196) and inalign()'sfinallyblock (near line 172).
- Add
Verification
Since this is real beamtime hardware/procedure code, verify without disrupting operation:
- Read back the diff carefully against the exact step sequence traced above
(k=0 FZP, k=1 first pass height-centering, k=1 second pass angle-0°,
k=2..5 angles 45°-180°) to confirm the lock is enabled exactly once,
right after
_height_centeredflips, and disabled on both run-start and run-teardown paths. - If a test/offline instance of the GUI is available (
test_wo_movementsflag onXrayEyeAlign, or a non-production BEC session), run through a full alignment: confirm the FZP box and height-centering box are completely unaffected (free vertical placement,delta_y_mmcomputed correctly), then confirm boxes drawn for steps 1-5 immediately snap their vertical center to the crosshair's horizontal line, and stay snapped after manual drag/resize. - Confirm no infinite signal loop / visual jitter when dragging or resizing
a locked box (guard against
_snap_roi_vertical's ownset_positioncall re-triggeringsigRegionChangeFinished). - Only test live on the actual flomni beamline once available and outside critical beamtime, per standing practice for this codebase.