Processing modules#
Beyond acquisition, ezmsg-blackrock ships a few stateful transformers that
operate on the AxisArray stream coming off the device. Each is available both
as an ezmsg Unit (for use in a graph) and as a plain transformer/processor
(for use in a script or test). Full signatures and settings live in the
API reference; this page covers what each does and when to
reach for it.
Channel mapping#
ChannelMapUnit attaches Blackrock .cmp channel-map
metadata to the ch axis of the stream. The incoming ch axis is replaced with
a structured CoordinateAxis carrying, per channel, the fields x, y,
label, bank, elec, and device — so downstream components (plots, spatial
filters, electrode-geometry-aware steps) can address channels by physical
position and label rather than by raw index.
The unit takes the complete set of per-headstage overlays in a single settings
object: ChannelMapUnitSettings holds a tuple of
ChannelMapSettings, one per headstage. On each reset it
rebuilds the ch axis from scratch in three phases:
Base layer — labels are taken from the incoming
chaxis (falling back toch1,ch2, … when none are present), and pass-through fields such asdeviceare carried forward.CMP overlays — for each configured
.cmpfile, the parsed entries are written at their channel index (chan_id - 1, withchan_idoffset bystart_chan), filling in position, bank, electrode, and label. Whenhs_idis nonzero, labels are prefixedhs{hs_id}-. A mask records which channels a CMP claimed.Auto-grid fill — any channel no CMP claimed is laid out on a generated grid, positioned below and to the right of the CMP geometry so the synthetic coordinates never collide with mapped electrodes.
Pushing an empty cmp_configs tuple clears the map and yields a pure auto-grid.
The same ChannelMapSettings record doubles as the
per-headstage entry in CereLinkSignalSettings.cmp_configs.
CerePlex impedance measurement#
CerePlexImpedance extracts per-channel electrode
impedance from a CerePlex headstage’s built-in impedance sweep. During the sweep
the headstage injects a 1 kHz, 1 nA sine wave for ~100 ms into one channel at a
time, cycling sequentially through the array; every channel not under test reads
exactly zero. The processor watches that exclusivity to lock onto the active
channel, buffers its burst, and recovers the impedance from a single-bin DFT at
1 kHz — impedance in kOhm is the peak-to-peak voltage (µV) divided by the
peak-to-peak test current (nA): Z = V_pp / I_pp.
Each impedance update emits an AxisArray whose data is a (1, n_ch) row of
values in kOhm, with NaN for channels not yet measured. Multiple headstages
are tracked independently via
headstage_channel_offsets,
each free to be at a different point in its own sweep.
Two requirements matter for correct results:
The input must be in microvolts. Passing raw ADC counts scales every impedance by the ADC factor. When the data comes from
CereLinkSignalSource, setmicrovolts=True.Device-side filtering must be disabled. A filter leaves small non-zero residuals on idle channels, which defeats the exact-zero exclusivity checks the tracker relies on.
Sampling-delay alignment#
SamplingDelayAlignment corrects the per-channel timing
skew introduced by the front-end’s sequential analog-to-digital converter. The
CerePlex headstages sample channels in banks of 32, one every ~969.7 ns, so within
a bank channel c’s sample is the signal delayed by c × channel_sample_interval
relative to the bank start. For any cross-channel operation — common-average
referencing, whitening, beamforming — this skew smears the common mode at high
frequency: negligible at 60 Hz (~0.65°) but ~81° across a bank near 7.5 kHz, so
CAR’s common-mode rejection collapses toward Nyquist.
The transformer removes the skew by delaying each channel back onto a common
time grid (the bank start) with a per-slot windowed-sinc fractional-delay FIR
filter. There are only bank_size distinct delays, so only that many distinct
filters. A windowed sinc is used deliberately instead of linear interpolation:
linear interpolation is a delay-dependent low-pass that would impose a
different high-frequency rolloff per channel — coloring the band exactly where
the misalignment mattered.
The per-channel delay defaults to acquisition order (slot c % bank_size). If
the ch axis carries bank/elec metadata — for example after
ChannelMapUnit has run — the slot is taken from elec
instead, so each channel’s delay stays correct even when channels have been
reordered relative to hardware acquisition.
Set filter_len to 0 to disable alignment entirely — the transformer becomes
a pass-through that returns its input unchanged, handy for A/B comparisons or for
leaving the unit wired in but inert.
A few things to keep in mind:
Latency. The causal FIR adds a common bulk delay of
(filter_len-1)//2samples (the per-channel fractional delays ride on top). The output time-axis offset is shifted so timestamps stay physically correct.It resamples the raw data. Downstream sees interpolated samples — fine for cross-channel cleaning, but be deliberate if a later step needs raw waveforms.
Railing. Clipped samples are corrupt and a fractional-delay filter would spread that corruption across its support. Setting
rail_thresholdholds railed samples at the last valid value before filtering as a basic mitigation.Backend portability. The module is Array-API compatible: it detects the input’s namespace and runs on numpy, MLX, torch, jax, cupy, and friends.