docs(flomni): add plan for auto-locking xrayeye selection box vertical center
CI for csaxs_bec / test (push) Successful in 2m16s
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:
@@ -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.
|
||||
Reference in New Issue
Block a user