Core Concepts¶
Event-Based Data¶
At a Free-Electron Laser facility (or any pulsed-source experiment), instruments record one measurement per X-ray pulse. Each pulse is identified by a unique integer pulse ID (sometimes called event ID or index). Across many diagnostic instruments the pulse IDs are the same physical quantity but the instruments may miss individual shots: a camera may deliver data for pulses 1, 2, 4, 5 while a fast digitiser delivers 1, 2, 3, 4, 5.
escape represents each measured channel as an Array — a pair
of aligned arrays:
data— the measured values, shape(N, ...).index— the corresponding pulse IDs, shape(N,).
Because index alignment is built in, arithmetic between two Arrays automatically finds the common pulse IDs and only operates on those events.
import numpy as np
import escape
a = escape.Array(data=np.array([8, 4, 3, 7]), index=np.array([0, 1, 3, 4]))
b = escape.Array(data=np.array([2, 2, 2, 2]), index=np.array([0, 1, 2, 4]))
c = a - b
print(c.index) # [0 1 4] — only the common pulse IDs
print(c.data) # [6 2 5]
Scan Steps¶
An experiment typically consists of many scan steps — for example, a delay scan where the pump-probe delay is varied between steps. Within each step, hundreds or thousands of pulses are recorded.
escape stores scan metadata inside each Array via a Scan
object (accessible as array.scan). The scan partitions the event stream into
sequential groups and carries the parameter value for each step.
Array
├── data shape (N_total, ...) ← all events stacked in step order
├── index shape (N_total,) ← unique integer ID per event
└── scan
├── step_lengths [n0, n1, n2, …] ← events per step
└── parameter {"delay_s": {"values": [t0, t1, t2, …]}}
Individual steps are accessed as ordinary Arrays via scan[i], and per-step
statistics are available directly on the scan object:
means = array.scan.nanmean(axis=0) # list, one entry per step
Lazy Evaluation with Dask¶
Raw FEL data is often too large to hold in memory. escape uses
dask arrays as its data backend. All arithmetic and
reduction operations build a lazy graph — no computation is triggered until you
explicitly call compute() or
store().
result = (sig / i0).nanmean(axis=0) # still lazy
result = result.compute() # trigger computation
For dask-backed Arrays, map_index_blocks() lets you apply
an arbitrary NumPy function chunk-by-chunk, keeping computation distributed.
Index Ordering Convention¶
When two Arrays are combined with an operator, the first argument determines the output index order and step grouping. The second argument is re-indexed to match. This means:
result_ab = a + b # order from a
result_ba = b + a # order from b
# result_ab.data may differ from result_ba.data in element order!
This convention is consistent across all binary operators, escaped()-
decorated functions, and categorize().