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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco
This commit is contained in:
1 parent
82ecab4c21
commit
3012cd3154
5 files changed
+129
-46
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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", "<f8"), ("PCAP.BITS0.Value", "<f8")],
|
||||
)
|
||||
with mock.patch.object(dev.data, "put") as mock_put:
|
||||
dev._receive_frame_data(FrameData(data))
|
||||
dev._flush_raw_stream_buffer()
|
||||
|
||||
out = mock_put.call_args.args[0]
|
||||
assert [v for v in out["gate_detector_active"]["value"]] == [1, 0, 1]
|
||||
|
||||
|
||||
def test_panda_omny_raw_stream_flush_on_demand(panda_omny_raw_stream):
|
||||
"""Buffered data below the threshold must still be flushed on demand (as on_complete/
|
||||
on_unstage do), and flushing an empty buffer must not publish again."""
|
||||
|
||||
Reference in new issue
Block a user