From 3012cd31548f8be5137cb2521c11bc6a13edde09 Mon Sep 17 00:00:00 2001 From: x12sa Date: Tue, 15 Sep 2026 14:13:22 +0200 Subject: [PATCH] fix(flomni): mask gate_detector_active to its own bit, record confirmed PandA wiring TTLIN1 (detector trigger) drives both COUNTER1 (frame_counter) and the PCAP.BITS0 gate-bit capture, confirmed 2026-09-15 against the omny-panda hardware via TTLIN1.VAL.CAPTURE_WORD?/OFFSET? (PCAP.BITS0, bit 0). PCAP.BITS0 is a shared 32-bit word -- other captured bit_out signals may occupy other bit positions of it, so the raw word is not itself a clean 0/1. PandaBoxOMNY now masks gate_detector_active down to bit 0 before publishing it, instead of trusting the whole word. Also records the confirmed free-running setup in the developer docs: all four encoders plus both FMC_IN channels switched to raw Value capture, CLOCK1 driving continuous PCAP gate/trig, and the initial 100 Hz test rate. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco --- csaxs_bec/device_configs/ptycho_flomni.yaml | 7 +- csaxs_bec/devices/panda_box/panda_box_omny.py | 23 ++++ .../panda_box_free_running_data_schema.md | 21 ++-- .../developer/panda_box_free_running_setup.md | 105 ++++++++++++------ tests/tests_devices/test_panda.py | 19 ++++ 5 files changed, 129 insertions(+), 46 deletions(-) diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index f0fb0959..5a51d42a 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -676,9 +676,10 @@ omny_panda: INENC4.VAL.Mean: interf_st_rotx_mean INENC4.VAL.Min: interf_st_rotx_min PCAP.GATE_DURATION.Value: pcap_gate_duration_value - # Free-running mode (raw_stream_mode: true) signals. Block numbers below are - # placeholders (TODO) pending the actual PandaBox wiring/layout - see - # docs/developer/panda_box_free_running_setup.md. + # Free-running mode (raw_stream_mode: true) signals. TTLIN1 (detector trigger) feeds + # both blocks below - confirmed 2026-09-15 against the omny-panda hardware, see + # docs/developer/panda_box_free_running_setup.md. PCAP.BITS0 is a shared 32-bit word; + # PandaBoxOMNY masks it to bit 0 (TTLIN1.VAL) before publishing gate_detector_active. PCAP.BITS0.Value: gate_detector_active COUNTER1.OUT.Value: frame_counter enabled: true diff --git a/csaxs_bec/devices/panda_box/panda_box_omny.py b/csaxs_bec/devices/panda_box/panda_box_omny.py index ccfa82f7..e48d4770 100644 --- a/csaxs_bec/devices/panda_box/panda_box_omny.py +++ b/csaxs_bec/devices/panda_box/panda_box_omny.py @@ -36,6 +36,17 @@ class PandaBoxOMNY(PandaBox): capture. """ + # PCAP.BITS0 is a shared 32-bit capture word; other captured bit_out signals (e.g. other + # TTLIN/LVDSIN/encoder bits also flagged for capture) may share it at different bit + # offsets, so the raw word is not itself a clean 0/1. TTLIN1.VAL (the detector-trigger + # gate signal, aliased to _GATE_SIGNAL_NAME below) was confirmed at bit offset 0 of + # PCAP.BITS0 on the omny-panda hardware on 2026-09-15 via + # `TTLIN1.VAL.CAPTURE_WORD?`/`TTLIN1.VAL.OFFSET?` (see + # docs/developer/panda_box_free_running_setup.md). Re-verify with the same query if the + # PandA layout is ever rebuilt. + _GATE_SIGNAL_NAME = "gate_detector_active" + _GATE_BIT_OFFSET = 0 + def __init__( self, *, @@ -176,6 +187,18 @@ class PandaBoxOMNY(PandaBox): self.cancel_on_stop(status_captured) return status_captured + def convert_frame_data(self, frame_data: FrameData) -> dict[str, Any]: + """Same as the base class, but in raw_stream_mode additionally masks + ``gate_detector_active`` down to its own bit within the shared PCAP.BITS0 capture + word -- see the class-level note on ``_GATE_BIT_OFFSET`` for why the raw word can't + be trusted as a clean 0/1 on its own. + """ + out = super().convert_frame_data(frame_data) + if self.raw_stream_mode and self._GATE_SIGNAL_NAME in out: + entry = out[self._GATE_SIGNAL_NAME] + entry["value"] = [(int(v) >> self._GATE_BIT_OFFSET) & 1 for v in entry["value"]] + return out + def _receive_frame_data(self, data: FrameData) -> None: """Callback to receive frame data from the PandaBox. diff --git a/docs/developer/panda_box_free_running_data_schema.md b/docs/developer/panda_box_free_running_data_schema.md index aabddc69..1151918e 100644 --- a/docs/developer/panda_box_free_running_data_schema.md +++ b/docs/developer/panda_box_free_running_data_schema.md @@ -3,10 +3,11 @@ > **Status: schema for the acquisition side only.** This document defines > what raw data `omny_panda` writes to its HDF5 async dataset when > `raw_stream_mode: true` (see `docs/developer/panda_box_free_running_setup.md` -> for the hardware/mode setup). Reconstructing the previous -> averaged/stdev-per-position view from this raw data (binning by the -> `gate_detector_active` transitions or timestamps) is a separate, -> not-yet-planned analysis task and is intentionally out of scope here. +> for the hardware/mode setup, confirmed against real PandA hardware +> 2026-09-15). Reconstructing the previous averaged/stdev-per-position view +> from this raw data (binning by the `gate_detector_active` transitions or +> timestamps) is a separate, not-yet-planned analysis task and is +> intentionally out of scope here. ## Where this data lands @@ -22,16 +23,16 @@ just batched differently for publishing efficiency. ## Per-row fields Each row corresponds to one PCAP capture tick (one raw sample at the -free-running capture rate, e.g. ~20 kHz — see the setup doc for the actual -configured rate): +free-running capture rate — `CLOCK1`'s configured rate, started at 100 Hz for +initial testing on 2026-09-15, see the setup doc for the current rate): | Field (BEC signal name) | Source PandA block | Meaning | |---|---|---| -| *(raw analog/position value(s), name TBD per setup doc)* | e.g. `FMC_IN.VAL1.Value` / `INENC1.VAL.Value` | The instantaneous raw sample at this capture tick, replacing the Mode A aggregate (Mean/Min/Max) for whichever channel(s) are switched to raw capture. | -| `gate_detector_active` | `PCAP.BITS0.Value` (block TBD) | The other detectors' gate signal, sampled at this same tick: `1` while the other detectors are actively gated/acquiring, `0` otherwise. This is what a future reconstruction step would use to delimit which raw samples belong to which detector-gate window. | -| `frame_counter` | `COUNTER1.OUT.Value` (block TBD) | Monotonically incrementing hardware counter, one increment per capture tick. Used to verify data completeness after acquisition: a gap-free, strictly increasing sequence means no samples were dropped between the PandA and the HDF5 file; a gap indicates lost data (e.g. from the Redis stream's `MAXLEN` trimming if the file writer fell behind — see the acquisition plan's data-path notes). | +| `cap_voltage_fzp_y`/`cap_voltage_fzp_x` etc. (raw analog/encoder value) | `FMC_IN.VAL1.Value` / `FMC_IN.VAL2.Value` / `INENC1`-`4.VAL.Value` | The instantaneous raw sample at this capture tick, replacing the Mode A aggregate (Mean/Min/Max). All six channels (both FMC analog inputs and all four encoders) are switched to raw capture together on this layout — not a subset. | +| `gate_detector_active` | `PCAP.BITS0.Value`, bit 0 (`TTLIN1.VAL`), masked in software — see setup doc | The detector-trigger signal, sampled at this same tick: `1` while a detector trigger is active, `0` otherwise. `PCAP.BITS0` is a shared 32-bit word; `PandaBoxOMNY` masks it down to bit 0 before this reaches the HDF5 file (see `_GATE_BIT_OFFSET` in `panda_box_omny.py`) — the raw word itself is not a clean 0/1. This is what a future reconstruction step would use to delimit which raw samples belong to which detector-gate window. | +| `frame_counter` | `COUNTER1.OUT.Value`, free-running off `TTLIN1.VAL` (i.e. increments once per detector trigger pulse, not once per PCAP capture tick as originally guessed here) | Monotonically incrementing hardware counter. Used to verify data completeness after acquisition: a gap-free, strictly increasing sequence means no detector triggers were missed between the PandA and the HDF5 file; a gap indicates lost data (e.g. from the Redis stream's `MAXLEN` trimming if the file writer fell behind — see the acquisition plan's data-path notes). Because it counts detector triggers rather than capture ticks, it verifies detector-frame completeness directly rather than raw-sample completeness. | | `PCAP.TS_TRIG.Value` (existing) | `PCAP.TS_TRIG` | Absolute/relative timestamp already captured today; usable for time-alignment against other detectors' data during future reconstruction. | -| `PCAP.GATE_DURATION.Value` (existing, aliased as `pcap_gate_duration_value`) | `PCAP.GATE_DURATION` | Existing field, retained; less meaningful once the gate is held open continuously for the whole burst window rather than pulsed per point (see setup doc, Mode B gate/arm row) — re-evaluate whether this still needs to be captured once real hardware is available. | +| `PCAP.GATE_DURATION.Value` (existing, aliased as `pcap_gate_duration_value`) | `PCAP.GATE_DURATION` | Existing field, retained; less meaningful once the gate is held open continuously for the whole burst window rather than pulsed per point (see setup doc, Mode B gate/arm row) — re-evaluate whether this still needs to be captured now that real hardware testing has started. | All rows across a burst window share the same set of fields; there is no per-point boundary marker in this raw stream other than what can be derived diff --git a/docs/developer/panda_box_free_running_setup.md b/docs/developer/panda_box_free_running_setup.md index a225228a..6775d3c8 100644 --- a/docs/developer/panda_box_free_running_setup.md +++ b/docs/developer/panda_box_free_running_setup.md @@ -1,10 +1,11 @@ # PandABox (`omny_panda`) burst acquisition: two operation modes -> **Status: design + software implemented, hardware not yet configured or -> tested.** The PandA layout changes and target sample rate below are -> placeholders (marked TODO) until real PandA hardware is available to -> configure and measure against. This document should be updated with actual -> values once that happens, per the flomni burst-acquisition redesign plan. +> **Status: software implemented, Mode B layout wired and under initial +> testing on real PandA hardware (2026-09-15, omny-panda.psi.ch).** The block +> assignments below reflect what is actually loaded on the OMNY PandA design +> as of that date. The target free-running rate is still being ramped up +> (started at 100 Hz for initial testing); update the rate row once a final +> operating rate is settled. ## Context @@ -28,7 +29,7 @@ the end) before that mode can be used. |---|---|---| | 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 a high internal sample rate (target: **TODO**, e.g. ~20 kHz) | +| 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` | Settle-based: waits for `*PCAP.CAPTURED?` to stop changing for `_settle_timeout_in_s`, since an exact expected sample count can't be predicted client-side for continuous capture | @@ -40,8 +41,10 @@ exact behavior unchanged. ## Physical wiring (Mode B only) -**TODO**: which PandA digital input connector/pin the other detectors' gate -TTL signal is patched into. Fill in once the cabling is done. +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 @@ -60,24 +63,50 @@ layout can be restored unambiguously (see `ptycho_flomni.yaml`): ### Mode B (new) — free-running raw capture -- **Gate-bit input**: the other detectors' gate signal (wired above) → - **TODO: which `PCAP.BITSx` block and bit position** — set - `CAPTURE=Value` so every captured row records the instantaneous gate - state (0/1). -- **Frame counter**: dedicate **TODO: which `COUNTERx.OUT` block** (any of - `COUNTER1`-`COUNTER8` not already in use) to free-run, incrementing once - per PCAP capture tick, with `CAPTURE=Value`. This is the - data-completeness signal (`frame_counter` in `signal_alias`, see below). -- **Raw analog/position capture**: switch **TODO: which of - `FMC_IN.VAL1/VAL2`/`INENC1`-`4.VAL`** from aggregate capture to - `CAPTURE=Value`, so the raw time series is recorded instead of (or in - addition to) the per-gate statistics. -- **Continuous gate/arm**: instead of pulsing PCAP per point, keep the gate - open for the whole burst window (armed once per point-group / scan) so - PCAP free-runs at the target sample rate — **TODO: confirm target rate** - (design target: ~20 kHz) and how the continuous gate is generated (e.g. a - PandA-internal clock/pulse-generator block feeding PCAP's gate input, - rather than the external per-point trigger used in Mode A). +- **Gate-bit input**: the detector-trigger signal, wired into `TTLIN1` (see + "Physical wiring" above). `TTLIN1.VAL` is captured into **`PCAP.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 =0 + ``` + + `PCAP.BITS0` is a shared 32-bit capture word — other captured `bit_out` + signals (several TTLIN/LVDSIN/encoder bits are also flagged for capture on + this layout) may occupy other bit positions of the same word, so the raw + `PCAP.BITS0.Value` is **not** itself a clean 0/1. `PandaBoxOMNY` masks it + down to bit 0 in `raw_stream_mode` (see `_GATE_BIT_OFFSET` in + `panda_box_omny.py`) before publishing it as `gate_detector_active` — treat + that masking as required, not optional, unless it's confirmed nothing else + shares `BITS0`. 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`, free-running off `TTLIN1.VAL` as its + `trig` input (see layout below) — i.e. it increments once per detector + trigger pulse, not once per PCAP capture tick as originally guessed here. + Captured with `CAPTURE=Value`; this is the data-completeness signal + (`frame_counter` in `signal_alias`, see below). +- **Raw analog/position capture**: `FMC_IN.VAL1`, `FMC_IN.VAL2` and all four + `INENC1`-`4.VAL` are switched from aggregate capture to `CAPTURE=Value` + on 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 constant `ONE`, drives + `PCAP`'s `gate` and `trig` inputs continuously (instead of the external + per-point trigger used in Mode A), so PCAP free-runs at `CLOCK1`'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, only `CLOCK1`'s period. + + ``` + TTLIN1 (TTL input 1, detector trigger) + ├──▶ COUNTER1.trig (Up/Down pulse counter) ──▶ COUNTER1.OUT = frame_counter (CAPTURE=Value) + └──▶ captured into PCAP.BITS0 bit 0 ──▶ masked in software = gate_detector_active + + CLOCK1 (enabled by constant ONE) ──▶ PCAP.gate, PCAP.trig (continuous arm/capture) + ``` ## Saving / restoring layouts @@ -103,20 +132,30 @@ in sync as a single source of truth: | `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 | `pcap_gate_duration_value` | -| `PCAP.BITS0.Value` (**TODO: confirm bit block**) | B | `gate_detector_active` | -| `COUNTER1.OUT.Value` (**TODO: confirm counter block**) | B | `frame_counter` | +| `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, free-running off `TTLIN1.VAL`) | 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`. -## Open question to resolve once hardware is available +## 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? If two layouts are required, -`raw_stream_mode` alone is necessary but not sufficient to switch modes, and -the operator must also load the matching PandA layout beforehand — update -this document with the answer and, if applicable, the load procedure. +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. diff --git a/tests/tests_devices/test_panda.py b/tests/tests_devices/test_panda.py index 90dd3d84..cc3d1a79 100644 --- a/tests/tests_devices/test_panda.py +++ b/tests/tests_devices/test_panda.py @@ -197,6 +197,25 @@ def test_panda_omny_raw_stream_coalesces_by_row_count(panda_omny_raw_stream): assert [v for v in out["gate_detector_active"]["value"]] == [0.0, 1.0, 0.0, 1.0, 0.0, 1.0] +def test_panda_omny_raw_stream_gate_bit_is_masked_from_shared_word(panda_omny_raw_stream): + """PCAP.BITS0 is a 32-bit word shared with other captured bit_out signals; only bit 0 + (TTLIN1.VAL, confirmed via CAPTURE_WORD?/OFFSET? against the omny-panda hardware) is the + detector-trigger gate. gate_detector_active must be masked down to that bit rather than + passed through as the raw word.""" + dev = panda_omny_raw_stream + data = np.array( + # bit 0 set (gate active) plus other bits from something else sharing BITS0 + [(0.0, 5.0), (1.0, 4.0), (2.0, 3.0)], + dtype=[("COUNTER1.OUT.Value", "