Files
csaxs_bec/docs/SIMULATED_ENDSTATIONS.md
T
x01dcandholler afcbbf66cc
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 1m30s
fix/galil back to original code
2026-07-12 07:52:37 +02:00

155 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Simulated Endstations for cSAXS BEC (flOMNI / LamNI / OMNY)
Status: implemented and validated (2026-07). flOMNI extensively live-tested against a real
BEC deployment; LamNI/OMNY validated by offline harness, live smoke test pending.
## Purpose
Full software development and testing of the flOMNI/LamNI/OMNY control code without
hardware and without a running instrument. The real ophyd device classes, controllers,
scan plugins, GUI widgets, and IPython-client scripts run **unchanged**; only the wire
protocols are simulated. Swapping a device config is the only difference between the
simulated and the real system.
## Architecture: protocol-level socket injection
All three endstations funnel every hardware interaction through socket-based controllers
(`ophyd_devices.utils.controller.Controller`), which accept a `socket_cls` constructor
parameter. The simulation injects `SocketIO`-compatible classes that implement the wire
protocols as in-process state machines:
- Commands are dispatched synchronously inside `put()`; replies go to a queue drained by
`receive()`. Commands that are reply-less on the real hardware must not enqueue replies
(the main desync risk of naive mocks) — reply behavior follows the real server sources.
- Simulation state is shared per (host, port) via `SimStateRegistry`, mirroring the
controller singleton. Device subclasses seed axis parameters at construction.
- The sim device classes are thin subclasses of the real ones whose only jobs are
(a) defaulting `socket_cls` and (b) seeding sim state from `sim_*` deviceConfig keys.
- Cross-device choreography (e.g. flOMNI `feedback_enable_with_reset` touching fsamroy,
fsamx and the ftrackz Galil) runs through the real Python code against each
controller's own sim socket — no logic is duplicated.
Protocol fidelity is grounded in the actual hardware-side sources: `fgalil.dmc`,
`galil_micos_upr.dmc` (FUPR), `lamni.dmc`, `controller1.dmc` (OMNY), flOMNI
`CommunicationServer.cpp`, `LAMNI_server.cpp`, `OMNY_server.cpp`.
## File inventory (all under csaxs_bec unless noted)
- `devices/sim/sim_socket.py``SimSocketBase` + `SimStateRegistry`
- `devices/sim/sim_galil.py` — shared Galil DMC state machine (motion at constant
velocity, threads, referencing `#FES/#FRM/#REFAX`, gripper transfer routines
`#GRGET/#GRPUT` with `confirm` handshake and `atconfir` autoconfirm, `#Tracker`,
`#MNTMODE/#POSMODE`, `#STOP`, `AB`/`ST`, direct axis commands `PA/PR/BG/SH/MO/AC/DC/SP/LD`,
encoder axes `TP`/`encpermm`, dialect variables via `sim_variables`) plus
`SimFlomniGalilMotor`, `SimFuprGalilMotor`
- `devices/sim/sim_smaract.py` — Smaract MCS protocol + `SimSmaractMotor` (serves
flOMNI fosa*, LamNI losa*, OMNY ocs*/oshield)
- `devices/sim/sim_rt_flomni.py` — flOMNI Orchestra server (feedback l0/l1/l2/l3,
tracker T/Ts, scans s/sc/sd/sr, 10-field sample rows, paced by `sim_point_dwell_s`)
- `devices/sim/sim_lamni.py` — LamNI Galil dialect (swver 28, encoder axes 05,
air-bearing interlocks) + LAMNI server (J0/J1/J5/J6, J2/J4/J7, As/Ar cap sensors,
Ss/Sr sampler, V velocity, 21-field rows)
- `devices/sim/sim_omny.py` — OMNY Galil dialect (swver 134, mntax=4, signed mntprgs,
tempab/timeab/IgNoFol, grstat/relpos/toppin, `#GRSTAT/#CRESET/#SAFETY`, NETIO power
switch stubbed always-on) + OMNY server dialect (J2 six fields, `y`→1 semantics,
19-field rows) + patched-EPICS `SimOMNYSampleStorage/Dewar/Temperatures`
- `devices/sim/sim_cameras.py``SimWebcamViewer` / `SimIDSCamera`: real camera classes
with synthetic frame source (crosshair + gaussian blob); optional per-frame noise via
`sim_noise_std` (default 0 = static, cheap over remote desktop)
- `devices/sim/sim_sample_storage.py``SimFlomniSampleStorage` (patched EPICS,
seedable via `sim_samples`, default slots 03 populated)
- `device_configs/simulated_omny/`**all** simulated configs live here:
`simulated_flomni.yaml` (hand-maintained), `simulated_lamni.yaml` and
`simulated_omny.yaml` (generated), `simulated_bl_endstation.yaml` (pre-existing)
- `bin/generate_simulated_configs.py` — regenerates the LamNI/OMNY configs from
`ptycho_lamni.yaml`/`ptycho_omny.yaml` (rerun after real-config changes)
- `tests/sim_flomni_harness.py`, `tests/sim_lamni_omny_harness.py` — offline harnesses
(no BEC deployment needed): 39 + 26 checks covering moves, referencing, feedback,
tracker, scans through the flyers, gripper transfers, storage, cameras
## Real-code fixes made along the way (separate commits)
- `ddg_1.py`: `bec_server.scan_server...ScanInfo` import moved under `TYPE_CHECKING`
(typing-only runtime dependency broke against the redeployed BEC layout).
- `flomni.py`: all four scilog sends wrapped in try/except RuntimeError — accounts
without scilog no longer crash `tomo_scan`/alignment/timing reports.
## Delivery policy
Tarballs contain ONLY simulation-package files (`devices/sim/*` except
`simulated_beamline_devices.py`, `device_configs/simulated_omny/*`, generator, harnesses,
this doc) plus the single agreed device-layer fix (`ddg_1.py`). Fixes to user-maintained files (`flomni.py`, ...) are delivered as diffs
only — never as full files, since the working copies evolve during testing.
## BEC integration lessons (important for future sim devices)
1. **Explicit signatures required**: the device server passes only deviceConfig keys that
appear as named parameters in `inspect.signature(dev_cls)`, and injects
`device_manager` only if named. `*args/**kwargs`-only subclasses silently lose their
entire config.
2. **msgpack strict_map_key**: no integer keys anywhere in deviceConfig — quote channel
numbers (`"1": -1.6`).
3. Hidden second transports exist outside the socket seam (OMNY NETIO power-switch HTTP,
camera MJPEG/pyueye) and need their own stubs.
## Deliberate simulation semantics ("working hardware", no error injection)
- Axes boot **referenced** (real init zeroes at limits; set `sim_referenced: false` to
exercise init procedures) at positions from `in` user params, clamped into limits.
- `allaxref`/`axisref[n]` are genuine writable DMC variables (forcing `allaxref=0` makes
moves refuse, as on hardware); recomputed by referencing routines.
- Constant-velocity motion with per-axis speeds from the DMC `#INIT` tables; RT scans
advance at `sim_point_dwell_s` per position (default 20 ms) so ETA/timing logic is
meaningfully testable.
- Sampled scan rows carry targets + ~2 nm gaussian noise.
- LamNI: the interferometer readback is coupled to the coarse stages — with feedback off,
measured (x, y) = lamni_from_stage_coordinates(lsamx-center, lsamy-center)*1000 um
(geometry constants imported from LamNIFermatScan). This makes
lamni_new_scan_center_interferometer converge. Coupling is configured via
`sim_coarse_coupling` on rtx/rty/rt_positions (generator fills it from the lsamx/lsamy
config). lsamx/lsamy boot at their 'center' user parameters; speeds follow the
lamni.dmc axspeed table.
- A LamNI fermat scan re-enables feedback itself (`feedback_enable_without_reset` in the
scan plugin) — starting a scan with user-disabled feedback therefore works, on the
real system as well as in the sim; it only fails on real hardware when feedback cannot
engage (air on, interferometer error), an error mode deliberately not simulated.
- Thread 0 is dispatch-only, as on the real controller: `#NEWPAR` keeps it active for
~50 ms while the motion itself is reported via the firmware flags (`MG_BG`, motor on).
The trailing thread-0 wait in the (unmodified) setpoint code therefore returns quickly
and move progress is published, matching real hardware. An earlier sim iteration
wrongly modeled thread 0 as busy for the whole move and "fixed" the device code to
match — that change was reverted; the device files are untouched.
- Referencing (`#FES/#FRM/#REFAX`) applies its outcome synchronously (limit switch,
position 0) while only the apparent motion is time-based, so status polls never race
the result.
- Transfer confirm handshakes are real (GUI + gripper-camera flow works); set
`atconfir=1` on the controller for unattended runs.
- `#GRSTAT` always reports success; NETIO always on; unknown Galil commands reply `:`
and log `[sim galil] unhandled` warnings — grep the device-server log after a session
to find fidelity gaps worth adding (full DMC command-reference emulation was
deliberately not pursued).
- LamNI `sd` reply uses the detector-trigger wording; `sd<cycles>` internal-timing
variant not modeled. `sr` replies are integer-formatted per LAMNI_server.
## Live-test status (flOMNI, testing account, redeployed BEC)
Passed: sample storage + GUI, moves incl. live progress bars and Ctrl-C, rotation→RT
angle coupling, feedback enable/disable/status, tracker, fermat scans incl. progress and
abort, X-ray eye GUI with camera frames, storage bookkeeping. Pending: full transfer
chain end-to-end, tomo_scan (unblocked by the scilog fix), tomo queue executor
(crash-resume checklist), then queue-reordering implementation. LamNI/OMNY: harness-
validated; a short live smoke test (config load, a move, one scan, one OMNY transfer)
recommended before relying on them, since flOMNI testing surfaced integration issues the
harness cannot see.
## Operational notes
- Deploy: extract the tarball in the repo root (overwrites sim files + the three fixed
real files). Remove stale `device_configs/simulated_flomni/` and `simulated_lamni/`
folders from earlier iterations if present.
- Sim tuning knobs (deviceConfig): `sim_initial_position`, `sim_velocity`,
`sim_stppermm`, `sim_encpermm`, `sim_referenced`, `sim_analog_inputs`,
`sim_digital_inputs/outputs`, `sim_variables`, `sim_limit_low/high_active`,
`sim_point_dwell_s`, `sim_noise_std`, `sim_samples`.
- Harnesses run standalone: `python tests/sim_flomni_harness.py` (repo on PYTHONPATH).