From 0ebe55f31ca1d12556777dd7e147aebf678c66d6 Mon Sep 17 00:00:00 2001 From: x12sa Date: Mon, 21 Sep 2026 15:43:56 +0200 Subject: [PATCH] docs(flomni): plan for a Skip button on the xrayeye alignment widget Idea only, not implemented. Includes the 4-point DAP sine-fit stability study and the decision to send submit=-1 as the skip signal. Co-Authored-By: Claude Sonnet 5 --- docs/plans/flomni-xrayeye-skip-button.md | 113 +++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 docs/plans/flomni-xrayeye-skip-button.md diff --git a/docs/plans/flomni-xrayeye-skip-button.md b/docs/plans/flomni-xrayeye-skip-button.md new file mode 100644 index 00000000..c18f0d5d --- /dev/null +++ b/docs/plans/flomni-xrayeye-skip-button.md @@ -0,0 +1,113 @@ +# Plan: "Skip" button on the X-ray-eye alignment widget (flomni: leave out one angle, e.g. 90 deg) + +Status: **not implemented (idea)** — written 2026-09-21 after a read-only stability study of the +DAP sine fit with 4 instead of 5 angles. No code was changed. + +## 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)?