What’s new ===

  • “Enhancements” for new features

  • “Bugs” for bug fixes

  • “API changes” for backward-incompatible changes

Version 1.8.0 (2026-10-06)#

Enhancements#

  • moabb.datasets.PerezBlanco2026, moabb.datasets.Wang2026 and moabb.datasets.Han2026 read from NEMAR: PerezBlanco2026 and Wang2026 take the subject’s original files from the published deposits nm000304 and nm000335 (nemar_seed_cache; Wang2026 declares the deposit’s sub-<id> subject labels), and Han2026 reads NEMAR’s raw import of OpenNeuro ds007327 (on007327) before OpenNeuro. The private deposits of Brodu2012 (nm000366), Lenaig2026 (nm000333), MartinezPeon2025 (nm000332), MIBMPI2024 (nm000331) and MIND2026 (nm000337) get their nemar_id once published (#1318 by Bruno Aristimunha).

  • moabb.datasets.Tavakolan2017, moabb.datasets.Liu2025, moabb.datasets.Zhou2020 and moabb.datasets.Wu2020 take the extracted per-subject tree from the NEMAR store (fill_tree_from_store) before downloading the per-subject ZIP, so a subject already mirrored in a deposit loads with no upstream request (#1313 by Bruno Aristimunha).

  • moabb.datasets.Chang2025 passes fname="MNE-chang2025-data/files/<id>" to data_dl so the NEMAR store lookup matches the deposit’s extra MNE-chang2025-data/ prefix instead of downloading the 3.6 GB figshare ZIP (#1314 by Bruno Aristimunha).

  • moabb.datasets.BI2013a places the extracted runs the NEMAR deposit keeps (subject_NN/Session*/ + meta.yml) via fill_tree_from_store before asking for the per-session ZIPs, so a mirrored subject loads with no upstream request (#1315 by Bruno Aristimunha).

  • moabb.datasets.Mainsah2025 subclasses build their EDF file list from the active NEMAR store’s sourcedata_provenance.json (verifying each file against its recorded SHA-256, the same hash SHA256SUMS.txt lists) when the store holds them, so no SHA256SUMS.txt request to PhysioNet is made on a store-backed load (#1316 by Bruno Aristimunha).

  • The four Castillos2023 classes take the shared 4Class-CVEP/ tree from the NEMAR store (fill_tree_from_store) before downloading the shared 0.6 GB ZIP, which is now fetched at most once into the shared cache folder instead of once per class (#1317 by Bruno Aristimunha).

  • Rename moabb.datasets.Lenaig2026 to moabb.datasets.Gueho2026 (the class used the first author’s given name; Crossref 10.1109/BCI69045.2026.11435072 lists Guého, Lénaïg as first author). Lenaig2026 stays as a deprecated alias until 1.9, code is unchanged so caches/results/NEMAR paths stay valid, and the renamed pair no longer triggers the is_abbrev warning. Also fix dataset metadata/docstrings found by the W42 class-name audit: PerezBlanco2026 reference [1] title/authors, BNCI2025_002 first author (Kobler), Daly2020 year (2020), Hinss2021 data URL (Zenodo 5055046, the record the loader downloads), Han2026 record title, ErpCore2021_* METADATA (doi/year 2021), and the Ofner2017 ‘ataset’ typo (#1318 by Bruno Aristimunha).

  • moabb.datasets.Chen2017SingleFlicker, moabb.datasets.EPFLP300, moabb.datasets.Nguyen2017_V, moabb.datasets.Vagaja2023, moabb.datasets.Jeong2020, moabb.datasets.Stieger2021, moabb.datasets.Yi2025, moabb.datasets.Yang2025, moabb.datasets.Schrag2026Pediatric and moabb.datasets.Brandl2020 seed their loader cache from their NEMAR deposits (nemar_seed_cache), so a subject mirrored in a deposit loads with no upstream request; EPFLP300 and Schrag2026Pediatric check the seeded tree before downloading their per-subject ZIP, Stieger2021 returns locally seeded session files without listing Figshare, and GuttmannFlury2025 also accepts the double-nested S05/S05/... tree its deposit holds for subjects 5 and 9 (by Bruno Aristimunha).

  • Add n_jobs_data to all evaluations (default 1, unchanged behavior): it is forwarded as n_jobs to get_data(), so subjects are loaded in parallel. moabb.datasets.base.BaseDataset.get_data() already supported n_jobs but evaluations never passed it, so loading was always sequential; this matters when reads are latency-bound, e.g. raw EDF files on a network filesystem (#1250 by Bruno Aristimunha).

  • Install a moabb console script ([project.scripts] moabb = "moabb.run:main"), so benchmarks run with moabb --pipelines=./pipelines/ --evaluations WithinSession instead of python -m moabb.run ...; the command-line logic of moabb.run moved into a reusable main(argv=None) (#175 by Bruno Aristimunha).

  • Test MOABB on free-threaded CPython 3.14 (3.14t) with an allowed-to-fail CI job that also checks importing MOABB keeps the GIL disabled, and require h5py>=3.16.0 on Python 3.14+, the first h5py release with free-threaded (cp314t) wheels (#829 by Bruno Aristimunha).

  • nemar_subject_template on moabb.datasets.base.BaseDataset now also accepts an explicit {moabb_subject: bids_label} mapping, so NEMAR deposits whose BIDS subject labels no format template can produce (sub-zdvm, sub-A01, non-contiguous ids) are declared as data instead of by overriding _nemar_subject; string templates keep working unchanged. moabb.datasets.Lioi2020_XP2 drops its _nemar_subject override for the mapping, with the same labels (#1098 by Bruno Aristimunha).

  • Evaluations now log datasets they drop from the paradigm’s default list (datasets=None) at INFO instead of WARNING, so a default benchmark run no longer floods the console with expected incompatibilities; datasets passed explicitly still warn when removed (#862 by Bruno Aristimunha).

  • Evaluations now save results as soon as they are computed instead of once per dataset: in process(), each (pipeline, subject) is written to the HDF5 file as soon as all of its folds have finished, so a crash later in a dataset no longer loses the subjects already evaluated. The file layout, the saved values and the overwrite / resume behaviour are unchanged: a rerun skips the saved subjects and computes only the missing ones. Writes still happen only in the main process, also with n_jobs > 1 (#419 by Bruno Aristimunha).

  • Fetch PhysioNet /files/ downloads (moabb.datasets.PhysionetMI, Mainsah2025) from the byte-identical AWS Open Data mirror physionet-open.s3.amazonaws.com first, falling back to physionet.org on any mirror error; physionet.org often serves eegmmidb at 10-40 kB/s and times out mid-file. Set MOABB_S3_MIRROR=0 to disable (#1245 by Bruno Aristimunha).

  • Add moabb.datasets.Wang2026Handwriting, a Chinese handwriting-imagery dataset from Wang et al. (2026, Scientific Data, CC-BY 4.0; figshare, BIDS): 21 healthy adults in two handwriting-imagery tasks – CCS-HI (five Chinese character strokes) and SV-HI (six Pinyin vowels) – each recorded in two sessions, 40 trials of 4 s per class, 32-channel 10-10 EEG at 1 kHz. The two BIDS tasks share a session and carry no run entity, so the loader keys them by task; the release’s trial_type labels are normalized to the class names and the two sessions with fewer valid trials (sub-06 ses-02 SV-HI, sub-13 ses-02 CCS-HI) are kept as released (#1244 by LiQing).

  • Add standalone moabb.paradigms.CognitiveTask for non-motor endogenous tasks (handwriting/stroke imagery, mental arithmetic, mental rotation, word generation, music imagery). It subclasses BaseParadigm directly — not motor imagery — with MotorImagery-matching 8-32 Hz defaults, N-class event logic, and validity on both paradigm="cognitive" and the existing paradigm="imagery" tag (follows the Cued Cognitive Tasks branch of #1056). moabb.datasets.Shin2017B (mental arithmetic vs rest), moabb.datasets.BNCI2024_001 (10-letter handwritten character task) and moabb.datasets.Wang2026Handwriting (Chinese stroke/vowel handwriting imagery) flip to paradigm="cognitive" with a new summary_cognitive.csv table, updated docstrings/METADATA, and runtime/docs/test allowlists (neural-signature handler, bubble-plot colormap, metadata catalog checks).

  • moabb.datasets.Dreyer2023 (and its A/B/C subclasses) now load their EEG from NEMAR’s nm000250 sourcedata/ store when the download provider allows it, falling back to the OSF host exactly like other NEMAR-mirrored datasets; this stops MOABB’s docs/example builds from failing on OSF’s files.de-1.osf.io 429 rate limiting. get_subject_info() reads the demographic performance.csv from the store once it is published there, else falls back to OSF (#1237 by Bruno Aristimunha).

  • Add moabb.datasets.Peters2026, the BCI-FIT longitudinal P300 calibration dataset (Peters et al., OpenNeuro ds007720, CC0; NEMAR mirror on007720): five participants with ALS, 40 visits (6-10 per participant), 88 Matrix and RSVP calibration runs recorded with BciPy 2.0 and dry Wearable Sensing caps at 300 Hz, 66,150 flashes (4,109 Target / 62,041 NonTarget). Sessions are visits; runs are named <k>matrix / <k>rsvp so analyses can split by layout. The closed-loop CopyPhrase runs are not loaded. Two participants switched between the DSI-Flex (7 channels) and the DSI-24 (19 channels) across visits and the caps share only Cz, P3, Pz and P4, so the default (code Peters2026) returns all visits with those four channels, while cap="DSI-24" (Peters-dsi24-2026; 32 visits, 19 channels) and cap="DSI-Flex" (Peters-dsiflex-2026; 8 visits, 7 channels) return one cap’s visits with its full montage. The DSI-Flex channel the README calls FCz is kept as Cz, as in the files and BciPy’s device definition (FCz is that cap’s ground). The RSVP P300 peaks later than the Matrix one (median 92 ms) while the flash-locked visual response does not, so it is a genuine latency difference and event times are used as released. Dataset proposed by LiQing in #1243 (#1274 by Bruno Aristimunha).

  • Add moabb.datasets.Yagan2023, a P300 row-column speller dataset (Mendeley Data, CC-BY 4.0; 18 healthy subjects, 160-character copy-spelling in 10 blocks, 32-channel BrainVision EEG at 1 kHz, ~560k intensification events with an exact 2/13 target ratio per block). S14 target markers are stamped 2-3 ms after their flash in the released marker files, so each S1-S13 flash is relabelled Target/NonTarget by pairing it with any S14 within 10 ms (#1220 by LiQing).

  • Document a download-free, paradigm-only metadata preflight using the existing MotorImagery.is_valid API, including explicit n_classes semantics and unknown evaluation compatibility; add a no-I/O regression test for the recipe (by Bruno Aristimunha).

  • Ship MOABB’s reference benchmark pipeline configs as package data and add moabb.pipelines.get_benchmark_pipelines(), so published baseline pipelines are available from normal wheel/sdist installs instead of only from a source checkout. Pipeline parsing now also accepts a single .py config and rejects valid-but-empty config directories instead of silently returning no pipelines (#1149 by lindicaphxag-tech).

  • Accept a metadata-aware splitter class (one that declares metadata_columns and implements split(self, y, metadata)) as cv_class in CrossSubjectEvaluation, CrossSubjectSplitter and CrossSessionSplitter. It is built with cv_kwargs and called directly instead of being wrapped as the inner groups-CV, so it can use any metadata column and leave rows out of both train and test (e.g. test one target subject on one session and drop its other sessions). Ordinary scikit-learn CVs keep the inner-CV path (#1104 by Bruno Aristimunha).

  • Allow CrossSubjectEvaluation to accept an optional top-level splitter instance, enabling transfer-learning protocols to reuse MOABB’s existing caching, parallel execution, and result handling while preserving the default protocol (#1088 by lindicaphxag-tech).

  • Add Leelakittisin2025 sit-stand transition imagery, PerezBlanco2026 wrist motor-execution, and Vagaja2023 VR motor-imagery datasets (:pr:`1199`) (by Bruno Aristimunha).

  • Add MartinezPeon2025, MILimbEEG dataset loaders with synthetic regression coverage ({gh}`1198` by Bruno Aristimunha).

  • Add Pan2023, Pan2025, PoloHortiguela2025 dataset loaders with synthetic regression coverage ({gh}`1197` by Bruno Aristimunha).

  • Add MIMED2024 and WRCC2023 MI-A/MI-B/MI-C motor-imagery datasets with isolated recording windows (#1196, by Bruno Aristimunha).

  • Add moabb.datasets.MIND2026, moabb.datasets.MOVING2024, moabb.datasets.NeBULA2025, and moabb.datasets.Thapa2025 large-recording motor-imagery and motor-execution datasets. Preserve the MIND acquisition-restart guard and document execution-only tasks (#1195 by Bruno Aristimunha).

  • Add Batista2022, Farabbi2020, Han2026, Kodera2023 and Kueper2024 dataset loaders (#1194).

  • Add Brodu2012, IMUMIA2026, Leeuwis2021, MartinezPeon2024 and PardoGarcia2026 motor-imagery dataset loaders by Bruno Aristimunha in #1193.

  • Add the continuous-recording datasets.Shin2022 and datasets.NeuroTUMBCI2025 motor-imagery datasets (#1192, by Bruno Aristimunha).

  • Add the offline calibration subsets of Perdikis2018 and Alawieh2025 (#1191) (by Bruno Aristimunha).

  • Add Jia2019, MIBMPI2024, Ortiz2023 and Wang2025 motor-imagery loaders (#1190 by Bruno Aristimunha).

  • Add three OpenNeuro motor-imagery datasets: moabb.datasets.Peterson2020, moabb.datasets.Daly2020, and moabb.datasets.Damm2026 (:pr:`1189`, by Bruno Aristimunha).

  • Add moabb.datasets.NETBCI2026 (NETBCI, Recherche Data Gouv doi:10.57745/RBJRC7): 19 subjects, 74-channel EEG, right-hand motor imagery vs rest over 4 longitudinal sessions x 6 online feedback runs; per-subject EEG is read out of the 49 GB archive by HTTP range requests (#1188 by Bruno Aristimunha).

  • Add four OpenNeuro motor-imagery datasets: moabb.datasets.Lee2022 (ds004022, 7 orthopedic-impairment patients, 4 upper-limb MI tasks), moabb.datasets.Lioi2020_XP1 (ds002336) and moabb.datasets.Lioi2020_XP2 (ds002338), the two EEG-fMRI right-hand MI / neurofeedback experiments of Lioi et al., and moabb.datasets.Iwama2023 (ds004444, 30 subjects, 129-channel HD-EEG, up to 16 sessions) (#1186 by Bruno Aristimunha).

  • Audit every dataset’s METADATA and docstring against its primary paper and data-repository record: country is now an ISO 3166-1 alpha-2 code everywhere, loader declarations (sessions_per_subject, subjects, runs_per_session) and the summary_*.csv tables match what each loader actually returns, and per-dataset fields (reference/ground, hardware, filters, licence, demographics, trial counts, DOIs) are corrected with quoted evidence; Cattan2019_VR declares its two sessions, Nakanishi2015 exposes subject 10, PhysionetMI/MAMEM3/BI2015b metadata now come from their own records (#1203 by Bruno Aristimunha).

  • Add moabb.analysis.plotting.plot_critical_difference() for comparing pipelines across complete multi-dataset benchmarks with Friedman ranks and Nemenyi critical-difference groups (#1127 by lindicaphxag-tech).

  • Spell MNE’s renamed template montages everywhere: MNE 1.13 renamed standard_1005/standard_1020 (and the other standard_* templates) to colin27_* (identical electrode files), warns on the old names and MNE 1.14 removes them, so every make_standard_montage/set_montage call in MOABB now spells colin27_*. METADATA montage labels are descriptive and unchanged (#1200 by Bruno Aristimunha).

  • Add moabb.datasets.Ma2022, the SHU cross-session motor-imagery dataset: 25 subjects, five sessions, 32 EEG channels at 250 Hz and 11,988 retained 4 s trials. Read the authors’ EDF release and BIDS events from NEMAR nm000288 (publication pending), preserving bad-channel flags. These are authors-preprocessed, concatenated imagery windows, not continuous amplifier recordings. The Ma-edf2022 code prevents reuse of legacy MATLAB caches; there is no MATLAB fallback (#1178 by LiQing and Bruno Aristimunha)

  • Document how to add a BIDS dataset: the BaseBIDSDataset docstring now names moabb.datasets.Zhou2016 as the template subclass and lists the steps, and the dataset-submission section of the data summary links to it (#726 by Bruno Aristimunha).

  • Finish moving the BIDS conversion example to the public dataset API: it locates the original files with data_path() instead of reading MNE_DATA and hard-coding the MNE-alexeeg-data folder, documents the format and overwrite options of convert_to_bids(), and points to LocalBIDSDataset for loading the BIDS copy back (#1082 by Bruno Aristimunha).

  • Make GitHub’s “Cite this repository” export the MOABB software citation as BibTeX @misc instead of @software, which many bibliography styles drop together with the DOI: CITATION.cff gains a preferred-citation of type generic that reuses the same authors, title, version and DOI, matching the @misc entries already in the README and Citing MOABB and related publications; the BibTeX block in Citing MOABB and related publications is re-indented with spaces (#1071 by Bruno Aristimunha).

  • Explain the default 8-32 Hz band-pass of moabb.paradigms.MotorImagery and moabb.paradigms.LeftRightImagery in a Notes section: it keeps the mu and beta rhythms, limits muscle and ocular artifacts, and matches the MOABB benchmark, and the section shows how to change it (fmin/fmax), disable it (fmin=None, fmax=None) or use a filter bank. The defaults are unchanged (#533 by Bruno Aristimunha).

API changes#

Requirements#

Bugs#

  • moabb.datasets.BNCI2015_004 names its five mental tasks again (word association, mental subtraction, spatial navigation, right hand and feet imagery, coded 1-5 as in the .mat files) and epochs the imagery period [3, 10] s; since v1.5 the events were those of another dataset, the window covered the fixation cross, and moabb.paradigms.MotorImagery rejected the dataset (#1323 by Bruno Aristimunha)

  • moabb.datasets.Wang2016 lists all 35 subjects (S35.mat was never loaded) (#1323 by Bruno Aristimunha)

  • moabb.datasets.BNCI2014_002 cites Steyrl et al. 2016 (DOI 10.1515/bmt-2014-0117) again, not a 2012 review (#1323 by Bruno Aristimunha)

  • METADATA.experiment.events of moabb.datasets.BNCI2014_002, moabb.datasets.BNCI2014_004, moabb.datasets.BNCI2014_008, moabb.datasets.BNCI2014_009 and moabb.datasets.BI2015b equal the datasets’ event_id (#1323 by Bruno Aristimunha)

  • Fix the trial and session counts of the dataset summary tables and of moabb.analysis.plotting.dataset_bubble_plot(). #Trials / class counts every session and run of one subject, but the bubble size multiplied it again by #Sessions (BNCI2014_004 was drawn with 3,600 trials per subject instead of 720, EPFLP300 with 13,216 instead of 3,304), and scripts/generating_metainfo.py computed Total_trials as #Sessions x #Runs x the first subject’s trials of all sessions x #Subj (62,208 instead of 5,184 for BNCI2014_001). The rows of BNCI2014_001, BNCI2014_002, BNCI2014_004, BNCI2015_001, Cho2017, Lee2019_MI, PhysionetMI, Schirrmeister2017, Shin2017A, Weibo2014, Zhou2016, MAMEM1, MAMEM2, MAMEM3, BI2013a, BI2014a, BI2015a, Cattan2019_VR, Huebner2017, Huebner2018 and Sosulski2019 now give the counts the loaders return, checked on every subject (Huebner2017 and Huebner2018 gave the trials of one run, 364 NT / 112 T, instead of 9,826 NT / 3,024 T and 10,920 NT / 3,360 T over three sessions). The rows of BCIComp2020UpperLimb, Brandl2020, Dreyer2023 (and A, B, C), Gao2026, GuttmannFlury2025_MI, GuttmannFlury2025_ME, GuttmannFlury2025_SSVEP, HefmiIch2025, Jeong2020, Ofner2017, Tavakolan2017, Wang2026Handwriting, Yang2025, Zhou2020 and Zuo2025 (a per-session or per-run #Trials / class; Yang2025 and Wang2026Handwriting also list the 2 and 11 classes their loaders return by default) and of BNCI2015_004, Kaneshiro2015, Liu2025 and Shin2017B (a Total_trials multiplied by the sessions or runs, or missing the subjects) now follow the counts their documentation states. When subjects have different numbers of sessions, #Sessions now starts with that of a typical subject, the number the plots read: BI2013a 1 (8 for subjects 1-7) instead of 8 for subjects 1-7 else 1, Sosulski2019 60 instead of 80, and BNCI2015_001, Gao2026, GuttmannFlury2025_*, Kaya2018, Liu2025, Ortiz2023, Peters2026, RomaniBF2025ERP and Thapa2025. The column definitions of Data Summary state these conventions (#1310 by Bruno Aristimunha).

  • moabb.datasets.Lee2021Mobile_ERP and Lee2021Mobile_SSVEP now always download the original recordings from OSF: the project (r7s9b) stores every BrainVision file twice under the same file name, as the BIDS copy /sub-XX/ses-YY/eeg/ (73 channels at 100 Hz, EEG merged with the IMU channels) and as /sourcedata/sub-XX/ses-YY/eeg/ (50 channels at 500 Hz). The listing kept only file names and the loader took whichever copy OSF listed first, so which recording a subject got depended on OSF’s listing order, and the .vhdr, .eeg and .vmrk of one recording could come from different copies. moabb.datasets.download.create_metainfo_osf() now returns each file’s OSF path, the loader takes the BrainVision files from /sourcedata/ only and raises FileNotFoundError when one of them is not listed there, and force_update=True now replaces files already in the cache. Cached files keep their location and names. METADATA now says 500 Hz and 48 EEG + 2 EOG channels, which is what the loader returns, instead of the 100 Hz / 73 channels of the BIDS copy (#1297 by Bruno Aristimunha).

  • Fix moabb.datasets.Kodera2023 returning two sampling rates: the 14 subjects of the 2020-2021 recordings are at 500 Hz and the 15 of the 2021-2023 recordings at 1000 Hz (checked on every header of the archive), while METADATA said 500 Hz. Without a paradigm resample, a call mixing both setups built epochs of 2001 and 4001 samples and stopped in np.concatenate. By default all 29 subjects now return the nine shared channels at 500 Hz (the 1000 Hz recordings are resampled when loaded). The new cohort option returns one setup with its full montage: cohort="16ch" (code Kodera-16ch2023) the 14 2020-2021 subjects with all 16 scalp channels at 500 Hz, cohort="9ch" (code Kodera-9ch2023) the 15 2021-2023 subjects at the native 1000 Hz; a subject of the other setup raises a ValueError and downloads are shared. Cached Kodera2023 epochs and results of subjects 3-10, 16, 17, 22, 23 and 27-29 computed at 1000 Hz must be deleted and recomputed (#1276 by Bruno Aristimunha).

  • Fix moabb.datasets.PerezBlanco2026 returning one run at another sampling rate: sub-09 run-01 is stored at about 1200 Hz (EDF header 1199.4 Hz) while the other 447 runs of the 45 subjects are at 512 Hz, so any paradigm call including subject 9 stopped in np.concatenate (“the array at index 0 has size 1281 and the array at index 2 has size 3000”). The loader now resamples any run that is not at 512 Hz to 512 Hz after locating its events. Cached PerezBlanco2026 epochs and results of subject 9 must be deleted and recomputed (#XXXX by Bruno Aristimunha).

  • Fix moabb.datasets.PerezBlanco2026 returning one run at another sampling rate: sub-09 run-01 is stored at about 1200 Hz (EDF header 1199.4 Hz) while the other 447 runs of the 45 subjects are at 512 Hz, so any paradigm call including subject 9 stopped in np.concatenate (“the array at index 0 has size 1281 and the array at index 2 has size 3000”). The loader now resamples any run that is not at 512 Hz to 512 Hz after locating its events. Cached PerezBlanco2026 epochs and results of subject 9 must be deleted and recomputed (#1277 by Bruno Aristimunha).

  • moabb.datasets.Zhang2017 no longer guesses channel names for files it was not checked against. Every released file has 17 channels and an empty BCI2000 ChannelNames parameter (all 180 .dat files of the 12 subjects, checked) and keeps the EGI sensor order E1-E16, E28. Before, a file with another channel count got EEG001.. names and a file with header names got those labels, which would have given its subject a channel set of its own; such files now raise a ValueError. The returned data are unchanged (#XXXX by Bruno Aristimunha).

  • moabb.datasets.Zhang2017 no longer guesses channel names for files it was not checked against. Every released file has 17 channels and an empty BCI2000 ChannelNames parameter (all 180 .dat files of the 12 subjects, checked) and keeps the EGI sensor order E1-E16, E28. Before, a file with another channel count got EEG001.. names and a file with header names got those labels, which would have given its subject a channel set of its own; such files now raise a ValueError. The returned data are unchanged (#1278 by Bruno Aristimunha).

  • Fix the Castillos2023 c-VEP loaders (moabb.datasets.CastillosBurstVEP100, CastillosBurstVEP40, CastillosCVEP100, CastillosCVEP40) failing for every subject with ValueError: The event_id dictionary has values that do not match any event codes ... Missing event codes: [0]: the trial labels were a view of epochs.events, so making them 0-based also rewrote the epochs’ event codes, which MNE now checks when the epochs are read. The labels are now a copy, and all 12 subjects of the four datasets load again (#XXXX by Bruno Aristimunha).

  • The Castillos2023 c-VEP datasets (moabb.datasets.CastillosBurstVEP100, CastillosBurstVEP40, CastillosCVEP100, CastillosCVEP40) now return the same 28 EEG channels, in one order, for every subject. The released files use two 32-channel layouts: subjects 1, 5, 6, 7, 11 and 12 carry the LiveAmp-32 labels (F9, P9, P10, F10) and subjects 2, 3, 4, 8, 9 and 10 the actiCAP-32 labels (FT9, TP9, TP10, FT10), in a different channel order, so arrays mixing the two groups stacked different electrodes at the same index (e.g. index 9 was CP5 for subject 1 and TP9 for subject 2) and return_epochs=True failed with “epochs[1].info[‘ch_names’] must match”. The four layout-specific electrodes are dropped; METADATA and the summary table list 28 channels. Cached Castillos2023 epochs and saved results must be deleted and recomputed (#XXXX by Bruno Aristimunha).

  • The Castillos2023 c-VEP datasets (moabb.datasets.CastillosBurstVEP100, CastillosBurstVEP40, CastillosCVEP100, CastillosCVEP40) now return the same 28 EEG channels, in one order, for every subject. The released files use two 32-channel layouts: subjects 1, 5, 6, 7, 11 and 12 carry the LiveAmp-32 labels (F9, P9, P10, F10) and subjects 2, 3, 4, 8, 9 and 10 the actiCAP-32 labels (FT9, TP9, TP10, FT10), in a different channel order, so arrays mixing the two groups stacked different electrodes at the same index (e.g. index 9 was CP5 for subject 1 and TP9 for subject 2) and return_epochs=True failed with “epochs[1].info[‘ch_names’] must match”. The four layout-specific electrodes are dropped; METADATA and the summary table list 28 channels. Cached Castillos2023 epochs and saved results must be deleted and recomputed (#1282 by Bruno Aristimunha).

  • Fix the Castillos2023 c-VEP loaders (moabb.datasets.CastillosBurstVEP100, CastillosBurstVEP40, CastillosCVEP100, CastillosCVEP40) failing for every subject with ValueError: The event_id dictionary has values that do not match any event codes ... Missing event codes: [0]: the trial labels were a view of epochs.events, so making them 0-based also rewrote the epochs’ event codes, which MNE now checks when the epochs are read. The labels are now a copy, and all 12 subjects of the four datasets load again (#1279 by Bruno Aristimunha).

  • Fix moabb.datasets.PhysionetMI failing in every paradigm call that mixes subjects 88, 92 or 100 with other subjects: these three subjects were recorded at 128 Hz (every imagery and execution run) and the other 106 at 160 Hz, so LeftRightImagery().get_data(PhysionetMI(), subjects=[1, 88]) stopped in np.concatenate (“the array at index 0 has size 481 and the array at index 6 has size 385”) and return_epochs=True in concatenate_epochs (“epochs[6].info[‘sfreq’] must match”); the docstring only mentioned subject 88. The loader now resamples any run that is not at 160 Hz to 160 Hz, so all 109 subjects load together. Cached epochs and saved results that include subjects 88, 92 or 100 must be deleted and recomputed (#XXXX by Bruno Aristimunha).

  • Fix moabb.datasets.PhysionetMI failing in every paradigm call that mixes subjects 88, 92 or 100 with other subjects: these three subjects were recorded at 128 Hz (every imagery and execution run) and the other 106 at 160 Hz, so LeftRightImagery().get_data(PhysionetMI(), subjects=[1, 88]) stopped in np.concatenate (“the array at index 0 has size 481 and the array at index 6 has size 385”) and return_epochs=True in concatenate_epochs (“epochs[6].info[‘sfreq’] must match”); the docstring only mentioned subject 88. The loader now resamples any run that is not at 160 Hz to 160 Hz, so all 109 subjects load together. Cached epochs and saved results that include subjects 88, 92 or 100 must be deleted and recomputed (#1280 by Bruno Aristimunha).

  • Fix the BigP3BCI loaders (moabb.datasets.Mainsah2025_A to moabb.datasets.Mainsah2025_S2) returning slightly different sampling rates from run to run (e.g. 256.0, 256.00008 and 256.00009 Hz within one subject): every EDF stores the run as one data record whose 8-character duration field is truncated, so the rate MNE derives from the header is off by less than 1 ppm and differs per file. Arrays were unaffected, but return_epochs=True (and anything concatenating epochs, e.g. deep-learning pipelines) stopped with “epochs[3].info[‘sfreq’] must match” even for a single subject. Runs whose header rate is explained by this truncation are now relabelled at exactly 256 Hz (no resampling; the samples are unchanged), and any other rate raises. Cached Mainsah2025-* epochs computed before this fix carry the per-file rates and should be deleted and recomputed (#XXXX by Bruno Aristimunha).

  • Fix the BigP3BCI loaders (moabb.datasets.Mainsah2025_A to moabb.datasets.Mainsah2025_S2) returning slightly different sampling rates from run to run (e.g. 256.0, 256.00008 and 256.00009 Hz within one subject): every EDF stores the run as one data record whose 8-character duration field is truncated, so the rate MNE derives from the header is off by less than 1 ppm and differs per file. Arrays were unaffected, but return_epochs=True (and anything concatenating epochs, e.g. deep-learning pipelines) stopped with “epochs[3].info[‘sfreq’] must match” even for a single subject. Runs whose header rate is explained by this truncation are now relabelled at exactly 256 Hz (no resampling; the samples are unchanged), and any other rate raises. Cached Mainsah2025-* epochs computed before this fix carry the per-file rates and should be deleted and recomputed (#1281 by Bruno Aristimunha).

  • moabb.datasets.Ofner2017 returns the same channel names in the same order for every run, which return_all_modalities needs: subject 1’s execution runs store the EOG channels as eog-r, eog-m, eog-l (all other runs: eog-l, eog-m, eog-r), so arrays with EOG swapped eog-l and eog-r, and its imagination runs hold 13 placeholder channels armeodummy-0..``armeodummy-12`` (random values) instead of the Armeo arm-kinematics channels; MotorImagery().get_data(Ofner2017(return_all_modalities=True), subjects=[1], return_epochs=True) failed with “epochs[10].info[‘ch_names’] must match”. The EOG channels are now reordered by name and the placeholders renamed to the Armeo names and set to 0, like the (empty) Armeo channels of the other runs; METADATA lists the Armeo names. The default EEG-only output is unchanged. Cached Ofner2017 epochs and results computed with return_all_modalities must be deleted and recomputed (#XXXX by Bruno Aristimunha).

  • moabb.datasets.Ofner2017 returns the same channel names in the same order for every run, which return_all_modalities needs: subject 1’s execution runs store the EOG channels as eog-r, eog-m, eog-l (all other runs: eog-l, eog-m, eog-r), so arrays with EOG swapped eog-l and eog-r, and its imagination runs hold 13 placeholder channels armeodummy-0..``armeodummy-12`` (random values) instead of the Armeo arm-kinematics channels; MotorImagery().get_data(Ofner2017(return_all_modalities=True), subjects=[1], return_epochs=True) failed with “epochs[10].info[‘ch_names’] must match”. The EOG channels are now reordered by name and the placeholders renamed to the Armeo names and set to 0, the value returned for runs without Armeo recording (NaN in 270 of the 300 released runs); METADATA lists the Armeo names. The default EEG-only output is unchanged. Cached Ofner2017 epochs and results computed with return_all_modalities must be deleted and recomputed (#XXXX by Bruno Aristimunha).

  • moabb.datasets.Ofner2017 returns the same channel names in the same order for every run, which return_all_modalities needs: subject 1’s execution runs store the EOG channels as eog-r, eog-m, eog-l (all other runs: eog-l, eog-m, eog-r), so arrays with EOG swapped eog-l and eog-r, and its imagination runs hold 13 placeholder channels armeodummy-0..``armeodummy-12`` (random values) instead of the Armeo arm-kinematics channels; MotorImagery().get_data(Ofner2017(return_all_modalities=True), subjects=[1], return_epochs=True) failed with “epochs[10].info[‘ch_names’] must match”. The EOG channels are now reordered by name and the placeholders renamed to the Armeo names and set to 0, the value returned for runs without Armeo recording (NaN in 270 of the 300 released runs); METADATA lists the Armeo names. The default EEG-only output is unchanged. Cached Ofner2017 epochs and results computed with return_all_modalities must be deleted and recomputed (#1283 by Bruno Aristimunha).

  • Fix moabb.datasets.Chang2025 silently skipping subjects 10, 15, 19, 20, 24 and 25 on a fresh download (“Subject directory for S211 not found … Skipping”): the figshare ZIPs are split by original subject ID (S201-S230, without S207 and S218), but the loader chose the ZIP by MOABB subject number, so these six subjects fetched the neighbouring ZIP and returned no data unless the next subject had been loaded first. The ZIP is now chosen by original ID. Results computed with these subjects missing should be recomputed (#XXXX by Bruno Aristimunha).

  • Fix moabb.datasets.Chang2025 silently skipping subjects 10, 15, 19, 20, 24 and 25 on a fresh download (“Subject directory for S211 not found … Skipping”): the figshare ZIPs are split by original subject ID (S201-S230, without S207 and S218), but the loader chose the ZIP by MOABB subject number, so these six subjects fetched the neighbouring ZIP and returned no data unless the next subject had been loaded first. The ZIP is now chosen by original ID. Results computed with these subjects missing should be recomputed (#1284 by Bruno Aristimunha).

  • The NEMAR sourcedata prefetch of get_data() now also fetches the files a deposit’s provenance attributes to no subject (montages, band and interval definitions, shared tables), which every subject’s load reads: before, a per-subject prefetch skipped them and the loader fetched them from the upstream host, e.g. Brandl2020’s mnt.mat from DepositOnce with the default "auto" provider, or failed under "nemar". A store that lacks them is no longer reported as complete (by Bruno Aristimunha).

  • moabb.datasets.download.download_if_missing() now takes a missing file from the active NEMAR store, as data_dl() already did, so a file a deposit keeps in its sourcedata/ under the URL’s trailing path segments no longer comes from the upstream host. This serves Mainsah2025’s PhysioNet index SHA256SUMS.txt from sourcedata/SHA256SUMS.txt of each study’s deposit once that file is added there, the last request those loaders still sent to PhysioNet with the default provider (#1294 by Bruno Aristimunha).

  • Archive-based loaders no longer download the archive when NEMAR already served its contents: NEMAR deposits mirror the extracted members, not the .zip, so the URL lookup in moabb.datasets.download.data_dl() could not match and BI2012, BI2014a, BI2014b, BI2015a, BI2015b, BNCI2019_001, BNCI2025_001 and Hinss2021 fetched the whole archive from the upstream host even with the default "auto" provider. They now ask the new moabb.datasets.download.fill_from_store() for the members they read (located with the new data_dl_destination()) and download only when the store lacks one of them; members are linked all-or-nothing, so a partial deposit never leaves a half-extracted tree (by Bruno Aristimunha).

  • moabb.datasets.Huebner2017, moabb.datasets.Huebner2018, moabb.datasets.Liu2024 and moabb.datasets.Sosulski2019 no longer download the Zenodo subjectXX.zip / figshare edffile.zip / FreiDok archive when NEMAR (nm000199, nm000195, nm000158, nm000266) or an earlier extraction already holds the files they read: data_path now looks for the extracted subject directory (Huebner, Sosulski2019) or EDF (Liu2024) first, through moabb.datasets.download.fill_from_store() and the new moabb.datasets.download.fill_tree_from_store(), which places a whole extracted directory from the store and refuses one whose BrainVision/EEGLAB headers name files the store lacks. Liu2024 now extracts only the archive members not on disk yet, so a later extraction does not write through the links into the NEMAR store, and Sosulski2019 extracts the archive again when a subject’s inner ZIP is missing instead of whenever extracted/ is absent (#1293 by Bruno Aristimunha).

  • Nieto2022 and TrianaGuzman2024 declared their NEMAR raw mirrors (on003626, on005342) but always downloaded from OpenNeuro’s S3 bucket; like the other OpenNeuro loaders they now read the mirror first under the "auto" and "nemar" providers and keep OpenNeuro as the fallback and for "upstream" (by Bruno Aristimunha).

  • Chailloux2020, Nieto2022 and TrianaGuzman2024 declared their NEMAR raw mirrors (on003190, on003626, on005342) but always downloaded from OpenNeuro’s S3 bucket; like the other OpenNeuro loaders they now read the mirror first under the "auto" and "nemar" providers and keep OpenNeuro as the fallback and for "upstream" (by Bruno Aristimunha).

  • moabb.datasets.HefmiIch2025 and moabb.datasets.AguileraRodriguez2025 read what their NEMAR deposits (nm000347, nm000174) hold instead of asking figshare/Mendeley again. HefmiIch2025 fetched its figshare file list with a bare requests.get on api.figshare.com; it now caches it as figshare_filelist_28955456.json (like the MAMEM loaders), takes it from the deposit’s sourcedata/ when present, else from moabb.datasets.download.fs_get_file_list(), and downloads each epoch file under its local name so the store serves it. The URL lookup of moabb.datasets.download.data_dl() also accepts a stored file that only adds an extension to an extension-less URL name (exactly one candidate, two or more path segments): the AguileraRodriguez2025 deposit keeps Mendeley’s <uuid>/file_downloaded as file_downloaded.edf/.xdf (#1295 by Bruno Aristimunha).

  • moabb.datasets.Peterson2020 loads again with the default "auto" download provider: the NEMAR mirror (on003810) ships the BIDS events.tsv sidecars, so read_raw_bids replaced the EDF’s OVTK_GDF_Right/OVTK_GDF_Tongue annotations with the sidecar values 7/9 and every load stopped with ValueError: Could not find any of the events you specified. Both codings now map to motor_imagery/rest (by Bruno Aristimunha).

  • Fix every moabb.datasets.Thapa2025 load failing with “The header file must be given to read the data, not a file with extension ‘.eeg’”: data_path listed the runs with mne_bids.find_matching_paths() without an extension filter, so it returned each run’s _eeg.eeg and _eeg.vmrk beside its _eeg.vhdr and read_raw_brainvision was handed the binary file. It now returns the .vhdr headers only; this affected both the Figshare archive and the NEMAR nm000324 copy (#1292 by Bruno Aristimunha).

  • With the default "auto" download provider, 28 datasets whose loader caches files under names their download URLs do not carry no longer re-download from the upstream host what NEMAR already served: NEMAR deposits are snapshots of the loader’s own cache (extracted archives, files named after figshare/Dataverse/Mendeley ids), which the URL-tail lookup of moabb.datasets.download.data_dl() cannot match. With the new class attribute nemar_seed_cache = True, get_data() links the subject’s prefetched store files into the loader cache at the location the deposit’s provenance records (upstream_cache_root), using the new moabb.datasets.download.seed_loader_cache(); recordings whose headers name files the deposit lacks (BrainVision, EEGLAB, Curry) are not placed. Seeding is opt-in because a loader that skips its download once anything is cached would load less data from an incomplete deposit (seen for Lee2021Mobile and BI2013a); it is enabled for the loaders an audit of every nemar_id verified to load the same runs from NEMAR alone: Batista2022, Beetl2021_A/B, Farabbi2020, GuttmannFlury2025_ME/MI/P300, Han2024Fatigue, Jia2019, Kaya2018, Liu2020BETA, Liu2022EldBETA, Ma2020, MILimbEEG, Mainsah2025 (all studies; the EDF list still comes from PhysioNet’s SHA256SUMS.txt), Nguyen2017_S/SL, Ortiz2023, PoloHortiguela2025, Pressel2016, RomaniBF2025ERP, Rozado2015, Simoes2020, Speier2017, Wairagkar2018, Wang2021Combined, Zhang2017 and Zuo2025 (by Bruno Aristimunha).

  • Enable nemar_seed_cache for moabb.datasets.NETBCI2026, moabb.datasets.NeBULA2025, moabb.datasets.Kodera2023, moabb.datasets.IMUMIA2026, moabb.datasets.MAMEM1, moabb.datasets.MAMEM2, moabb.datasets.MAMEM3, moabb.datasets.Kojima2024A, moabb.datasets.Kojima2024B and moabb.datasets.Thapa2025, so with the default "auto" provider they load from NEMAR alone instead of fetching the archive, figshare file list or Dataverse export again. Their deposits were completed for every subject (BrainVision .eeg/.vmrk, Curry .cdt.dpa/.ceo, the figshare file lists and the Dataverse export added to sourcedata/), checked against the loaders’ subject lists and, for archives, against the upstream archive listing. moabb.datasets.IMUMIA2026 now picks a subject’s .cdt by the subject-position field of its name instead of by its rank in the task folder, which picked another participant’s file (or failed) when the cache holds only some subjects (#1296 by Bruno Aristimunha).

  • moabb.datasets.Thielen2021 and moabb.datasets.Kaneshiro2015 no longer contact their upstream host for files the NEMAR store holds: the URL lookup of moabb.datasets.download.data_dl() probes four trailing URL segments instead of three (the Thielen2021 deposit nm000214 keeps offline/sub-NN/block_k/<file>), and Kaneshiro2015 skips its truncation check (a Range request to stacks.stanford.edu on every load) for a file the size of the store copy (nm000263) (#1302 by Bruno Aristimunha).

  • Enable nemar_seed_cache for moabb.datasets.GuttmannFlury2025_SSVEP, moabb.datasets.Nguyen2017_L, moabb.datasets.Zhang2025 and moabb.datasets.Zheng2020, so with the default "auto" provider they load from NEMAR alone instead of downloading the Zenodo per-subject ZIP or the figshare files again. Their loaders check their own cache before downloading, and their deposits (nm000310, nm000252, nm000211, nm000205) hold every file they read for every subject (checked against the loaders’ subject and session lists). Zheng2020’s manifest records no subject, so its first load seeds every group (#1299 by Bruno Aristimunha).

  • moabb.datasets.Leelakittisin2025, moabb.datasets.MOVING2024, moabb.datasets.Perdikis2018 and moabb.datasets.Shin2022 no longer download their archive (Zenodo v1_raw_SNN.zip, edf.rar, the pilot .tar.gz; the figshare ZIP) when NEMAR (nm000330, nm000293, nm000307, nm000315) or an earlier extraction already holds the subject: data_path looks for the extracted subject first and places it from the store with moabb.datasets.download.fill_tree_from_store(). The new moabb.datasets.download.store_glob() finds Shin2022’s date-named subject directory in the store (#1300 by Bruno Aristimunha).

  • moabb.datasets.MIMED2024, moabb.datasets.Weibo2014, moabb.datasets.Shin2017A and moabb.datasets.Shin2017B take the subject’s files from their NEMAR deposits (nm000296, nm000146, nm000267, nm000268) with moabb.datasets.download.fill_from_store() before downloading the Mendeley/Dataverse/TU Berlin archive (Shin2017 only with accept=True, as for the download). MIMED2024 now downloads whenever the subject’s files are absent instead of only when the Motor Imagery directory is, and moabb.datasets.utils.download_and_extract_subject_zip() gained skip_existing so that extraction never writes through a link into the store (#1301 by Bruno Aristimunha).

  • moabb.datasets.Liu2024 no longer loads subjects 5, 15 and 49 by default: their released files are derived from the recordings of subjects 4, 11 and 47 (EDF of sub-05 bit-identical to sub-04; raw MAT files equal to the partner’s recording minus uniform noise), while participants.tsv lists them as different patients, so keeping them leaked one subject’s EEG across cross-subject folds. The dataset now exposes 47 subjects; include_derived_subjects=True restores all 50 released IDs with a warning. Scores computed on all 50 subjects are not comparable with the new default (#1248 by Bruno Aristimunha).

  • Follow-ups from the review of #1257-#1270: moabb.benchmark() lets the evaluation auto-select datasets when no include_datasets/exclude_datasets is given, so its default run no longer warns about filtered datasets (#862), and “No datasets left” now lists the reasons; the weekly scheduled link check no longer accepts 5xx answers, so a host that stays down is reported; the PhysioNet S3 mirror is skipped for the rest of the process after its first failure instead of warning and timing out once per file (#1245); a metadata-aware cv_class now rejects a groups argument it would silently ignore, and the CrossSession limitation is documented (#1104); a Zhou2020 file that does not match the channel layout raises instead of being skipped (#1120); the Rozado2015 unit check accepts common spellings of microvolts (#1070); Lioi2020_XP2.nemar_subject_template is read-only and a missing mapping key names the expected key type (#1098); the process() Notes state that saving finished subjects is best effort with n_jobs > 1 (#419) (by Bruno Aristimunha).

  • With optuna=True, ROC-AUC scoring failed with AttributeError: ... has no attribute 'decision_function' for classifiers that only provide predict_proba (skorch/braindecode nets, GaussianNB, …): optuna-integration’s OptunaSearchCV defines decision_function unconditionally, so scikit-learn’s scorer picked it. OptunaSearchCVClassifier now exposes it only when the underlying estimator has it, as scikit-learn’s BaseSearchCV does (#1251 by Bruno Aristimunha).

  • Hyper-parameter search failed with multi-metric paradigm scorers: a list of metric names (scorer=["accuracy", "balanced_accuracy"]) reached GridSearchCV with refit=True (scikit-learn requires a metric name), and with optuna=True every trial failed with KeyError: 'test_score' because OptunaSearchCV takes a single scorer. Grid search now refits on the primary (first) metric and Optuna optimises it; all metrics are still reported on the outer folds (#1252 by Bruno Aristimunha).

  • LearningCurveSplitter with policy='ratio' took an unstratified prefix of the shuffled training pool, so small subsets often missed classes: splits were skipped with a warning, or fitting crashed with y contains previously unseen labels when a test class was absent from training (multi-class MI, SSVEP). Ratio subsets are now prefixes of a class-interleaved ordering of the same pool: sizes, nesting across sizes and per-permutation randomness are unchanged, and every class is present as soon as the subset holds at least one sample per class (#1253 by Bruno Aristimunha).

  • Saving a fold model (save_model=True) for a hyper-parameter search (GridSearchCV / Optuna) over a torch (skorch/braindecode) pipeline pickled the whole search object with the live network instead of using the torch-aware path used for unwrapped pipelines. moabb.evaluations.utils._save_model_cv() now saves the search’s best_estimator_ like an unwrapped pipeline (save_params for the net) and writes best_params_<fold>.json; searches over non-torch pipelines are saved as before (#1256 by Bruno Aristimunha).

  • moabb.benchmark() matched include_datasets given as dataset objects by code but then evaluated the paradigm’s freshly constructed default instances, silently dropping the caller’s constructor arguments (e.g. Shin2017A(accept=True) still failed with the licence error, subject restrictions were ignored). The caller’s instances are now used (#1254 by Bruno Aristimunha).

  • moabb.benchmark() chose the epochs branch (return_epochs=True, mne_labels=True) by pipeline name, so SSVEP template classifiers (SSVEP_TRCA, SSVEP_CCA, …) in a config named e.g. TRCA instead of TRCA-SSVEP still received epochs but with label-encoded integers, which they read as stimulus frequencies: no error, chance-level scores (Wang2016 TRCA 0.04 instead of 0.81 on subject 1). Routing now also uses the estimator type (_pipeline_requires_epochs()) (#1255 by Bruno Aristimunha).

  • moabb.pipelines.utils.parse_pipelines_from_directory() (and moabb.benchmark()) ran every .py pipeline config as a module named custom that was not registered in sys.modules, so a function or class defined in the config could not be pickled: save_model=True failed with PicklingError: Can't pickle <function _zscore ...>: import of module 'custom' failed for e.g. a FunctionTransformer(_zscore), as did pickle.dumps of the parsed pipeline. Each config now runs as its own module, registered under a name derived from its path (moabb_pipeline_config_<file name>_<hash>), so configs sharing a file name in different directories do not collide, and parsing the same file first lets another Python session unpickle the saved models. Worker processes (n_jobs > 1) cannot import such a module: they still receive its objects by value, and save_model now falls back to cloudpickle when pickle cannot store an object by reference, so models saved in workers (where the same PicklingError was raised) are written too and load with pickle.load() (#1309 by Bruno Aristimunha).

  • Models saved with save_model=True could not be loaded: after each fit, the evaluations mark every step that has no __sklearn_is_fitted__ (e.g. LinearDiscriminantAnalysis, SVC, StandardScaler, pyRiemann’s CSP, TangentSpace and MDM; Pipeline, FunctionTransformer and pyRiemann’s Covariances define it) with a method bound to a local function, which pickle stores as getattr(step, '_sklearn_is_fitted_true'), so pickle.load of fitted_model_<fold>.pkl raised AttributeError: ... has no attribute '_sklearn_is_fitted_true'. The marker is now a module-level function, and the saved files load (#1309 by Bruno Aristimunha).

  • Fix moabb.datasets.Hinss2021 building its recording at a hard-coded 500 Hz from EEGLAB files that hold 2 s epochs at 250 Hz: every time span was halved and every frequency doubled, so each “2 s” trial covered two released epochs (4 s), and each event sat one sample after its epoch’s start, which dropped the last trial of every session in MOABB and braindecode. The sampling rate is now read from the files, events mark the first sample of each epoch, and interval is [0, 1.996] so a trial is exactly one released epoch. Hinss2021 scores computed before this fix used time-compressed data: delete cached epochs and results and re-run (#663 by Bruno Aristimunha).

  • Fix moabb.datasets.Zhou2020 S-subjects (1-12) loading as EEG1..``EEG41`` without a montage. The loader took the first 41 of the 45 data columns of the NPZ signal array as EEG, so hEOG/vEOG and the M1/M2 mastoids were served as EEG and POz, PO4, Oz and F6 were dropped. Channel names now come from the authors’ readme_MIdataset.docx and MI_dataset.py on the original IEEE DataPort deposit (doi:10.21227/f1c7-7x89): columns 0-25 are the 26 shared fronto-central channels, 26-27 hEOG/vEOG, 28-29 M1/M2 and 30-44 F5, AF3, AF4, P5-P6, PO3, POz, PO4, Oz, F6, a placement confirmed against the recordings’ correlation structure. Both cohorts get the colin27_1005 montage; EOG (eog) and mastoids (misc) are kept only with return_all_modalities. Results and cached epochs computed before this fix used the wrong channels: delete them and re-run (#1120 by Bruno Aristimunha).

  • Fix moabb.datasets.Zhou2020 crashing every paradigm and evaluation that loads S- and A-subjects together: since #1120 the S-subjects (1-12) returned 41 EEG channels and the A-subjects (13-20) 26, so MotorImagery().get_data(Zhou2020()), WithinSessionEvaluation and CrossSubjectEvaluation stopped in np.concatenate (“along dimension 1, the array at index 0 has size 41 and the array at index 2 has size 26”). By default all 20 subjects now return the 26 channels both cohorts share (plus hEOG/vEOG with return_all_modalities). The new cohort option selects one group with its full montage: cohort="S" returns subjects 1-12 with all 41 EEG channels (and the M1/M2 mastoids with return_all_modalities), cohort="A" subjects 13-20; subject numbers stay 1-20 and asking a cohort for a subject of the other one raises a ValueError. Each option has its own code (Zhou2020, Zhou-s2020, Zhou-a2020) and METADATA, so caches and results never mix channel sets; downloads are shared. Cached Zhou2020 epochs of S-subjects computed with the 41-channel layout must be deleted and recomputed (#1275 by Bruno Aristimunha).

  • Fix moabb.datasets.Beetl2021_A and moabb.datasets.Beetl2021_B annotating the final phase’s test run with the training runs’ four-class map. final_MI_label.txt holds the competition’s three-class labels, whose class 2 is “other” (right hand or feet for A, feet or rest for B; 100 of each subject’s 200 test trials), so those trials were served as right_hand (A) or feet (B). The test run now has its own map and the merged class is a separate event, right_hand_or_feet / feet_or_rest, listed last in event_id; pass events to select the four training classes or the competition’s three. The leaderboard phase, whose test labels were never released, now serves its training run only instead of slices of the final-phase label file. interval stops one sample before 4 s, so MNE’s inclusive tmax no longer drops the last trial of every run (300 instead of 298 trials per dataset-A subject). Synthetic regression tests cover the maps, the paradigm selection, the leaderboard phase and the trial count (#1241 by Bhargav Kowshik).

  • Warn that moabb.datasets.Han2024Fatigue session '1' (fatigue) trial labels are unreliable: the fatigue MAT blocks do not store trials in the documented target order — single-trial SSVEP scoring against the loader-assigned labels sits at the 1/16 chance level while training-session labels verify at 0.70-0.87 accuracy, and per-block one-to-one assignments show no fixed permutation, so the intended order must come from the authors’ acquisition logs. The loader now emits a UserWarning when the fatigue session is loaded, documents the caveat, and keeps both sessions loadable; the training-session event order is regression-tested (#1238 by LiQing).

  • Fix moabb.datasets.MIND2026 download 404ing immediately on every fresh fetch: the loader hardcoded ScienceDB’s getZipFile?...&version=V3 endpoint, which ScienceDB has since removed (HEAD -> 404). version=V4 is the current replacement and was confirmed live with an exact byte-count match to this loader’s own ~74 GB docstring estimate (HEAD -> 200, content-length: 74409645600). No other behavior changes; the synthetic regression test is unaffected since it never hits the network (by Bruno Aristimunha).

  • Fix moabb.datasets.Lenaig2026’s data_path() raising FileNotFoundError: Some data files are missing. on every subject: it hard-coded an EEG_24Chan_AudioStim/ wrapper directory for the extracted RAR, but the current Zenodo v2 archive (record 21156618) extracts EXP1//EXP2/ directly at the root – confirmed by direct inspection on Voyager (find ... -iname '*EEG_24Chan*' only finds the .rar itself). data_path() now locates each run’s file with a recursive glob that matches either layout, without changing which events/labels are read (#1225 by Bruno Aristimunha).

  • Fix moabb.datasets.Schrag2026Pediatric crashing convert_to_bids() with ValueError: Raw object must have annotations to be saved in BIDS format on subject 1’s personalized-stimulus game run: the loader’s own documented policy of dropping all trial labels when a run’s Trial Started marker count drifts more than 10%% from its movements-CSV row count (true for that run, 15%% drift) produced a zero-annotation Raw, which bids_interface’s writer then rejected. _get_single_subject_data now skips a run that ends up with zero events instead of returning it, logging a warning that names the subject and run; the docstring documents this (#1225 by Bruno Aristimunha).

  • Fix moabb.datasets.Lenaig2026 crashing convert_to_bids() with AttributeError: 'int' object has no attribute 'items': METADATA.experiment.trials_per_class was a bare int (10) instead of the Dict[str, int] the schema declares, and bids_interface._build_readme unconditionally calls _format_dict() on it. Now a per-class dict ({"Stimulus": 10, "Silence": 10}), matching the documented 10 repetitions per condition (#1225 by Bruno Aristimunha).

  • Fix moabb.datasets.Dataset.convert_to_bids() aborting the whole multi-subject convert when exactly one subject raises FileNotFoundError (a loader’s own “nothing to write for this subject” signal, e.g. moabb.datasets.Schrag2026Pediatric subject 16’s single game run being dropped entirely by the >10%% drift policy): every subject after the one that raised was previously silently skipped as well. The per-subject loop now catches FileNotFoundError, logs a warning naming the subject, and continues converting the rest (#1225 by Bruno Aristimunha).

  • Keep moabb.evaluations.CrossSubjectEvaluation result provenance subject-specific when a grouped cross-validation fold holds out multiple subjects at once. The estimator is still fitted once per fold, while scores, cache identities, and saved-model paths are emitted per held-out subject and session instead of assigning the whole fold to its first subject (by lindicaphxag-tech).

  • Fix moabb.datasets.Dataset.convert_to_bids() crashing on datasets whose MOABB run-label suffix is literally "calibration" or "crosstalk" (Wang2025, Leeuwis2021, Brandl2020, Romani_BF2025_ERP). After the acq- switch (#1217), those descriptions collided with mne-bids’ reserved acq-calibration / acq-crosstalk MEG fine-calibration/crosstalk magic strings (which require task=None) and raised ValueError: task must be None if the acquisition is "calibration" or "crosstalk". run_moabb_to_bids now rewrites the two reserved tokens to short aliases ("calib"/"xtalk") and run_bids_to_moabb inverts the mapping, keeping the round trip exact for every run label currently emitted on develop and preserving the legacy recording- read fallback (#1218 by Bruno Aristimunha).

  • Fix moabb.datasets.MILimbEEG convert aborting on subjects S4 and S7 with ValueError: Expected a complete four-second stored trial. A dataset-wide scan of all 3719 motor-imagery CSVs confirms every file has exactly 500 data rows; 124 files (62 per subject across S4 and S7) start with a Mendeley v2 header variant whose first field is literally NaN (NaN,0,1,...,15) instead of empty. float("NaN") succeeds, so the previous first-line sniff mis-flagged them as header-less and pandas kept the extra row. _read_trial now also treats a first line with a NaN-or-empty leading cell and sequential column-index remainder as a header; a synthetic regression covers both the NaN variant and the empty variant and short trials remain a hard error (no silent zero-pad) (#1218 by Bruno Aristimunha).

  • Fix moabb.datasets.Dataset.convert_to_bids() producing files the official bids-validator (@bids/validator v2) rejects. _build_sidecar_enrichment now wraps string or flat-dict acq.filters so every top-level value of HardwareFilters is an object (schema requires objects, or the exact string "n/a"), fixing JSON_SCHEMA_VALIDATION_ERROR. run_moabb_to_bids now stores the MOABB run-label description (e.g. the feedback1 in "1feedback1") in the acquisition (acq-) entity instead of recording (rec-), which is not valid for _eeg/_events/_channels files and raised ALL_FILENAME_RULES_HAVE_ISSUES. run_bids_to_moabb still accepts the legacy rec- entity so existing MOABB caches keep reading back to the same run label (#1217 by Bruno Aristimunha).

  • Keep moabb.evaluations.CrossSessionEvaluation result provenance session-specific when a custom cross-validator holds out more than one recording session in the same fold. The estimator is still fitted once per fold, but each held-out session is scored and stored separately, matching the evaluation’s session-level result contract in both the flattened and legacy execution paths (by lindicaphxag-tech).

  • Fix metadata-aware splitters collapsing distinct compound groups when column values contain the - separator. Multi-column group identities now use a canonical collision-free encoding, preserving the intended cross-validation and leakage boundary (by lindicaphxag-tech).

  • Make moabb.analysis.chance_level.chance_by_chance() independent of result-row order when test-fold sizes vary: dataset-level adjusted thresholds now take the strictest exact-binomial cutoff across the fold sizes actually present (the cutoff is discrete and not strictly monotone in fold size), while inconsistent class counts are rejected (by lindicaphxag-tech).

  • Fix SSVEP_TRCA trial centering: the inter-trial covariance step subtracted the mean across channels at each sample instead of each channel’s mean over time, and did it in place on the filterbank data it received, which also fed Q and the class templates. Centering is now per channel over time, on a copy (#1183 by Arthur031221).

  • Use gmean in TRCA and TRCSP for compatibility with pyRiemann 0.12 and 0.13, and pass the TRCSP mean metric by keyword (by Bruno Aristimunha).

  • Fix the -e/--evaluations flag of python -m moabb.run, which used type=list and so split its value into single characters: -e WithinSession reached moabb.benchmark() as ['W', 'i', 't', ...] and raised KeyError: 'W'. It now takes one or more evaluation names, space separated (by Iain)

  • Fix the two install pages asking for optional extras MOABB does not have: the pip install page gave pip install moabb[deepleaning,carbonemission,docs], which is missing the r of deeplearning, and pip only warns about an unrecognised extra, so following that page left braindecode uninstalled. The from-sources page asked for external, removed in 1.2.0 (by Iain).

  • Fix evaluations passing NumPy arrays instead of mne.Epochs to moabb.pipelines.classification.SSVEP_itCCA and moabb.pipelines.classification.SSVEP_eCCA, whose fit then raised ValueError: X should be an MNE Epochs object. The check that switches an evaluation to epochs only listed SSVEP_CCA, SSVEP_TRCA and SSVEP_MsetCCA (by Arthur031221)

  • Fix moabb.analysis.meta_analysis.compute_pvals_wilcoxon() reporting the wrong tail when the sign of the mean paired difference disagrees with the signed-rank statistic: the one-tailed p-value is now taken directly from scipy.stats.wilcoxon(..., alternative="greater") instead of halving the two-sided value and choosing the side from the mean (#1177 by Azra Bano)

  • Fix equation formatting in moabb.analysis.meta_analysis.compute_pvals_corrected_ttest() and the surrounding docstrings so Sphinx renders the Nadeau & Bengio variance correction and \(n_2/n_1\) ratio instead of raw \frac source (#1204 by Quentin Barthelemy).

  • Resample the 600 Hz recordings of subject zdvm (bases 2, 3, 5 and 7) in moabb.datasets.MartinezCagigal2023Pary to the declared 256 Hz on load, fixing the ValueError raised when the CVEP paradigm concatenated the 601-sample epochs of those sessions with the 257-sample epochs of every other recording (#1201 by LiQing)

  • Read and write the BIDS JSON sidecars and the electrodes TSV in bids_interface as UTF-8, the encoding mne_bids writes them in, completing #1059. On Windows the locale codepage was used, so adding PublicationYear to a dataset_description.json with non-ASCII author names wrote them back garbled, or raised UnicodeDecodeError (#1231 by Rızgar Ozan)

  • Document why moabb.datasets.BNCI2015_003 subjects 1 and 2 have a 1:35 Target/NonTarget ratio (150 / 5250 epochs) instead of the 1:5 of subjects 3-10 (300 / 1500): s1.mat and s2.mat were recorded with the single-character speller (36 flash codes, one target), the others with the row/column speller (12 flash codes, two targets). The loader returns every flash in the released files; the counts come from the data, not from event decoding (#274 by Bruno Aristimunha)

  • Fix moabb.datasets.Rozado2015 returning a trigger channel as Fp1 and EEG 32 times too small. Channel 0 of the 57-channel BioSemi XDF stream is Trig1, but the loader took the first 32 channels by position, so Fp1 held the trigger code, every other label was shifted by one electrode and Cz was dropped; it also scaled the stream’s microvolt values as raw ADC counts (31.25 nV) instead of 1e-6. The scalp channels are now picked by their stream label and scaled from the declared microvolts; the median absolute 8-30 Hz epoch amplitude goes from 0.11 uV to 3.5 uV (median over the 30 subjects); the docstring also flags the two noisy subjects (10, 15) and the two 75-trial subjects (16, 18) (#1070 by Bruno Aristimunha)

  • Fix moabb.datasets.Rozado2015 returning a trigger channel as Fp1 and EEG 32 times too small. Channel 0 of the 57-channel BioSemi XDF stream is Trig1, but the loader took the first 32 channels by position, so Fp1 held the trigger code, every other label was shifted by one electrode and Cz was dropped; it also scaled the stream’s microvolt values as raw ADC counts (31.25 nV) instead of 1e-6. The scalp channels are now picked by their stream label and scaled from the declared microvolts; the median absolute 8-30 Hz epoch amplitude goes from 0.11 uV to 3.5 uV (median over the 30 subjects); the docstring also flags the two noisy subjects (10, 15) and the two 75-trial subjects (16, 18). Delete cached epochs and results computed before this fix and re-run (#1070 by Bruno Aristimunha)

  • Fix the log record moabb.datasets.EPFLP300 writes when it unzips a subject archive: logger.info("unzip", path_zip) passed the path without a %s placeholder, so formatting the record raised TypeError: not all arguments converted during string formatting. Stock handlers only print that error, but any handler that formats records strictly, such as one that re-raises formatting errors, made get_data fail (#1298 by Bruno Aristimunha).

  • Document in moabb.datasets.EPFLP300 that subject 3’s sessions "0" and "1" store T7 and T8 as copies of the mastoid channels MA1 and MA2, so that after the mastoid reference T7 is the opposite of T8 and the 32 EEG channels have rank 31: covariance estimators without shrinkage (e.g. xdawn_estimator="scm" of XdawnCovariances, as in pipelines/XdawnCov_MDM.yml) fail there with a LinAlgError (#1311 by Bruno Aristimunha).

  • moabb.datasets.Zhou2016 now reads its BIDS release from NEMAR (nm000115) under the default "auto" download provider instead of always downloading the Zenodo sub-N.zip archives: the deposit has no sourcedata/, so the base class asked NEMAR for it, failed, and fell back to Zenodo on every load. The deposit holds the same EDF, events.tsv, channels.tsv and electrodes.tsv files as the archives (without their desc-<hash> entity); loading subjects 1 and 2 from each source gives identical raw data, annotations, channels, epochs, labels and metadata. A subject already fetched loads without a request; "upstream" keeps the Zenodo path, and under "auto" a NEMAR failure falls back to Zenodo with a warning (#1303 by Bruno Aristimunha).

  • Fix epochs of one dataset coming out with different numbers of samples when its subjects were recorded at different sampling rates and the paradigm resamples. MOABB resamples the epochs, and MNE gives round(n_times * new_sfreq / native_sfreq) samples, so a 0-3 s epoch has 601 samples at 200 Hz when it was recorded at 160 Hz (481 samples) but 602 when it was recorded at 128 Hz (385 samples), the last one at 3.005 s, after tmax; any call that stacked such subjects (get_data over several subjects, CrossSubjectEvaluation) stopped in np.concatenate. Resampled epochs now keep at most the number of samples mne.Epochs gives for the window at the new rate (601 here), and a call whose recordings still differ by a sample (downsampling by more than a factor of two rounds the length down, e.g. 769 samples at 256 Hz give 384 at 128 Hz against 385 from 250 Hz) cuts every epoch to the shortest length; nothing is padded. Data at a single native rate are unchanged unless resampling placed samples after tmax (e.g. 128 Hz resampled to 200 Hz), which are now dropped. The resample step is part of the cache key, so cached epochs and arrays of resampled data are recomputed once (#1308 by Bruno Aristimunha).

  • moabb.datasets.ErpCore2021 (all seven components) now reads its recordings from NEMAR (nm000132) under the default "auto" download provider instead of always downloading them from OSF: the deposit has no sourcedata/, so the base class asked NEMAR for it, failed, and fell back to OSF on every load. nm000132 is the ERP CORE BIDS dataset itself, one dataset whose components are the task- entity; each class now downloads its subject’s files of its task (ERN and LRP share task-flankers) plus the subject’s electrodes.tsv and coordsystem.json into one store shared by the seven classes, MNE-erpcore2021-data/nm000132. A recording already on disk, from NEMAR or from an earlier OSF download, loads without a request; "upstream" keeps the OSF path, and under "auto" a NEMAR failure falls back to OSF with a warning (#1304 by Bruno Aristimunha).

Code health#

  • Share loader code across the new dataset PRs instead of private copies: download_and_extract_subject_zip() gains fname and redownload_corrupted (one warned re-download of a corrupted archive), rename_stimulus_codes() maps BrainVision Stimulus/S <n> markers to class labels, SetRawAnnotations keeps EDGE boundary markers, and the OpenNeuro raw-mirror mixin (moabb.datasets._openneuro_mirror) lives here once (#1200 by Bruno Aristimunha).

  • Modernise the BIDS layer on mne-bids>=0.20 and drop MOABB workarounds it made obsolete: use mne_bids.find_matching_paths() with ignore_json=True and the public mne_bids.BIDSPath.find_matching_sidecar() instead of a private helper, let mne_bids.write_raw_bids() serialise annotation extras into events.tsv and skip its README (readme=False) instead of patching both after writing, rely on mne-bids for the standard participants.json column descriptions, remove an obsolete EDF-conversion warning filter, and locate Thapa2025 files with find_matching_paths instead of probing every session/run pair (by Bruno Aristimunha).

Version 1.7.1 (Stable - PyPi)#

Enhancements#

  • None.

API changes#

  • None.

Requirements#

  • None.

Bugs#

  • Let the "auto" download provider fall back when nemar-py leaks an httpx.TransportError, such as a proxy failure, so locally cached upstream data remains usable when NEMAR is unreachable (by Bruno Aristimunha).

Code health#

  • None.

Version 1.7 (Stable - PyPi)#

Enhancements#

API changes#

  • None.

Requirements#

  • None.

Bugs#

Code health#

  • None.

Version 1.6.1 (Stable - PyPi)#

Enhancements#

  • None.

API changes#

  • moabb.datasets.metadata.AcquisitionMetadata.n_channels is derived from channel_types instead of stored beside it. The two could disagree, and for 34 of the 147 catalogued datasets they did – the field was being used with two meanings, some counting EEG electrodes only and others every recorded channel. It now consistently means the total, which is what each dataset’s own sensors list and test_n_channels_matches_raw_data (which counts every non-stim channel) already assumed: moabb.datasets.Dreyer2023A, for instance, reported 27 against its own 32 sensors and now reports 32. Passing n_channels= to the constructor is no longer accepted (by Bruno Aristimunha).

Requirements#

  • None.

Bugs#

  • Point the monthly download job at moabb/tests/test_download.py; it ran download.py, a file that does not exist, so it collected nothing and has been failing every month while no @pytest.mark.download test executed anywhere in CI (by Bruno Aristimunha).

  • Stop importing moabb from restyling the caller’s matplotlib. moabb.analysis.plotting applies a seaborn theme to the global rcParams when it is imported, and moabb/analysis/__init__.py imported it eagerly – so import moabb.datasets alone changed font.family, axes.grid and axes.spines.* for every figure the caller drew afterwards, values matplotlib reads at axes creation and a caller therefore cannot undo. The path was indirect: moabb.datasets.bids_interface imports moabb.analysis.results for get_digest, which runs that __init__. The plotting imports are now deferred to the functions that use them and to a module-level __getattr__ for the three re-exported plotting helpers, so every public name still resolves and an explicit import moabb.analysis.plotting still applies the theme, leaving MOABB’s own figures unchanged (by Bruno Aristimunha).

  • Type the seven non-EEG channels of moabb.datasets.BNCI2025_001 as misc. read_raw_eeglab types anything it does not recognise as eeg, so x/y/vx/vy/validity/targetPosX/targetPoxY – the hand kinematics and the target position of the reaching task – were picked as EEG by every paradigm and fed to classifiers as features, leaking the labels they encode. The declared n_channels (67) also disagreed with the dataset’s own 60-electrode montage; it is now 71 with channel_types={"eeg": 60, "eog": 4, "misc": 7}. Accuracies on this dataset will fall, which is the point (by Bruno Aristimunha).

  • Give moabb.datasets.Rodrigues2017 the montage #700 announced but never shipped, the same omission as moabb.datasets.Cattan2019_PHMD below: both share the 16-electrode setup, both spelled Fc5/Fc6 – the only two names standard_1020 cannot resolve – and neither loader called set_montage. Its METADATA also declared standard_1010, which is not a montage MNE can build (by Bruno Aristimunha).

  • Give moabb.datasets.Cattan2019_PHMD the montage #700 announced but never shipped: that PR fixed only the unit scaling, leaving the loader with no set_montage call at all. Its channel list also spelled the two frontal-central electrodes Fc5/Fc6, the only two of its sixteen names that standard_1020 cannot resolve; they are now FC5/FC6 (by Bruno Aristimunha).

  • Correct moabb.datasets.Cattan2019_PHMD interval from [0, 1] to [0, 60]. Each marker starts a one-minute relaxation block – as the dataset’s own block_duration_s=60.0 records – but SetRawAnnotations derives annotation durations from interval, so every block was annotated as lasting one second. interval[0] is unchanged, so onsets do not move (by Bruno Aristimunha).

  • Prefetch the NEMAR sourcedata store per subject rather than per dataset. The guard returned as soon as the store held anything, and the deposit’s provenance manifest lands inside it, so a store holding only a manifest counted as complete. Presence is now settled per subject from the cached manifest, at no network cost: nemar.download() walks index, version, metadata and manifest before it consults trust_existing, so a call with nothing to do is still four round-trips. Note that deposits whose manifest predates the subject field – which is every deposit today – are fetched as a whole tree, so for those the older whole-store rule was already right and is kept; the per-subject check matters once manifests record subjects, and today for a store left partial by an interrupted fetch (by Bruno Aristimunha).

Code health#

  • None.

Version 1.6.0 (Stable - PyPi)#

Enhancements#

  • Declare nemar_id for 15 datasets whose NEMAR deposits are now public, so moabb.set_download_provider() can serve them from NEMAR’s sourcedata/ instead of falling back to the upstream host (by Bruno Aristimunha).

  • Add moabb.analysis.meta_analysis.compute_pvals_corrected_ttest(), the Nadeau & Bengio corrected resampled t-test, for comparing pipelines evaluated with overlapping cross-validation resamples (e.g. within-session k-fold): the variance of the per-fold score differences is inflated by the test/train ratio \(n_2/n_1\), so the test does not inherit the optimistic variance of the naive resampled t-test (#1075 by Azra Bano)

  • Add cross-subject transfer learning to moabb.evaluations.CrossSubjectEvaluation through an optional target-calibration slice. moabb.evaluations.splitters.CrossSubjectSplitter gains calibration_size (the fraction of each held-out subject/session pair set aside for adaptation, in [0, 1]) and calibration_labeled; when calibration_size > 0 each fold becomes (train, calibration, test). The calibration trials never enter the training fold - they are handed to the pipeline steps that opt in via set_fit_request (X_target_unlabeled, or X_target_labeled and y_target_labeled when calibration_labeled=True, plus the per-trial subjects array), so train, calibration and test stay trial-disjoint. Named presets are available through the new moabb.evaluations.CrossSubjectMode enum and the cs_mode argument, covering train-only, unlabeled target adaptation at 20 / 50 / 100 percent, and labeled target calibration at 20 / 50 percent; TRAIN_TRIALWISE additionally scores one target trial at a time, so a method cannot exploit statistics of the whole test block. Two examples are added under examples/how_to_benchmark/ (#1093 by Bruno Aristimunha and Anton Andreev)

  • Add moabb.datasets.Wang2026, one motor-imagery dataset from the Wang et al. 2026 sensory-guided joint-learning study. The release contains 39 globally unique participants: 31 in the primary randomized experiment (joint learning, n=15; BCI2000 control, n=8; tactile control, n=8) plus an independently recruited EEGNet-control cohort (n=8). A group filter preserves stable global IDs across the four cohort archives. The longitudinal protocol used online 1D and 2D cursor control with 62 EEG channels at 1000 Hz and four classes over at least four regular sessions plus a baseline. Bounded 8-MiB HTTP range read-ahead extracts one subject without downloading a complete 8.7–25.3 GB archive. Raw construction is explicitly disabled until the authors supply the physical units or gain/offset needed to convert released values to volts (#1126 by Paul-Adrien Graignic and Bruno Aristimunha)

  • Add moabb.datasets.Lenaig2026 - SSAEP-BCI data of 48 participants in response to a set of four auditory stimuli: a pure tone (used as a reference), cicada song and cat’s purr, and brownian noise. EEG acquisition is performed using a 24-channel (international 10-20 system, passive electrodes, impedance maintained below 10 kΩ) at a sampling rate of 500 Hz. The stimuli are amplitude-modulated by a 40 Hz sinusoid and have a duration of 10 seconds. The experiment is conducted at two loudness levels (60 and 66 phons, diotic presentation), with 24 participants each. The measurement consists in one session of two 10-minute runs (separated by a 5-minute break), each including 50 trials (10 repetitions per condition). (#1121 by Henrique Lefundes)

  • Add moabb.datasets.Schrag2026Pediatric — open-access pediatric SSVEP-BCI dataset (47 children aged 5-18, g.tec g.GAMMAsys + g.USBamp at 256 Hz, 16 channels) covering both an online 4-target SSVEP game (6.25 / 10 / 11.11 / 14.28 Hz) and an opt-in 12-stimulus personalization recording (4 contrasts x 3 sizes at 10 Hz). XDF + Unity markers; trial labels are derived from the matching Movements/ CSV (live fbCCA classifier output). Single 1.2 GB zip on Zenodo (10.5281/zenodo.19440997) extracted per-subject on first use; the SSVEP game is exposed as two runs (standard and personal stimulus) of a single session (by Bruno Aristimunha and Emily Schrag)

  • Add 7 new imagined speech dataset adapters: moabb.datasets.AguileraRodriguez2025 (15 subjects, 4 Spanish words, traditional vs gamified paradigm), moabb.datasets.Nguyen2017_V, moabb.datasets.Nguyen2017_S, moabb.datasets.Nguyen2017_L, and moabb.datasets.Nguyen2017_SL (Nguyen et al. 2017 vowels / short words / long words / short-vs-long conditions), moabb.datasets.Nieto2022 (10 subjects, 4 directional tasks across inner / pronounced / visualized speech modalities, 128-ch BioSemi), and moabb.datasets.Pressel2016 (15 subjects, 11-class Spanish vowels and directional commands) (by Bruno Aristimunha)

  • Welcome imagined speech as a distinct category of imagery datasets with a dedicated documentation section (see Data Summary), a new moabb/datasets/summary_imagined_speech.csv summary table, and a grouped Imagined Speech Datasets listing in the API reference. The new datasets continue to use the existing paradigm="imagery" tag so all motor-imagery paradigm classes work unchanged (by Bruno Aristimunha)

  • Add 2 new BCI Competition 2020 dataset adapters: moabb.datasets.BCIComp2020UpperLimb (Track 4, 15 subjects, 3 grasping tasks on a single right arm, 3 recording days 7 days apart for session-to-session transfer evaluation) and moabb.datasets.BCIComp2020WalkingERP (Track 5, 15 subjects, visual P300 oddball during walking at 1.6 m/s on a treadmill, simultaneous scalp-EEG + ear-EEG + EOG + IMU recording) (by Bruno Aristimunha)

  • Remove moabb.datasets.BCIComp2020IS (BCI Competition 2020 Track 3 Imagined Speech): the released .mat files publish trials in a scrambled order without the original time information, so per-trial temporal context is lost and the dataset cannot be evaluated reliably — any reported accuracy on random splits is likely inflated by residual intra-block leakage. The authors could not be reached to recover the trial timing, so the loader is removed entirely (by Bruno Aristimunha)

  • Re-host moabb.datasets.Pressel2016 (from Google Drive) and moabb.datasets.Nguyen2017_V/_S/_L/_SL (from Dropbox) to Zenodo records 10.5281/zenodo.19502780 and 10.5281/zenodo.19502794 so automated download works without gdown, Dropbox rate limits, or the 4 GB ZIP64 prefix bug in the upstream Nguyen archive. Metadata now points at the Zenodo DOI + repository (by Bruno Aristimunha)

  • Mark artifact trials in moabb.datasets.Pressel2016 with BAD_artifact annotations instead of silently dropping them at load time, so downstream reject_by_annotation pipelines decide how to handle them. ~52 percent artifact rate on subject 1, matching the 10-52 percent range reported in the paper (by Bruno Aristimunha)

  • Preserve moabb.datasets.BNCI2014_001 source artifact flags with optional artifact_handling annotation modes. SetRawAnnotations now keeps BAD_artifact/bnci_artifact (and any bad-prefixed) annotations when it re-derives event annotations, so per-trial artifact flags survive the full get_data pipeline (this also makes the moabb.datasets.Pressel2016 BAD_artifact markers effective). reject_by_annotation is now exposed on all epoching paradigms (motor imagery, P300, SSVEP, c-VEP, resting state), letting users choose whether BAD_artifact trials are dropped during epoch generation (by copilot-swe-agent and Bruno Aristimunha).

  • Add unified interactive macro table for dataset summary page with 58 metadata columns, SearchPanes filtering, paradigm distribution bar, and CSV export (#1043 by Bruno Aristimunha).

  • Add rich HTML repr for preprocessing pipeline transformers: pipelines now render as interactive sklearn diagrams in Jupyter notebooks and sphinx-gallery docs, showing the three-stage flow (Raw, Epochs, Array) with readable step names, event lists, and key parameters instead of raw dict dumps (by Zach Munro and Bruno Aristimunha).

  • Expose motor_imagery and mental_arithmetic keyword-only parameters on moabb.datasets.Shin2017A (default: MI=True, MA=False) and moabb.datasets.Shin2017B (default: MI=False, MA=True), allowing users to load both conditions simultaneously while preserving backward compatibility (by Bruno Aristimunha)

  • Add resting state annotations and EMG channel support to moabb.datasets.Lee2019 resting state runs for BIDS export compatibility (by Bruno Aristimunha)

  • Skip zip extraction in moabb.datasets.GuttmannFlury2025 when files are already extracted, with /scratch fallback for NFS filesystems on compute nodes (by Bruno Aristimunha)

  • Re-enable auto-execution of the Riemannian Artifact Rejection tutorial (examples/advanced_examples/plot_riemannian_artifact_rejection.py) now that pyRiemann 0.11 is on PyPI with per-potato metrics and method_combination support on PotatoField (by Bruno Aristimunha)

  • Use NEMAR as the default download source for datasets with an assigned nemar_id, while preserving existing dataset-specific downloaders as a fallback (by Bruno Aristimunha).

  • moabb.datasets.base.BaseDataset.download() now fetches NEMAR’s sourcedata/ — the original pre-BIDS distribution, byte-identical to what the upstream host serves and stored under the same upstream filenames — instead of the deposit’s BIDS copy. The BIDS copy is a re-encoding whose events and session/run labels differ from what each dataset’s own loader produces, so substituting it would silently change results rather than only change where the bytes come from; sourcedata/ changes the source without touching the science. Loading is unaffected: get_data still runs each dataset’s own parser. Falls back to the dataset’s upstream downloader when a deposit publishes no sourcedata/ (by Bruno Aristimunha).

  • Serve the original, pre-BIDS distribution from NEMAR through the new moabb.datasets.base.BaseDataset.sourcedata_path(). NEMAR deposits republish the files exactly as the authors distributed them under sourcedata/, keeping the upstream filenames, so NEMAR can now stand in for upstream hosts that are slow, rate-limited, behind a bot gate, or retired. Passing subject= fetches a single subject, resolved through the deposit’s sourcedata_provenance.json manifest (deposits enriched before that manifest recorded subjects fall back to the whole tree with a warning); without it the full sourcedata/ is downloaded, because that tree keeps the upstream layout rather than a BIDS one. Backed by moabb.datasets.download.nemar_sourcedata_dl() (by Bruno Aristimunha).

  • Add moabb.set_download_provider() / moabb.get_download_provider() to pin where MOABB fetches data from: "auto" (default — NEMAR first, upstream fallback), "nemar" (NEMAR only; a failure is raised rather than silently falling back to a host the caller opted out of), or "upstream" (never use NEMAR). Also settable per run via the MOABB_DOWNLOAD_PROVIDER environment variable, which takes precedence over the stored config (by Bruno Aristimunha).

  • Add moabb.datasets.preprocessing.EuclideanAlignment, a trial-level Euclidean Alignment transformer (He & Wu 2020; Junqueira et al. 2024) that whitens each trial by the inverse square root of the Euclidean mean covariance to remove per-domain covariance shift before a (deep) model sees the data. Inductive and leakage-free by default (fit learns the reference from training trials, transform re-applies it to unseen trials); fit_transform gives the transductive, per-recording form. Accepts an mne.BaseEpochs or an (n_trials, n_channels, n_times) ndarray, uses a shrinkage covariance estimator ("lwf") for robustness, and adds no new dependency (pyriemann >= 0.11 is already required). Distinct from pyriemann.transfer.TLCenter, which recenters covariance matrices (#1108 by Bruno Aristimunha).

  • Add an n_jobs parameter to moabb.paradigms.base.BaseParadigm.get_data() and moabb.datasets.base.BaseDataset.get_data() to load and preprocess subjects in parallel with joblib.Parallel. Per-subject processing (reading, filtering, resampling, epoching) is independent, so this gives a near-linear speedup on datasets with many subjects, with identical numerical results. moabb’s own patches to the shared BIDS cache files (participants.tsv/.json, dataset_description.json) now take the mne-bids cross-process file lock, so parallel caching stays consistent (#1124 by Bruno Aristimunha).

  • Add examples/advanced_examples/plot_geometry_aware_recentering.py, comparing a fitted training reference with a transductive, test-batch-updated tangent-space reference under within- and cross-session evaluation (by Meysam Rahimipour).

  • Drive cross-validation folds with any stock scikit-learn cross-validator passed as cv_class, controlled by a groups argument — a metadata column name, a list of column names (compound key, e.g. ["subject", "session"]), or a callable metadata -> array — together with callable cv_kwargs resolved against the metadata (e.g. cv_class=PredefinedSplit with a test_fold callable to target a single fold). groups is exposed on moabb.evaluations.WithinSessionEvaluation, moabb.evaluations.WithinSubjectEvaluation, moabb.evaluations.CrossSessionEvaluation and moabb.evaluations.CrossSubjectEvaluation and threaded to their splitters; each splitter keeps its default grouping ("subject" / "session" / labels) when groups is None. moabb.evaluations.splitters.CrossDatasetSplitter gains groups (its group_column argument is now a deprecated alias) (#1104 by Bruno Aristimunha).

  • Add moabb.analysis.meta_analysis.compute_lowest_subject_scores(), which selects one shared lowest-performing cohort per dataset from a mandatory, explicit reference pipeline and scores every pipeline on those exact subjects. The helper requires identical subject coverage across pipelines and macro-averages the supplied session scores per subject before ranking; it does not recompute subject-level F1 scores from predictions (#1133 by Aditya Singh)

API changes#

  • None.

Requirements#

  • Use pyriemann.clustering.PotatoField per-potato metrics and the method_combination parameter (pyRiemann PR #423) used by the Riemannian Artifact Rejection tutorial (by Bruno Aristimunha)

  • Bump minimum supported Python to 3.11 (required by mne-bids >= 0.18) (#1124 by Bruno Aristimunha)

  • Bump minimum mne-bids to 0.19 for its cross-process file lock (mne_bids._fileio._open_lock), used to keep parallel BIDS caching (get_data(n_jobs>1)) consistent. This also lets the BIDS cache drop the module-level monkey-patch of mne_bids.dig._write_dig_bids (mne-bids >= 0.19 writes the *_electrodes.json SpatialReference sidecar itself) and derive the Keywords field via make_dataset_description(keywords=...) instead of a post-write patch (#1124 by Bruno Aristimunha)

Bugs#

  • Restore the final AttributeError of the moabb.pipelines module-level __getattr__, dropped in #692. Unknown names evaluated to None instead of raising, so from moabb.pipelines import * failed with TypeError: 'NoneType' object is not iterable and a mistyped class name in a pipeline YAML surfaced as TypeError: 'NoneType' object is not callable (#1159 by Iain)

  • Fix moabb.analysis.meta_analysis rejecting non-finite score inputs instead of letting NaN or infinite values leak into low-level statistics. moabb.analysis.meta_analysis.compute_pvals_wilcoxon(), moabb.analysis.meta_analysis.compute_pvals_perm(), and moabb.analysis.meta_analysis.compute_effect() now require finite score tables; the all-zero Wilcoxon case still reports a one-tailed p=0.5 in both directions and keeps off-diagonal p-values strictly inside (0, 1) for Stouffer’s method; moabb.analysis.meta_analysis.compute_effect() reports 0 for identical zero-spread pairs and signed infinity for constant nonzero offsets; and moabb.analysis.meta_analysis.compute_pvals_corrected_ttest() now rejects non-finite score differences and non-finite n_train/n_test instead of collapsing a non-finite statistic to p=0.5 (#678 by Aditya Singh)

  • Point moabb.datasets.Schrag2026Pediatric at Zenodo version 3.0 and correct its licence. Versions 1.0 and 2 registered CC-BY-ND-4.0 while the preprint stated CC-BY-4.0; the authors resolved that in version 3.0, whose Zenodo record registers CC-BY-4.0 – so the NoDerivatives term MOABB reported no longer applies. Version 3.0 also publishes one archive per subject, so loading a subject now downloads ~20-40 MB instead of the single ~1.2 GB archive that held all 47. (by Bruno Aristimunha)

  • Fix the LogVariance+LDA row of the benchmark results page (The largest EEG-based Benchmark for Open Science) linking to pipelines/LogVar_grid.yml, which is the Log Variance SVM grid pipeline and classifies with SVC. It now links to pipelines/LogVar.yml, the Log Variance LDA pipeline the row reports. The LogVariance+SVM row keeps LogVar_grid.yml and loses its #L7 fragment, which highlighted the shared LogVariance step rather than the SVC one (by Bhargav Kowshik).

  • Fix moabb.datasets.BNCI2022_001 epoching its instantaneous waypoint markers as 90-second trials: only trajectory_start is a trial event now (32 bounded, non-overlapping 90 s epochs per subject), and the loader annotates each trigger pulse once (rising edge) instead of once per held sample, which had inflated subject 1 to 33,114 annotations and a 364 GiB allocation. Waypoint and trajectory-end markers stay available as annotations on the loaded raws (#1143 by Bruno Aristimunha)

  • Retag moabb.datasets.Kaneshiro2015 from p300 to imagery so its declared default paradigm accepts it: its six visual object categories carry no Target/NonTarget pair, so the P300 paradigm rejected every subject. The catalog row and documentation grouping move with it (#1143 by Bruno Aristimunha)

  • Allow moabb.paradigms.RestingStateToP300Adapter to be constructed with defaults: events=None now means all of the dataset’s events, matching how every in-repo usage builds it (#1143 by Bruno Aristimunha)

  • Fix moabb.datasets.Lee2024 silently dropping 345 upstream files. data_path no longer guesses filenames and skips failures: it downloads a real inventory – the NEMAR deposit’s provenance manifest when the sourcedata store is in use (so the NEMAR path never contacts the upstream host), or the upstream git tree otherwise – through the shared moabb.datasets.download.data_dl(), which also serves the files from the NEMAR store. Subject 8’s unpadded upstream names (sub8_*) are normalized to the padded names the loader reads, mixtures of combined and per-block training files and param.mat are covered by the inventory, and any download failure now raises instead of leaving a silently incomplete directory (#1142 by Bruno Aristimunha)

  • Read XDF recordings with a built-in minimal reader (moabb.datasets._xdf) instead of the optional pyxdf dependency, for all four XDF datasets: moabb.datasets.AguileraRodriguez2025, moabb.datasets.Chen2017SingleFlicker, moabb.datasets.Rozado2015 and moabb.datasets.Schrag2026Pediatric. The immediate defect: AguileraRodriguez2025’s recorder wrote a non-conforming XDF footer (float sample_count), which makes pyxdf 1.17.5 raise ValueError for every gamified session; the built-in reader ignores footers entirely. It returns pyxdf-shaped streams and applies the same per-segment linear dejittering to regular-rate streams, and applies pyxdf’s clock-offset synchronization (Huber-ADMM robust fit, ported, BSD-2-Clause) before dejittering, in pyxdf’s order. Validated against pyxdf on real recordings of each dataset: bit-identical loaded data for all four. The moabb[xdf] extra is kept as an empty no-op so existing install commands still work (by Bruno Aristimunha)

  • Prefetch the NEMAR sourcedata store inside moabb.datasets.base.BaseDataset.get_data(): the store introduced in #1146 was only filled by an explicit download() call, so a plain get_data() on a fresh machine still fetched from the upstream host even with the provider pinned to "nemar". The requested subjects’ sourcedata/ is now fetched before loading, with the provider policy download already implements: "upstream" skips NEMAR, "nemar" treats a failure as fatal, "auto" warns per subject and leaves that subject to the dataset’s own downloader. Also fix the store fill in the deprecated moabb.datasets.download.data_path(): pooch.retrieve treats its destination as a directory holding <md5(url)>-<basename>, and loaders such as moabb.datasets.Rodrigues2017’s os.listdir() it, so a store hit written as a plain file at the destination raised NotADirectoryError; the hit now lands inside the wrapper directory under pooch’s unique name (by Bruno Aristimunha)

  • Wire the NEMAR sourcedata store into loading (#1147): data_dl and the deprecated data_path now serve a requested file from the dataset’s NEMAR/<nemar_id>/sourcedata/ store before consulting the URL-derived layout, probing the store by the trailing segments of the URL path since it keeps the upstream filenames. dataset.download() followed by get_data() therefore no longer re-contacts the upstream host – verified live against Schirrmeister2017, whose host is currently unreachable. The URL-derived trees remain as lookups so nothing already downloaded is fetched again; force_update still refetches upstream and pinning the provider to "upstream" opts loading out of the store (#1146 by Bruno Aristimunha)

  • Store moabb.datasets.ErpCore2021 as the single combined BIDS dataset it is, with components separated by the task- entity in one shared MNE-erpcore2021-data root, instead of seven standalone per-component BIDS datasets; likewise store Dreyer2023 in one shared MNE-dreyer2023-data root, since the A/B/C classes only select subject ranges of one globally numbered dataset. Pre-existing downloads in the legacy separated layouts are still read without re-fetching (#1146 by Bruno Aristimunha)

  • Honor the download-flag contract across every dataset: CacheConfig.overwrite_* now erases the cache even when use=False (previously a silent no-op with the default config), moabb.datasets.CompoundDataset forwards path/force_update/update_path/verbose to the wrapped dataset, data_path implementations that ignored path or force_update (14 datasets, including Kojima2024’s narrowed signature that made download() raise TypeError) now honor them, force_update also re-extracts stale archives, and a regression test enforces the contract for every future dataset. On the NEMAR side, moabb.datasets.base.BaseDataset.download() falls back to the upstream host per failing subject instead of discarding the whole NEMAR batch, sourcedata_path() matches provenance subjects by both the raw MOABB id and the dataset’s nemar_subject_template label, transport failures are no longer misreported as “deposit publishes no sourcedata”, and manifest filenames containing glob metacharacters are escaped before selection (#1146 by Bruno Aristimunha)

  • Fix moabb.datasets.Brandl2020 downloading HTML error pages instead of data. DepositOnce migrated to DSpace 7, whose web host now answers every path - including ones that were files - with HTTP 200 and a 1306-byte application shell, so all 16 subjects and the montage were cached as byte-identical HTML under their .mat names, with no error raised. Downloads now go to the DSpace REST host that serves the bytes, addressed by the per-file bitstream UUIDs that were already present in the module but unused, and each download is rejected and removed unless it really begins with the MATLAB file banner, so an error page can no longer be cached as data. moabb.datasets.download.data_dl() gains an optional fname for APIs whose download URLs do not end in the filename (#1141 by Bruno Aristimunha)

  • Fix dataset download paths that silently omitted files, returned nonexistent paths, or repeatedly extracted archives (#1135 by Bruno Aristimunha).

  • Use NEMAR’s OpenNeuro rehosts for moabb.datasets.TrianaGuzman2024 and moabb.datasets.Chailloux2020 (#1136 by Bruno Aristimunha).

  • Fix moabb.datasets.Schirrmeister2017 re-downloading every recording. data_path moved each freshly fetched EDF out of the directory that moabb.datasets.download.data_dl() owns and into MNE-schirrmeister2017-data/<train|test>/, so the next call found the download cache empty and fetched the whole file again. The refetched copy was then left behind because the destination already existed, leaving two copies of a multi-gigabyte recording on disk. data_path now returns the path data_dl() reports and only reads from the old location when a file is already there, so an existing local copy is still reused and never re-downloaded (#851 by Aditya Singh)

  • Fix how the benchmark results page (The largest EEG-based Benchmark for Open Science) describes what its tables report. It stated that results are “mean accuracy and standard deviation across all folds for all sessions and subjects”, and both halves are inaccurate: moabb.paradigms.MotorImagery selects the metric from the number of classes, so two-class scenarios are scored with ROC-AUC rather than accuracy, and moabb.evaluations.WithinSessionEvaluation averages the cross-validation folds within each session before returning a score, so the reported standard deviation is across (subject, session) pairs and not across individual folds (#1128 by Bhargav Kowshik)

  • Fix datasets ignoring a change of download directory: datasets now inherit MNE_DATA without persisting a redundant per-dataset mirror, and moabb.utils.set_download_dir() removes legacy MNE_DATASETS_<SIGN>_PATH entries that still mirror the previous shared location while preserving explicit overrides. moabb.datasets.RomaniBF2025ERP now uses the same path mechanism and honours path and force_update. Adds isolated regression coverage across every dataset (#1115 by Bruno Aristimunha).

  • Add a __repr__ to moabb.datasets.base.BaseDataset so datasets display by their code (e.g. BNCI2014-001) when printed, instead of the verbose default <...object at 0x...>. This declutters the output of print(paradigm.datasets) in the tutorials and of the paradigm and evaluation compatibility warnings (by Danae)

  • Add age_median field to moabb.datasets.metadata.schema.ParticipantMetadata and populate age_std / age_median / n_blocks metadata for moabb.datasets.Rodrigues2017 (Alphawaves), fixing a TypeError at import time (by Grace Xu)

  • Fix moabb.datasets.BNCI2014_001 descriptive METADATA, which had many fields copied from BCI Competition IV Data set 1. Correct n_subjects (4 → 9), n_classes (2 → 4), class_labels / events / imagery_tasks (now left_hand / right_hand / feet / tongue), synchronicity (asynchronous → synchronous), sessions_per_subject (1 → 2), the per-session trial structure (288 trials, 6 runs of 48), the acquisition reference / ground / filters and the preprocessing band (0.5–100 Hz with a 50 Hz notch, no 100 Hz downsampling), and the dataset description so they all match the dataset’s own constructor and docstring (#1094 by YG-paaleee)

  • Fix moabb.datasets.BNCI2014_001 stimulus protocol timing in the generated documentation figure to show 2 s fixation, 1.25 s cue, and motor imagery through t=6 s (by Bruno Aristimunha)

  • Fix stim-marker placement in moabb.datasets.BCIComp2020WalkingERP (Track 5): build_raw_from_epochs was called with onset_sample=0, which placed the event at sample 0 of each trial — the start of the pre-stim baseline (t=-190 ms), not the actual stimulus onset. With interval=[-0.19, 0.8], the paradigm was therefore reading the leading zero buffer as “pre-stim data” and missing the last 180 ms of real post-stim data. Now the loader passes onset_sample=19 so the marker lands on t=0 and the interval picks the real 100-sample epoch as published (by Bruno Aristimunha)

  • Fix session key off-by-one in moabb.datasets.Lee2019 that caused silent data loss when filtering sessions, and improve session filtering in moabb.datasets.base.BaseDataset to match compound session keys (e.g., "0train") by integer prefix (#1046 by Benedetto Leto and Bruno Aristimunha).

  • Fix BIDS conversion failures across multiple datasets: crop BDF/EDF signals to exact data records in bids_interface, add standard montage fiducials when missing, fix moabb.datasets.BNCI2016_002 KeyError in event mapping, handle lowercase trigger attribute in moabb.datasets.BNCI2022_001 .mat files, detect and re-download truncated files in moabb.datasets.Kaneshiro2015, add stim-channel annotations in moabb.datasets.Lee2024 for BIDS compatibility, convert µV to V in moabb.datasets.MartinezCagigal2023Checker and moabb.datasets.MartinezCagigal2023Pary to fix BDF physical range overflow, and handle alternate data key in moabb.datasets.Zuo2025 .mat files (by Bruno Aristimunha)

  • Fix moabb.datasets.Chang2025 BIDS conversion crash by gracefully skipping subjects with missing directories or .set files (by Bruno Aristimunha)

  • Fix moabb.datasets.GuttmannFlury2025 BIDS export OSError by correcting channel types (Trig → stim, HEO/VEO → eog, M1/M2 → misc) so trigger channel values no longer exceed EEG physical range limits (by Bruno Aristimunha)

  • Fix numpy.void.get() error in moabb.datasets.Lee2019 resting state EMG channel handling (by Bruno Aristimunha)

  • Fix moabb.datasets.castillos2023.BaseCastillos2023 extraction check using wrong directory name (4Class-VEP instead of 4Class-CVEP), causing re-extraction on every call, and replace fragile rstrip path derivation with proper os.path manipulation (by Bruno Aristimunha)

  • Fix data path lookup in moabb.datasets.Forenzo2023 that makes MOABB unable to find the downloaded data (#1048 by Ethan Davis).

  • Fix wrong paper reference in moabb.datasets.Thielen2021 (associated_paper_doi pointed to the Ahmadi electrode-montage reference instead of the dataset’s primary publication), restore Radboud data-repository DOI as __init__.doi, and add regression test test_primary_paper_matches_dataset_code that validates every <Surname><Year> dataset against its cited primary paper (by Bruno Aristimunha)

  • Fix UnicodeEncodeError when the GBK codec fails on '\xef' in BIDS metadata export by explicitly setting encoding="utf-8" on file writes in bids_interface (#1059 by sli930)

  • Modified example usage and fixed epoch extraction with an adjustable buffer that prevents last epochs being dropped in moabb.datasets.RomaniBF2025ERP (#1065 by Michele Romani).

  • Fix zip extraction in moabb.datasets.Wairagkar2018 dataset loader (#1066 by Barış Talar).

  • Fix EEG layout corruption in moabb.datasets.BNCI2020_002: the F-contiguous bciexp.data was reshaped in default C-order, producing a trial-fastest interleaved layout that disagreed with the per-trial stim markers and made every epoch sample the wrong trial. The reshape now transposes to trial-major before flattening (by Bruno Aristimunha).

  • Fix stim_trial content in moabb.datasets.MartinezCagigal2023Checker and moabb.datasets.MartinezCagigal2023Pary: the channel was carrying the per-recording trial index instead of the attended command id, breaking multiclass classification across recordings. The marker is now the command id (resolved via the new moabb.datasets.utils.resolve_cvep_command_ids() helper), and the _trial_meta annotation extras gain a command_id key alongside trial_id (by Bruno Aristimunha).

  • Cache Figshare’s file listing in moabb.datasets.download.fs_get_file_list() (process-level lru_cache) and persist it on disk next to the data for MAMEM (moabb.datasets.MAMEM1/MAMEM2/MAMEM3). Once a dataset has been downloaded, subsequent calls never contact Figshare; pass force_update=True to bypass both layers (by Bruno Aristimunha).

  • Fix Windows download path sanitization that changed absolute paths like C:\data into relative C-\data paths (#1079 by Anton Andreev).

  • Fix missing electrode positions (NaN xyz) in six motor-imagery datasets so topographic maps, interpolation, and spatial methods work: moabb.datasets.Forenzo2023 and moabb.datasets.GuttmannFlury2025_MI/_ME normalize Neuroscan ALL_CAPS labels and apply standard_1005 (CB1/CB2 kept as misc); moabb.datasets.Dreyer2023 falls back to standard_1005 when the BIDS archive ships no electrodes.tsv; moabb.datasets.BNCI2003_004 maps its 26 legacy Berlin channel labels to their modern 10-5 equivalents for exact positions; moabb.datasets.BNCI2014_002 applies an approximate 3x5 grid for its unlabeled small-Laplacian channels; and moabb.datasets.Zhang2017 applies the GSN-HydroCel-32 montage in EGI sensor order. Adds the shared moabb.datasets.utils.set_neuroscan_montage() helper (#1089 by Bruno Aristimunha).

  • Fix BaseEvaluation._aggregate_fold_results aborting the whole evaluation with TypeError: agg function failed [how->mean,dtype->object] when a single fold contributes a non-numeric score (e.g. an error fold). The numeric aggregation columns are now coerced with pandas.to_numeric(errors="coerce") before groupby.agg, so a bad fold becomes NaN and is skipped instead of taking down every subject/pipeline (#1095 by Bruno Aristimunha).

  • Fix moabb.evaluations.splitters.WithinSessionSplitter and moabb.evaluations.splitters.WithinSubjectSplitter overwriting an explicit n_splits passed through cv_kwargs with the n_folds default; the caller-provided n_splits now takes precedence, so a single holdout split can be requested directly via cv_class=StratifiedShuffleSplit, n_splits=1. moabb.evaluations.WithinSessionEvaluation and moabb.evaluations.WithinSubjectEvaluation now honour the n_splits argument instead of always running 5 folds, and moabb.evaluations.splitters.WithinSubjectSplitter now yields reproducible per-subject folds for a fixed random_state (#1106 by Bruno Aristimunha).

  • Evaluations now apply cv_kwargs to the default cross-validation class. Caller settings override splitter defaults, and splitter construction forwards each setting without duplicate keyword arguments (by Stanley C.).

  • Fix numeric sorting in the dataset summary tables (Data Summary): columns containing the varies sentinel (e.g. Total_trials) were auto-detected as strings by DataTables and sorted lexicographically (11000 < 1114 < 11496). A custom num-varies column type now treats such columns as numeric, sorting sentinel rows last while keeping their displayed text unchanged (#1118 by Bhargav Kowshik).

  • Fix make html crash in scripts/generate_macro_table.py when a dataset has a missing (NaN) value in an optional metadata column (country, DOI, data URL, …): the float NaN is truthy, so it slipped past the if not value guards and crashed the string formatters (TypeError: object of type 'float' has no len()). _format_cell now normalizes NaN to None before dispatching, and _dataset_link/_paradigm_tag – the only two format branches without an empty-value guard, which raised AttributeError on html.escape(None) – guard it too, so all eleven branches render a missing cell as empty (#1117 by Bhargav Kowshik).

  • Fix extra_runs parameter behaviour in moabb.datasets.RomaniBF2025ERP, and defaulted to False for fair cross-subject comparability.(by Michele Romani).

Code health#

  • Point MANIFEST.in at the files the repository actually ships. Three of its six directives named README.rst, LICENSE.txt and NOTICE.txt; the repository has README.md and LICENSE, and no notice file at all, so every python -m build --sdist printed three warning: no files found matching ... lines. The sdist contents are unchanged – setuptools already picked up the readme and the license through project.readme and its default license-files – only the warnings go away (by Bhargav Kowshik).

  • Bump the ruff-pre-commit hook from v0.15.9 to v0.15.20 (quarterly pre-commit.ci autoupdate). The bump is lint-neutral on the current tree: ruff check and ruff format --check return identical results at both pins across all 229 tracked *.py / *.pyi files, and pre-commit run --all-files rewrites no file (#1119 by pre-commit-ci).

  • Fix deprecated pyriemann.utils.{mean,covariance,base} import paths: bump the minimum pyriemann to 0.12 and update all import sites in moabb/pipelines/csp.py, moabb/pipelines/classification.py, moabb/datasets/preprocessing.py, and the Riemannian artifact rejection example to use pyriemann.geometry.* (introduced in pyriemann 0.12, removal of the old paths scheduled for 0.14), and import Potato/PotatoField from pyriemann.artifact_detection (moved from pyriemann.clustering in 0.12) (by copilot-swe-agent).

  • Install CPU-only PyTorch wheels in CI by setting UV_TORCH_BACKEND=cpu in the test, braindecode, and docs workflows, so runners no longer download multi-GB CUDA builds of torch (pulled transitively via the deeplearning extra / braindecode) (#1083 by Bhargav Kowshik).

  • Fix moabb.evaluations.splitters.LearningCurveSplitter not randomising its training subsample when groups are passed, which covers every learning curve run through moabb.evaluations.CrossSessionEvaluation or moabb.evaluations.CrossSubjectEvaluation. _get_data_size_subsets takes a prefix of the training indices, which is only a random draw because StratifiedShuffleSplit shuffles them; GroupShuffleSplit returns them ascending, so subsets could repeat across permutations and were biased toward the earliest samples in recording order. The grouped branch now lazily shuffles each consumed training fold before the prefix is taken; the ungrouped branch and held-out folds are unchanged for a given seed (#1158 by Bhargav Kowshik).

Version 1.5.0 (Stable - PyPi)#

Enhancements#

API changes#

  • Removed SinglePass and FilterBank intermediate classes from motor imagery and P300 paradigms. FilterBankLeftRightImagery now inherits from LeftRightImagery, FilterBankMotorImagery inherits from MotorImagery, and P300 inherits directly from BaseP300. RestingStateToP300Adapter now inherits from BaseP300. Docstring inheritance is handled via NumpyDocstringInheritanceInitMeta (#467 by Bruno Aristimunha).

  • Allow CodeCarbon script level configurations when instantiating a moabb.evaluations.base.BaseEvaluation child class (#866 by Ethan Davis).

  • When CodeCarbon is installed, MOABB HDF5 results have an additional column codecarbon_task_name. If CodeCarbon is configured to save to file, its own tabular results have a column task_name. These columns are unique UUID4s. Related rows can be joined to see detailed costs and benefits of predictive performance and computing profiling metrics (#866 by Ethan Davis).

  • Isolated model fitting, duration tracking, and CodeCarbon compute profiling tracking. New and consistent ordering of duration and CodeCarbon tracking across all evaluations: (Higher priority, closest to model fitting) required duration tracking, (lower priority, second closest to model fitting) optional CodeCarbon tracking (#866 by Ethan Davis).

  • Replaced unreliable wall clock duration tracking (Python’s time.time()) in favor of performance counter duration tracking (Python’s time.perf_counter()) (#866 by Ethan Davis).

  • Enable choice of online or offline CodeCarbon through the parameterization of codecarbon_config when instantiating a moabb.evaluations.base.BaseEvaluation child class (#956 by Ethan Davis)

  • Renamed stimulus channel from stim to STI in BNCI motor imagery and error-related potential datasets for clarity and BIDS compliance (by Bruno Aristimunha).

  • Added four new BNCI P300/ERP dataset classes: moabb.datasets.BNCI2015_009 (AMUSE), moabb.datasets.BNCI2015_010 (RSVP), moabb.datasets.BNCI2015_012 (PASS2D), and moabb.datasets.BNCI2015_013 (ErrP) (by Bruno Aristimunha).

  • Removed data_size and n_perms parameters from moabb.evaluations.WithinSessionEvaluation. Use cv_class=LearningCurveSplitter with cv_kwargs=dict(data_size=..., n_perms=...) instead (#963 by Bruno Aristimunha)

  • Learning curve results now automatically include “data_size” and “permutation” columns when using LearningCurveSplitter (#963 by Bruno Aristimunha)

  • Replace wildcard imports with explicit class imports in moabb.paradigms (#1004 by Bruno Aristimunha)

Requirements#

  • Allows CodeCarbon environment variables or a configuration file to be defined in the home directory or the current working directory (#866 by Ethan Davis).

  • Added filelock as a core dependency to fix missing import errors in utils (#959 by Mateusz Naklicki).

  • Switch pyriemann dependency from GitHub source back to PyPI (>=0.7). The Riemannian Artifact Rejection tutorial still requires pyriemann from source for PotatoField features and is no longer auto-executed during doc builds (#1011 by Bruno Aristimunha)

  • Add type hints to moabb.evaluations.base.BaseEvaluation and all concrete evaluation classes (#732 by Sarthak Tayal)

  • Add plotly>=5.18.0 as optional interactive dependency (pip install moabb[interactive]), included in moabb[all] (#1039 by Bruno Aristimunha)

Bugs#

  • Fix trial acceptance condition in moabb.datasets.Stieger2021 that allowed epochs to extend beyond actual motor imagery duration into adjacent trials’ resting periods (#816)

  • Fix DOI escaping in “See DOI” fallback link on dataset citation cards (#1000 by Bruno Aristimunha)

  • Prefer paper DOI over data DOI in dataset citation card when both are available (#1000 by Bruno Aristimunha)

  • Fix timeline SVG card artifact caused by link styling on dataset pages (by Bruno Aristimunha)

  • Fix dataset documentation teaser rendering by skipping raw reStructuredText directives in previews, hiding empty page-view rows when analytics are unavailable, and clearing dataset summary reST warnings (#1025 by Bruno Aristimunha)

  • Fix class-balance visualization counts by normalizing metadata/event class labels (e.g., NonTarget vs non-target) and use the first valid dataset subject in generated quickstart snippets instead of hardcoded subjects=[1] (#1000 by Bruno Aristimunha)

  • Fix missing P300 from the list of valid paradigms in the moabb.benchmark() docstring (by Bruno Aristimunha)

  • Fix critical trigger alignment bug in moabb.datasets.Liu2024 where create_event_array() selected the first 40 of 120 STI triggers (mixing instruction, MI, and break onsets) instead of filtering for only the MI onset triggers (value=2). Also fix swapped left/right hand label mapping in encoding() and correct epoch interval from (2, 6) to (0, 4) to match MI onset triggers (by Bruno Aristimunha)

  • Fixed incorrect DOIs in Dreyer2023, RomaniBF2025ERP, BNCI2015_003, BNCI2015_004, and BNCI2015_012 datasets (#977 by Bruno Aristimunha)

  • Added missing metadata DOIs for AlexMI, PhysionetMI, GrosseWentrup2009, Shin2017A, Shin2017B, BNCI2014_004, and BNCI2003_004 datasets (#977 by Bruno Aristimunha)

  • Fixed montage not being set before BIDS cache conversion in BNCI datasets (by Bruno Aristimunha)

  • Fixed measurement date setting for BNCI datasets to use specific collection years from papers (by Bruno Aristimunha)

  • Ensured proper subject ID assignment for BIDS compliance across all BNCI datasets (by Bruno Aristimunha)

  • Correct moabb.pipelines.classification.SSVEP_CCA, moabb.pipelines.classification.SSVEP_TRCA and moabb.pipelines.classification.SSVEP_MsetCCA behavior (#625 by Sylvain Chevallier)

  • Fix scikit-learn LogisticRegression elasticnet penalty parameter deprecation by re-adding penalty=’elasticnet’ for ElasticNet configurations with 0 < l1_ratio < 1 (#869 by Bruno Aristimunha)

  • Fixing option to pickle model (#870 by Ethan Davis)

  • Normalize Zenodo download paths and add a custom user-agent to improve download robustness (#946 by Bruno Aristimunha)

  • Use the BNCI mirror host to avoid download timeouts (#946 by Bruno Aristimunha)

  • Repair incomplete or corrupted moabb.datasets.Zhou2016 subject downloads by validating extracted EEG/events files and re-downloading under a subject-level lock, preventing empty-session failures during parallel docs/CI runs (by Bruno Aristimunha)

  • Prevent Python mutable default argument when defining CodeCarbon configurations (#956 by Ethan Davis)

  • Fix copytree FileExistsError in BrainInvaders2013a download by adding dirs_exist_ok=True (by Bruno Aristimunha)

  • Ensure optional additional scoring columns in evaluation results (#957 by Ethan Davis)

  • Fix pandas ArrowStringArray shuffle warning by converting .unique() results to numpy arrays in splitters, avoiding issues with newer pandas versions (#963 by Bruno Aristimunha)

  • Fix crash in moabb.datasets.Huebner2017 when regex match on vhdr filenames returns None (#1036 by Sarthak Tayal)

  • Replace production assert statements with proper ValueError / TypeError exceptions across analysis, pipelines, paradigms, and datasets modules (#1036 by Sarthak Tayal)

  • Fix silent pipeline name collision in moabb.pipelines.utils.create_pipeline_from_config by raising ValueError on duplicate names (#1036 by Sarthak Tayal)

  • Replace bare print() calls with proper logging in moabb.datasets.MartinezCagigal2023Checker (#1036 by Sarthak Tayal)

  • LearningCurveSplitter now skips training splits that collapse to a single class (e.g., with very small data_size) and emits a RuntimeWarning instead of producing NaN results (#963 by Bruno Aristimunha)

  • Fix double µV-to-V conversion in BNCI2003-004 and BNCI2015-006: data loaded in microvolts was labeled as volts without unit conversion, causing a second scaling during EDF export via mne_bids (by Bruno Aristimunha)

  • Fix Beetl2021_A and Beetl2021_B 403 Forbidden errors by skipping Figshare API calls when data already exists locally, and fix double-nested zip extraction directory structure (#969 by Bruno Aristimunha)

  • Fix wrong channel names in Riemannian Artifact Rejection tutorial that caused pick() to fail on BNCI2014-009 (by Bruno Aristimunha)

  • Fix moabb.datasets.RomaniBF2025ERP to follow MOABB nomenclature pattern by using dynamic folder name MNE-{code}-data instead of hardcoded folder name. Automatically migrates legacy folder BrainForm-BIDS-eeg-dataset to new nomenclature for backward compatibility (by Bruno Aristimunha)

  • Move BIDS cache lock file from the BIDS subject folder to the code/ folder for BIDS validator compliance. Lock files are now written per-session as code/sub-{subject}_ses-{session}_desc-{hash}_lockfile.json. Backward compatibility is preserved for caches created with the old location (#986 by Pierre Guetschel and Bruno Aristimunha)

  • Fixed erase() in BIDSInterfaceBase to handle multi-session datasets correctly by using per-session rm() calls instead of a single subject-level call, which previously caused a RuntimeError when looking up scans.tsv across multiple sessions (#986 by Pierre Guetschel and Bruno Aristimunha)

  • Fix MOABB_RESULTS default path to respect MNE_DATA configuration instead of hardcoding ~/mne_data, and fix docs CI cache to use workspace-relative MNE_DATA path and cache ~/.mne config directory (by Bruno Aristimunha)

  • Fix moabb.datasets.RomaniBF2025ERP get_data() failing with description merge error when adding stim channel, causing sessions to be silently dropped (#991 by Bruno Aristimunha)

  • Fix docs CI cache: set MNE_DATA env var and persist ~/.mne config directory so dataset paths survive cache restore (by Bruno Aristimunha)

  • Fix CI dataset cache reuse across commits/PR updates by using stable cache keys and default-branch cache saves for docs/tests workflows, avoiding repeated dataset downloads (by Bruno Aristimunha)

  • Fix moabb.datasets.Liu2024 download failure by switching Figshare URLs from figshare.com/ndownloader to ndownloader.figshare.com and adding BadZipFile recovery for corrupted cached downloads (#992 by Bruno Aristimunha)

  • Remove redundant autoattribute METADATA from MartinezCagigal2023 Checker and Pary docstrings (#1022 by Bruno Aristimunha)

  • Fix TRCA Riemannian mean convergence failure by regularizing ill-conditioned cross-covariance matrices in moabb.pipelines.classification.SSVEP_TRCA. Eigenvalue clamping bounds the condition number, eliminating Convergence not reached and invalid value encountered in log warnings (by Bruno Aristimunha)

  • Fix SSVEP CCA-family estimator consistency and eCCA formulation: moabb.pipelines.classification.SSVEP_CCA, moabb.pipelines.classification.SSVEP_MsetCCA, moabb.pipelines.classification.SSVEP_itCCA, and moabb.pipelines.classification.SSVEP_eCCA now return predictions in the same label space as classes_ and align predict_proba columns with classes_ order. moabb.pipelines.classification.SSVEP_CCA and moabb.pipelines.classification.SSVEP_eCCA now infer frequencies robustly from epochs metadata (with freq_map override), and moabb.pipelines.classification.SSVEP_eCCA now uses the corrected 4-feature filter assignments with updated reference/citation alignment (by Bruno Aristimunha)

  • Add documentation note to moabb.datasets.PhysionetMI that subject 88 was recorded at 128 Hz instead of 160 Hz, which causes errors when loaded alongside other subjects (#538 by Bruno Aristimunha)

  • Fix BIDS validator compliance: monkey-patch mne_bids to generate electrodes.json sidecar with SpatialReference key required by BIDS validator v2.4.0 when space-CapTrak entity is present (by Bruno Aristimunha)

  • Fix HardwareFilters BIDS sidecar format: wrap flat filter dicts in the required nested structure {"FilterName": {"key": "value"}} instead of writing a flat dict, and wrap string filters similarly (by Bruno Aristimunha)

  • Fix doi field in dataset_description.json to use BIDS-required doi:<value> format by adding the doi: prefix when missing (by Bruno Aristimunha)

  • Fix write_raw_bids overwrite error for multi-session datasets by detecting when a subject already exists in participants.tsv and setting overwrite=True for subsequent sessions (by Bruno Aristimunha)

  • Fix moabb.datasets.Ofner2017 generic channel names (eeg-0 .. eeg-60) in subject 1 execution GDF files by mapping them to correct 10-20 montage labels (by Bruno Aristimunha)

  • Fix moabb.datasets.Wang2021Combined segfault during BIDS conversion by switching from mne.io.read_raw_ant to mne.io.read_raw_cnt, avoiding a crash in the ANT reader’s C library (libEep) on macOS (by Bruno Aristimunha)

  • Fix brittle channel picking in BIDSInterfaceRawEDF that enumerated every MNE channel type keyword; replaced with stim-exclusion approach (#1030 by Bruno Aristimunha)

  • Fix set_montage crash in moabb.datasets.Thielen2015 when return_all_modalities=True by retyping ANA/EXG channels to misc (#1030 by Bruno Aristimunha)

  • Fix duplicate stim channels and dead CPz reference channel in moabb.datasets.Liu2024 when return_all_modalities=True (#1030 by Bruno Aristimunha)

  • Fix RawToEpochs silently stripping all non-EEG channels regardless of return_all_modalities setting (#1030 by Bruno Aristimunha)

  • Fix HED annotation semantics for Motor Imagery events per expert review: decompose each MI event into separate (Sensory-event, Experimental-stimulus, Visual-presentation) and (Agent-action, ...) top-level groups per HED Rules 2b/2e/2f, remove conflated Cue + Experimental-stimulus roles, fix SSVEP rest tag to Experiment-structure, and extract shared _MI_SENSORY constant to reduce tag duplication. Revert arrow-specific cues from paradigm-level HED defaults to generic sensory prefix, and add per-dataset hed_tags overrides for the 6 datasets that actually use arrow cues (BNCI2014-001, BNCI2014-004, Lee2019_MI, Zhou2016, Shin2017A, GrosseWentrup2009) (#1035 by Bruno Aristimunha)

Code health#

  • Generate dataset timeline SVGs at Sphinx build time instead of tracking pre-rendered files in git (by Bruno Aristimunha)

  • Fix Sphinx documentation warnings and move sliding-estimator tutorial to advanced examples (by Bruno Aristimunha)

  • Fix autosummary descriptions in API page by skipping dataset_timeline_ext in summary context (by Bruno Aristimunha)

  • Hide right sidebar and admonition icons on dataset pages for a cleaner layout using per-page meta directives (by Bruno Aristimunha)

  • Add GA4 pageview export script and CI workflow integration for automated dataset traffic metrics (by Bruno Aristimunha)

  • Resolve all 216 pytest warnings across the test suite by addressing root causes: clear Epochs annotations before concatenation, replace lambda with named function in test pipelines, re-apply montage after add_reference_channels, conditionally pass groups parameter in splitters using GroupsConsumerMixin, use os.environ in FakeDataset to avoid non-standard config warnings, and suppress intentional OptunaSearchCV experimental warnings at init (by Bruno Aristimunha)

  • Added systematic DOI validation test suite that checks format, docstring tracking, resolution, and author overlap across all datasets (#977 by Bruno Aristimunha)

  • Further reorganized BNCI datasets into year-specific modules (bnci_2003, bnci_2014, bnci_2015, bnci_2019) with shared helpers in legacy_base for clearer maintenance. The temporary legacy.py file has been removed (by Bruno Aristimunha).

  • Added new datasets moabb.datasets.BNCI2020_001, moabb.datasets.BNCI2020_002, moabb.datasets.BNCI2022_001, moabb.datasets.BNCI2025_001, and moabb.datasets.BNCI2025_002 (by Bruno Aristimunha).

  • Persist docs/test CI MNE dataset cache across runs to reduce cold-cache downloads (#946 by Bruno Aristimunha)

  • Refactor evaluation scoring into shared utility functions for future improvements (#948 by Bruno Aristimunha)

  • Centralize CV resolution in BaseEvaluation with new _resolve_cv() method for consistent cross-validation handling across all evaluation types. Add _build_result() and _build_scored_result() helpers to centralize result dict construction across WithinSession, CrossSession, and CrossSubject evaluations, replacing manual dict assembly in each (#963 by Bruno Aristimunha)

  • Remove redundant learning curve methods (get_data_size_subsets(), score_explicit(), _evaluate_learning_curve()) from WithinSessionEvaluation in favor of unified splitter-based approach (#963 by Bruno Aristimunha)

  • Generic metadata column registration: LearningCurveSplitter declares a metadata_columns class attribute, and BaseEvaluation auto-detects it via hasattr(cv_class, "metadata_columns") instead of hardcoding class checks, making it extensible to future custom splitters (#963 by Bruno Aristimunha)

  • Fix get_n_splits() delegation in WithinSessionSplitter and WithinSubjectSplitter to properly forward to the inner cv_class.get_n_splits() instead of hardcoding n_folds, giving correct split counts when using custom CV classes like LearningCurveSplitter (#963 by Bruno Aristimunha)

  • Remove dead _fit_and_score() function and unused paradigm/mne_labels parameters from _evaluate_fold() in evaluations/base.py (by Bruno Aristimunha)

  • Memory optimization in _process_parallel(): pass X, y, metadata as top-level positional args to joblib.delayed() so the loky backend can auto-mmap large numpy arrays, avoiding N full copies for N parallel tasks (by Bruno Aristimunha)

  • Remove duplicate get_inner_splitter_metadata() from WithinSessionSplitter, WithinSubjectSplitter, and CrossSubjectSplitter. All splitters now store a _current_splitter reference, and BaseEvaluation._build_scored_result() reads metadata generically from it (#963 by Bruno Aristimunha)

  • Extract _fit_cv(), _maybe_save_model_cv(), and _attach_emissions() into BaseEvaluation, removing duplicated model-fitting, model-saving, and carbon-tracking boilerplate from WithinSessionEvaluation, CrossSessionEvaluation, and CrossSubjectEvaluation (#963 by Bruno Aristimunha)

  • Extract _load_data() helper into BaseEvaluation to centralize data loading logic (epoch requirement checking and paradigm.get_data() call) that was duplicated across all three evaluation classes (#963 by Bruno Aristimunha)

  • Extract _get_nchan() helper into BaseEvaluation to replace repeated channel count extraction (X.info["nchan"] if isinstance(X, BaseEpochs) else X.shape[1]) in all evaluation classes (#963 by Bruno Aristimunha)

  • Move _pipeline_requires_epochs() from evaluations.py to utils.py for shared access by BaseEvaluation._load_data() (#963 by Bruno Aristimunha)

  • Move WithinSessionSplitter creation outside the per-session loop in WithinSessionEvaluation, since splitter parameters do not change per session (#963 by Bruno Aristimunha)

  • Add a compile smoke test (moabb/tests/test_compilation.py) that validates syntax for all Python files under moabb/ using py_compile (#960 by Bruno Aristimunha)

  • Add persistent DOI resolution cache (moabb/tests/doi_cache.json) for test_doi_validation.py to avoid network requests on every test run, reducing DOI test time from ~9 minutes to <1 second. Refresh with --update-doi-cache (#996 by Bruno Aristimunha)

  • Fix UtilEvaluation test class not discovered by pytest: renamed to TestUtilEvaluation and replaced self.skipTest (unittest-only) with pytest.skip (#1005 by Bruno Aristimunha)

  • Add 282 parametrized tests in moabb/tests/test_neural_signatures.py covering all five paradigms, HTML generation, template/colorscale utilities, and edge cases for the moabb.analysis.neural_signatures module (#1039 by Bruno Aristimunha)

Version 1.4.3 (Stable - PyPi)#

Enhancements#

API changes#

  • None.

Requirements#

Bugs#

Code health#

Version 1.4.2#

Enhancements#

Bugs#

API changes#

  • None.

Version - 1.4#

Enhancements#

Bugs#

API changes#

  • None.

Version - 1.3#

Enhancements#

Bugs#

API changes#

  • Removing the deep learning module from inside moabb in favour of braindecode integration (#692 by Bruno Aristimunha )

Version - 1.2.0#

Enhancements#

Bugs#

API changes#

Version - 1.1.1#

Enhancements#

Bugs#

API changes#

  • Include optuna as soft-dependency in the benchmark function and in the base of evaluation (#630 by Igor Carrara)

Version - 1.1.0#

Enhancements#

Bugs#

API changes#

  • None

Version - 1.0.0#

Enhancements#

Bugs#

API changes#

  • None

Version - 0.5.0#

Enhancements#

Bugs#

API changes#

  • None

Version - 0.4.6#

Enhancements#

Bugs#

Version - 0.4.5#

Enhancements#

Bugs#

Version - 0.4.4#

Enhancements#

Bugs#

API changes#

  • Minimum supported Python version is now 3.7

  • MOABB now depends on scikit-learn >= 1.0

Version - 0.4.3#

Enhancements#

Bugs#

API changes#

Version - 0.4.2#

Enhancements#

  • None

Bugs#

API changes#

  • None

Version - 0.4.1#

Enhancements#

  • None

Bugs#

API changes#

  • Remove update_path on all datasets, update_path parameter in dataset.data_path() is deprecated (#207 by Sylvain Chevallier)

Version - 0.4.0#

Enhancements#

Bugs#

API changes#

  • Drop update_path from moabb.download.data_path and moabb.download.data_dl

Version 0.3.0#

Enhancements#

Bugs#

API changes#

  • None

Version 0.2.1#

Enhancements#

Bugs#

API changes#

  • None

Version 0.2.0#

Enhancements#

Bugs#

  • None

API changes#

  • None