Record the exposure-active-gated PCAP.gate variant discussed for reducing idle-time data volume in raw_stream_mode (currently PCAP free-runs via CLOCK1 regardless of detector state). Documentation only -- not implemented or tested against hardware. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
16 KiB
PandABox (omny_panda) burst acquisition: two operation modes
Status: Mode B confirmed working end-to-end against real PandA hardware (2026-09-15, omny-panda.psi.ch) — correct per-channel raw values, masked
gate_detector_active, andframe_counter-based completion all verified, then speed-tested up to 20 kHz with no reported issues (though see the caveat under "Saving / restoring layouts" — the savedomny_freerun.inilayout itself was captured at 10 kHz, not 20 kHz).ptycho_flomni.yamlcurrently hasomny_panda_continuous(Mode B) active — note the PandA's Design was switched back toOMNYon 2026-09-15 solely to capture theomny.inilayout file below, so double-check which Design is actually loaded on the physical PandA before running a scan against either config.
Context
flomni burst scans acquire frames_per_trigger sub-frames per scan point.
Historically, the PandABox (omny_panda, device class PandaBoxOMNY in
csaxs_bec/devices/panda_box/panda_box_omny.py) reports one PCAP-aggregated
statistic (Mean/Min/Max/Sum/Diff) per gate, discarding the time structure
within a gate. This document describes the two supported operation modes —
the existing per-gate aggregate mode, and a new continuous free-running raw
mode — and how the PandA hardware and ptycho_flomni.yaml device config must
be set up for each.
Mode selection is a single software flag (raw_stream_mode in the device
config); it does not by itself reconfigure the PandA hardware. The PandA
layout must independently match the intended mode (see "Open question" at
the end) before that mode can be used.
Mode comparison
| Mode A: legacy (default) | Mode B: free-running (new) | |
|---|---|---|
| Selected by | raw_stream_mode unset / false |
raw_stream_mode: true |
| PCAP capture fields | Mean/Min/Max/Sum/Diff per gate |
Value (raw instantaneous sample) + gate bit + frame counter |
| Gate/arm timing | Pulsed per point / sub-frame (rate = scan rate × frames_per_trigger) |
PCAP held open continuously for the burst window, free-running at CLOCK1's configured rate (started at 100 Hz for initial testing on 2026-09-15; will be ramped up as testing progresses) |
_acquisition_group (BEC async metadata) |
fly / monitored / burst, chosen from scan_type/frames_per_trigger |
free_running |
| Message publishing | One .data.put() per PandA network frame, unchanged |
Coalesced client-side: flushed every raw_stream_flush_row_count rows or raw_stream_flush_interval seconds, whichever comes first (mitigates Redis/HDF5 message-rate risk — see the acquisition plan's data-path notes) |
Completion check (on_complete) |
Exact count: waits for *PCAP.CAPTURED? to reach num_points * frames_per_trigger |
Exact count: waits for COUNTER1.OUT? (frame_counter) to reach num_points * frames_per_trigger. *PCAP.CAPTURED? can't be used here — CLOCK1 drives PCAP.gate/PCAP.trig continuously (see "Continuous gate/arm" below), so the captured count never naturally stops increasing while armed; COUNTER1 instead counts completed detector exposures directly (it's clocked off the detector-trigger's falling edge), so it reaches the expected total exactly when the last exposure finishes |
| Data completeness verification | Implicit in the exact-count completion check | Same mechanism as Mode A: frame_counter (COUNTER1) is both the completion signal and the post-hoc completeness record — a gap in an otherwise-monotonic sequence in the HDF5 data means dropped rows even though the expected total was reached |
Both modes are implemented in the same PandaBoxOMNY class; raw_stream_mode
defaults to false so any device config that doesn't set it keeps today's
exact behavior unchanged.
Physical wiring (Mode B only)
The detector-trigger signal is patched into TTL input 1 (TTLIN1) on the
PandA front panel. That same TTLIN1.VAL signal feeds both downstream
consumers (see "Block configuration" below): it is the source counted by
COUNTER1 (the frame counter) and the source captured into the gate bit.
Block configuration
Mode A (legacy) — reference baseline
The blocks already configured and aliased today, for reference / so this
layout can be restored unambiguously (see ptycho_flomni.yaml):
FMC_IN.VAL1/FMC_IN.VAL2(capacitive sensor analog inputs) —Min,Max,Meancapture fields.INENC1-INENC4.VAL(position-capture encoder inputs) —Min,Max,Meancapture fields.PCAP.GATE_DURATION—Value.- PCAP gate is pulsed once per point / sub-frame by the existing external trigger electronics (unchanged by this work).
Mode B (new) — free-running raw capture
-
Gate-bit input: the detector-trigger signal, wired into
TTLIN1(see "Physical wiring" above).TTLIN1.VALis captured intoPCAP.BITS0, bit offset 0 — confirmed 2026-09-15 against the omny-panda hardware via:TTLIN1.VAL.CAPTURE_WORD? -> OK =PCAP.BITS0 TTLIN1.VAL.OFFSET? -> OK =0PCAP.BITS0is a shared 32-bit capture word — other capturedbit_outsignals (several TTLIN/LVDSIN/encoder bits are also flagged for capture on this layout) may occupy other bit positions of the same word, so the rawPCAP.BITS0.Valueis not itself a clean 0/1.PandaBoxOMNYmasks it down to bit 0 inraw_stream_mode(see_GATE_BIT_OFFSETinpanda_box_omny.py) before publishing it asgate_detector_active— treat that masking as required, not optional, unless it's confirmed nothing else sharesBITS0. Re-run the query above (read-only, safe against live hardware) if the PandA layout is ever rebuilt, since bit assignment is fixed by the FPGA build and not guaranteed stable across rebuilds. -
Frame counter:
COUNTER1, triggered offTTLIN1.VAL's falling edge (see layout below) — i.e. it increments once per completed detector exposure, not once per PCAP capture tick as originally guessed here. Captured withCAPTURE=Value; this is both the data-completeness signal and the completion-detection signal (frame_counterinsignal_alias, see below, and "Completion check" in the mode-comparison table above) — once the last expected exposure's trigger pulse has fallen,COUNTER1.OUTreachesnum_points * frames_per_triggerexactly. -
Raw analog/position capture:
FMC_IN.VAL1,FMC_IN.VAL2and all fourINENC1-4.VALare switched from aggregate capture toCAPTURE=Valueon this layout (confirmed 2026-09-15) — i.e. every analog/encoder channel goes raw together, not a subset. This means the current PandA layout cannot simultaneously produce Mode A's aggregate (Min/Max/Mean) data — see "Open question" below. -
Continuous gate/arm:
CLOCK1, enabled by the constantONE, drivesPCAP'sgateandtriginputs continuously (instead of the external per-point trigger used in Mode A), so PCAP free-runs atCLOCK1's configured rate. Testing started at 100 Hz on 2026-09-15 and will be ramped up; there is no PandA-side layout change needed to change the rate, onlyCLOCK1's period.TTLIN1 (TTL input 1, detector trigger) ├──▶ COUNTER1.trig, falling edge (Up/Down pulse counter) ──▶ COUNTER1.OUT = frame_counter (CAPTURE=Value) │ = completion signal too: reaches num_points * frames_per_trigger when the last exposure finishes └──▶ captured into PCAP.BITS0 bit 0 ──▶ masked in software = gate_detector_active CLOCK1 (enabled by constant ONE) ──▶ PCAP.gate, PCAP.trig (continuous arm/capture)
Possible variant: gate continuous capture by exposure-active (proposed, not implemented)
Discussed 2026-09-19, not yet implemented or tested against hardware. Today
PCAP.gate and PCAP.trig are both driven by CLOCK1.OUT, so PCAP
free-runs continuously at CLOCK1's rate regardless of whether the detector
is actually exposing -- including idle/dead time between exposures. The
proposal is to split the two roles so they're driven by different sources:
CLOCK1 (enabled by constant ONE) ──▶ PCAP.trig (sample-rate clock, unchanged)
TTLIN1.VAL (detector-trigger / exposure-active) ──▶ PCAP.gate (only capture while exposing)
PCAP.trig keeps ticking at CLOCK1's configured rate exactly as today;
PCAP.gate switches from CLOCK1.OUT to TTLIN1.VAL, so a CLOCK1 tick
only produces a captured row while the detector is actively exposing --
idle-time ticks are simply dropped, not written.
This should be a pure PandA-layout change -- no ptycho_flomni.yaml or
panda_box_omny.py change would be needed, since it doesn't change which
BEC signal names exist or what they mean:
TTLIN1.VALis already proven usable as aPCAP.gatesource -- it's what Mode A already uses (PCAP.GATE/PCAP.TRIG=TTLIN1.VALthere).frame_counter(COUNTER1, clocked directly offTTLIN1.VAL's falling edge, not offPCAP) is unaffected -- still counts every completed exposure and still works as the completion/grouping signal exactly as today.gate_detector_active(PCAP.BITS0bit 0, alsoTTLIN1.VAL) would become trivially always1on every captured row, since a row is only ever captured while that signal is high -- harmless, sinceframe_counteralready does the actual per-exposure grouping, not the gate bit.
Motivation: at 20 kHz continuous (current, ungated behavior), the
free-running raw stream produces roughly 67-86 MB/minute (~4-5 GB/hour) of
raw payload across the 10 captured fields (see
panda_box_free_running_data_schema.md for the field list; the range
reflects uncertainty over whether the four INENC channels report as
int32 or float64). Gating capture to only actual exposure windows would
cut that in proportion to the detector's duty cycle -- e.g. roughly 10x less
at a 10% duty cycle -- with no BEC-side software change required.
If this is implemented: update this section (and the "Continuous gate/arm"
wiring diagram above) to confirmed status, record which .ini layout
captures it, and re-verify the PCAP.BITS0 bit-0 assignment per the caveat
under "Gate-bit input" above (bit assignment is fixed by the FPGA build, not
guaranteed stable across a layout rebuild).
Saving / restoring layouts
Use ophyd_devices/devices/panda_box/utility_scripts.py to save the current
PandA configuration to a .ini layout file and reload it later, so a
configuration survives a PandA power cycle. Both layouts are saved in this
repo under csaxs_bec/devices/panda_box/layouts/ — omny.ini (Mode A) and
omny_freerun.ini (Mode B), captured 2026-09-15 directly from the PandA
design named OMNY/omny_freerun respectively (switch the PandA's active
Design first, then save):
python ophyd_devices/devices/panda_box/utility_scripts.py \
--host omny-panda.psi.ch --save-layout csaxs_bec/devices/panda_box/layouts/omny.ini
python ophyd_devices/devices/panda_box/utility_scripts.py \
--host omny-panda.psi.ch --save-layout csaxs_bec/devices/panda_box/layouts/omny_freerun.ini
To restore either, swap --save-layout for --load-layout with the same
file. Diffing the two confirms the mode-specific differences documented
above: PCAP.GATE/PCAP.TRIG are TTLIN1.VAL in Mode A vs CLOCK1.OUT in
Mode B; CLOCK1.ENABLE is ZERO (off) in Mode A vs ONE in Mode B;
INENC1-4.VAL.CAPTURE/FMC_IN.VAL1/VAL2.CAPTURE are Min Max Mean in
Mode A vs Value in Mode B; PCAP.BITS0.CAPTURE/COUNTER1.OUT.CAPTURE are
No in Mode A vs Value in Mode B.
omny_freerun.ini was captured with CLOCK1.PERIOD=0.0001 (100 µs → 10 kHz)
— note this doesn't match the 20 kHz reported as tested successfully the
same day, so it may not be the exact layout state the 20 kHz test ran at;
worth double-checking before relying on this file as "the" validated
high-rate layout.
Signal alias mapping
The physical PandA block feeding each BEC signal name, kept here and in
csaxs_bec/device_configs/ptycho_flomni.yaml (omny_panda.deviceConfig.signal_alias for
Mode A, omny_panda_continuous.deviceConfig.signal_alias for Mode B) in sync as a single
source of truth:
| PandA block | Mode | BEC signal name |
|---|---|---|
FMC_IN.VAL1.Min/.Max/.Mean |
A | cap_voltage_fzp_y_min/_max/_mean |
FMC_IN.VAL2.Min/.Max/.Mean |
A | cap_voltage_fzp_x_min/_max/_mean |
INENC1.VAL.Min/.Max/.Mean |
A | interf_st_fzp_y_min/_max/_mean |
INENC2.VAL.Min/.Max/.Mean |
A | interf_st_fzp_x_min/_max/_mean |
INENC3.VAL.Min/.Max/.Mean |
A | interf_st_rotz_min/_max/_mean |
INENC4.VAL.Min/.Max/.Mean |
A | interf_st_rotx_min/_max/_mean |
PCAP.GATE_DURATION.Value |
A, B | pcap_gate_duration_value |
FMC_IN.VAL1/VAL2.Value, INENC1-4.VAL.Value |
B | cap_voltage_fzp_y/_x, interf_st_fzp_y/_x/_rotz/_rotx |
PCAP.BITS0.Value (bit 0 = TTLIN1.VAL, confirmed 2026-09-15; masked in software, see above) |
B | gate_detector_active |
COUNTER1.OUT.Value (confirmed 2026-09-15, triggered off TTLIN1.VAL's falling edge) |
B | frame_counter |
The BEC-side data schema produced by Mode B (what ends up in the HDF5 async
dataset) is documented separately in
docs/developer/panda_box_free_running_data_schema.md.
Switching between modes
omny_panda (Mode A) and omny_panda_continuous (Mode B) need different PandA hardware
layouts and can't run simultaneously (see "Open question" below), so exactly one should be
active in ptycho_flomni.yaml at a time, matching whichever layout is currently loaded on
the PandA — comment out the other one's block entirely, don't rely on enabled: false
alone.
That's a correction from earlier in this project: enabled: false on a device BEC has never
connected to is an inert placeholder — connect_device in
bec_server/device_server/devices/devicemanager.py is only called when enabled — so the
theory was that both entries could stay uncommented and switching would just be flipping the
flag. In practice (2026-09-15), disabling omny_panda_continuous this way was not
sufficient once it had already been connected in the running session: both devices ended up
active simultaneously against the same physical PandA. A device that's already connected
doesn't appear to get disconnected just because a later config reload marks it disabled.
Commenting the block out (so BEC never even constructs the device object) is the reliable
way to guarantee it's gone; if in doubt, confirm the other device has actually dropped its
connection (e.g. via a device server restart) rather than trusting enabled: false alone.
Confirmed end-to-end 2026-09-15: Mode B tested working (correct per-channel values, masked
gate_detector_active, frame_counter-based completion), then switched back to Mode A
(commented out) for A/B comparison testing.
Open question, now partially resolved
Can both modes coexist under a single static PandA layout — i.e. is mode
switching purely the raw_stream_mode software flag, with the PandA always
capturing both the aggregate and raw fields simultaneously — or do Mode A
and Mode B require two distinct saved layouts that must be loaded before a
scan depending on the desired mode?
As currently wired for testing (2026-09-15), the answer looks like two
distinct layouts are required: FMC_IN.VAL1/VAL2 and INENC1-4.VAL are
each set to CAPTURE=Value (single selection), not simultaneously
CAPTURE=Value,Min,Max,Mean — so this layout cannot also produce Mode A's
aggregate statistics at the same time. This has not yet been deliberately
tested against the alternative (whether the PandA capture UI actually
supports selecting Value together with Min/Max/Mean on the same
field) — if that turns out to work, a single layout could serve both modes.
Once Mode B testing is far enough along to save a named layout (see
"Saving / restoring layouts" below), settle this explicitly and record the
answer plus, if two layouts are needed, the load procedure to switch between
them.