docs(flomni): document at_each_angle hooks in the user manual and AI_docs
CI for csaxs_bec / test (push) Successful in 1m33s

Adds a "Custom behavior at each projection angle" subsection to the
Tomography section of the flOMNI user manual (docs/user/ptychography/flomni.md):
what a hook function looks like, a polarizer-modulation example, and
how to register/activate/deactivate/list/unregister one, with an
explicit warning about resetting at_each_angle_hook before queuing a
normal follow-up job. Also links it from the quick-start queueing
section. AI_docs/TOMO_QUEUE_COMMAND_JOBS_PLAN.md gets a new section
3.4 recording the design decision (session-only registry, not a
hardcoded one; a new _TOMO_SCAN_PARAM_NAMES entry, not a new job kind;
the GUI param-snapshot leak found and fixed while wiring this up; no
GUI widget yet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
x01dc
2026-07-13 16:37:56 +02:00
co-authored by Claude Sonnet 5
parent 40a3b78afe
commit 48647bf0d9
2 changed files with 103 additions and 1 deletions
@@ -268,6 +268,55 @@ you can validate at add time.
form). A refusal here trips the executor's `incomplete`-and-pause path, which is the
correct outcome.
### 3.4 A separate, session-only registry: `at_each_angle_hook`
A different escape hatch predates the command-job registry: `Flomni._at_each_angle()`
already checks for a `flomni_at_each_angle` function in `builtins.__dict__` and, if
present, calls it instead of the default per-projection acquisition — the motivating
case being something like "record a projection, move a polarizer in, record again,
move it back out." That hook is arbitrary code, which is fine for a one-off
interactive run, but it isn't a snapshotted parameter, so a **queued** tomo job has no
way to say "use this hook for this job." Decided with Mirko: add it as a curated
registry, same spirit as `_TOMO_QUEUE_ACTIONS` but distinct from it —
- **Curated here means "queue stores a name, never code," not "hardcoded in
flomni.py."** Hooks are registered from the IPython session at runtime via
`Flomni.register_at_each_angle_hook(name, func)` into a plain instance dict
(`self._at_each_angle_hooks`), *not* published as a class-level registry the way
`_TOMO_QUEUE_ACTIONS` is. This keeps the "any hook a job references is a Python
callable that was deliberately named and registered, never raw code sitting in the
queue" invariant, while still letting operators write ad hoc hooks per experiment
(unlike `move`/`optimize_idgap`, these aren't pre-reviewed methods shipped in
`flomni.py`).
- **`at_each_angle_hook` is a new entry in `_TOMO_SCAN_PARAM_NAMES`** (a
`_GlobalVarParam(None)`, defaulting to no hook) rather than a new job `kind` or
queue mechanism — it's just another snapshotted tomo parameter, so
`tomo_queue_add()`/`tomo_queue_execute()` restore it per job for free, same as
`fovx` or `tomo_countingtime`.
- **Session-only, and that's surfaced loudly.** Hooks live in memory, not a global
var (functions aren't JSON-serializable), so they don't survive a kernel restart.
`_at_each_angle()` raises a clear `FlomniError` — not a silent fallback to default
behaviour — if a job's `at_each_angle_hook` name isn't registered in the session
currently running the queue.
- **Found and fixed a real param-snapshot leak in the GUI while wiring this up.**
`tomo_params.py`'s `QUEUE_PARAM_NAMES`/`DEFAULTS` mirror `_TOMO_SCAN_PARAM_NAMES`
by hand (the same drift risk `tomo_queue_actions` was built to avoid for command
jobs — see section 7). Before adding `at_each_angle_hook` to that mirror, a
GUI-added job's `params` dict would have no `at_each_angle_hook` key at all, so
the executor's `for name, value in job["params"].items(): setattr(...)` loop would
never touch it — silently leaving whatever hook the *previous* queued job had
active in place for a job that never asked for one. Fixed by adding the key to
`QUEUE_PARAM_NAMES`/`DEFAULTS`, and by having `add_edited_to_queue()` explicitly
pass through the live value (there's no editable widget for it yet).
- **No GUI widget yet** — CLI-only for now (`register_at_each_angle_hook()`,
`at_each_angle_hook = "name"`, `tomo_queue_add()`), same "CLI first" rollout
command jobs took. A natural follow-up: publish currently-registered hook names via
a global var (mirroring `tomo_queue_actions`, section 7) so the GUI could offer a
dropdown instead of requiring direct attribute assignment.
- See `docs/user/ptychography/flomni.md`'s Tomography section for the full
operator-facing writeup (what a hook function looks like, how to register/activate/
unregister/list one).
---
## 4. New / changed CLI methods (`flomni.py`)
+54 -1
View File
@@ -88,7 +88,7 @@ Several parameter sets can be queued and run one after another automatically, e.
If a queued scan is interrupted, the next call to `flomni.tomo_queue_execute()` resumes it automatically rather than starting over, then continues with the remaining entries.
The same queue can also hold **command jobs** that reconfigure the beamline between scans (e.g. change energy, then re-peak the undulator gap) instead of running a tomogram, so "scan A, reconfigure, scan B" runs unattended as one queue. See [Tomography](user.ptychography.flomni.tomography) below for the full command reference, including command jobs.
The same queue can also hold **command jobs** that reconfigure the beamline between scans (e.g. change energy, then re-peak the undulator gap) instead of running a tomogram, so "scan A, reconfigure, scan B" runs unattended as one queue. A queued tomogram can also run custom code at each projection angle (e.g. moving a polarizer in and out between two exposures) via a registered **at_each_angle hook**. See [Tomography](user.ptychography.flomni.tomography) below for the full command reference, including command jobs and hooks.
#### If something went wrong…
@@ -406,6 +406,59 @@ whole job is either safe to redo from the top or it isn't:
- If any step is not idempotent, you are asked whether to re-run the job from the top
or mark it done as-is.
#### Custom behavior at each projection angle
By default, every projection in a tomo scan is a single ptychography scan at that
angle. For cases that need something more — e.g. record a projection, move a
polarizer in, record again, move it back out, at every angle — write a small Python
function and register it as an **at_each_angle hook**. Once registered, it can be
selected per tomo-queue job, so the whole modulated tomogram runs unattended along
with any other queued jobs.
A hook is a function `func(flomni, angle)`, called once per projection angle instead
of the normal acquisition:
```python
def polarizer_modulation(flomni, angle):
flomni.tomo_scan_projection(angle, _internal=True)
umv(dev.polarizer, "in")
flomni.tomo_scan_projection(angle, _internal=True)
umv(dev.polarizer, "out")
```
| command | explanation |
| --- | --- |
| `flomni.register_at_each_angle_hook("name", func)` | Load a hook function under `name`. |
| `flomni.at_each_angle_hook = "name"` | Activate it — the next tomo-queue job added will use it. |
| `flomni.at_each_angle_hook = None` | Deactivate it — back to normal projections for the next job. |
| `flomni.list_at_each_angle_hooks()` | Print the names of all currently registered hooks (the active one is marked). |
| `flomni.unregister_at_each_angle_hook("name")` | Remove a hook by name. |
Example — queue a polarizer-modulated tomogram, then a normal one:
```python
flomni.register_at_each_angle_hook("polarizer_mod", polarizer_modulation)
flomni.tomo_parameters() # set up the scan parameters as usual
flomni.at_each_angle_hook = "polarizer_mod" # activate the hook
flomni.tomo_queue_add("polarizer run")
flomni.at_each_angle_hook = None # back to normal -- do not skip this!
flomni.tomo_queue_add("plain follow-up scan")
flomni.tomo_queue_execute() # runs both, hook active only for the first
```
**Do not forget to reset `flomni.at_each_angle_hook = None`** before adding a job
that should run normally — each queued job remembers whatever `at_each_angle_hook`
was set to at the moment it was added (like every other tomo parameter), so a job
added right after a hook-using one without resetting it first will silently run with
that hook still active.
Registered hooks are **session-only** — they live in memory and do not survive a
kernel restart. If `tomo_queue_execute()` reaches a job whose `at_each_angle_hook`
isn't registered in the current session (e.g. after restarting BEC), it stops with a
clear error rather than silently running a plain scan; re-run
`register_at_each_angle_hook()` for that hook, then call `tomo_queue_execute()`
again to resume.
### Sample storage and transfer
[See short version](user.ptychography.flomni.transfer)