concord-metrics-utils
Shared signal processing primitives used by all metric packages
Package Structure
concord-metrics-utils/
src/concord_metrics_utils/
__init__.py exports segment, window_times, bandpass, notch,
harmonic_regression_notch
windows.py sliding window segmentation
filters.py bandpass, notch, and advanced powerline removal
windows.py
segment
Divides a signal array into overlapping windows using
numpy.lib.stride_tricks.sliding_window_view.
No data is copied during slicing (stride tricks share the underlying buffer);
the result is then made contiguous with .copy() so downstream code can write to it safely.
| Parameter | Type | Description |
|---|---|---|
| data | np.ndarray | Input array. Can be any shape (..., n_samples). The last axis is segmented. |
| fs | float | Sampling rate in Hz. Used to convert window/step from seconds to samples. |
| window_s | float | Window length in seconds. |
| step_s | float | None | Step between window starts in seconds. If None, defaults to window_s (non-overlapping). |
Returns: Array of shape (..., n_windows, window_samples). The last axis of the input is replaced by two axes: windows and samples-per-window.
Example: Input (72, 307200) at 1024 Hz with 1-second windows → output (72, 300, 1024).
window_times
Returns the center time (in seconds) of each window produced by segment().
Use this to build the time_axis field of a MetricResult.
Shape: (n_windows,). Center time = start of window + window_length / 2.
filters.py
bandpass
Zero-phase Butterworth bandpass filter applied along the last axis.
Uses scipy.signal.butter to design the filter (second-order sections form),
then sosfiltfilt for forward-backward application — this cancels phase distortion,
so time events in the output align correctly with the input.
| Parameter | Description |
|---|---|
| data | Array of any shape. Filter is applied along last axis. |
| fs | Sampling rate in Hz. |
| l_freq | Lower cutoff frequency in Hz. |
| h_freq | Upper cutoff frequency in Hz. |
| order | Filter order. Higher = sharper rolloff but more ringing. Default 4. |
notch
Zero-phase narrow notch filter using scipy.signal.iirnotch.
Removes a single frequency (and a narrow band around it) from the signal.
Applied forward-backward to maintain zero phase shift.
The quality factor controls bandwidth: Q = f₀/bandwidth.
Q=30 at 60 Hz gives a notch width of 2 Hz.
Called twice per notch setting (e.g. 60 Hz and 120 Hz) by the server's _rebuild_recording().
harmonic_regression_notch
An advanced powerline removal method that handles non-stationary line frequency. Where a simple IIR notch assumes the powerline is exactly at the specified frequency, this method estimates the actual fundamental frequency (f₀) from the data itself, then tracks its harmonics across time.
Algorithm
- Estimate f₀: Compute FFT of the full recording. Find the spectral peak near
freq_hzusing parabolic interpolation on the FFT magnitude for sub-bin precision. This gives the actual powerline frequency (e.g. 59.97 Hz instead of 60.0 Hz). - Windowed regression: Slide overlapping 2-second Hann-weighted windows over the signal. In each window, fit a least-squares sinusoidal model: sum of sin/cos pairs at f₀, 2f₀, 3f₀ (the harmonics). This fit gives the powerline component at that time.
- Overlap-add synthesis: Subtract the fitted harmonic components from the signal using overlap-add, so the subtraction is smooth across window boundaries.
Advantage over simple notch: Works even when powerline frequency drifts (e.g. in low-quality hospital power), and avoids the ringing artifact of narrow IIR notch filters near seizure onset.
Why These Are Separate from Core
The signal processing primitives in this package have no knowledge of the Metric ABC
or Recording containers. They operate on plain numpy arrays.
This separation means:
- The server can use
notch()directly without pulling in any metric logic - You can use
segment()on any array — audio, simulation output, etc. — not just EEG - Each function is independently testable with simple numpy inputs