Files
csaxs_bec/docs/plans/flomni-xrayeye-skip-button.md
x01dcandClaude Sonnet 5 527113340e
CI for csaxs_bec / test (push) Failing after 2m53s
docs(flomni): xrayeye skip button tested in the flomni simulator
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 16:20:48 +02:00

7.6 KiB

Plan: "Skip" button on the X-ray-eye alignment widget (flomni: leave out one angle, e.g. 90 deg)

Status: implemented 2026-09-21 on a BEC test server (tests: tests_bec_ipython_client/test_flomni_xray_eye_skip.py, tests_bec_widgets/test_x_ray_eye_skip_button.py); tested live in the flomni simulator (works, ok'd by Ana); not yet tried on real hardware (see Verification). Written after a read-only stability study of the DAP sine fit with 4 instead of 5 angles.

Deviations from the plan below: the widget couples the Skip button to the Submit button (enable_skip_button(True) only takes effect while Submit is enabled), so the client calls it just once per run phase instead of next to every enable_submit_button. A refused skip re-sends the pending step prompt. The h5 alignment_values dataset got a step_k attr and alignment_fit a skipped_angles_deg attr. Skipping 0/180 is warned about, not forbidden.

Context

Flat samples are edge-on at 90 deg, so their centre cannot be submitted there. The flomni X-ray-eye alignment (XrayEyeAlign._align_impl in csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py) submits 5 angles 0/45/90/135/180. The GUI fit tab (OMNY_XRayEye in csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py) fits A*sin(f*x + shift) + intercept through the DAP server (LmfitService1D, model SineModel + LinearModel, f fixed to 1 deg, start values SINE_FIT_DAP_PARAMETERS), i.e. 3 free parameters. With 4 points there is 1 degree of freedom (today 2).

Wanted: a Skip button on the widget. One position may be skipped as long as at least 4 values remain for the fit; otherwise error.

Stability study (does the fit still work with 4 points?)

Yes. Method: (1) linear noise propagation for several angle sets, (2) Monte Carlo through the real LmfitService1D (configure(data_x=, data_y=, parameters=SINE_FIT_DAP_PARAMETERS, oversample=5) then process(), in bec_venv) with the widget's exact start values, compared with the global least-squares optimum. Truth A*sin(rad(angle)+phase)+C plus Gaussian noise. There is no real alignment data on disk, so the click noise sigma is unknown (pixel = 0.44 um, hand-placed ROI centre, probably 1-3 um); results are in units of sigma.

  • ~49000 fits, 0 failed fits.

  • RMS error of the fitted correction curve over 0-180 / 0-360 deg (units of sigma):

    angles used cond. number 0-180 0-360
    0,45,90,135,180 (today) 3.11 0.71 1.12
    skip 90: 0,45,135,180 3.23 0.88 1.29
    skip 45 or 135 2.87 0.82 1.17
    skip 0 or 180 5.36 1.02 1.91

    Monte Carlo at sigma = 1 um agrees (skip 90: median 0.72 sigma vs 0.61 sigma, p95 1.6 vs 1.2). So skipping 90/45/135 is cheap (+15..25% noise); skipping an end point (0/180) is clearly worse but still usable.

  • Optimizer misses versus the global optimum only occur for a large offset |C| >~ 45 um (start intercept C = 0 is far away): 15/4000 in that regime for both 4 and 5 points, 0/4000 for |C| <= 20 um. A property of the start values, not of the point count. Optional hardening: seed the intercept from the data mean (the static widget parameters cannot do that themselves).

  • What is really lost with 4 points is redundancy: 1 dof, the residual has one fixed pattern (0.71, -1, 1, -0.71 for 0,45,135,180), so chi-square / the plot cannot tell which click was bad, and a mis-click at 0/180 (leverage 0.83) is mostly absorbed into phase/offset.

  • Other 4-point layouts (0,60,120,180: 0.80 sigma; 0,30,150,180: 1.17 sigma). Extending beyond 180 deg is the most robust option if the flat-sample geometry and fsamroy travel allow it: 0,45,135,225 gives 0.84 / 0.93 sigma (0-180 / 0-360).

Decision: what does Skip send to the client script?

Reuse the existing submit signal with a sentinel: omny_xray_gui.submit.set(-1) (0 = idle, 1 = submit, -1 = skip). The widget writes no xval_x_k / yval_y_k / width_*_k for a skip.

  • The client already polls exactly this one signal (submit.get() == 1), so there is no new polling path and a skip is atomic (no race between "values written" and "flag set").
  • No device change. A new skip Signal on OMNYXRayAlignGUI (csaxs_bec/devices/omny/xray_epics_gui.py) would need a device-server reload; submit exists.
  • No NaN/stale-data trick. submit = 1 with NaN in xval_x_k would make every consumer (alignment_values, update_fov, roi_pixel_data) responsible for noticing it, and the stale values of an earlier run would be read if one forgets. With -1 the client never reads that step.

Rejected: separate skip signal (cleaner, but needs device reload), NaN payload (implicit, error-prone).

Implementation plan

Widget (x_ray_eye.py; shared with LamNI and omny, so it must be inert for them):

  • self.skip_button = QPushButton("Skip", ...) under the Submit button, disabled by default. Its slot does self.dev.omny_xray_gui.submit.set(-1) (no ROI needed, unlike submit()).
  • New RPC enable_skip_button(enable: bool): add to USER_ACCESS next to enable_submit_button and regenerate csaxs_bec/bec_widgets/widgets/client.py (auto-generated by generate_cli.py). LamNI/omny never call it, so the button stays disabled there.

Client (flomni/x_ray_eye_align.py):

  • Loop: submit_val = dev.omny_xray_gui.submit.get(); act on 1 or -1; reset to 0 as today.
  • MIN_FIT_POINTS = 4; track self.skipped_steps. A skip is legal only for real fit points (steps 1-5 after _height_centered; not k = 0 FZP centre, not the height-centering submission at k == 1) and only while 5 - n_skipped - 1 >= MIN_FIT_POINTS. Otherwise do nothing to the run, send_message("Cannot skip: at least 4 positions are needed for the fit") and keep waiting, so the error is immediate and not after the whole rotation series. Call enable_skip_button(False) once the budget is used and True wherever enable_submit_button(True) is called for steps 1-5.
  • Skip branch: no alignment_values[k], store a NaN / "skipped" row in roi_pixel_data, no update_fov(k) (would read stale widths), then advance like a submit: for 1 <= k < 5 rotate to the next angle (tomo_rotate(k * 45), update_frame, next step message); at k == 5 finish (hide_crosshair, break) without update_fov.
  • write_output(): build angles/offsets only from non-skipped steps (e.g. [0,45,135,180]); final guard if len(angles) < MIN_FIT_POINTS: raise. submit_fit_array and the fit tab are already length-agnostic. Record the skipped angles in the h5 (alignment_fit attrs).
  • Messages: with 4 points "1 dof: the fit cannot flag a bad click - check the plot"; extra warning if the skipped angle is 0 or 180 (noisier fit). Step numbering "k/5" stays.

Verification

  • Mock/test_wo_movements=True run of the client loop with a fake submit sequence: (1,1,-1,1,1) ends with 4 fit points; a second -1 is refused with a message; -1 at k == 0 or at the height-centering submission is refused; -1 at the last step finishes cleanly.
  • write_output() gives the 4-point angles/offsets; the fit tab plots 4 points; DAP parameters arrive in omny_xray_gui.fit_params_x; flomni.read_alignment_offset(get_data_from_gui=True) loads sane tomo_alignment_fit[0].
  • Regression: the default 5-submit run is unchanged; LamNI/omny show Skip disabled.
  • Hardware: dry run at the beamline with a flat sample, skipping 90 deg.

Open questions

  • Real click noise sigma (no saved alignment data on the analysis machine).
  • Should skipping 0/180 be forbidden instead of only warned about?
  • Is an angle beyond 180 deg possible for flat samples (better 4-point layout)?