exg
Native Rust EEG/ECG/EMG preprocessing — 100% numerical parity with MNE-Python, no Python required.
exg is a pure-Rust crate for EEG signal processing. Every DSP operation is
ported from MNE-Python and verified at the coefficient
level against MNE ground truth (200+ tests, < 5 × 10⁻⁸ max error).
No Python, no BLAS, no C libraries. Pure Rust + RustFFT.
Workspace
| Crate | Description |
|---|---|
exg |
Core DSP primitives, file I/O, generic preprocessing pipeline |
exg-luna |
LUNA seizure-detection pipeline (TCP bipolar montage, 0.1–75 Hz BP, 60 Hz notch) |
exg-source |
Source localisation (eLORETA, MNE/dSPM/sLORETA, forward models, resolution metrics) |
Features
| Category | What's included |
|---|---|
| File I/O | FIF (.fif), EDF/EDF+ (.edf), CSV; HDF5 (--features hdf5); safetensors export |
| FIR Filters | Highpass, lowpass, bandpass, notch — all via MNE's _firwin_design with per-transition sub-filter lengths |
| DSP | FFT polyphase resampling, overlap-add zero-phase convolution, average reference |
| Normalisation | Global z-score (ddof=0), channel-wise z-score, per-epoch baseline correction |
| Montages | TCP bipolar (22-ch TUH), Siena unipolar (29-ch), SEED-V unipolar (62-ch), custom |
| Pipelines | Generic preprocess() in exg; LUNA-specific preprocess_luna() in exg-luna |
| Export | Safetensors batch writer; LUNA-compatible epoch export (via exg-luna) |
| Source localisation | eLORETA, MNE/dSPM/sLORETA, forward models, resolution metrics (via exg-source) |
Quick start
Generic pipeline (FIF input)
use ;
use Array2;
let raw = open_raw?;
let data = raw.read_all_data?; // [C, T] f64
let pos = zeros;
let cfg = default; // 256 Hz · 0.5 Hz HP · 5 s epochs
let epochs = preprocess?;
// → Vec<([C, 1280], [C, 3])>
LUNA seizure-detection pipeline (EDF input)
Add both crates to your Cargo.toml:
[]
= "0.0.3"
= "0.0.3"
use open_raw_edf;
use ;
let raw = open_raw_edf?;
let data = raw.read_all_data?; // [C, T] f32
let names = raw.channel_names;
let cfg = default; // 0.1–75 Hz BP · 60 Hz notch · TCP bipolar
let epochs = preprocess_luna?;
// → Vec<([22, 1280], channel_names)>
Individual DSP steps
use ;
use zscore_channelwise_inplace;
use ;
use Array2;
let mut data: = /* ... */;
// Bandpass 0.1–75 Hz + notch 60 Hz
let h = design_bandpass;
apply_fir_zero_phase?;
let h = design_notch;
apply_fir_zero_phase?;
// Bipolar montage
let = make_bipolar;
// Channel-wise z-score
zscore_channelwise_inplace;
Export for LUNA inference
use ;
let epoch = LunaEpoch ;
export_luna_epochs?;
Pipelines
Generic pipeline (exg::preprocess)
.fif / .edf / .csv
│
├─ open_raw() / open_raw_edf() native file reader
├─ resample() FFT polyphase → 256 Hz
├─ highpass FIR firwin + overlap-add → 0.5 Hz
├─ average reference per-timepoint channel mean removed
├─ global z-score (data − μ) / σ (ddof=0)
├─ epoch non-overlapping 5 s windows
├─ baseline correct per-epoch per-channel mean removed
└─ ÷ data_norm ÷ 10 → std ≈ 0.1
LUNA pipeline (exg_luna::preprocess_luna)
.edf (TUH corpus)
│
├─ channel rename strip "EEG ", "-REF", "-LE"
├─ pick 10-20 channels 21 standard electrodes
├─ bandpass FIR 0.1–75 Hz (MNE _firwin_design)
├─ notch FIR 60 Hz (configurable 50 Hz)
├─ resample → 256 Hz
├─ TCP bipolar montage 22 channels from 21 electrodes
└─ epoch non-overlapping 5 s windows
│
└─→ Vec<([22, 1280], channel_names)>
Numerical parity with MNE-Python
All filter coefficients are verified at the individual-coefficient level against MNE-Python 1.11.0.
| Operation | Algorithm match | Max error |
|---|---|---|
| FIR design (HP/LP/BP/Notch) | ✅ _firwin_design exact |
< 5 × 10⁻⁸ (f32 output) |
| Filter application | ✅ _overlap_add_filter |
< 4 × 10⁻⁶ |
| Resampling | ✅ _fft_resample |
< 5 × 10⁻⁴ |
| Average reference | ✅ bit-exact | 0 |
| Z-score (global / channel-wise) | ✅ ddof=0 | < 1 × 10⁻⁶ |
| Baseline correction | ✅ bit-exact | 0 |
| EDF read | ✅ digital→physical + unit scaling | < 1 × 10⁻³ (16-bit quantisation) |
Internally, filter design runs in f64 (matching numpy/scipy). The final coefficients are returned as f32 for the signal path, introducing the sole precision gap of ~5 × 10⁻⁸.
Crate API
Preprocessing (exg)
// Full pipeline
// Filter design (100% MNE _firwin_design parity)
// DSP
// Montages
// Safetensors export
LUNA pipeline (exg-luna)
pub const STANDARD_10_20: & // 21 electrodes
// Safetensors I/O (luna-rs InputBatch compatible)
Source localisation (exg-source)
Enabled by the default source feature on exg. Disable with default-features = false.
Feature flags
| Flag | Default | What it enables |
|---|---|---|
source |
✅ | Source localisation (eLORETA/MNE/dSPM/sLORETA) via exg-source |
hdf5 |
❌ | HDF5 dataset reader (requires libhdf5 system library) |
# Just preprocessing, no source localisation
= { = "0.0.3", = false }
# Everything including HDF5
= { = "0.0.3", = ["hdf5"] }
Running
Benchmarks
| Step | MNE (ms) | Rust (ms) | Speedup |
|---|---|---|---|
| Read FIF | 1.83 | 0.63 | 2.9× |
| Resample | 0.03 | 0.02 | 1.4× |
| HP filter | 5.48 | 3.68 | 1.5× |
| Avg reference | 0.49 | 0.03 | 16.6× |
| Z-score | 0.29 | 0.09 | 3.3× |
| Epoch | 1.98 | 0.06 | 33.3× |
| Total | 10.11 | 4.51 | 2.2× |
Project layout
exg/
├── Cargo.toml workspace root; features: source, hdf5
├── src/
│ ├── lib.rs preprocess() + re-exports
│ ├── config.rs PipelineConfig
│ ├── edf/mod.rs EDF/EDF+ reader (header, data, annotations)
│ ├── csv.rs CSV reader (auto-detect delimiter/timestamps)
│ ├── hdf5.rs HDF5 dataset reader (feature-gated)
│ ├── montage.rs TCP bipolar, Siena, SEED-V montages
│ ├── resample.rs FFT polyphase resampler
│ ├── filter/
│ │ ├── design.rs _firwin_design: HP, LP, BP, notch (MNE parity)
│ │ └── apply.rs overlap-add zero-phase FIR
│ ├── reference.rs average reference
│ ├── normalize.rs global z-score, channel-wise z-score, baseline
│ ├── epoch.rs fixed-length epoching
│ ├── io.rs safetensors I/O, batch writer
│ └── fiff/ FIFF file format reader
│ ├── constants.rs FIFF constants
│ ├── tag.rs tag header I/O
│ ├── tree.rs block tree + directory reader
│ ├── info.rs MeasInfo + ChannelInfo
│ └── raw.rs open_raw / read_all_data / read_slice
├── exg-luna/ LUNA seizure-detection pipeline
│ ├── src/
│ │ ├── lib.rs re-exports
│ │ ├── pipeline.rs preprocess_luna + LunaPipelineConfig
│ │ └── io.rs LunaEpoch export/load (safetensors)
│ └── README.md
├── exg-source/ source localisation crate
│ ├── src/
│ │ ├── lib.rs re-exports
│ │ ├── forward.rs forward models (sphere)
│ │ ├── inverse.rs MNE / dSPM / sLORETA
│ │ ├── eloreta.rs eLORETA
│ │ ├── covariance.rs noise covariance estimation
│ │ ├── resolution.rs resolution metrics (PSF, CTF)
│ │ ├── snr.rs SNR estimation
│ │ ├── source_space.rs ico / grid source spaces
│ │ └── linalg.rs SVD / regularisation helpers
│ └── README.md
├── tests/ integration tests
├── benches/ Criterion benchmarks
└── scripts/ Python ground-truth generators