From 9f922d869fa0ea1a7d481f98170cd5cc7d4d4670 Mon Sep 17 00:00:00 2001 From: x12sa Date: Wed, 16 Sep 2026 12:28:05 +0200 Subject: [PATCH] docs(flomni): add plan for auto-locking xrayeye selection box vertical center 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 Claude-Session: https://claude.ai/code/session_01CpPtD7tCRwRZZnREhWcJi8 --- docs/plans/xrayeye-vertical-lock.md | 145 ++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 docs/plans/xrayeye-vertical-lock.md diff --git a/docs/plans/xrayeye-vertical-lock.md b/docs/plans/xrayeye-vertical-lock.md new file mode 100644 index 00000000..a0cfd008 --- /dev/null +++ b/docs/plans/xrayeye-vertical-lock.md @@ -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.