Note
Go to the end to download the full example code.
Run pyLossless on a BIDS dataset.¶
In this notebook, we will run the pyLossless pipeline on a publicly available dataset.
- Installation (in the terminal or through Jupyter)
!pip install -q pylossless !pip install -q openneuro-py
Imports¶
from pathlib import Path
import shutil
import pylossless as ll
Get the data.
Note: If you are testing this examples in Google Colab, the openneuro-py download function does not always work flawlessly. You may have to run this cell twice for the files to be properly downloaded and usable by the next cell. ————
👋 Hello! This is openneuro-py 2026.1.0. Great to see you! 🤗
👉 Please report problems 🤯 and bugs 🪲 at
https://github.com/hoechenberger/openneuro-py/issues
🌍 Preparing to download ds002778 …
📁 Traversing directories for ds002778 : 0 entities [00:00, ? entities/s]
📁 Traversing directories for ds002778 : 7 entities [00:04, 1.55 entities/s]
📁 Traversing directories for ds002778 : 8 entities [00:04, 1.77 entities/s]
📁 Traversing directories for ds002778 : 10 entities [00:04, 2.48 entities/s]
📁 Traversing directories for ds002778 : 14 entities [00:04, 4.43 entities/s]
📁 Traversing directories for ds002778 : 16 entities [00:05, 5.39 entities/s]
📁 Traversing directories for ds002778 : 18 entities [00:05, 6.44 entities/s]
📁 Traversing directories for ds002778 : 20 entities [00:05, 3.65 entities/s]
📥 Retrieving up to 19 files (5 concurrent downloads).
participants.json: 0%| | 0.00/1.24k [00:00<?, ?B/s]
participants.tsv: 0%| | 0.00/1.62k [00:00<?, ?B/s]
sub-pd6_ses-off_scans.tsv: 0%| | 0.00/75.0 [00:00<?, ?B/s]
README: 0%| | 0.00/4.55k [00:00<?, ?B/s]
dataset_description.json: 0%| | 0.00/354 [00:00<?, ?B/s]
CHANGES: 0%| | 0.00/379 [00:00<?, ?B/s]
sub-pd6_ses-off_task-rest_beh.tsv: 0%| | 0.00/10.0 [00:00<?, ?B/s]
sub-pd6_ses-off_task-rest_channels.tsv: 0%| | 0.00/2.22k [00:00<?, ?B/s]
sub-pd6_ses-off_task-rest_eeg.json: 0%| | 0.00/471 [00:00<?, ?B/s]
sub-pd6_ses-off_task-rest_beh.json: 0%| | 0.00/433 [00:00<?, ?B/s]
sub-pd6_ses-off_task-rest_eeg.bdf: 0%| | 0.00/11.5M [00:00<?, ?B/s]
sub-pd6_ses-off_task-rest_events.tsv: 0%| | 0.00/66.0 [00:00<?, ?B/s]
sub-pd6_ses-on_task-rest_beh.tsv: 0%| | 0.00/10.0 [00:00<?, ?B/s]
sub-pd6_ses-on_task-rest_beh.json: 0%| | 0.00/433 [00:00<?, ?B/s]
sub-pd6_ses-on_scans.tsv: 0%| | 0.00/74.0 [00:00<?, ?B/s]
sub-pd6_ses-off_task-rest_eeg.bdf: 5%|▍ | 537k/11.5M [00:00<00:04, 2.75MB/s]
sub-pd6_ses-on_task-rest_channels.tsv: 0%| | 0.00/2.22k [00:00<?, ?B/s]
sub-pd6_ses-on_task-rest_eeg.json: 0%| | 0.00/471 [00:00<?, ?B/s]
sub-pd6_ses-on_task-rest_events.tsv: 0%| | 0.00/51.0 [00:00<?, ?B/s]
sub-pd6_ses-off_task-rest_eeg.bdf: 31%|███ | 3.59M/11.5M [00:00<00:00, 15.0MB/s]
sub-pd6_ses-on_task-rest_eeg.bdf: 0%| | 0.00/17.4M [00:00<?, ?B/s]
sub-pd6_ses-off_task-rest_eeg.bdf: 100%|█████████▉| 11.5M/11.5M [00:00<00:00, 40.2MB/s]
sub-pd6_ses-on_task-rest_eeg.bdf: 17%|█▋ | 2.93M/17.4M [00:00<00:00, 30.7MB/s]
sub-pd6_ses-on_task-rest_eeg.bdf: 66%|██████▌ | 11.4M/17.4M [00:00<00:00, 65.0MB/s]
✅ Finished downloading ds002778.
🧠 Please enjoy your brains.
Prep the Raw object¶
This data has EOG channels that are not labeled as such. We will manually set the channel types to be “eog” for these channels (i.e. “EXG1”). We will also crop the data to 60 seconds for speed, and load the data in memory, which is required for running the pipeline.
raw.set_channel_types({ch: "eog" for ch in raw.ch_names if ch.startswith("EX")})
raw.load_data().crop(0, 60)
Reading 0 ... 97791 = 0.000 ... 190.998 secs...
Initialize the pipeline¶
The LosslessPipeline instance is the main object that will
run the pipeline. It takes a file path to a Config object
as input. load_openneuro_bids() returned a
Config object, so we will save it to disk and pass the
file path to the LosslessPipeline constructor.
config_path = Path("lossless_config.yaml")
config["filtering"]["notch_filter_args"]["freqs"] = [60]
config.save(config_path)
pipeline = ll.LosslessPipeline(config_path)
Run the pipeline¶
The LosslessPipeline object has a
run_with_raw() method that takes a
Raw object as input.
We will use the Raw object that was returned by
load_openneuro_bids() with the pipeline.
⏩ LOSSLESS: Starting Pylossless Pipeline.
LOSSLESS: Skipping Looking for break periods between tasks
LOSSLESS: 👇 Flagging Noisy Channels.
🧹 Epoching..
Not setting metadata
60 matching events found
No baseline correction applied
0 projection items activated
Using data from preloaded Raw for 60 events and 512 original time points ...
0 bad epochs dropped
🔍 Detecting channels to leave out of reference.
EEG channel type selected for re-referencing
Applying a custom ('EEG',) reference.
📋 LOSSLESS: Noisy channels: []
LOSSLESS: 🏁 Finished Flagging Noisy Channels after 0.07 seconds.
LOSSLESS: 👇 Flagging Noisy Time periods.
🧹 Epoching..
Not setting metadata
60 matching events found
No baseline correction applied
0 projection items activated
Using data from preloaded Raw for 60 events and 512 original time points ...
0 bad epochs dropped
🔍 Detecting channels to leave out of reference.
EEG channel type selected for re-referencing
Applying a custom ('EEG',) reference.
📋 LOSSLESS: 1.0 second(s) flagged as BAD_LL_noisy
LOSSLESS: 🏁 Finished Flagging Noisy Time periods after 0.07 seconds.
LOSSLESS: 👇 Filtering.
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 1 - 1e+02 Hz
FIR filter parameters
---------------------
Designing a one-pass, zero-phase, non-causal bandpass filter:
- Windowed time-domain design (firwin) method
- Hamming window with 0.0194 passband ripple and 53 dB stopband attenuation
- Lower passband edge: 1.00
- Lower transition bandwidth: 1.00 Hz (-6 dB cutoff frequency: 0.50 Hz)
- Upper passband edge: 100.00 Hz
- Upper transition bandwidth: 25.00 Hz (-6 dB cutoff frequency: 112.50 Hz)
- Filter length: 1691 samples (3.303 s)
Filtering raw data in 1 contiguous segment
Setting up band-stop filter from 59 - 61 Hz
FIR filter parameters
---------------------
Designing a one-pass, zero-phase, non-causal bandstop filter:
- Windowed time-domain design (firwin) method
- Hamming window with 0.0194 passband ripple and 53 dB stopband attenuation
- Lower passband edge: 59.35
- Lower transition bandwidth: 0.50 Hz (-6 dB cutoff frequency: 59.10 Hz)
- Upper passband edge: 60.65 Hz
- Upper transition bandwidth: 0.50 Hz (-6 dB cutoff frequency: 60.90 Hz)
- Filter length: 3381 samples (6.604 s)
LOSSLESS: 🏁 Finished Filtering after 0.11 seconds.
LOSSLESS: 👇 Flagging uncorrelated channels.
🧹 Epoching..
Not setting metadata
60 matching events found
No baseline correction applied
0 projection items activated
Using data from preloaded Raw for 60 events and 512 original time points ...
1 bad epochs dropped
🔍 Detecting channels to leave out of reference.
EEG channel type selected for re-referencing
Applying a custom ('EEG',) reference.
0%| | 0/32 [00:00<?, ?it/s]
22%|██▏ | 7/32 [00:00<00:00, 60.91it/s]
44%|████▍ | 14/32 [00:00<00:00, 60.63it/s]
66%|██████▌ | 21/32 [00:00<00:00, 57.13it/s]
84%|████████▍ | 27/32 [00:00<00:00, 56.37it/s]
100%|██████████| 32/32 [00:00<00:00, 57.68it/s]
📋 LOSSLESS: Uncorrelated channels: []
LOSSLESS: 🏁 Finished Flagging uncorrelated channels after 0.65 seconds.
LOSSLESS: 👇 Flagging Bridged channels.
📋 LOSSLESS: Bridged channels: ['PO3' 'O1' 'Oz' 'O2' 'PO4']
LOSSLESS: 🏁 Finished Flagging Bridged channels after 0.00 seconds.
LOSSLESS: 👇 Flagging the rank channel.
📋 LOSSLESS: Rank channel: ['Fp2']
LOSSLESS: 🏁 Finished Flagging the rank channel after 0.00 seconds.
LOSSLESS: 👇 Flagging Uncorrelated epochs.
🧹 Epoching..
Not setting metadata
60 matching events found
No baseline correction applied
0 projection items activated
Using data from preloaded Raw for 60 events and 512 original time points ...
1 bad epochs dropped
🔍 Detecting channels to leave out of reference.
EEG channel type selected for re-referencing
Applying a custom ('EEG',) reference.
0%| | 0/26 [00:00<?, ?it/s]
27%|██▋ | 7/26 [00:00<00:00, 61.90it/s]
54%|█████▍ | 14/26 [00:00<00:00, 61.85it/s]
81%|████████ | 21/26 [00:00<00:00, 61.90it/s]
100%|██████████| 26/26 [00:00<00:00, 61.73it/s]
LOSSLESS: 🏁 Finished Flagging Uncorrelated epochs after 0.50 seconds.
LOSSLESS: 👇 Running Initial ICA.
🧹 Epoching..
Not setting metadata
60 matching events found
No baseline correction applied
0 projection items activated
Using data from preloaded Raw for 60 events and 512 original time points ...
1 bad epochs dropped
🔍 Detecting channels to leave out of reference.
EEG channel type selected for re-referencing
Applying a custom ('EEG',) reference.
Fitting ICA to data using 26 channels (please be patient, this may take a while)
Selecting by non-zero PCA components: 25 components
/home/docs/checkouts/readthedocs.org/user_builds/pylossless/envs/latest/lib/python3.10/site-packages/sklearn/decomposition/_fastica.py:127: ConvergenceWarning: FastICA did not converge. Consider increasing tolerance or the maximum number of iterations.
warnings.warn(
Fitting ICA took 6.1s.
LOSSLESS: 🏁 Finished Running Initial ICA after 6.20 seconds.
LOSSLESS: 👇 Flagging time periods with noisy IC's..
🧹 Epoching..
Not setting metadata
60 matching events found
No baseline correction applied
0 projection items activated
Using data from preloaded Raw for 60 events and 512 original time points ...
1 bad epochs dropped
🔍 Detecting channels to leave out of reference.
EEG channel type selected for re-referencing
Applying a custom ('EEG',) reference.
📋 LOSSLESS: 3.99 second(s) flagged as BAD_LL_noisy_ICs
LOSSLESS: 🏁 Finished Flagging time periods with noisy IC's. after 0.09 seconds.
LOSSLESS: 👇 Running Final ICA and ICLabel..
🧹 Epoching..
Not setting metadata
60 matching events found
No baseline correction applied
0 projection items activated
Using data from preloaded Raw for 60 events and 512 original time points ...
5 bad epochs dropped
🔍 Detecting channels to leave out of reference.
EEG channel type selected for re-referencing
Applying a custom ('EEG',) reference.
Fitting ICA to data using 26 channels (please be patient, this may take a while)
Selecting by non-zero PCA components: 25 components
Computing Extended Infomax ICA
Fitting ICA took 4.1s.
LOSSLESS: 🏁 Finished Running Final ICA and ICLabel. after 8.43 seconds.
✅ LOSSLESS: Pipeline completed! took 0.27 minutes.
View the results¶
The LosslessPipeline object stores flagged channels and ICs in
the flags attribute:
pipeline.flags["ic"]
Get the cleaned data¶
The LosslessPipeline by default does not modify the
Raw object that is passed to it, so none of the flagged channels
or ICs are removed from the Raw object yet. To get the cleaned
Raw object, we need to call the
apply() method. This method takes a
LosslessPipeline as input, which specifies how to apply the flags
to generate a new Raw object.
RejectionPolicy: |
config_fname: None
ch_flags_to_reject: ['noisy', 'uncorrelated', 'bridged']
ic_flags_to_reject: ['muscle', 'ecg', 'eog', 'channel_noise', 'line_noise']
ic_rejection_threshold: 0.3
ch_cleaning_mode: None
remove_flagged_ics: True
Note that we are using the default channel cleaning mode, which is None, meaning
that that the flagged channels will simply be added to raw.info["bads"]. We also
could have specified "interpolate", which means that the flagged
channels would be interpolated using interpolate_bads().
cleaned_raw = rejection_policy.apply(pipeline)
cleaned_raw.plot()

Applying ICA to Raw instance
Transforming to ICA space (25 components)
Zeroing out 8 ICA components
Projecting back using 26 PCA components
Using matplotlib as 2D backend.
<MNEBrowseFigure size 800x800 with 4 Axes>
Note
Below is an example of how one might customize the rejection policy to achieve a difference output.
rejection_policy_2 = ll.RejectionPolicy(
ch_flags_to_reject=["noisy"],
ch_cleaning_mode="interpolate",
ic_flags_to_reject=["eog"],
)
Save the PyLossless Derivative¶
Let’s save our pipeline output to disk. We need to use our
BIDSPath object to set up a derivatives path to save the
pipeline output to:
derivatives_path = pipeline.get_derivative_path(bids_path)
derivatives_path.update(suffix="eeg")
BIDSPath(
root: ds002778/derivatives/pylossless
datatype: eeg
basename: sub-pd6_ses-off_task-rest_eeg)
Writing 'ds002778/derivatives/pylossless/README'...
Writing 'ds002778/derivatives/pylossless/participants.tsv'...
Writing 'ds002778/derivatives/pylossless/participants.json'...
Writing 'ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_space-CapTrak_electrodes.tsv'...
Writing 'ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_space-CapTrak_coordsystem.json'...
The provided raw data contains annotations, but you did not pass an "event_id" mapping from annotation descriptions to event codes. We will generate arbitrary event codes. To specify custom event codes, please pass "event_id".
Used Annotations descriptions: [np.str_('1'), np.str_('65536'), np.str_('BAD_LL_noisy'), np.str_('BAD_LL_noisy_ICs')]
Writing 'ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_events.tsv'...
Writing 'ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_events.json'...
Writing 'ds002778/derivatives/pylossless/dataset_description.json'...
Writing 'ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_eeg.json'...
Writing 'ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_channels.tsv'...
Copying data files to sub-pd6_ses-off_task-rest_eeg.edf
/home/docs/checkouts/readthedocs.org/user_builds/pylossless/checkouts/latest/pylossless/pipeline.py:1255: RuntimeWarning: Converting data files to EDF format
mne_bids.write_raw_bids(
/home/docs/checkouts/readthedocs.org/user_builds/pylossless/checkouts/latest/pylossless/pipeline.py:1255: RuntimeWarning: EDF format requires equal-length data blocks, so 0.998 seconds of edge values were appended to all channels when writing the final block.
mne_bids.write_raw_bids(
Writing 'ds002778/derivatives/pylossless/sub-pd6/ses-off/sub-pd6_ses-off_scans.tsv'...
Wrote ds002778/derivatives/pylossless/sub-pd6/ses-off/sub-pd6_ses-off_scans.tsv entry with eeg/sub-pd6_ses-off_task-rest_eeg.edf.
Writing ICA solution to /home/docs/checkouts/readthedocs.org/user_builds/pylossless/checkouts/latest/examples/ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_ica1_ica.fif...
Writing ICA solution to /home/docs/checkouts/readthedocs.org/user_builds/pylossless/checkouts/latest/examples/ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_ica2_ica.fif...
Loading PyLossless Derivative data¶
If you or a colleague wants to load a previously saved PyLossless Derivative,
you can use the load_ll_derivative() method of the
pylossless.LosslessPipeline class:
Extracting EDF parameters from ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_eeg.edf...
Setting channel info structure...
Creating raw.info structure...
Reading events from ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_events.tsv.
Reading channel info from ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_channels.tsv.
Reading electrode coords from ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_space-CapTrak_electrodes.tsv.
/home/docs/checkouts/readthedocs.org/user_builds/pylossless/checkouts/latest/pylossless/pipeline.py:1454: RuntimeWarning: There are channels without locations (n/a) that are not marked as bad: ['EXG1', 'EXG2', 'EXG3', 'EXG4', 'EXG5', 'EXG6', 'EXG7', 'EXG8']
self.raw = mne_bids.read_raw_bids(derivatives_path)
/home/docs/checkouts/readthedocs.org/user_builds/pylossless/checkouts/latest/pylossless/pipeline.py:1454: RuntimeWarning: Not setting positions of 8 eog channels found in montage:
['EXG1', 'EXG2', 'EXG3', 'EXG4', 'EXG5', 'EXG6', 'EXG7', 'EXG8']
Consider setting the channel types to be of EEG/sEEG/ECoG/DBS/fNIRS using inst.set_channel_types before calling inst.set_montage, or omit these channels when creating your montage.
self.raw = mne_bids.read_raw_bids(derivatives_path)
Not fully anonymizing info - keeping his_id, sex, and hand info
Reading /home/docs/checkouts/readthedocs.org/user_builds/pylossless/checkouts/latest/examples/ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_ica1_ica.fif ...
Now restoring ICA solution ...
Ready.
Reading /home/docs/checkouts/readthedocs.org/user_builds/pylossless/checkouts/latest/examples/ds002778/derivatives/pylossless/sub-pd6/ses-off/eeg/sub-pd6_ses-off_task-rest_ica2_ica.fif ...
Now restoring ICA solution ...
Ready.
Clean up¶
Total running time of the script: (0 minutes 24.854 seconds)