docs(flomni): plan for a Skip button on the xrayeye alignment widget
CI for csaxs_bec / test (push) Failing after 2m12s

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 <noreply@anthropic.com>
This commit is contained in:
x12saandClaude Sonnet 5 committed 2026-09-21 15:43:56 +02:00
1 parent e802d55112
commit 0ebe55f31c
1 file changed
+113
+113
View File
@@ -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)?