Eyes Open vs. Closed Classification#

Estimated reading time:1 minute

EEGDash example for eyes open vs. closed classification.

CHANGES: - Uses the EEGDash API (no local OpenNeuro mirror required) - Multi-subject run: auto-discover subjects from API and use ~10 valid subjects - Skip subjects that produce empty EEGDashDataset (no recordings) - Skip subjects that produce 0 windows after preprocessing/windowing - Subject-wise train/test split (no leakage) - Robust windowing: one 2s window per event (avoids braindecode trial overlap errors) - Save plot to file (no GUI needed on compute nodes)

tutorial eoec
/home/runner/work/EEGDash/EEGDash/.venv/lib/python3.11/site-packages/braindecode/preprocessing/preprocess.py:77: UserWarning: apply_on_array can only be True if fn is a callable function. Automatically correcting to apply_on_array=False.
  warn(
Discovered subjects (first 20): ['NDARAC589YMB', 'NDARAC853CR6', 'NDARAE710YWG', 'NDARAH239PGG', 'NDARAL897CYV', 'NDARAN160GUF', 'NDARAP049KXJ', 'NDARAP457WB5', 'NDARAU939WUK', 'NDARAW216PM7', 'NDARAW298ZA9', 'NDARAX075WL9', 'NDARAX722PKY', 'NDARAZ068TNJ', 'NDARBA004KBT', 'NDARBD328NUQ', 'NDARBD992CH7', 'NDARBE719PMB', 'NDARBF042LDM', 'NDARBH019KPD']

=== Subject NDARAC589YMB ===
╭────────────────────── EEG 2025 Competition Data Notice ──────────────────────╮
│ This notice is only for users who are participating in the EEG 2025          │
│ Competition.                                                                 │
│                                                                              │
│ EEG 2025 Competition Data Notice!                                            │
│ You are loading one of the datasets that is used in competition, but via     │
│ `EEGDashDataset`.                                                            │
│                                                                              │
│ IMPORTANT:                                                                   │
│ If you download data from `EEGDashDataset`, it is NOT identical to the       │
│ official                                                                     │
│ competition data, which is accessed via `EEGChallengeDataset`. The           │
│ competition data has been downsampled and filtered.                          │
│                                                                              │
│ If you are participating in the competition,                                 │
│ you must use the `EEGChallengeDataset` object to ensure consistency.         │
│                                                                              │
│ If you are not participating in the competition, you can ignore this         │
│ message.                                                                     │
╰─────────────────────────── Source: EEGDashDataset ───────────────────────────╯

Downloading sub-NDARAC589YMB_task-RestingState_channels.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAC589YMB_task-RestingState_channels.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 11.1B/s]

Downloading sub-NDARAC589YMB_task-RestingState_events.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAC589YMB_task-RestingState_events.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 10.5B/s]

Downloading sub-NDARAC589YMB_task-RestingState_eeg.json:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAC589YMB_task-RestingState_eeg.json: 100%|██████████| 1.00/1.00 [00:00<00:00, 13.7B/s]
[04/16/26 10:53:44] WARNING  File not found on S3, skipping:   downloader.py:146
                             s3://openneuro.org/ds005514/sub-N
                             DARAC589YMB/eeg/sub-NDARAC589YMB_
                             task-RestingState_eeg.fdt

Downloading sub-NDARAC589YMB_task-RestingState_eeg.set:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAC589YMB_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.81s/B]
Downloading sub-NDARAC589YMB_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.82s/B]
Used Annotations descriptions: [np.str_('boundary'), np.str_('break cnt'), np.str_('instructed_toCloseEyes'), np.str_('instructed_toOpenEyes'), np.str_('resting_start')]
[04/16/26 10:53:47] INFO     Original events found with ids: preprocessing.py:66
                             {np.str_('boundary'): 1,
                             np.str_('break cnt'): 2,
                             np.str_('instructed_toCloseEyes
                             '): 3,
                             np.str_('instructed_toOpenEyes'
                             ): 4, np.str_('resting_start'):
                             5}
NOTE: pick_channels() is a legacy function. New code should use inst.pick(...).
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 1 - 55 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: 55.00 Hz
- Upper transition bandwidth: 9.00 Hz (-6 dB cutoff frequency: 59.50 Hz)
- Filter length: 423 samples (3.305 s)

Windows for subject: 70

=== Subject NDARAC853CR6 ===
╭────────────────────── EEG 2025 Competition Data Notice ──────────────────────╮
│ This notice is only for users who are participating in the EEG 2025          │
│ Competition.                                                                 │
│                                                                              │
│ EEG 2025 Competition Data Notice!                                            │
│ You are loading one of the datasets that is used in competition, but via     │
│ `EEGDashDataset`.                                                            │
│                                                                              │
│ IMPORTANT:                                                                   │
│ If you download data from `EEGDashDataset`, it is NOT identical to the       │
│ official                                                                     │
│ competition data, which is accessed via `EEGChallengeDataset`. The           │
│ competition data has been downsampled and filtered.                          │
│                                                                              │
│ If you are participating in the competition,                                 │
│ you must use the `EEGChallengeDataset` object to ensure consistency.         │
│                                                                              │
│ If you are not participating in the competition, you can ignore this         │
│ message.                                                                     │
╰─────────────────────────── Source: EEGDashDataset ───────────────────────────╯

Downloading sub-NDARAC853CR6_task-RestingState_channels.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAC853CR6_task-RestingState_channels.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 7.78B/s]

Downloading sub-NDARAC853CR6_task-RestingState_events.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAC853CR6_task-RestingState_events.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 11.9B/s]

Downloading sub-NDARAC853CR6_task-RestingState_eeg.json:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAC853CR6_task-RestingState_eeg.json: 100%|██████████| 1.00/1.00 [00:00<00:00, 6.83B/s]
[04/16/26 10:53:49] WARNING  File not found on S3, skipping:   downloader.py:146
                             s3://openneuro.org/ds005514/sub-N
                             DARAC853CR6/eeg/sub-NDARAC853CR6_
                             task-RestingState_eeg.fdt

Downloading sub-NDARAC853CR6_task-RestingState_eeg.set:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAC853CR6_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.29s/B]
Downloading sub-NDARAC853CR6_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.29s/B]
Used Annotations descriptions: [np.str_('boundary'), np.str_('break cnt'), np.str_('instructed_toCloseEyes'), np.str_('instructed_toOpenEyes'), np.str_('resting_start')]
[04/16/26 10:53:51] INFO     Original events found with ids: preprocessing.py:66
                             {np.str_('boundary'): 1,
                             np.str_('break cnt'): 2,
                             np.str_('instructed_toCloseEyes
                             '): 3,
                             np.str_('instructed_toOpenEyes'
                             ): 4, np.str_('resting_start'):
                             5}
NOTE: pick_channels() is a legacy function. New code should use inst.pick(...).
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 1 - 55 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: 55.00 Hz
- Upper transition bandwidth: 9.00 Hz (-6 dB cutoff frequency: 59.50 Hz)
- Filter length: 423 samples (3.305 s)

Windows for subject: 70

=== Subject NDARAE710YWG ===
╭────────────────────── EEG 2025 Competition Data Notice ──────────────────────╮
│ This notice is only for users who are participating in the EEG 2025          │
│ Competition.                                                                 │
│                                                                              │
│ EEG 2025 Competition Data Notice!                                            │
│ You are loading one of the datasets that is used in competition, but via     │
│ `EEGDashDataset`.                                                            │
│                                                                              │
│ IMPORTANT:                                                                   │
│ If you download data from `EEGDashDataset`, it is NOT identical to the       │
│ official                                                                     │
│ competition data, which is accessed via `EEGChallengeDataset`. The           │
│ competition data has been downsampled and filtered.                          │
│                                                                              │
│ If you are participating in the competition,                                 │
│ you must use the `EEGChallengeDataset` object to ensure consistency.         │
│                                                                              │
│ If you are not participating in the competition, you can ignore this         │
│ message.                                                                     │
╰─────────────────────────── Source: EEGDashDataset ───────────────────────────╯

Downloading sub-NDARAE710YWG_task-RestingState_channels.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAE710YWG_task-RestingState_channels.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 12.6B/s]

Downloading sub-NDARAE710YWG_task-RestingState_events.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAE710YWG_task-RestingState_events.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 14.9B/s]

Downloading sub-NDARAE710YWG_task-RestingState_eeg.json:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAE710YWG_task-RestingState_eeg.json: 100%|██████████| 1.00/1.00 [00:00<00:00, 10.8B/s]
[04/16/26 10:53:53] WARNING  File not found on S3, skipping:   downloader.py:146
                             s3://openneuro.org/ds005514/sub-N
                             DARAE710YWG/eeg/sub-NDARAE710YWG_
                             task-RestingState_eeg.fdt

Downloading sub-NDARAE710YWG_task-RestingState_eeg.set:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAE710YWG_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.36s/B]
Downloading sub-NDARAE710YWG_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.36s/B]
Used Annotations descriptions: [np.str_('boundary'), np.str_('break cnt'), np.str_('instructed_toCloseEyes'), np.str_('instructed_toOpenEyes'), np.str_('resting_start')]
[04/16/26 10:53:55] INFO     Original events found with ids: preprocessing.py:66
                             {np.str_('boundary'): 1,
                             np.str_('break cnt'): 2,
                             np.str_('instructed_toCloseEyes
                             '): 3,
                             np.str_('instructed_toOpenEyes'
                             ): 4, np.str_('resting_start'):
                             5}
NOTE: pick_channels() is a legacy function. New code should use inst.pick(...).
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 1 - 55 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: 55.00 Hz
- Upper transition bandwidth: 9.00 Hz (-6 dB cutoff frequency: 59.50 Hz)
- Filter length: 423 samples (3.305 s)

Windows for subject: 70

=== Subject NDARAH239PGG ===
╭────────────────────── EEG 2025 Competition Data Notice ──────────────────────╮
│ This notice is only for users who are participating in the EEG 2025          │
│ Competition.                                                                 │
│                                                                              │
│ EEG 2025 Competition Data Notice!                                            │
│ You are loading one of the datasets that is used in competition, but via     │
│ `EEGDashDataset`.                                                            │
│                                                                              │
│ IMPORTANT:                                                                   │
│ If you download data from `EEGDashDataset`, it is NOT identical to the       │
│ official                                                                     │
│ competition data, which is accessed via `EEGChallengeDataset`. The           │
│ competition data has been downsampled and filtered.                          │
│                                                                              │
│ If you are participating in the competition,                                 │
│ you must use the `EEGChallengeDataset` object to ensure consistency.         │
│                                                                              │
│ If you are not participating in the competition, you can ignore this         │
│ message.                                                                     │
╰─────────────────────────── Source: EEGDashDataset ───────────────────────────╯

Downloading sub-NDARAH239PGG_task-RestingState_channels.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAH239PGG_task-RestingState_channels.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 12.7B/s]

Downloading sub-NDARAH239PGG_task-RestingState_events.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAH239PGG_task-RestingState_events.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 11.6B/s]

Downloading sub-NDARAH239PGG_task-RestingState_eeg.json:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAH239PGG_task-RestingState_eeg.json: 100%|██████████| 1.00/1.00 [00:00<00:00, 15.5B/s]
[04/16/26 10:53:57] WARNING  File not found on S3, skipping:   downloader.py:146
                             s3://openneuro.org/ds005514/sub-N
                             DARAH239PGG/eeg/sub-NDARAH239PGG_
                             task-RestingState_eeg.fdt

Downloading sub-NDARAH239PGG_task-RestingState_eeg.set:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAH239PGG_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.45s/B]
Downloading sub-NDARAH239PGG_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.45s/B]
Used Annotations descriptions: [np.str_('boundary'), np.str_('break cnt'), np.str_('instructed_toCloseEyes'), np.str_('instructed_toOpenEyes'), np.str_('resting_start')]
[04/16/26 10:53:59] INFO     Original events found with ids: preprocessing.py:66
                             {np.str_('boundary'): 1,
                             np.str_('break cnt'): 2,
                             np.str_('instructed_toCloseEyes
                             '): 3,
                             np.str_('instructed_toOpenEyes'
                             ): 4, np.str_('resting_start'):
                             5}
NOTE: pick_channels() is a legacy function. New code should use inst.pick(...).
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 1 - 55 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: 55.00 Hz
- Upper transition bandwidth: 9.00 Hz (-6 dB cutoff frequency: 59.50 Hz)
- Filter length: 423 samples (3.305 s)

Windows for subject: 70

=== Subject NDARAL897CYV ===
╭────────────────────── EEG 2025 Competition Data Notice ──────────────────────╮
│ This notice is only for users who are participating in the EEG 2025          │
│ Competition.                                                                 │
│                                                                              │
│ EEG 2025 Competition Data Notice!                                            │
│ You are loading one of the datasets that is used in competition, but via     │
│ `EEGDashDataset`.                                                            │
│                                                                              │
│ IMPORTANT:                                                                   │
│ If you download data from `EEGDashDataset`, it is NOT identical to the       │
│ official                                                                     │
│ competition data, which is accessed via `EEGChallengeDataset`. The           │
│ competition data has been downsampled and filtered.                          │
│                                                                              │
│ If you are participating in the competition,                                 │
│ you must use the `EEGChallengeDataset` object to ensure consistency.         │
│                                                                              │
│ If you are not participating in the competition, you can ignore this         │
│ message.                                                                     │
╰─────────────────────────── Source: EEGDashDataset ───────────────────────────╯

Downloading sub-NDARAL897CYV_task-RestingState_channels.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAL897CYV_task-RestingState_channels.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 12.8B/s]

Downloading sub-NDARAL897CYV_task-RestingState_events.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAL897CYV_task-RestingState_events.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 7.20B/s]

Downloading sub-NDARAL897CYV_task-RestingState_eeg.json:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAL897CYV_task-RestingState_eeg.json: 100%|██████████| 1.00/1.00 [00:00<00:00, 14.7B/s]
[04/16/26 10:54:01] WARNING  File not found on S3, skipping:   downloader.py:146
                             s3://openneuro.org/ds005514/sub-N
                             DARAL897CYV/eeg/sub-NDARAL897CYV_
                             task-RestingState_eeg.fdt

Downloading sub-NDARAL897CYV_task-RestingState_eeg.set:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAL897CYV_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.47s/B]
Downloading sub-NDARAL897CYV_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.47s/B]
Used Annotations descriptions: [np.str_('boundary'), np.str_('break cnt'), np.str_('instructed_toCloseEyes'), np.str_('instructed_toOpenEyes'), np.str_('resting_start')]
[04/16/26 10:54:03] INFO     Original events found with ids: preprocessing.py:66
                             {np.str_('boundary'): 1,
                             np.str_('break cnt'): 2,
                             np.str_('instructed_toCloseEyes
                             '): 3,
                             np.str_('instructed_toOpenEyes'
                             ): 4, np.str_('resting_start'):
                             5}
NOTE: pick_channels() is a legacy function. New code should use inst.pick(...).
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 1 - 55 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: 55.00 Hz
- Upper transition bandwidth: 9.00 Hz (-6 dB cutoff frequency: 59.50 Hz)
- Filter length: 423 samples (3.305 s)

Windows for subject: 70

=== Subject NDARAN160GUF ===
╭────────────────────── EEG 2025 Competition Data Notice ──────────────────────╮
│ This notice is only for users who are participating in the EEG 2025          │
│ Competition.                                                                 │
│                                                                              │
│ EEG 2025 Competition Data Notice!                                            │
│ You are loading one of the datasets that is used in competition, but via     │
│ `EEGDashDataset`.                                                            │
│                                                                              │
│ IMPORTANT:                                                                   │
│ If you download data from `EEGDashDataset`, it is NOT identical to the       │
│ official                                                                     │
│ competition data, which is accessed via `EEGChallengeDataset`. The           │
│ competition data has been downsampled and filtered.                          │
│                                                                              │
│ If you are participating in the competition,                                 │
│ you must use the `EEGChallengeDataset` object to ensure consistency.         │
│                                                                              │
│ If you are not participating in the competition, you can ignore this         │
│ message.                                                                     │
╰─────────────────────────── Source: EEGDashDataset ───────────────────────────╯

Downloading sub-NDARAN160GUF_task-RestingState_channels.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAN160GUF_task-RestingState_channels.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 9.01B/s]

Downloading sub-NDARAN160GUF_task-RestingState_events.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAN160GUF_task-RestingState_events.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 7.40B/s]

Downloading sub-NDARAN160GUF_task-RestingState_eeg.json:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAN160GUF_task-RestingState_eeg.json: 100%|██████████| 1.00/1.00 [00:00<00:00, 15.6B/s]
[04/16/26 10:54:05] WARNING  File not found on S3, skipping:   downloader.py:146
                             s3://openneuro.org/ds005514/sub-N
                             DARAN160GUF/eeg/sub-NDARAN160GUF_
                             task-RestingState_eeg.fdt

Downloading sub-NDARAN160GUF_task-RestingState_eeg.set:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAN160GUF_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.58s/B]
Downloading sub-NDARAN160GUF_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.58s/B]
Used Annotations descriptions: [np.str_('boundary'), np.str_('break cnt'), np.str_('instructed_toCloseEyes'), np.str_('instructed_toOpenEyes'), np.str_('resting_start')]
[04/16/26 10:54:07] INFO     Original events found with ids: preprocessing.py:66
                             {np.str_('boundary'): 1,
                             np.str_('break cnt'): 2,
                             np.str_('instructed_toCloseEyes
                             '): 3,
                             np.str_('instructed_toOpenEyes'
                             ): 4, np.str_('resting_start'):
                             5}
NOTE: pick_channels() is a legacy function. New code should use inst.pick(...).
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 1 - 55 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: 55.00 Hz
- Upper transition bandwidth: 9.00 Hz (-6 dB cutoff frequency: 59.50 Hz)
- Filter length: 423 samples (3.305 s)

Windows for subject: 70

=== Subject NDARAP049KXJ ===
╭────────────────────── EEG 2025 Competition Data Notice ──────────────────────╮
│ This notice is only for users who are participating in the EEG 2025          │
│ Competition.                                                                 │
│                                                                              │
│ EEG 2025 Competition Data Notice!                                            │
│ You are loading one of the datasets that is used in competition, but via     │
│ `EEGDashDataset`.                                                            │
│                                                                              │
│ IMPORTANT:                                                                   │
│ If you download data from `EEGDashDataset`, it is NOT identical to the       │
│ official                                                                     │
│ competition data, which is accessed via `EEGChallengeDataset`. The           │
│ competition data has been downsampled and filtered.                          │
│                                                                              │
│ If you are participating in the competition,                                 │
│ you must use the `EEGChallengeDataset` object to ensure consistency.         │
│                                                                              │
│ If you are not participating in the competition, you can ignore this         │
│ message.                                                                     │
╰─────────────────────────── Source: EEGDashDataset ───────────────────────────╯

Downloading sub-NDARAP049KXJ_task-RestingState_channels.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAP049KXJ_task-RestingState_channels.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 13.7B/s]

Downloading sub-NDARAP049KXJ_task-RestingState_events.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAP049KXJ_task-RestingState_events.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 11.0B/s]

Downloading sub-NDARAP049KXJ_task-RestingState_eeg.json:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAP049KXJ_task-RestingState_eeg.json: 100%|██████████| 1.00/1.00 [00:00<00:00, 14.1B/s]
[04/16/26 10:54:10] WARNING  File not found on S3, skipping:   downloader.py:146
                             s3://openneuro.org/ds005514/sub-N
                             DARAP049KXJ/eeg/sub-NDARAP049KXJ_
                             task-RestingState_eeg.fdt

Downloading sub-NDARAP049KXJ_task-RestingState_eeg.set:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAP049KXJ_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.04s/B]
Downloading sub-NDARAP049KXJ_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.04s/B]
Used Annotations descriptions: [np.str_('boundary'), np.str_('break cnt'), np.str_('instructed_toCloseEyes'), np.str_('instructed_toOpenEyes'), np.str_('resting_start')]
[04/16/26 10:54:12] INFO     Original events found with ids: preprocessing.py:66
                             {np.str_('boundary'): 1,
                             np.str_('break cnt'): 2,
                             np.str_('instructed_toCloseEyes
                             '): 3,
                             np.str_('instructed_toOpenEyes'
                             ): 4, np.str_('resting_start'):
                             5}
NOTE: pick_channels() is a legacy function. New code should use inst.pick(...).
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 1 - 55 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: 55.00 Hz
- Upper transition bandwidth: 9.00 Hz (-6 dB cutoff frequency: 59.50 Hz)
- Filter length: 423 samples (3.305 s)

Windows for subject: 70

=== Subject NDARAP457WB5 ===
╭────────────────────── EEG 2025 Competition Data Notice ──────────────────────╮
│ This notice is only for users who are participating in the EEG 2025          │
│ Competition.                                                                 │
│                                                                              │
│ EEG 2025 Competition Data Notice!                                            │
│ You are loading one of the datasets that is used in competition, but via     │
│ `EEGDashDataset`.                                                            │
│                                                                              │
│ IMPORTANT:                                                                   │
│ If you download data from `EEGDashDataset`, it is NOT identical to the       │
│ official                                                                     │
│ competition data, which is accessed via `EEGChallengeDataset`. The           │
│ competition data has been downsampled and filtered.                          │
│                                                                              │
│ If you are participating in the competition,                                 │
│ you must use the `EEGChallengeDataset` object to ensure consistency.         │
│                                                                              │
│ If you are not participating in the competition, you can ignore this         │
│ message.                                                                     │
╰─────────────────────────── Source: EEGDashDataset ───────────────────────────╯

Downloading sub-NDARAP457WB5_task-RestingState_channels.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAP457WB5_task-RestingState_channels.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 14.5B/s]

Downloading sub-NDARAP457WB5_task-RestingState_events.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAP457WB5_task-RestingState_events.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 12.4B/s]

Downloading sub-NDARAP457WB5_task-RestingState_eeg.json:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAP457WB5_task-RestingState_eeg.json: 100%|██████████| 1.00/1.00 [00:00<00:00, 14.1B/s]
[04/16/26 10:54:14] WARNING  File not found on S3, skipping:   downloader.py:146
                             s3://openneuro.org/ds005514/sub-N
                             DARAP457WB5/eeg/sub-NDARAP457WB5_
                             task-RestingState_eeg.fdt

Downloading sub-NDARAP457WB5_task-RestingState_eeg.set:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAP457WB5_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.46s/B]
Downloading sub-NDARAP457WB5_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.46s/B]
Used Annotations descriptions: [np.str_('boundary'), np.str_('break cnt'), np.str_('instructed_toCloseEyes'), np.str_('instructed_toOpenEyes'), np.str_('resting_start')]
[04/16/26 10:54:16] INFO     Original events found with ids: preprocessing.py:66
                             {np.str_('boundary'): 1,
                             np.str_('break cnt'): 2,
                             np.str_('instructed_toCloseEyes
                             '): 3,
                             np.str_('instructed_toOpenEyes'
                             ): 4, np.str_('resting_start'):
                             5}
NOTE: pick_channels() is a legacy function. New code should use inst.pick(...).
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 1 - 55 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: 55.00 Hz
- Upper transition bandwidth: 9.00 Hz (-6 dB cutoff frequency: 59.50 Hz)
- Filter length: 423 samples (3.305 s)

Windows for subject: 70

=== Subject NDARAU939WUK ===
╭────────────────────── EEG 2025 Competition Data Notice ──────────────────────╮
│ This notice is only for users who are participating in the EEG 2025          │
│ Competition.                                                                 │
│                                                                              │
│ EEG 2025 Competition Data Notice!                                            │
│ You are loading one of the datasets that is used in competition, but via     │
│ `EEGDashDataset`.                                                            │
│                                                                              │
│ IMPORTANT:                                                                   │
│ If you download data from `EEGDashDataset`, it is NOT identical to the       │
│ official                                                                     │
│ competition data, which is accessed via `EEGChallengeDataset`. The           │
│ competition data has been downsampled and filtered.                          │
│                                                                              │
│ If you are participating in the competition,                                 │
│ you must use the `EEGChallengeDataset` object to ensure consistency.         │
│                                                                              │
│ If you are not participating in the competition, you can ignore this         │
│ message.                                                                     │
╰─────────────────────────── Source: EEGDashDataset ───────────────────────────╯

Downloading sub-NDARAU939WUK_task-RestingState_channels.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAU939WUK_task-RestingState_channels.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 15.6B/s]

Downloading sub-NDARAU939WUK_task-RestingState_events.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAU939WUK_task-RestingState_events.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 14.3B/s]

Downloading sub-NDARAU939WUK_task-RestingState_eeg.json:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAU939WUK_task-RestingState_eeg.json: 100%|██████████| 1.00/1.00 [00:00<00:00, 15.6B/s]
[04/16/26 10:54:18] WARNING  File not found on S3, skipping:   downloader.py:146
                             s3://openneuro.org/ds005514/sub-N
                             DARAU939WUK/eeg/sub-NDARAU939WUK_
                             task-RestingState_eeg.fdt

Downloading sub-NDARAU939WUK_task-RestingState_eeg.set:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAU939WUK_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:02<00:00, 2.03s/B]
Downloading sub-NDARAU939WUK_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:02<00:00, 2.03s/B]
Used Annotations descriptions: [np.str_('boundary'), np.str_('break cnt'), np.str_('instructed_toCloseEyes'), np.str_('instructed_toOpenEyes'), np.str_('resting_start')]
[04/16/26 10:54:21] INFO     Original events found with ids: preprocessing.py:66
                             {np.str_('boundary'): 1,
                             np.str_('break cnt'): 2,
                             np.str_('instructed_toCloseEyes
                             '): 3,
                             np.str_('instructed_toOpenEyes'
                             ): 4, np.str_('resting_start'):
                             5}
NOTE: pick_channels() is a legacy function. New code should use inst.pick(...).
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 1 - 55 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: 55.00 Hz
- Upper transition bandwidth: 9.00 Hz (-6 dB cutoff frequency: 59.50 Hz)
- Filter length: 423 samples (3.305 s)

Windows for subject: 70

=== Subject NDARAW216PM7 ===
╭────────────────────── EEG 2025 Competition Data Notice ──────────────────────╮
│ This notice is only for users who are participating in the EEG 2025          │
│ Competition.                                                                 │
│                                                                              │
│ EEG 2025 Competition Data Notice!                                            │
│ You are loading one of the datasets that is used in competition, but via     │
│ `EEGDashDataset`.                                                            │
│                                                                              │
│ IMPORTANT:                                                                   │
│ If you download data from `EEGDashDataset`, it is NOT identical to the       │
│ official                                                                     │
│ competition data, which is accessed via `EEGChallengeDataset`. The           │
│ competition data has been downsampled and filtered.                          │
│                                                                              │
│ If you are participating in the competition,                                 │
│ you must use the `EEGChallengeDataset` object to ensure consistency.         │
│                                                                              │
│ If you are not participating in the competition, you can ignore this         │
│ message.                                                                     │
╰─────────────────────────── Source: EEGDashDataset ───────────────────────────╯

Downloading sub-NDARAW216PM7_task-RestingState_channels.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAW216PM7_task-RestingState_channels.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 13.9B/s]

Downloading sub-NDARAW216PM7_task-RestingState_events.tsv:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAW216PM7_task-RestingState_events.tsv: 100%|██████████| 1.00/1.00 [00:00<00:00, 14.7B/s]

Downloading sub-NDARAW216PM7_task-RestingState_eeg.json:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAW216PM7_task-RestingState_eeg.json: 100%|██████████| 1.00/1.00 [00:00<00:00, 14.5B/s]
[04/16/26 10:54:23] WARNING  File not found on S3, skipping:   downloader.py:146
                             s3://openneuro.org/ds005514/sub-N
                             DARAW216PM7/eeg/sub-NDARAW216PM7_
                             task-RestingState_eeg.fdt

Downloading sub-NDARAW216PM7_task-RestingState_eeg.set:   0%|          | 0.00/1.00 [00:00<?, ?B/s]
Downloading sub-NDARAW216PM7_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.48s/B]
Downloading sub-NDARAW216PM7_task-RestingState_eeg.set: 100%|██████████| 1.00/1.00 [00:01<00:00, 1.48s/B]
Used Annotations descriptions: [np.str_('break cnt'), np.str_('dot_no1_OFF'), np.str_('dot_no1_ON'), np.str_('dot_no2_OFF'), np.str_('dot_no2_ON'), np.str_('dot_no3_OFF'), np.str_('dot_no3_ON'), np.str_('dot_no4_OFF'), np.str_('dot_no4_ON'), np.str_('dot_no5_OFF'), np.str_('dot_no5_ON'), np.str_('dot_no6_OFF'), np.str_('dot_no6_ON'), np.str_('dot_no7_OFF'), np.str_('dot_no7_ON'), np.str_('dot_no8_OFF'), np.str_('dot_no8_ON'), np.str_('instructed_toCloseEyes'), np.str_('instructed_toOpenEyes'), np.str_('learningBlock_1'), np.str_('resting_start'), np.str_('seqLearning_start')]
[04/16/26 10:54:25] INFO     Original events found with ids: preprocessing.py:66
                             {np.str_('break cnt'): 1,
                             np.str_('dot_no1_OFF'): 2,
                             np.str_('dot_no1_ON'): 3,
                             np.str_('dot_no2_OFF'): 4,
                             np.str_('dot_no2_ON'): 5,
                             np.str_('dot_no3_OFF'): 6,
                             np.str_('dot_no3_ON'): 7,
                             np.str_('dot_no4_OFF'): 8,
                             np.str_('dot_no4_ON'): 9,
                             np.str_('dot_no5_OFF'): 10,
                             np.str_('dot_no5_ON'): 11,
                             np.str_('dot_no6_OFF'): 12,
                             np.str_('dot_no6_ON'): 13,
                             np.str_('dot_no7_OFF'): 14,
                             np.str_('dot_no7_ON'): 15,
                             np.str_('dot_no8_OFF'): 16,
                             np.str_('dot_no8_ON'): 17,
                             np.str_('instructed_toCloseEyes
                             '): 18,
                             np.str_('instructed_toOpenEyes'
                             ): 19,
                             np.str_('learningBlock_1'): 20,
                             np.str_('resting_start'): 21,
                             np.str_('seqLearning_start'):
                             22}
NOTE: pick_channels() is a legacy function. New code should use inst.pick(...).
Filtering raw data in 1 contiguous segment
Setting up band-pass filter from 1 - 55 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: 55.00 Hz
- Upper transition bandwidth: 9.00 Hz (-6 dB cutoff frequency: 59.50 Hz)
- Filter length: 423 samples (3.305 s)

Windows for subject: 77

Using valid subjects: ['NDARAC589YMB', 'NDARAC853CR6', 'NDARAE710YWG', 'NDARAH239PGG', 'NDARAL897CYV', 'NDARAN160GUF', 'NDARAP049KXJ', 'NDARAP457WB5', 'NDARAU939WUK', 'NDARAW216PM7']
Total subjects requested: 10  | collected: 10
Total windows across valid subjects: 707
Saved plot to sample_epoch.png

Train subjects: ['NDARAC589YMB', 'NDARAE710YWG', 'NDARAH239PGG', 'NDARAL897CYV', 'NDARAN160GUF', 'NDARAP049KXJ', 'NDARAP457WB5', 'NDARAW216PM7']
Test subjects : ['NDARAC853CR6', 'NDARAU939WUK']
Train windows: 567 Test windows: 140
X_train torch.Size([567, 24, 256]) | Train batches: 18 | Test batches: 5
Label balance train: 0.51 | test: 0.50
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
ShallowFBCSPNet                          [1, 2]                    --
├─Ensure4d: 1-1                          [1, 24, 256, 1]           --
├─Rearrange: 1-2                         [1, 1, 256, 24]           --
├─CombinedConv: 1-3                      [1, 40, 232, 1]           39,440
├─BatchNorm2d: 1-4                       [1, 40, 232, 1]           80
├─Square: 1-5                            [1, 40, 232, 1]           --
├─AvgPool2d: 1-6                         [1, 40, 11, 1]            --
├─SafeLog: 1-7                           [1, 40, 11, 1]            --
├─Dropout: 1-8                           [1, 40, 11, 1]            --
├─Sequential: 1-9                        [1, 2]                    --
│    └─Conv2d: 2-1                       [1, 2, 1, 1]              882
│    └─SqueezeFinalOutput: 2-2           [1, 2]                    --
│    │    └─Rearrange: 3-1               [1, 2, 1]                 --
==========================================================================================
Total params: 40,402
Trainable params: 40,402
Non-trainable params: 0
Total mult-adds (Units.MEGABYTES): 0.00
==========================================================================================
Input size (MB): 0.02
Forward/backward pass size (MB): 0.07
Params size (MB): 0.00
Estimated Total Size (MB): 0.10
==========================================================================================
Using epochs = 6 | device = cpu | batch_size = 32
Epoch 0, Train accuracy: 0.55, Test accuracy: 0.53
Epoch 1, Train accuracy: 0.62, Test accuracy: 0.53
Epoch 2, Train accuracy: 0.69, Test accuracy: 0.53
Epoch 3, Train accuracy: 0.73, Test accuracy: 0.57
Epoch 4, Train accuracy: 0.76, Test accuracy: 0.57
Epoch 5, Train accuracy: 0.77, Test accuracy: 0.56
X_train torch.Size([567, 24, 256]) | Train batches: 18 | Test batches: 5
Label balance train: 0.51 | test: 0.50
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
ShallowFBCSPNet                          [1, 2]                    --
├─Ensure4d: 1-1                          [1, 24, 256, 1]           --
├─Rearrange: 1-2                         [1, 1, 256, 24]           --
├─CombinedConv: 1-3                      [1, 40, 232, 1]           39,440
├─BatchNorm2d: 1-4                       [1, 40, 232, 1]           80
├─Square: 1-5                            [1, 40, 232, 1]           --
├─AvgPool2d: 1-6                         [1, 40, 11, 1]            --
├─SafeLog: 1-7                           [1, 40, 11, 1]            --
├─Dropout: 1-8                           [1, 40, 11, 1]            --
├─Sequential: 1-9                        [1, 2]                    --
│    └─Conv2d: 2-1                       [1, 2, 1, 1]              882
│    └─SqueezeFinalOutput: 2-2           [1, 2]                    --
│    │    └─Rearrange: 3-1               [1, 2, 1]                 --
==========================================================================================
Total params: 40,402
Trainable params: 40,402
Non-trainable params: 0
Total mult-adds (Units.MEGABYTES): 0.00
==========================================================================================
Input size (MB): 0.02
Forward/backward pass size (MB): 0.07
Params size (MB): 0.00
Estimated Total Size (MB): 0.10
==========================================================================================
Using epochs = 6 | device = cpu | batch_size = 32
Epoch 0, Train accuracy: 0.55, Test accuracy: 0.53
Epoch 1, Train accuracy: 0.62, Test accuracy: 0.53
Epoch 2, Train accuracy: 0.69, Test accuracy: 0.53
Epoch 3, Train accuracy: 0.73, Test accuracy: 0.57
Epoch 4, Train accuracy: 0.76, Test accuracy: 0.57
Epoch 5, Train accuracy: 0.77, Test accuracy: 0.56

from pathlib import Path
import os
import warnings

import numpy as np
import torch

warnings.simplefilter("ignore", category=RuntimeWarning)

os.environ.setdefault("NUMBA_DISABLE_JIT", "1")
os.environ.setdefault("MNE_USE_NUMBA", "false")
os.environ.setdefault("_MNE_FAKE_HOME_DIR", str(Path.cwd()))
(Path(os.environ["_MNE_FAKE_HOME_DIR"]) / ".mne").mkdir(exist_ok=True)

from eegdash import EEGDash, EEGDashDataset
from eegdash.paths import get_default_cache_dir
from braindecode.preprocessing import (
    preprocess,
    Preprocessor,
    create_windows_from_events,
)
from eegdash.hbn.preprocessing import hbn_ec_ec_reannotation


# -----------------------------
# Config
# -----------------------------
cache_folder = get_default_cache_dir()
cache_folder.mkdir(parents=True, exist_ok=True)
dataset_id = "ds005514"
task = "RestingState"

# number of *valid* subjects to use
num_subjects = int(os.environ.get("NUM_SUBJECTS", "10"))
num_test_subjects = int(os.environ.get("NUM_TEST_SUBJECTS", "2"))
random_state = int(os.environ.get("SEED", "42"))

# 2 seconds at 128 Hz
window_size_samples = 256

# training params
epochs = int(os.environ.get("EPOCHS", "6"))
batch_size = int(os.environ.get("BATCH_SIZE", "32"))


# -----------------------------
# Preprocessors
# -----------------------------
preprocessors = [
    hbn_ec_ec_reannotation(),
    Preprocessor(
        "pick_channels",
        ch_names=[
            "E22",
            "E9",
            "E33",
            "E24",
            "E11",
            "E124",
            "E122",
            "E29",
            "E6",
            "E111",
            "E45",
            "E36",
            "E104",
            "E108",
            "E42",
            "E55",
            "E93",
            "E58",
            "E52",
            "E62",
            "E92",
            "E96",
            "E70",
            "Cz",
        ],
    ),
    Preprocessor("resample", sfreq=128),
    Preprocessor("filter", l_freq=1, h_freq=55),
]


# -----------------------------
# Build multi-subject windows (skip empties)
# -----------------------------
eegdash = EEGDash()
records = eegdash.find(dataset=dataset_id, task=task, limit=500)
subjects_all = sorted({rec.get("subject") for rec in records if rec.get("subject")})
if len(subjects_all) == 0:
    raise RuntimeError(
        f"No subjects returned by API for dataset={dataset_id}, task={task}"
    )

print("Discovered subjects (first 20):", subjects_all[:20])

all_windows = []
all_subject_ids = []
valid_subjects = []

for subj in subjects_all:
    if len(valid_subjects) >= num_subjects:
        break

    print(f"\n=== Subject {subj} ===")
    try:
        ds_eoec = EEGDashDataset(
            dataset=dataset_id,
            task=task,
            subject=subj,
            cache_dir=cache_folder,
        )
    except AssertionError as e:
        # This happens when EEGDashDataset finds 0 recordings (empty iterable)
        print(f"[SKIP] EEGDashDataset empty for subject {subj}: {e}")
        continue
    except Exception as e:
        print(
            f"[SKIP] Failed to construct EEGDashDataset for subject {subj}: {type(e).__name__}: {e}"
        )
        continue

    try:
        preprocess(ds_eoec, preprocessors)
        windows_ds = create_windows_from_events(
            ds_eoec,
            trial_start_offset_samples=0,
            trial_stop_offset_samples=window_size_samples,  # one 2s window per event
            preload=True,
        )
    except Exception as e:
        print(
            f"[SKIP] Preprocess/windowing failed for subject {subj}: {type(e).__name__}: {e}"
        )
        continue

    n_win = len(windows_ds)
    if n_win == 0:
        print(f"[SKIP] 0 windows for subject {subj}")
        continue

    print("Windows for subject:", n_win)
    all_windows.append(windows_ds)
    all_subject_ids.extend([subj] * n_win)
    valid_subjects.append(subj)

if len(valid_subjects) < 2:
    raise RuntimeError(
        f"Only {len(valid_subjects)} valid subject(s) collected; need >=2."
    )

if num_test_subjects >= len(valid_subjects):
    raise ValueError("NUM_TEST_SUBJECTS must be < number of valid subjects found.")

print("\nUsing valid subjects:", valid_subjects)
print("Total subjects requested:", num_subjects, " | collected:", len(valid_subjects))

# Concatenate
from braindecode.datasets import BaseConcatDataset

concat_ds = BaseConcatDataset(all_windows)
print("Total windows across valid subjects:", len(concat_ds))


# -----------------------------
# Save a sanity plot (no GUI)
# -----------------------------
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt

if len(concat_ds) > 2:
    plt.figure()
    plt.plot(concat_ds[2][0][0, :].transpose())
    plt.savefig("sample_epoch.png", dpi=150, bbox_inches="tight")
    print("Saved plot to sample_epoch.png")


# -----------------------------
# Subject-wise train/test split
# -----------------------------
rng = np.random.RandomState(random_state)
subjects_shuffled = valid_subjects.copy()
rng.shuffle(subjects_shuffled)

test_subjects = set(subjects_shuffled[:num_test_subjects])
train_subjects = set(subjects_shuffled[num_test_subjects:])

print("\nTrain subjects:", sorted(train_subjects))
print("Test subjects :", sorted(test_subjects))

indices = np.arange(len(concat_ds))
subj_arr = np.array(all_subject_ids)

train_indices = indices[np.isin(subj_arr, list(train_subjects))]
test_indices = indices[np.isin(subj_arr, list(test_subjects))]

print("Train windows:", len(train_indices), "Test windows:", len(test_indices))


# -----------------------------
# Tensors + loaders
# -----------------------------
# -----------------------------
torch.manual_seed(random_state)
np.random.seed(random_state)

X_train = torch.FloatTensor(np.array([concat_ds[i][0] for i in train_indices]))
X_test = torch.FloatTensor(np.array([concat_ds[i][0] for i in test_indices]))
y_train = torch.LongTensor(np.array([concat_ds[i][1] for i in train_indices]))
y_test = torch.LongTensor(np.array([concat_ds[i][1] for i in test_indices]))

from torch.utils.data import DataLoader, TensorDataset

dataset_train = TensorDataset(X_train, y_train)
dataset_test = TensorDataset(X_test, y_test)

train_loader = DataLoader(dataset_train, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(dataset_test, batch_size=batch_size, shuffle=False)

print(
    f"X_train {X_train.shape} | Train batches: {len(train_loader)} | Test batches: {len(test_loader)}"
)
print(
    f"Label balance train: {float(y_train.float().mean()):.2f} | test: {float(y_test.float().mean()):.2f}"
)


# -----------------------------
# Model
# -----------------------------
from torch.nn import functional as F
from braindecode.models import ShallowFBCSPNet
from torchinfo import summary

model = ShallowFBCSPNet(24, 2, n_times=256, final_conv_length="auto")
summary(model, input_size=(1, 24, 256))


# -----------------------------
# Train
# -----------------------------
optimizer = torch.optim.Adamax(model.parameters(), lr=0.002, weight_decay=0.001)
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=1)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device=device)

print("Using epochs =", epochs, "| device =", device, "| batch_size =", batch_size)


def normalize_data(x):
    mean = x.mean(dim=2, keepdim=True)
    std = x.std(dim=2, keepdim=True) + 1e-7
    x = (x - mean) / std
    return x.to(device=device, dtype=torch.float32)


for e in range(epochs):
    model.train()
    correct_train = 0.0
    for x, y in train_loader:
        scores = model(normalize_data(x))
        y = y.to(device=device, dtype=torch.long)

        loss = F.cross_entropy(scores, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        scheduler.step()

        preds = scores.argmax(dim=1)
        correct_train += (preds == y).sum().item()

    model.eval()
    correct_test = 0.0
    with torch.no_grad():
        for x, y in test_loader:
            scores = model(normalize_data(x))
            y = y.to(device=device, dtype=torch.long)
            preds = scores.argmax(dim=1)
            correct_test += (preds == y).sum().item()

    train_acc = correct_train / len(dataset_train)
    test_acc = correct_test / len(dataset_test)
    print(f"Epoch {e}, Train accuracy: {train_acc:.2f}, Test accuracy: {test_acc:.2f}")

# -----------------------------
torch.manual_seed(random_state)
np.random.seed(random_state)

X_train = torch.FloatTensor(np.array([concat_ds[i][0] for i in train_indices]))
X_test = torch.FloatTensor(np.array([concat_ds[i][0] for i in test_indices]))
y_train = torch.LongTensor(np.array([concat_ds[i][1] for i in train_indices]))
y_test = torch.LongTensor(np.array([concat_ds[i][1] for i in test_indices]))

from torch.utils.data import DataLoader, TensorDataset

dataset_train = TensorDataset(X_train, y_train)
dataset_test = TensorDataset(X_test, y_test)

train_loader = DataLoader(dataset_train, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(dataset_test, batch_size=batch_size, shuffle=False)

print(
    f"X_train {X_train.shape} | Train batches: {len(train_loader)} | Test batches: {len(test_loader)}"
)
print(
    f"Label balance train: {float(y_train.float().mean()):.2f} | test: {float(y_test.float().mean()):.2f}"
)


# -----------------------------
# Model
# -----------------------------
from torch.nn import functional as F
from braindecode.models import ShallowFBCSPNet
from torchinfo import summary

model = ShallowFBCSPNet(24, 2, n_times=256, final_conv_length="auto")
summary(model, input_size=(1, 24, 256))


# -----------------------------
# Train
# -----------------------------
optimizer = torch.optim.Adamax(model.parameters(), lr=0.002, weight_decay=0.001)
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=1)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device=device)

print("Using epochs =", epochs, "| device =", device, "| batch_size =", batch_size)


def normalize_data(x):
    mean = x.mean(dim=2, keepdim=True)
    std = x.std(dim=2, keepdim=True) + 1e-7
    x = (x - mean) / std
    return x.to(device=device, dtype=torch.float32)


for e in range(epochs):
    model.train()
    correct_train = 0.0
    for x, y in train_loader:
        scores = model(normalize_data(x))
        y = y.to(device=device, dtype=torch.long)

        loss = F.cross_entropy(scores, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        scheduler.step()

        preds = scores.argmax(dim=1)
        correct_train += (preds == y).sum().item()

    model.eval()
    correct_test = 0.0
    with torch.no_grad():
        for x, y in test_loader:
            scores = model(normalize_data(x))
            y = y.to(device=device, dtype=torch.long)
            preds = scores.argmax(dim=1)
            correct_test += (preds == y).sum().item()

    train_acc = correct_train / len(dataset_train)
    test_acc = correct_test / len(dataset_test)
    print(f"Epoch {e}, Train accuracy: {train_acc:.2f}, Test accuracy: {test_acc:.2f}")

Total running time of the script: (0 minutes 48.543 seconds)

Gallery generated by Sphinx-Gallery