Files
csaxs_bec/docs/plans/xrayeye-vertical-lock.md
x12saandClaude Sonnet 5 9f922d869f
CI for csaxs_bec / test (push) Successful in 2m16s
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpPtD7tCRwRZZnREhWcJi8
2026-09-16 12:28:05 +02:00

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:

  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.