diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/AI_docs/TOMO_QUEUE_COMMAND_JOBS_PLAN.md b/csaxs_bec/bec_ipython_client/plugins/flomni/AI_docs/TOMO_QUEUE_COMMAND_JOBS_PLAN.md index 29aa9e7..bbe269c 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/AI_docs/TOMO_QUEUE_COMMAND_JOBS_PLAN.md +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/AI_docs/TOMO_QUEUE_COMMAND_JOBS_PLAN.md @@ -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`) diff --git a/docs/user/ptychography/flomni.md b/docs/user/ptychography/flomni.md index e43f5d1..b7039f5 100644 --- a/docs/user/ptychography/flomni.md +++ b/docs/user/ptychography/flomni.md @@ -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)