docs(flomni): add plan for auto-locking xrayeye selection box vertical center
CI for csaxs_bec / test (push) Successful in 2m16s

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
This commit is contained in:
x12sa
2026-09-16 12:28:05 +02:00
co-authored by Claude Sonnet 5
parent 43aa84ac0e
commit 9f922d869f
+145
View File
@@ -0,0 +1,145 @@
# 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:
1. **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 via `self.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.
2. **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_calibration`
and moves `fsamy` so the sample lines up with the FZP height on camera
(`x_ray_eye_align.py:307-340`). `self._height_centered` flips to `True`
right after this move.
3. **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 `x`
center 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
alongside `self.target_crosshair` (`x_ray_eye.py:299`).
- New RPC methods on `OMNY_XRayEye` (USER_ACCESS, same section as
`show_crosshair`/`hide_crosshair`): `lock_vertical_center()` /
`unlock_vertical_center()`. They just set `self._vertical_lock_enabled`.
- New helper `_snap_roi_vertical(self, roi)`:
- No-op if `not self._vertical_lock_enabled` or the crosshair isn't
visible.
- Otherwise reads the crosshair's fixed `y` (the same value used by
`crosshair_position()`, `x_ray_eye.py:868-874`), reads the roi's current
`x` origin and height via `roi.pos()` / `roi.get_coordinates(typed=True)`,
computes `new_origin_y = fzp_y - height / 2` (ROIs are positioned by
bottom-left origin + size, not by center — confirmed via
`bec_widgets/widgets/plots/roi/image_roi.py`), and calls
`roi.set_position(current_x, new_origin_y)`. Guard against signal
re-entrancy (block signals or a re-entrancy flag) while calling
`set_position`, since this will itself be triggered from a
region-changed signal.
- Wire this helper into two places:
- `_style_new_roi` (`x_ray_eye.py:630-636`, connected to
`self.roi_manager.controller.roiAdded`) — call it right after the ROI is
styled, so a freshly-drawn box snaps immediately (`roiAdded` already
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 place `roiAdded`
is 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 after
`self._height_centered = True` is 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()`'s `finally` block (`x_ray_eye_align.py:172`), alongside the
existing `self.gui.hide_crosshair()` cleanup call, so the lock never
outlives a run (including on error/interruption).
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_enabled` state near `self.target_crosshair`
init (~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 connect `roi.sigRegionChangeFinished` to
`lambda: self._snap_roi_vertical(roi)`.
- `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 in `align()`'s `finally` block (near line 172).
## Verification
Since this is real beamtime hardware/procedure code, verify without
disrupting operation:
1. 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_centered` flips, and disabled on both run-start and
run-teardown paths.
2. If a test/offline instance of the GUI is available (`test_wo_movements`
flag on `XrayEyeAlign`, 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_mm` computed
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.
3. Confirm no infinite signal loop / visual jitter when dragging or resizing
a locked box (guard against `_snap_roi_vertical`'s own `set_position`
call re-triggering `sigRegionChangeFinished`).
4. Only test live on the actual flomni beamline once available and outside
critical beamtime, per standing practice for this codebase.