Skip to content

Add sampling-delay alignment transformer (+ docs rework) - #31

Merged
cboulay merged 5 commits into
mainfrom
cboulay/delay_alignment
May 30, 2026
Merged

Add sampling-delay alignment transformer (+ docs rework)#31
cboulay merged 5 commits into
mainfrom
cboulay/delay_alignment

Conversation

@cboulay

@cboulay cboulay commented May 30, 2026

Copy link
Copy Markdown
Member

Summary

This branch adds a new SamplingDelayAlignment transformer that corrects the per-channel timing skew introduced by the front-end's sequential A/D converter, and reworks the package documentation to the README-include pattern used across the other ezmsg-namespaced packages.

The front-end samples channels in banks of bank_size (32), one every channel_sample_interval (~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 (CAR, 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, where CAR's common-mode rejection collapses. The transformer removes the skew by delaying each channel back onto a common time grid with a per-slot windowed-sinc fractional-delay FIR filter.

What's included

  • New module src/ezmsg/blackrock/sampling_delay_alignment.py exposing SamplingDelayAlignment (Unit), SamplingDelayAlignmentTransformer, SamplingDelayAlignmentSettings, and SamplingDelayAlignmentState, wired into the package's public exports in __init__.py.
  • Docs rework: docs/source/index.rst is replaced by docs/source/index.md, which simply includes README.md and the toctree — matching the pattern in the sibling ezmsg packages (template, neo, event, tools). All content previously duplicated in index.rst (setup notes, network/IP config, troubleshooting) already lives in the README in more detail.
  • New prose guide docs/source/guides/processing.md documenting the three processing transformers — channel mapping, CerePlex impedance, and sampling-delay alignment — with MyST cross-references into the auto-generated API reference.
  • .gitignore now ignores the generated API docs (docs/source/api/generated).
  • Tests in tests/test_sampling_delay_alignment.py.

Design notes

  • Windowed-sinc, not 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. There are only bank_size distinct delays, so only that many distinct filters are designed.
  • Metadata-driven slot selection. The within-bank A/D slot defaults to acquisition order (c % bank_size), but when the ch axis carries structured bank/elec metadata (e.g. attached upstream by ChannelMapUnit) the slot is taken from elec - 1 instead, so each channel's delay stays correct even when channels are reordered relative to hardware acquisition. The slot layout is folded into the reset hash, so a metadata change (e.g. a newly-pushed channel map) re-designs the filters even when shape/key/gain are unchanged.
  • Passthrough mode. Setting filter_len = 0 disables alignment entirely: _process returns the input unchanged and _reset_state skips filter design (the FIR is undefined for zero taps). Useful for A/B comparisons or leaving the unit wired in but inert.
  • Rail handling. With rail_threshold set, clipped samples are held at the last valid value before filtering, so the fractional-delay FIR doesn't ring the corruption across its support. rail_threshold is listed in NONRESET_SETTINGS_FIELDS, since changing it gates only the forward-fill in _process and does not require rebuilding the (relatively expensive) filter state.
  • Latency. The causal FIR adds a common bulk delay of (filter_len-1)//2 samples; the output time-axis offset is shifted so timestamps stay physically correct.
  • Backend portability. The module is Array-API compatible: the sinc taps are designed in numpy and moved to the working backend, and everything else (the FIR tap-sum, concat/state handling, and the rail forward-fill) runs on the backend using only standard Array-API ops, so it works on numpy, MLX, torch, jax, and cupy.

Scope assumptions

The transformer always operates on the "time" axis of a (time, ch) stream, since it only ever sits downstream of a CereLink source and (optionally) a channel-map transformer, both of which assume a "ch" axis. There is no configurable axis setting.

Testing

tests/test_sampling_delay_alignment.py pins the behavior: chunk-invariant streaming (any chunking reproduces the whole-buffer result via carried FIR history), high-frequency common-mode collapse after alignment (CAR residual drops by orders of magnitude where un-aligned CAR fails), rail handling bounds the output, the output offset accounts for the bulk delay, shape/dtype passthrough, cross-backend parity (mlx/torch matching numpy), metadata-driven slot selection overriding acquisition order, filter_len = 0 passthrough, and rail_threshold being applied without a state reset.

All tests pass locally (the mlx backend test skips when mlx is not installed).

@cboulay
cboulay merged commit 26f5971 into main May 30, 2026
16 checks passed
@cboulay
cboulay deleted the cboulay/delay_alignment branch May 30, 2026 04:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant