Note
Go to the end to download the full example code.
Understanding the adaptive extension of multiband GEDAI🔗
This tutorial demonstrates how to use Adaptive Multiband GEDAI.
The method tackles the limitations of the standard multiband GEDAI
by automatically determining the optimal epoch duration for each
band (i.e., wavelet level) based on the frequency content of the band.
By doing so, it allows to capture both transient and sustained artifacts
across different frequency ranges.
Note
The purpose of this tutorial is to explain the different parameters of
the AdaptiveMultibandGedai model and help you
better understand the underlying algorithm. If you want to learn how to
use Adaptive Multiband GEDAI in a practical, end-to-end offline
denoising workflow, please refer to the
Practical Pipelines section.
from mne.io import read_raw
from gedai import AdaptiveMultibandGedai
from gedai.data import get_contaminated_eeg_set_path
from gedai.metrics import compute_enova_per_epoch, enova_summary
from gedai.viz import plot_mne_style_overlay_interactive
n_jobs = 1
Filtering raw data in 1 contiguous segment
Setting up high-pass filter at 0.5 Hz
FIR filter parameters
---------------------
Designing a one-pass, zero-phase, non-causal highpass filter:
- Windowed time-domain design (firwin) method
- Hamming window with 0.0194 passband ripple and 53 dB stopband attenuation
- Lower passband edge: 0.50
- Lower transition bandwidth: 0.50 Hz (-6 dB cutoff frequency: 0.25 Hz)
- Filter length: 1321 samples (6.605 s)
For simplicity, we will only use the first 30 seconds of the data in this tutorial. In practice, it is recommended to use the full recording for fitting the GEDAI model, as this allows the model to better capture the noise characteristics of the data.
raw.crop(0, 30)
GEDAI Adaptive Multiband model🔗
The AdaptiveMultibandGedai model uses wavelet decomposition to
separate the EEG data into different frequency bands and applies GEDAI
separately to each band.
The wavelet decomposition is controlled by:
wavelet_type: The wavelet family (default:"haar").wavelet_level: Number of decomposition levels (default:"auto").broadband_pass: Whether to run an initial broadband GEDAI pass (default:True).
wavelet_type = "haar"
The wavelet level (wavelet_level) controls the number of frequency
bands that the data is decomposed into.
When set to "auto" (default), the optimal level is computed automatically
based on the sampling frequency and the high-pass cutoff.
For example, for a sampling frequency of 200 Hz, 9 wavelet levels
provide wavelet bands covering classical EEG frequency bands:
(0.00 - 0.20 Hz)
(0.20 - 0.39 Hz)
(0.39 - 0.78 Hz)
(0.78 - 1.56 Hz)
(1.56 - 3.12 Hz) Delta
(3.12 - 6.25 Hz) Theta
(6.25 - 12.5 Hz) Alpha
(12.5 - 25 Hz) Beta
(25 - 50 Hz) Gamma
(50 - 100 Hz) High Gamma
(100 - 200 Hz)
wavelet_level = "auto"
For each wavelet level, AdaptiveMultibandGedai automatically
determines the optimal epoch duration. Slower frequency bands are
estimated on longer epochs, while faster frequency bands are estimated on
shorter epochs.
This adaptive approach allows both transient and sustained artifacts to be
captured across different frequency ranges.
The cycles_per_wavelet parameter controls the number of cycles of the
wavelet included in each epoch (default: 10).
cycles_per_wavelet = 10
With these parameters defined, we instantiate the model.
Enabling broadband_pass=True (default) runs an initial broadband GEDAI
pass to eliminate gross artifacts before wavelet decomposition.
adaptive_multiband_gedai = AdaptiveMultibandGedai(
wavelet_type=wavelet_type,
wavelet_level=wavelet_level,
cycles_per_wavelet=cycles_per_wavelet,
broadband_pass=True,
)
Model Fitting🔗
The fitting process of AdaptiveMultibandGedai is performed separately for
each wavelet level using continuous SENSAI optimization
(sensai_method="optimize").
The wavelet_low_cutoff parameter controls which low-frequency bands are
ignored (e.g. below high-pass filtering). Setting wavelet_low_cutoff="auto"
uses raw.info['highpass'] to automatically exclude sub-cutoff levels.
wavelet_low_cutoff = "auto"
noise_multiplier = 3.0
Fit the model directly on raw data:
343 x 343 full covariance (kind = 1) found.
[adaptive.fit_raw] INFO: Applying wavelet HP pre-filter (sub-0.50 Hz) and running broadband GEDAI pass...
The different wavelet parameters are stored in the
adaptive_multiband_gedai._wavelets_fits attribute. The ignore key
indicates which wavelet levels were ignored based on the
wavelet_low_cutoff setting. The duration key indicates the epoch
duration used to estimate the GEDAI model of the corresponding wavelet level.
[{'band_index': 0, 'fmin': 50.0, 'fmax': 100.0, 'model': <Gedai (fitted)>, 'duration': 0.2, 'n_samples': 40, 'ignore': False, 'sensai_bounds': (0.0, 12.0), 'sensai': 36.73285285032292, 'enova': 0.0, 'band_data': array([[-1.09572043e-06, 3.25221944e-07, 7.94420418e-07, ...,
-1.08130904e-07, 1.33772564e-07, 1.31297257e-07],
[ 5.44052286e-08, 1.51500700e-07, 1.61772204e-07, ...,
-3.45927627e-09, -1.97135864e-08, -2.63958402e-07],
[-2.58964183e-07, 1.81415924e-07, 2.70810295e-07, ...,
4.08327042e-08, 5.87490579e-08, -1.29650135e-07],
...,
[-1.89092396e-07, 8.26537742e-08, -8.31172748e-08, ...,
2.17560704e-08, 1.46501967e-07, 9.54601432e-08],
[ 2.24027555e-07, 4.36244061e-09, -3.94490448e-07, ...,
1.49829214e-07, 2.36462004e-07, -4.46289196e-08],
[-1.06350226e-06, 3.29180060e-07, 7.85320112e-07, ...,
1.57343345e-07, 3.59517962e-07, -1.46093997e-07]],
shape=(27, 6001))}, {'band_index': 1, 'fmin': 25.0, 'fmax': 50.0, 'model': <Gedai (fitted)>, 'duration': 0.4, 'n_samples': 80, 'ignore': False, 'sensai_bounds': (-6.0, 12.0), 'sensai': 56.598527836957274, 'enova': 0.0, 'band_data': array([[-9.55013198e-07, 2.24978806e-07, 1.15518649e-06, ...,
-1.81518522e-07, -9.65692717e-08, -7.10729220e-07],
[ 4.66479464e-09, 3.54928275e-07, 2.92804300e-07, ...,
6.87223182e-09, -2.60857636e-07, -3.24235606e-07],
[-2.54290647e-07, 2.45693579e-07, 4.73616269e-07, ...,
7.95575469e-08, -6.57730046e-08, -3.39128170e-07],
...,
[-8.96785035e-08, -1.60196514e-07, -2.05242474e-07, ...,
1.98064724e-07, 2.86518463e-07, 1.26702247e-07],
[ 2.56267742e-07, -2.37382898e-07, -7.30590086e-07, ...,
2.27938597e-07, 4.74428831e-07, 4.32094093e-07],
[-1.12595781e-06, 1.76145484e-07, 1.09276276e-06, ...,
2.38893927e-07, 2.15225299e-07, -8.01494442e-07]],
shape=(27, 6001))}, {'band_index': 2, 'fmin': 12.5, 'fmax': 25.0, 'model': <Gedai (fitted)>, 'duration': 0.8, 'n_samples': 160, 'ignore': False, 'sensai_bounds': (-6.0, 12.0), 'sensai': 62.195880622833776, 'enova': 0.0, 'band_data': array([[-4.88917881e-07, 1.78425394e-07, 7.88535251e-07, ...,
-1.01665255e-06, -1.10520905e-06, -9.16219899e-07],
[ 5.33565176e-08, 2.32050847e-07, 3.48388404e-07, ...,
-3.27451522e-07, -2.61262698e-07, -1.35927224e-07],
[ 1.86546430e-08, 2.27687179e-07, 3.55138863e-07, ...,
-2.52902109e-07, -2.45062007e-07, -1.53911834e-07],
...,
[ 9.61135841e-09, -1.07340677e-07, -1.35675352e-07, ...,
1.52672984e-07, 2.49125822e-07, 1.74994833e-07],
[ 1.04177535e-07, -2.78488288e-07, -5.24375299e-07, ...,
3.22183128e-07, 4.97225595e-07, 4.08800279e-07],
[-5.48074194e-07, -7.31082116e-08, 4.51163588e-07, ...,
-7.09345629e-07, -8.14255260e-07, -7.77807530e-07]],
shape=(27, 6001))}, {'band_index': 3, 'fmin': 6.25, 'fmax': 12.5, 'model': <Gedai (fitted)>, 'duration': 1.6, 'n_samples': 320, 'ignore': False, 'sensai_bounds': (-6.0, 12.0), 'sensai': 64.01591213118714, 'enova': 0.0, 'band_data': array([[-3.65673675e-07, 2.09843335e-08, 3.68469187e-07, ...,
-1.18112977e-06, -9.98847985e-07, -7.12978877e-07],
[ 5.27859314e-08, 1.95237015e-07, 3.00634476e-07, ...,
-3.00746973e-07, -2.17461863e-07, -9.72355237e-08],
[ 2.10559521e-08, 1.36036903e-07, 2.32981286e-07, ...,
-2.58148102e-07, -1.79523464e-07, -8.63189683e-08],
...,
[-9.18536616e-08, -6.31199288e-08, -4.59767194e-08, ...,
-2.00199700e-07, -1.64668845e-07, -1.24352386e-07],
[-1.21814213e-07, -2.08857859e-07, -2.80219325e-07, ...,
3.44554731e-08, 3.84770955e-09, -4.80323467e-08],
[-4.00755926e-07, -6.27172383e-08, 2.36199292e-07, ...,
-1.05078805e-06, -9.24519527e-07, -6.99914216e-07]],
shape=(27, 6001))}, {'band_index': 4, 'fmin': 3.125, 'fmax': 6.25, 'model': <Gedai (fitted)>, 'duration': 3.2, 'n_samples': 640, 'ignore': False, 'sensai_bounds': (-6.0, 12.0), 'sensai': 67.47566478439519, 'enova': 0.0, 'band_data': array([[-2.27281571e-07, -2.78429532e-08, 1.71831920e-07, ...,
-6.72822968e-07, -5.47006702e-07, -3.99909639e-07],
[ 1.11589829e-07, 1.82116714e-07, 2.48325334e-07, ...,
-7.73157013e-08, -1.99824445e-08, 4.25976760e-08],
[ 4.21814715e-08, 1.00910287e-07, 1.60714653e-07, ...,
-8.94385947e-08, -5.01245231e-08, -7.72064584e-09],
...,
[ 7.27474847e-09, 3.55592779e-08, 6.89022290e-08, ...,
-3.36806576e-08, -2.48136758e-08, -1.20957803e-08],
[-2.33032090e-07, -2.69063374e-07, -2.99026925e-07, ...,
-1.34918750e-07, -1.62928063e-07, -1.97077384e-07],
[-2.27526024e-07, -2.64666081e-08, 1.71203219e-07, ...,
-6.67292935e-07, -5.43686294e-07, -4.01056161e-07]],
shape=(27, 6001))}, {'band_index': 5, 'fmin': 1.5625, 'fmax': 3.125, 'model': <Gedai (fitted)>, 'duration': 6.4, 'n_samples': 1280, 'ignore': False, 'sensai_bounds': (-6.0, 12.0), 'sensai': 60.395374864838296, 'enova': 0.0, 'band_data': array([[ 6.07220808e-08, 1.85195773e-07, 3.06859513e-07, ...,
-2.93927451e-07, -1.79726731e-07, -6.09025588e-08],
[ 3.98213290e-08, 8.22719996e-08, 1.22518401e-07, ...,
-8.77946584e-08, -4.59165380e-08, -3.33530857e-09],
[ 1.42450961e-07, 1.69634955e-07, 1.93384285e-07, ...,
5.08471213e-08, 8.30123076e-08, 1.13538865e-07],
...,
[ 1.40487242e-07, 1.57256190e-07, 1.74254993e-07, ...,
9.45297181e-08, 1.09613352e-07, 1.24711493e-07],
[-2.93616208e-07, -2.96092000e-07, -2.96426488e-07, ...,
-2.77826784e-07, -2.84505733e-07, -2.89896179e-07],
[ 6.22075123e-08, 1.76762236e-07, 2.90328491e-07, ...,
-2.58639707e-07, -1.54838400e-07, -4.80852410e-08]],
shape=(27, 6001))}, {'band_index': 6, 'fmin': 0.78125, 'fmax': 1.5625, 'model': <Gedai (fitted)>, 'duration': 12.8, 'n_samples': 2560, 'ignore': False, 'sensai_bounds': (-6.0, 12.0), 'sensai': 57.86511822646126, 'enova': 0.0, 'band_data': array([[-1.96520693e-07, -1.36975758e-07, -7.77838582e-08, ...,
-3.68255439e-07, -3.12085391e-07, -2.54867074e-07],
[-2.17696030e-07, -2.01158624e-07, -1.85602979e-07, ...,
-2.68994673e-07, -2.52007823e-07, -2.34824327e-07],
[-1.65487524e-07, -1.52411942e-07, -1.40070248e-07, ...,
-2.06461256e-07, -1.92634267e-07, -1.78916765e-07],
...,
[-1.08681267e-07, -9.79942609e-08, -8.82612626e-08, ...,
-1.42023866e-07, -1.30958335e-07, -1.19826051e-07],
[-2.56066603e-07, -2.64130593e-07, -2.71679795e-07, ...,
-2.29171889e-07, -2.38727990e-07, -2.47744196e-07],
[-6.86211801e-08, -1.94514467e-08, 2.92573781e-08, ...,
-2.09086633e-07, -1.63343786e-07, -1.16684357e-07]],
shape=(27, 6001))}, {'band_index': 7, 'fmin': 0.390625, 'fmax': 0.78125, 'model': <Gedai (fitted)>, 'duration': 25.6, 'n_samples': 5120, 'ignore': False, 'sensai_bounds': (-6.0, 12.0), 'sensai': 51.40474118991697, 'enova': 0.0, 'band_data': array([[ 1.34370320e-07, 1.60545141e-07, 1.86768948e-07, ...,
5.76226181e-08, 8.29496504e-08, 1.08545044e-07],
[-4.69336430e-07, -4.56480465e-07, -4.43486743e-07, ...,
-5.06685079e-07, -4.94455251e-07, -4.82007714e-07],
[-1.82998380e-07, -1.73747366e-07, -1.64378725e-07, ...,
-2.09809632e-07, -2.00995531e-07, -1.92064672e-07],
...,
[ 8.15515909e-10, 6.18269204e-09, 1.16238855e-08, ...,
-1.50528500e-08, -9.72514512e-09, -4.44957426e-09],
[ 2.03525423e-07, 2.04812366e-07, 2.06172949e-07, ...,
1.98908041e-07, 2.00634509e-07, 2.02164746e-07],
[ 2.84506841e-07, 3.11176721e-07, 3.37887249e-07, ...,
2.05878769e-07, 2.31914393e-07, 2.58120542e-07]],
shape=(27, 6001))}, {'band_index': 8, 'fmin': 0.0, 'fmax': 0.390625, 'model': None, 'duration': 51.2, 'n_samples': 10240, 'ignore': True, 'sensai_bounds': (0.0, 12.0), 'enova': 0.0, 'band_data': None}]
The wavelet model results can also be visualized using the plot_fit method:
[<Figure size 1200x500 with 2 Axes>, <Figure size 1200x500 with 2 Axes>, <Figure size 1200x500 with 2 Axes>, <Figure size 1200x500 with 2 Axes>, <Figure size 1200x500 with 2 Axes>, <Figure size 1200x500 with 2 Axes>, <Figure size 1200x500 with 2 Axes>, <Figure size 1200x500 with 2 Axes>]
%% Transform the Data (Denoising) —————————— Denoising is performed seamlessly using continuous dual-stream cosine overlap-add blending across each band:
adaptive_multiband_denoised_raw = adaptive_multiband_gedai.transform_raw(
raw, n_jobs=n_jobs
)
Model Summary Table🔗
We can inspect the model fitting parameters and subband thresholds:
==================================================================================
AdaptiveMultibandGedai Summary Table
==================================================================================
Frequency Band | Epoch (s) | Threshold | SENSAI (%) | ENOVA (%)
---------------------------+------------+------------+-------------+-----------
Pass 1: Broadband | 1.00 s | 2.41e-16 | 65.61 % | --
Band 0 (50.00-100.00 Hz) | 0.20 s | 1.36e-18 | 36.73 % | --
Band 1 (25.00-50.00 Hz) | 0.40 s | 4.63e-18 | 56.60 % | --
Band 2 (12.50-25.00 Hz) | 0.80 s | 2.743e-16 | 62.20 % | --
Band 3 (6.25-12.50 Hz) | 1.60 s | 2.742e-16 | 64.02 % | --
Band 4 (3.12-6.25 Hz) | 3.20 s | 2.357e-18 | 67.48 % | --
Band 5 (1.56-3.12 Hz) | 6.40 s | 2.794e-16 | 60.40 % | --
Band 6 (0.78-1.56 Hz) | 12.80 s | 2.336e-16 | 57.87 % | --
Band 7 (0.39-0.78 Hz) | 25.60 s | 1.033e-16 | 51.40 % | --
Band 8 (0.00-0.39 Hz) | 51.20 s | IGNORED | -- | --
==================================================================================
Fitted SENSAI Optimization Score: 57.09 %
==================================================================================
Quality Evaluation: Explained Noise Variance (ENOVA)🔗
ENOVA (Explained Noise Variance) quantifies the proportion of signal variance removed as artifact:
ENOVA = var(noise) / var(original)
Clean EEG segments: ENOVA is near 0% (typically < 5-10%), indicating minimal alteration of genuine brain activity.
Artifact-contaminated segments: ENOVA spikes to 50-95%, showing selective rejection of high-power ocular, muscular, or movement artifacts.
We can compute ENOVA across epochs and channels using the gedai.metrics module:
original_data = raw.get_data()
clean_data = adaptive_multiband_denoised_raw.get_data()
noise_data = original_data - clean_data
sfreq = raw.info["sfreq"]
epoch_samples = max(1, round(sfreq * 1.0))
enova_epochs = compute_enova_per_epoch(clean_data, noise_data, epoch_samples)
enova_stats = enova_summary(enova_epochs)
print(f"Mean ENOVA across epochs: {enova_stats['mean'] * 100:.2f} %")
print(f"Median ENOVA: {enova_stats['median'] * 100:.2f} %")
print(f"Max ENOVA (peak artifact): {enova_stats['max'] * 100:.2f} %")
Mean ENOVA across epochs: 34.58 %
Median ENOVA: 33.46 %
Max ENOVA (peak artifact): 84.88 %
Finally, we can visualize the before-and-after denoising overlay:
plot_mne_style_overlay_interactive(raw, adaptive_multiband_denoised_raw, duration=15.0)

(<Figure size 1200x1750 with 1 Axes>, <Axes: xlabel='Time (s)', ylabel='Channels'>)
Total running time of the script: (0 minutes 7.270 seconds)
Estimated memory usage: 321 MB







