From d7ccfd49a6a63ccb8f827b3c1d4356c78ff0e4a2 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Thu, 16 Nov 2023 15:49:33 +0100 Subject: [PATCH 01/65] function to reject epochs per channel --- mne/epochs.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/mne/epochs.py b/mne/epochs.py index b7afada3d1a..60ba0273e22 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -4643,3 +4643,36 @@ def make_fixed_length_epochs( proj=proj, verbose=verbose, ) + + +def channel_specific_epoch_rejection(data: np.ndarray, + outliers: float) -> np.ndarray[bool]: + """Mask outlier epochs for each channel. + + Parameters + ---------- + data : np.ndarray + The data to find outliers in. The data should be in the shape + (epochs X channels X (frequency) X time). + outliers : float + The number of standard deviations to use as a cutoff for outliers. + + Returns + ------- + np.ndarray[bool] with the shape epochs x channels x (frequency). + A boolean array with the first dimension epochs and the second + dimension channels. The third dimension is only present if the data + is frequency data. The boolean array indicates which epochs should be + kept (True) and which should be rejected (False) for each channel. + """ + # get absolut values + abs_data = np.abs(data) # (epochs X channels X (frequency) X time) + # get the maximum voltage per epoch + max = np.max(abs_data, axis=-1) # (epochs X channels X (frequency)) + # get the standard deviation per channel + std = np.std(abs_data, axis=(-1, 0)) # (channels X (frequency)) + # get the mean per channel + mean = np.mean(abs_data, axis=(-1, 0)) # (channels X (frequency)) + # keep epochs where the maximum voltage is smaller than the mean + (outliers * std) + keep = max < ((outliers * std) + mean) # (epochs X channels X (frequency)) + return keep \ No newline at end of file From f94c7e474d6a64564415fcd696d3bb3bf58b671a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:55:35 +0000 Subject: [PATCH 02/65] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/epochs.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 60ba0273e22..20396487ad3 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -4645,8 +4645,9 @@ def make_fixed_length_epochs( ) -def channel_specific_epoch_rejection(data: np.ndarray, - outliers: float) -> np.ndarray[bool]: +def channel_specific_epoch_rejection( + data: np.ndarray, outliers: float +) -> np.ndarray[bool]: """Mask outlier epochs for each channel. Parameters @@ -4675,4 +4676,4 @@ def channel_specific_epoch_rejection(data: np.ndarray, mean = np.mean(abs_data, axis=(-1, 0)) # (channels X (frequency)) # keep epochs where the maximum voltage is smaller than the mean + (outliers * std) keep = max < ((outliers * std) + mean) # (epochs X channels X (frequency)) - return keep \ No newline at end of file + return keep From 32a87f80c443cd740df790a7f7a62813f04eea3c Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Thu, 16 Nov 2023 16:51:17 +0100 Subject: [PATCH 03/65] added masked data to channel specific rejection --- mne/epochs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mne/epochs.py b/mne/epochs.py index 60ba0273e22..bc60274f3e4 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -4675,4 +4675,6 @@ def channel_specific_epoch_rejection(data: np.ndarray, mean = np.mean(abs_data, axis=(-1, 0)) # (channels X (frequency)) # keep epochs where the maximum voltage is smaller than the mean + (outliers * std) keep = max < ((outliers * std) + mean) # (epochs X channels X (frequency)) - return keep \ No newline at end of file + # set values in 3D data arry to NaN where 2D mask is False + data[keep is False] = np.nan + return keep, data From 6e6c4a4d6e6bc3c39314205a67bc851f747c2615 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Thu, 16 Nov 2023 16:53:49 +0100 Subject: [PATCH 04/65] added masked data to function and to return --- mne/epochs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mne/epochs.py b/mne/epochs.py index 20396487ad3..b380d5b658d 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -4676,4 +4676,6 @@ def channel_specific_epoch_rejection( mean = np.mean(abs_data, axis=(-1, 0)) # (channels X (frequency)) # keep epochs where the maximum voltage is smaller than the mean + (outliers * std) keep = max < ((outliers * std) + mean) # (epochs X channels X (frequency)) + # set values to NaN where 2D mask is False in the 3D data array + data[keep is False] = np.nan return keep From 300ce43052d1b94055f552839835cefc2e299f83 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Thu, 16 Nov 2023 17:34:58 +0100 Subject: [PATCH 05/65] updated epochs.average with np.nanmean --- mne/epochs.py | 72 +++++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index b380d5b658d..afb300102ae 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -1167,7 +1167,7 @@ def _compute_aggregate(self, picks, mode="mean"): n_events += 1 if n_events > 0: - data /= n_events + data = np.nanmean(data) else: data.fill(np.nan) @@ -2044,6 +2044,40 @@ def _repr_html_(self): t = t.render(epochs=self, baseline=baseline, events=event_strings) return t + def channel_specific_epoch_rejection(self, outliers: float): + """Mask outlier epochs for each channel. + + Parameters + ---------- + data : np.ndarray + The data to find outliers in. The data should be in the shape + (epochs X channels X (frequency) X time). + outliers : float + The number of standard deviations to use as a cutoff for outliers. + + Returns + ------- + epochs : instance of Epochs + The masked epochs object, modified in-place. + mask: np.ndarray + The array used to mask the epochs. True == Keep epochs, + False = reject epochs. + """ + # extract data from Epochs object + # get absolut values + abs_data = np.abs(self.get_data()) # (epochs X channels X (frequency) X time) + # get the maximum voltage per epoch + max = np.max(abs_data, axis=-1) # (epochs X channels X (frequency)) + # get the standard deviation per channel + std = np.std(abs_data, axis=(-1, 0)) # (channels X (frequency)) + # get the mean per channel + mean = np.mean(abs_data, axis=(-1, 0)) # (channels X (frequency)) + # keep epochs where the maximum voltage is smaller than the mean + (outliers * std) + keep = max < ((outliers * std) + mean) # (epochs X channels X (frequency)) + # set values to NaN where 2D mask is False in the 3D data array + self.get_data()[keep is False] = np.nan + return self, keep + @verbose def crop(self, tmin=None, tmax=None, include_tmax=True, verbose=None): """Crop a time interval from the epochs. @@ -4643,39 +4677,3 @@ def make_fixed_length_epochs( proj=proj, verbose=verbose, ) - - -def channel_specific_epoch_rejection( - data: np.ndarray, outliers: float -) -> np.ndarray[bool]: - """Mask outlier epochs for each channel. - - Parameters - ---------- - data : np.ndarray - The data to find outliers in. The data should be in the shape - (epochs X channels X (frequency) X time). - outliers : float - The number of standard deviations to use as a cutoff for outliers. - - Returns - ------- - np.ndarray[bool] with the shape epochs x channels x (frequency). - A boolean array with the first dimension epochs and the second - dimension channels. The third dimension is only present if the data - is frequency data. The boolean array indicates which epochs should be - kept (True) and which should be rejected (False) for each channel. - """ - # get absolut values - abs_data = np.abs(data) # (epochs X channels X (frequency) X time) - # get the maximum voltage per epoch - max = np.max(abs_data, axis=-1) # (epochs X channels X (frequency)) - # get the standard deviation per channel - std = np.std(abs_data, axis=(-1, 0)) # (channels X (frequency)) - # get the mean per channel - mean = np.mean(abs_data, axis=(-1, 0)) # (channels X (frequency)) - # keep epochs where the maximum voltage is smaller than the mean + (outliers * std) - keep = max < ((outliers * std) + mean) # (epochs X channels X (frequency)) - # set values to NaN where 2D mask is False in the 3D data array - data[keep is False] = np.nan - return keep From afed69da58c6ee1ac62d0f0181c48ecdc293a1e4 Mon Sep 17 00:00:00 2001 From: Carina Date: Fri, 17 Nov 2023 17:06:57 +0100 Subject: [PATCH 06/65] Update mne/epochs.py Co-authored-by: Dominik Welke --- mne/epochs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mne/epochs.py b/mne/epochs.py index c88a2577256..f5df76c2d7a 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -2066,6 +2066,7 @@ def channel_specific_epoch_rejection(self, outliers: float): The array used to mask the epochs. True == Keep epochs, False = reject epochs. """ + _check_preload(self, "Modifying data of epochs") # extract data from Epochs object # get absolut values abs_data = np.abs(self.get_data()) # (epochs X channels X (frequency) X time) From b74d4ded2e4b200e9a6c2ce1906f157a4744bf51 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 7 Feb 2024 10:30:21 +1000 Subject: [PATCH 07/65] updated changelog for PR11776 --- doc/changes/devel.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 9558f8fe0ea..f37ec26971c 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -23,6 +23,7 @@ Version 1.6.dev0 (development) Enhancements ~~~~~~~~~~~~ +- Add equalize_event_counts method to :class:`mne.EpochsTFR` (:gh:`11776` by `Carina Forster`_) - Add support for Neuralynx data files with ``mne.io.read_raw_neuralynx`` (:gh:`11969` by :newcontrib:`Kristijan Armeni` and :newcontrib:`Ivan Skelin`) - Improve tests for saving splits with :class:`mne.Epochs` (:gh:`11884` by `Dmitrii Altukhov`_) - Added functionality for linking interactive figures together, such that changing one figure will affect another, see :ref:`tut-ui-events` and :mod:`mne.viz.ui_events`. Current figures implementing UI events are :func:`mne.viz.plot_topomap` and :func:`mne.viz.plot_source_estimates` (:gh:`11685` :gh:`11891` by `Marijn van Vliet`_) From cbd7083a455c126c3db82ff4fb5ed9dbaeb8a300 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 7 Feb 2024 10:59:17 +1000 Subject: [PATCH 08/65] updated github account name --- doc/changes/names.inc | 1182 ++++++++++++++++++++--------------------- 1 file changed, 591 insertions(+), 591 deletions(-) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index da884792c4f..dcb4c6cca6a 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -1,591 +1,591 @@ -.. _Aaron Earle-Richardson: https://github.com/Aaronearlerichardson - -.. _Abram Hindle: https://softwareprocess.es - -.. _Adam Li: https://github.com/adam2392 - -.. _Adeline Fecker: https://github.com/adelinefecker - -.. _Adina Wagner: https://github.com/adswa - -.. _Adonay Nunes: https://github.com/AdoNunes - -.. _Alan Leggitt: https://github.com/leggitta - -.. _Alejandro Weinstein: http://ocam.cl - -.. _Alessandro Tonin: https://www.linkedin.com/in/alessandro-tonin-7892b046 - -.. _Alex Ciok: https://github.com/alexCiok - -.. _Alex Gramfort: https://alexandre.gramfort.net - -.. _Alex Rockhill: https://github.com/alexrockhill/ - -.. _Alexander Rudiuk: https://github.com/ARudiuk - -.. _Alexandre Barachant: https://alexandre.barachant.org - -.. _Andrea Brovelli: https://andrea-brovelli.net - -.. _Andreas Hojlund: https://github.com/ahoejlund - -.. _Andres Rodriguez: https://github.com/infinitejest/ - -.. _Andrew Dykstra: https://github.com/adykstra - -.. _Andrew Gilbert: https://github.com/adgilbert - -.. _Andrew Quinn: https://github.com/ajquinn - -.. _Aniket Pradhan: https://github.com/Aniket-Pradhan - -.. _Anna Padee: https://github.com/apadee/ - -.. _Annalisa Pascarella: https://www.dima.unige.it/~pascarel/html/cv.html - -.. _Anne-Sophie Dubarry: https://github.com/annesodub - -.. _Antoine Gauthier: https://github.com/Okamille - -.. _Antti Rantala: https://github.com/Odingod - -.. _Apoorva Karekal: https://github.com/apoorva6262 - -.. _Archit Singhal: https://github.com/architsinghal-mriirs - -.. _Arne Pelzer: https://github.com/aplzr - -.. _Ashley Drew: https://github.com/ashdrew - -.. _Asish Panda: https://github.com/kaichogami - -.. _Austin Hurst: https://github.com/a-hurst - -.. _Ben Beasley: https://github.com/musicinmybrain - -.. _Britta Westner: https://britta-wstnr.github.io - -.. _Bruno Nicenboim: https://bnicenboim.github.io - -.. _buildqa: https://github.com/buildqa - -.. _Carlos de la Torre-Ortiz: https://ctorre.me - -.. _Carina Forster: https://github.com/carinafo - -.. _Cathy Nangini: https://github.com/KatiRG - -.. _Chetan Gohil: https://github.com/cgohil8 - -.. _Chris Bailey: https://github.com/cjayb - -.. _Chris Holdgraf: https://chrisholdgraf.com - -.. _Chris Mullins: https://crmullins.com - -.. _Christian Brodbeck: https://github.com/christianbrodbeck - -.. _Christian O'Reilly: https://github.com/christian-oreilly - -.. _Christopher Dinh: https://github.com/chdinh - -.. _Chun-Hui Li: https://github.com/iamsc - -.. _Clemens Brunner: https://github.com/cbrnr - -.. _Cora Kim: https://github.com/kimcoco - -.. _Cristóbal Moënne-Loccoz: https://github.com/cmmoenne - -.. _Dan Wakeman: https://github.com/dgwakeman - -.. _Daniel Carlström Schad: https://github.com/Dod12 - -.. _Daniel Hasegan: https://daniel.hasegan.com - -.. _Daniel McCloy: https://dan.mccloy.info - -.. _Daniel Strohmeier: https://github.com/joewalter - -.. _Daniel Tse: https://github.com/Xiezhibin - -.. _Darin Erat Sleiter: https://github.com/dsleiter - -.. _David Haslacher: https://github.com/davidhaslacher - -.. _David Julien: https://github.com/Swy7ch - -.. _David Sabbagh: https://github.com/DavidSabbagh - -.. _Demetres Kostas: https://github.com/kostasde - -.. _Denis Engemann: https://denis-engemann.de - -.. _Dinara Issagaliyeva: https://github.com/dissagaliyeva - -.. _Diptyajit Das: https://github.com/dasdiptyajit - -.. _Dirk Gütlin: https://github.com/DiGyt - -.. _Dmitrii Altukhov: https://github.com/dmalt - -.. _Dominik Welke: https://github.com/dominikwelke/ - -.. _Dominik Wetzel: https://github.com/schmetzler - -.. _Dominique Makowski: https://dominiquemakowski.github.io/ - -.. _Eberhard Eich: https://github.com/ebeich - -.. _Eduard Ort: https://github.com/eort - -.. _Emily Stephen: https://github.com/emilyps14 - -.. _Enrico Varano: https://github.com/enricovara/ - -.. _Enzo Altamiranda: https://www.linkedin.com/in/enzoalt - -.. _Eric Larson: https://larsoner.com - -.. _Erica Peterson: https://github.com/nordme - -.. _Erkka Heinila: https://github.com/Teekuningas - -.. _Etienne de Montalivet: https://github.com/etiennedemontalivet - -.. _Evan Hathaway: https://github.com/ephathaway - -.. _Evgeny Goldstein: https://github.com/evgenygoldstein - -.. _Ezequiel Mikulan: https://github.com/ezemikulan - -.. _Ezequiel Mikulan: https://github.com/ezemikulan - -.. _Fahimeh Mamashli: https://github.com/fmamashli - -.. _Federico Raimondo: https://github.com/fraimondo - -.. _Federico Zamberlan: https://github.com/fzamberlan - -.. _Felix Klotzsche: https://github.com/eioe - -.. _Felix Raimundo: https://github.com/gamazeps - -.. _Florin Pop: https://github.com/florin-pop - -.. _Frederik Weber: https://github.com/Frederik-D-Weber - -.. _Fu-Te Wong: https://github.com/zuxfoucault - -.. _Gennadiy Belonosov: https://github.com/Genuster - -.. _Geoff Brookshire: https://github.com/gbrookshire - -.. _George O'Neill: https://georgeoneill.github.io - -.. _Gonzalo Reina: https://github.com/Gon-reina - -.. _Guillaume Dumas: https://mila.quebec/en/person/guillaume-dumas - -.. _Guillaume Favelier: https://github.com/GuillaumeFavelier - -.. _Hakimeh Aslsardroud: https://www.researchgate.net/profile/Hakimeh-Pourakbari - -.. _Hamid Maymandi: https://github.com/HamidMandi - -.. _Hamza Abdelhedi: https://github.com/BabaSanfour - -.. _Hakimeh Pourakbari: https://github.com/Hpakbari - -.. _Hari Bharadwaj: https://github.com/haribharadwaj - -.. _Henrich Kolkhorst: https://github.com/hekolk - -.. _Hongjiang Ye: https://github.com/rubyyhj - -.. _Hubert Banville: https://github.com/hubertjb - -.. _Hüseyin Orkun Elmas: https://github.com/HuseyinOrkun - -.. _Hyonyoung Shin: https://github.com/mcvain - -.. _Ilias Machairas: https://github.com/JungleHippo - -.. _Ivan Skelin: https://github.com/ivan-skelin - -.. _Ivan Zubarev: https://github.com/zubara - -.. _Ivana Kojcic: https://github.com/ikojcic - -.. _Jaakko Leppakangas: https://github.com/jaeilepp - -.. _Jack Zhang: https://github.com/jackz314 - -.. _Jacob Woessner: https://github.com/withmywoessner - -.. _Jair Montoya Martinez: https://github.com/jmontoyam - -.. _Jan Ebert: https://www.jan-ebert.com/ - -.. _Jan Sedivy: https://github.com/honzaseda - -.. _Jan Sosulski: https://jan-sosulski.de - -.. _Jan Zerfowski: https://github.com/jzerfowski - -.. _Jasper van den Bosch: https://github.com/ilogue - -.. _Jean-Baptiste Schiratti: https://github.com/jbschiratti - -.. _Jean-Remi King: https://github.com/kingjr - -.. _Jeff Stout: https://megcore.nih.gov/index.php/Staff - -.. _Jennifer Behnke: https://github.com/JKBehnke - -.. _Jeroen Van Der Donckt: https://github.com/jvdd - -.. _Jesper Duemose Nielsen: https://github.com/jdue - -.. _Jevri Hanna: https://github.com/jshanna100 - -.. _jeythekey: https://github.com/jeythekey - -.. _Joan Massich: https://github.com/massich - -.. _Johann Benerradi: https://github.com/HanBnrd - -.. _Johannes Niediek: https://github.com/jniediek - -.. _John Samuelsson: https://github.com/johnsam7 - -.. _John Veillette: https://psychology.uchicago.edu/directory/john-veillette - -.. _Jon Houck: https://www.mrn.org/people/jon-m.-houck/principal-investigators - -.. _Jona Sassenhagen: https://github.com/jona-sassenhagen - -.. _Jonathan Kuziek: https://github.com/kuziekj - -.. _Jordan Drew: https://github.com/jadrew43 - -.. _Jose Alanis: https://github.com/JoseAlanis - -.. _Joshua Bear: https://github.com/joshbear - -.. _Joshua Calder-Travis: https://github.com/jCalderTravis - -.. _Joshua Teves: https://github.com/jbteves - -.. _Judy D Zhu: https://github.com/JD-Zhu - -.. _Juergen Dammers: https://github.com/jdammers - -.. _Jukka Nenonen: https://www.linkedin.com/pub/jukka-nenonen/28/b5a/684 - -.. _Jussi Nurminen: https://github.com/jjnurminen - -.. _Kaisu Lankinen: http://bishoplab.berkeley.edu/Kaisu.html - -.. _kalenkovich: https://github.com/kalenkovich - -.. _Katarina Slama: https://github.com/katarinaslama - -.. _Keith Doelling: https://github.com/kdoelling1919 - -.. _Kostiantyn Maksymenko: https://github.com/makkostya - -.. _Kristijan Armeni: https://github.com/kristijanarmeni - -.. _Kyle Mathewson: https://github.com/kylemath - -.. _Larry Eisenman: https://github.com/lneisenman - -.. _Lau Møller Andersen: https://github.com/ualsbombe - -.. _Laura Gwilliams: https://lauragwilliams.github.io - -.. _Leonardo Barbosa: https://github.com/noreun - -.. _Liberty Hamilton: https://github.com/libertyh - -.. _Lorenzo Desantis: https://github.com/lorenzo-desantis/ - -.. _Lukas Breuer: https://www.researchgate.net/profile/Lukas-Breuer-2 - -.. _Lukas Gemein: https://github.com/gemeinl - -.. _Lukáš Hejtmánek: https://github.com/hejtmy - -.. _Luke Bloy: https://www.research.chop.edu/imaging/team - -.. _Lx37: https://github.com/Lx37 - -.. _Mads Jensen: https://github.com/MadsJensen - -.. _Maggie Clarke: https://github.com/mdclarke - -.. _Mainak Jas: https://jasmainak.github.io - -.. _Maksym Balatsko: https://github.com/mbalatsko - -.. _Marcin Koculak: https://github.com/mkoculak - -.. _Marian Dovgialo: https://github.com/mdovgialo - -.. _Marijn van Vliet: https://github.com/wmvanvliet - -.. _Mark Alexander Henney: https://github.com/henneysq - -.. _Mark Wronkiewicz: https://ml.jpl.nasa.gov/people/wronkiewicz/wronkiewicz.html - -.. _Marmaduke Woodman: https://github.com/maedoc - -.. _Martin Billinger: https://github.com/mbillingr - -.. _Martin Luessi: https://github.com/mluessi - -.. _Martin Schulz: https://github.com/marsipu - -.. _Mathieu Scheltienne: https://github.com/mscheltienne - -.. _Mathurin Massias: https://mathurinm.github.io/ - -.. _Mats van Es: https://github.com/matsvanes - -.. _Matt Boggess: https://github.com/mattboggess - -.. _Matt Courtemanche: https://github.com/mjcourte - -.. _Matt Sanderson: https://github.com/monkeyman192 - -.. _Matteo Anelli: https://github.com/matteoanelli - -.. _Matthias Dold: https://matthiasdold.de - -.. _Matthias Eberlein: https://github.com/MatthiasEb - -.. _Matti Toivonen: https://github.com/mattitoi - -.. _Mauricio Cespedes Tenorio: https://github.com/mcespedes99 - -.. _Michiru Kaneda: https://github.com/rcmdnk - -.. _Mikołaj Magnuski: https://github.com/mmagnuski - -.. _Milan Rybář: https://milanrybar.cz - -.. _Mingjian He: https://github.com/mh105 - -.. _Mohammad Daneshzand: https://github.com/mdaneshzand - -.. _Moritz Gerster: https://github.com/moritz-gerster - -.. _Natalie Klein: https://github.com/natalieklein - -.. _Nathalie Gayraud: https://github.com/ngayraud - -.. _Naveen Srinivasan: https://github.com/naveensrinivasan - -.. _Nick Foti: https://nfoti.github.io - -.. _Nick Ward: https://www.ucl.ac.uk/ion/departments/sobell/Research/NWard - -.. _Nicolas Barascud: https://github.com/nbara - -.. _Niels Focke: https://neurologie.umg.eu/forschung/arbeitsgruppen/epilepsie-und-bildgebungsforschung - -.. _Niklas Wilming: https://github.com/nwilming - -.. _Nikolai Chapochnikov: https://github.com/chapochn - -.. _Nikolas Chalas: https://github.com/Nichalas - -.. _Okba Bekhelifi: https://github.com/okbalefthanded - -.. _Olaf Hauk: https://www.neuroscience.cam.ac.uk/directory/profile.php?olafhauk - -.. _Oleh Kozynets: https://github.com/OlehKSS - -.. _Pablo-Arias: https://github.com/Pablo-Arias - -.. _Pablo Mainar: https://github.com/pablomainar - -.. _Padma Sundaram: https://www.nmr.mgh.harvard.edu/user/8071 - -.. _Paul Pasler: https://github.com/ppasler - -.. _Paul Roujansky: https://github.com/paulroujansky - -.. _Pavel Navratil: https://github.com/navrpa13 - -.. _Peter Molfese: https://github.com/pmolfese - -.. _Phillip Alday: https://palday.bitbucket.io - -.. _Pierre Ablin: https://pierreablin.com - -.. _Pierre-Antoine Bannier: https://github.com/PABannier - -.. _Proloy Das: https://github.com/proloyd - -.. _Qian Chu: https://github.com/qian-chu - -.. _Qianliang Li: https://www.dtu.dk/english/service/phonebook/person?id=126774 - -.. _Quentin Barthélemy: https://github.com/qbarthelemy - -.. _Quentin Bertrand: https://github.com/QB3 - -.. _Qunxi Dong: https://github.com/dongqunxi - -.. _Rahul Nadkarni: https://github.com/rahuln - -.. _Ram Pari: https://github.com/ramkpari - -.. _Ramiro Gatti: https://github.com/ragatti - -.. _ramonapariciog: https://github.com/ramonapariciog - -.. _Rasmus Aagaard: https://github.com/rasgaard - -.. _Rasmus Zetter: https://people.aalto.fi/rasmus.zetter - -.. _Reza Nasri: https://github.com/rznas - -.. _Reza Shoorangiz: https://github.com/rezashr - -.. _Richard Höchenberger: https://github.com/hoechenberger - -.. _Richard Koehler: https://github.com/richardkoehler - -.. _Riessarius Stargardsky: https://github.com/Riessarius - -.. _Roan LaPlante: https://github.com/aestrivex - -.. _Robert Luke: https://github.com/rob-luke - -.. _Robert Seymour: https://neurofractal.github.io - -.. _Romain Derollepot: https://github.com/rderollepot - -.. _Romain Trachel: https://fr.linkedin.com/in/trachelr - -.. _Roman Goj: https://romanmne.blogspot.co.uk - -.. _Ross Maddox: https://www.urmc.rochester.edu/labs/maddox-lab.aspx - -.. _Rotem Falach: https://github.com/Falach - -.. _Samu Taulu: https://phys.washington.edu/people/samu-taulu - -.. _Samuel Deslauriers-Gauthier: https://github.com/sdeslauriers - -.. _Samuel Louviot: https://github.com/Sam54000 - -.. _Samuel Powell: https://github.com/samuelpowell - -.. _Santeri Ruuskanen: https://github.com/ruuskas - -.. _Sara Sommariva: https://www.dima.unige.it/~sommariva/ - -.. _Sawradip Saha: https://sawradip.github.io/ - -.. _Scott Huberty: https://orcid.org/0000-0003-2637-031X - -.. _Sebastiaan Mathot: https://www.cogsci.nl/smathot - -.. _Sebastian Castano: https://github.com/jscastanoc - -.. _Sebastian Major: https://github.com/major-s - -.. _Sébastien Marti: https://www.researchgate.net/profile/Sebastien-Marti - -.. _Sena Er: https://github.com/sena-neuro - -.. _Senwen Deng: https://snwn.de - -.. _Sheraz Khan: https://github.com/SherazKhan - -.. _Silvia Cotroneo: https://github.com/sfc-neuro - -.. _Simeon Wong: https://github.com/dtxe - -.. _Simon Kern: https://skjerns.de - -.. _Simon Kornblith: https://simonster.com - -.. _Sondre Foslien: https://github.com/sondrfos - -.. _Sophie Herbst: https://github.com/SophieHerbst - -.. _Stanislas Chambon: https://github.com/Slasnista - -.. _Stefan Appelhoff: https://stefanappelhoff.com - -.. _Stefan Repplinger: https://github.com/stfnrpplngr - -.. _Steven Bethard: https://github.com/bethard - -.. _Steven Bierer: https://github.com/neurolaunch - -.. _Steven Gutstein: https://github.com/smgutstein - -.. _Sumalyo Datta: https://github.com/Sumalyo - -.. _Susanna Aro: https://www.linkedin.com/in/susanna-aro - -.. _Svea Marie Meyer: https://github.com/SveaMeyer13 - -.. _T. Wang: https://github.com/twang5 - -.. _Tal Linzen: https://tallinzen.net/ - -.. _Teon Brooks: https://teonbrooks.com - -.. _Théodore Papadopoulo: https://github.com/papadop - -.. _Thomas Binns: https://github.com/tsbinns - -.. _Thomas Hartmann: https://github.com/thht - -.. _Thomas Radman: https://github.com/tradman - -.. _Timothy Gates: https://au.linkedin.com/in/tim-gates-0528a4199 - -.. _Timur Sokhin: https://github.com/Qwinpin - -.. _Tod Flak: https://github.com/todflak - -.. _Tom Ma: https://github.com/myd7349 - -.. _Tom Stone: https://github.com/tomdstone - -.. _Tommy Clausner: https://github.com/TommyClausner - -.. _Toomas Erik Anijärv: https://www.toomaserikanijarv.com/ - -.. _Tristan Stenner: https://github.com/tstenner/ - -.. _Tziona NessAiver: https://github.com/TzionaN - -.. _Valerii Chirkov: https://github.com/vagechirkov - -.. _Victor Ferat: https://github.com/vferat - -.. _Victoria Peterson: https://github.com/vpeterson - -.. _Xiaokai Xia: https://github.com/dddd1007 - -.. _Yaroslav Halchenko: http://haxbylab.dartmouth.edu/ppl/yarik.html - -.. _Yiping Zuo: https://github.com/frostime - -.. _Yousra Bekhti: https://www.linkedin.com/pub/yousra-bekhti/56/886/421 - -.. _Yu-Han Luo: https://github.com/yh-luo - -.. _Zhi Zhang: https://github.com/tczhangzhi/ - -.. _Zvi Baratz: https://github.com/ZviBaratz +.. _Aaron Earle-Richardson: https://github.com/Aaronearlerichardson + +.. _Abram Hindle: https://softwareprocess.es + +.. _Adam Li: https://github.com/adam2392 + +.. _Adeline Fecker: https://github.com/adelinefecker + +.. _Adina Wagner: https://github.com/adswa + +.. _Adonay Nunes: https://github.com/AdoNunes + +.. _Alan Leggitt: https://github.com/leggitta + +.. _Alejandro Weinstein: http://ocam.cl + +.. _Alessandro Tonin: https://www.linkedin.com/in/alessandro-tonin-7892b046 + +.. _Alex Ciok: https://github.com/alexCiok + +.. _Alex Gramfort: https://alexandre.gramfort.net + +.. _Alex Rockhill: https://github.com/alexrockhill/ + +.. _Alexander Rudiuk: https://github.com/ARudiuk + +.. _Alexandre Barachant: https://alexandre.barachant.org + +.. _Andrea Brovelli: https://andrea-brovelli.net + +.. _Andreas Hojlund: https://github.com/ahoejlund + +.. _Andres Rodriguez: https://github.com/infinitejest/ + +.. _Andrew Dykstra: https://github.com/adykstra + +.. _Andrew Gilbert: https://github.com/adgilbert + +.. _Andrew Quinn: https://github.com/ajquinn + +.. _Aniket Pradhan: https://github.com/Aniket-Pradhan + +.. _Anna Padee: https://github.com/apadee/ + +.. _Annalisa Pascarella: https://www.dima.unige.it/~pascarel/html/cv.html + +.. _Anne-Sophie Dubarry: https://github.com/annesodub + +.. _Antoine Gauthier: https://github.com/Okamille + +.. _Antti Rantala: https://github.com/Odingod + +.. _Apoorva Karekal: https://github.com/apoorva6262 + +.. _Archit Singhal: https://github.com/architsinghal-mriirs + +.. _Arne Pelzer: https://github.com/aplzr + +.. _Ashley Drew: https://github.com/ashdrew + +.. _Asish Panda: https://github.com/kaichogami + +.. _Austin Hurst: https://github.com/a-hurst + +.. _Ben Beasley: https://github.com/musicinmybrain + +.. _Britta Westner: https://britta-wstnr.github.io + +.. _Bruno Nicenboim: https://bnicenboim.github.io + +.. _buildqa: https://github.com/buildqa + +.. _Carlos de la Torre-Ortiz: https://ctorre.me + +.. _Carina Forster: https://github.com/CarinaFo + +.. _Cathy Nangini: https://github.com/KatiRG + +.. _Chetan Gohil: https://github.com/cgohil8 + +.. _Chris Bailey: https://github.com/cjayb + +.. _Chris Holdgraf: https://chrisholdgraf.com + +.. _Chris Mullins: https://crmullins.com + +.. _Christian Brodbeck: https://github.com/christianbrodbeck + +.. _Christian O'Reilly: https://github.com/christian-oreilly + +.. _Christopher Dinh: https://github.com/chdinh + +.. _Chun-Hui Li: https://github.com/iamsc + +.. _Clemens Brunner: https://github.com/cbrnr + +.. _Cora Kim: https://github.com/kimcoco + +.. _Cristóbal Moënne-Loccoz: https://github.com/cmmoenne + +.. _Dan Wakeman: https://github.com/dgwakeman + +.. _Daniel Carlström Schad: https://github.com/Dod12 + +.. _Daniel Hasegan: https://daniel.hasegan.com + +.. _Daniel McCloy: https://dan.mccloy.info + +.. _Daniel Strohmeier: https://github.com/joewalter + +.. _Daniel Tse: https://github.com/Xiezhibin + +.. _Darin Erat Sleiter: https://github.com/dsleiter + +.. _David Haslacher: https://github.com/davidhaslacher + +.. _David Julien: https://github.com/Swy7ch + +.. _David Sabbagh: https://github.com/DavidSabbagh + +.. _Demetres Kostas: https://github.com/kostasde + +.. _Denis Engemann: https://denis-engemann.de + +.. _Dinara Issagaliyeva: https://github.com/dissagaliyeva + +.. _Diptyajit Das: https://github.com/dasdiptyajit + +.. _Dirk Gütlin: https://github.com/DiGyt + +.. _Dmitrii Altukhov: https://github.com/dmalt + +.. _Dominik Welke: https://github.com/dominikwelke/ + +.. _Dominik Wetzel: https://github.com/schmetzler + +.. _Dominique Makowski: https://dominiquemakowski.github.io/ + +.. _Eberhard Eich: https://github.com/ebeich + +.. _Eduard Ort: https://github.com/eort + +.. _Emily Stephen: https://github.com/emilyps14 + +.. _Enrico Varano: https://github.com/enricovara/ + +.. _Enzo Altamiranda: https://www.linkedin.com/in/enzoalt + +.. _Eric Larson: https://larsoner.com + +.. _Erica Peterson: https://github.com/nordme + +.. _Erkka Heinila: https://github.com/Teekuningas + +.. _Etienne de Montalivet: https://github.com/etiennedemontalivet + +.. _Evan Hathaway: https://github.com/ephathaway + +.. _Evgeny Goldstein: https://github.com/evgenygoldstein + +.. _Ezequiel Mikulan: https://github.com/ezemikulan + +.. _Ezequiel Mikulan: https://github.com/ezemikulan + +.. _Fahimeh Mamashli: https://github.com/fmamashli + +.. _Federico Raimondo: https://github.com/fraimondo + +.. _Federico Zamberlan: https://github.com/fzamberlan + +.. _Felix Klotzsche: https://github.com/eioe + +.. _Felix Raimundo: https://github.com/gamazeps + +.. _Florin Pop: https://github.com/florin-pop + +.. _Frederik Weber: https://github.com/Frederik-D-Weber + +.. _Fu-Te Wong: https://github.com/zuxfoucault + +.. _Gennadiy Belonosov: https://github.com/Genuster + +.. _Geoff Brookshire: https://github.com/gbrookshire + +.. _George O'Neill: https://georgeoneill.github.io + +.. _Gonzalo Reina: https://github.com/Gon-reina + +.. _Guillaume Dumas: https://mila.quebec/en/person/guillaume-dumas + +.. _Guillaume Favelier: https://github.com/GuillaumeFavelier + +.. _Hakimeh Aslsardroud: https://www.researchgate.net/profile/Hakimeh-Pourakbari + +.. _Hamid Maymandi: https://github.com/HamidMandi + +.. _Hamza Abdelhedi: https://github.com/BabaSanfour + +.. _Hakimeh Pourakbari: https://github.com/Hpakbari + +.. _Hari Bharadwaj: https://github.com/haribharadwaj + +.. _Henrich Kolkhorst: https://github.com/hekolk + +.. _Hongjiang Ye: https://github.com/rubyyhj + +.. _Hubert Banville: https://github.com/hubertjb + +.. _Hüseyin Orkun Elmas: https://github.com/HuseyinOrkun + +.. _Hyonyoung Shin: https://github.com/mcvain + +.. _Ilias Machairas: https://github.com/JungleHippo + +.. _Ivan Skelin: https://github.com/ivan-skelin + +.. _Ivan Zubarev: https://github.com/zubara + +.. _Ivana Kojcic: https://github.com/ikojcic + +.. _Jaakko Leppakangas: https://github.com/jaeilepp + +.. _Jack Zhang: https://github.com/jackz314 + +.. _Jacob Woessner: https://github.com/withmywoessner + +.. _Jair Montoya Martinez: https://github.com/jmontoyam + +.. _Jan Ebert: https://www.jan-ebert.com/ + +.. _Jan Sedivy: https://github.com/honzaseda + +.. _Jan Sosulski: https://jan-sosulski.de + +.. _Jan Zerfowski: https://github.com/jzerfowski + +.. _Jasper van den Bosch: https://github.com/ilogue + +.. _Jean-Baptiste Schiratti: https://github.com/jbschiratti + +.. _Jean-Remi King: https://github.com/kingjr + +.. _Jeff Stout: https://megcore.nih.gov/index.php/Staff + +.. _Jennifer Behnke: https://github.com/JKBehnke + +.. _Jeroen Van Der Donckt: https://github.com/jvdd + +.. _Jesper Duemose Nielsen: https://github.com/jdue + +.. _Jevri Hanna: https://github.com/jshanna100 + +.. _jeythekey: https://github.com/jeythekey + +.. _Joan Massich: https://github.com/massich + +.. _Johann Benerradi: https://github.com/HanBnrd + +.. _Johannes Niediek: https://github.com/jniediek + +.. _John Samuelsson: https://github.com/johnsam7 + +.. _John Veillette: https://psychology.uchicago.edu/directory/john-veillette + +.. _Jon Houck: https://www.mrn.org/people/jon-m.-houck/principal-investigators + +.. _Jona Sassenhagen: https://github.com/jona-sassenhagen + +.. _Jonathan Kuziek: https://github.com/kuziekj + +.. _Jordan Drew: https://github.com/jadrew43 + +.. _Jose Alanis: https://github.com/JoseAlanis + +.. _Joshua Bear: https://github.com/joshbear + +.. _Joshua Calder-Travis: https://github.com/jCalderTravis + +.. _Joshua Teves: https://github.com/jbteves + +.. _Judy D Zhu: https://github.com/JD-Zhu + +.. _Juergen Dammers: https://github.com/jdammers + +.. _Jukka Nenonen: https://www.linkedin.com/pub/jukka-nenonen/28/b5a/684 + +.. _Jussi Nurminen: https://github.com/jjnurminen + +.. _Kaisu Lankinen: http://bishoplab.berkeley.edu/Kaisu.html + +.. _kalenkovich: https://github.com/kalenkovich + +.. _Katarina Slama: https://github.com/katarinaslama + +.. _Keith Doelling: https://github.com/kdoelling1919 + +.. _Kostiantyn Maksymenko: https://github.com/makkostya + +.. _Kristijan Armeni: https://github.com/kristijanarmeni + +.. _Kyle Mathewson: https://github.com/kylemath + +.. _Larry Eisenman: https://github.com/lneisenman + +.. _Lau Møller Andersen: https://github.com/ualsbombe + +.. _Laura Gwilliams: https://lauragwilliams.github.io + +.. _Leonardo Barbosa: https://github.com/noreun + +.. _Liberty Hamilton: https://github.com/libertyh + +.. _Lorenzo Desantis: https://github.com/lorenzo-desantis/ + +.. _Lukas Breuer: https://www.researchgate.net/profile/Lukas-Breuer-2 + +.. _Lukas Gemein: https://github.com/gemeinl + +.. _Lukáš Hejtmánek: https://github.com/hejtmy + +.. _Luke Bloy: https://www.research.chop.edu/imaging/team + +.. _Lx37: https://github.com/Lx37 + +.. _Mads Jensen: https://github.com/MadsJensen + +.. _Maggie Clarke: https://github.com/mdclarke + +.. _Mainak Jas: https://jasmainak.github.io + +.. _Maksym Balatsko: https://github.com/mbalatsko + +.. _Marcin Koculak: https://github.com/mkoculak + +.. _Marian Dovgialo: https://github.com/mdovgialo + +.. _Marijn van Vliet: https://github.com/wmvanvliet + +.. _Mark Alexander Henney: https://github.com/henneysq + +.. _Mark Wronkiewicz: https://ml.jpl.nasa.gov/people/wronkiewicz/wronkiewicz.html + +.. _Marmaduke Woodman: https://github.com/maedoc + +.. _Martin Billinger: https://github.com/mbillingr + +.. _Martin Luessi: https://github.com/mluessi + +.. _Martin Schulz: https://github.com/marsipu + +.. _Mathieu Scheltienne: https://github.com/mscheltienne + +.. _Mathurin Massias: https://mathurinm.github.io/ + +.. _Mats van Es: https://github.com/matsvanes + +.. _Matt Boggess: https://github.com/mattboggess + +.. _Matt Courtemanche: https://github.com/mjcourte + +.. _Matt Sanderson: https://github.com/monkeyman192 + +.. _Matteo Anelli: https://github.com/matteoanelli + +.. _Matthias Dold: https://matthiasdold.de + +.. _Matthias Eberlein: https://github.com/MatthiasEb + +.. _Matti Toivonen: https://github.com/mattitoi + +.. _Mauricio Cespedes Tenorio: https://github.com/mcespedes99 + +.. _Michiru Kaneda: https://github.com/rcmdnk + +.. _Mikołaj Magnuski: https://github.com/mmagnuski + +.. _Milan Rybář: https://milanrybar.cz + +.. _Mingjian He: https://github.com/mh105 + +.. _Mohammad Daneshzand: https://github.com/mdaneshzand + +.. _Moritz Gerster: https://github.com/moritz-gerster + +.. _Natalie Klein: https://github.com/natalieklein + +.. _Nathalie Gayraud: https://github.com/ngayraud + +.. _Naveen Srinivasan: https://github.com/naveensrinivasan + +.. _Nick Foti: https://nfoti.github.io + +.. _Nick Ward: https://www.ucl.ac.uk/ion/departments/sobell/Research/NWard + +.. _Nicolas Barascud: https://github.com/nbara + +.. _Niels Focke: https://neurologie.umg.eu/forschung/arbeitsgruppen/epilepsie-und-bildgebungsforschung + +.. _Niklas Wilming: https://github.com/nwilming + +.. _Nikolai Chapochnikov: https://github.com/chapochn + +.. _Nikolas Chalas: https://github.com/Nichalas + +.. _Okba Bekhelifi: https://github.com/okbalefthanded + +.. _Olaf Hauk: https://www.neuroscience.cam.ac.uk/directory/profile.php?olafhauk + +.. _Oleh Kozynets: https://github.com/OlehKSS + +.. _Pablo-Arias: https://github.com/Pablo-Arias + +.. _Pablo Mainar: https://github.com/pablomainar + +.. _Padma Sundaram: https://www.nmr.mgh.harvard.edu/user/8071 + +.. _Paul Pasler: https://github.com/ppasler + +.. _Paul Roujansky: https://github.com/paulroujansky + +.. _Pavel Navratil: https://github.com/navrpa13 + +.. _Peter Molfese: https://github.com/pmolfese + +.. _Phillip Alday: https://palday.bitbucket.io + +.. _Pierre Ablin: https://pierreablin.com + +.. _Pierre-Antoine Bannier: https://github.com/PABannier + +.. _Proloy Das: https://github.com/proloyd + +.. _Qian Chu: https://github.com/qian-chu + +.. _Qianliang Li: https://www.dtu.dk/english/service/phonebook/person?id=126774 + +.. _Quentin Barthélemy: https://github.com/qbarthelemy + +.. _Quentin Bertrand: https://github.com/QB3 + +.. _Qunxi Dong: https://github.com/dongqunxi + +.. _Rahul Nadkarni: https://github.com/rahuln + +.. _Ram Pari: https://github.com/ramkpari + +.. _Ramiro Gatti: https://github.com/ragatti + +.. _ramonapariciog: https://github.com/ramonapariciog + +.. _Rasmus Aagaard: https://github.com/rasgaard + +.. _Rasmus Zetter: https://people.aalto.fi/rasmus.zetter + +.. _Reza Nasri: https://github.com/rznas + +.. _Reza Shoorangiz: https://github.com/rezashr + +.. _Richard Höchenberger: https://github.com/hoechenberger + +.. _Richard Koehler: https://github.com/richardkoehler + +.. _Riessarius Stargardsky: https://github.com/Riessarius + +.. _Roan LaPlante: https://github.com/aestrivex + +.. _Robert Luke: https://github.com/rob-luke + +.. _Robert Seymour: https://neurofractal.github.io + +.. _Romain Derollepot: https://github.com/rderollepot + +.. _Romain Trachel: https://fr.linkedin.com/in/trachelr + +.. _Roman Goj: https://romanmne.blogspot.co.uk + +.. _Ross Maddox: https://www.urmc.rochester.edu/labs/maddox-lab.aspx + +.. _Rotem Falach: https://github.com/Falach + +.. _Samu Taulu: https://phys.washington.edu/people/samu-taulu + +.. _Samuel Deslauriers-Gauthier: https://github.com/sdeslauriers + +.. _Samuel Louviot: https://github.com/Sam54000 + +.. _Samuel Powell: https://github.com/samuelpowell + +.. _Santeri Ruuskanen: https://github.com/ruuskas + +.. _Sara Sommariva: https://www.dima.unige.it/~sommariva/ + +.. _Sawradip Saha: https://sawradip.github.io/ + +.. _Scott Huberty: https://orcid.org/0000-0003-2637-031X + +.. _Sebastiaan Mathot: https://www.cogsci.nl/smathot + +.. _Sebastian Castano: https://github.com/jscastanoc + +.. _Sebastian Major: https://github.com/major-s + +.. _Sébastien Marti: https://www.researchgate.net/profile/Sebastien-Marti + +.. _Sena Er: https://github.com/sena-neuro + +.. _Senwen Deng: https://snwn.de + +.. _Sheraz Khan: https://github.com/SherazKhan + +.. _Silvia Cotroneo: https://github.com/sfc-neuro + +.. _Simeon Wong: https://github.com/dtxe + +.. _Simon Kern: https://skjerns.de + +.. _Simon Kornblith: https://simonster.com + +.. _Sondre Foslien: https://github.com/sondrfos + +.. _Sophie Herbst: https://github.com/SophieHerbst + +.. _Stanislas Chambon: https://github.com/Slasnista + +.. _Stefan Appelhoff: https://stefanappelhoff.com + +.. _Stefan Repplinger: https://github.com/stfnrpplngr + +.. _Steven Bethard: https://github.com/bethard + +.. _Steven Bierer: https://github.com/neurolaunch + +.. _Steven Gutstein: https://github.com/smgutstein + +.. _Sumalyo Datta: https://github.com/Sumalyo + +.. _Susanna Aro: https://www.linkedin.com/in/susanna-aro + +.. _Svea Marie Meyer: https://github.com/SveaMeyer13 + +.. _T. Wang: https://github.com/twang5 + +.. _Tal Linzen: https://tallinzen.net/ + +.. _Teon Brooks: https://teonbrooks.com + +.. _Théodore Papadopoulo: https://github.com/papadop + +.. _Thomas Binns: https://github.com/tsbinns + +.. _Thomas Hartmann: https://github.com/thht + +.. _Thomas Radman: https://github.com/tradman + +.. _Timothy Gates: https://au.linkedin.com/in/tim-gates-0528a4199 + +.. _Timur Sokhin: https://github.com/Qwinpin + +.. _Tod Flak: https://github.com/todflak + +.. _Tom Ma: https://github.com/myd7349 + +.. _Tom Stone: https://github.com/tomdstone + +.. _Tommy Clausner: https://github.com/TommyClausner + +.. _Toomas Erik Anijärv: https://www.toomaserikanijarv.com/ + +.. _Tristan Stenner: https://github.com/tstenner/ + +.. _Tziona NessAiver: https://github.com/TzionaN + +.. _Valerii Chirkov: https://github.com/vagechirkov + +.. _Victor Ferat: https://github.com/vferat + +.. _Victoria Peterson: https://github.com/vpeterson + +.. _Xiaokai Xia: https://github.com/dddd1007 + +.. _Yaroslav Halchenko: http://haxbylab.dartmouth.edu/ppl/yarik.html + +.. _Yiping Zuo: https://github.com/frostime + +.. _Yousra Bekhti: https://www.linkedin.com/pub/yousra-bekhti/56/886/421 + +.. _Yu-Han Luo: https://github.com/yh-luo + +.. _Zhi Zhang: https://github.com/tczhangzhi/ + +.. _Zvi Baratz: https://github.com/ZviBaratz From eb6ac3dca92a730787dfa33b08b0dd12f98d1e89 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 7 Feb 2024 11:29:35 +1000 Subject: [PATCH 09/65] added contribution to changelog --- doc/changes/devel.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 3f3e8036419..40563f17223 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -24,7 +24,7 @@ Version 1.7.dev0 (development) Enhancements ~~~~~~~~~~~~ - Speed up export to .edf in :func:`mne.export.export_raw` by using ``edfio`` instead of ``EDFlib-Python`` (:gh:`12218` by :newcontrib:`Florian Hofer`) - +- Allow :class:`EpochsTFR` as input to :func:`mne.epoch.Epochs.equalize_epoch_counts` (:gh:`11776` by `Carina Forster`_) Bugs ~~~~ From 7d17f89a2dec29ee0b528f89a5a45438e4889eff Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 7 Feb 2024 11:37:10 +1000 Subject: [PATCH 10/65] added second PR number to close both --- doc/changes/devel.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 40563f17223..710b1b7f0a2 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -24,7 +24,7 @@ Version 1.7.dev0 (development) Enhancements ~~~~~~~~~~~~ - Speed up export to .edf in :func:`mne.export.export_raw` by using ``edfio`` instead of ``EDFlib-Python`` (:gh:`12218` by :newcontrib:`Florian Hofer`) -- Allow :class:`EpochsTFR` as input to :func:`mne.epoch.Epochs.equalize_epoch_counts` (:gh:`11776` by `Carina Forster`_) +- Allow :class:`EpochsTFR` as input to :func:`mne.epoch.Epochs.equalize_epoch_counts` (:gh:`11776`, :gh: `12207` by `Carina Forster`_) Bugs ~~~~ From 0d03885d00d582e98dad4c0c272eb4e59482ea57 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 14 Feb 2024 17:18:43 +1000 Subject: [PATCH 11/65] deleted bad epochs function --- mne/epochs.py | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 47be359af81..738b108a117 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -2092,40 +2092,6 @@ def _repr_html_(self): t = t.render(epochs=self, baseline=baseline, events=event_strings) return t - def channel_specific_epoch_rejection(self, outliers: float): - """Mask outlier epochs for each channel. - - Parameters - ---------- - data : np.ndarray - The data to find outliers in. The data should be in the shape - (epochs X channels X (frequency) X time). - outliers : float - The number of standard deviations to use as a cutoff for outliers. - - Returns - ------- - epochs : instance of Epochs - The masked epochs object, modified in-place. - mask: np.ndarray - The array used to mask the epochs. True == Keep epochs, - False = reject epochs. - """ - _check_preload(self, "Modifying data of epochs") - # extract data from Epochs object - # get absolut values - abs_data = np.abs(self.get_data()) # (epochs X channels X (frequency) X time) - # get the maximum voltage per epoch - max = np.max(abs_data, axis=-1) # (epochs X channels X (frequency)) - # get the standard deviation per channel - std = np.std(abs_data, axis=(-1, 0)) # (channels X (frequency)) - # get the mean per channel - mean = np.mean(abs_data, axis=(-1, 0)) # (channels X (frequency)) - # keep epochs where the maximum voltage is smaller than the mean + (outliers * std) - keep = max < ((outliers * std) + mean) # (epochs X channels X (frequency)) - # set values to NaN where 2D mask is False in the 3D data array - self.get_data()[keep is False] = np.nan - return self, keep @verbose def crop(self, tmin=None, tmax=None, include_tmax=True, verbose=None): From e16cf7402bbb2a4946dd55e2063b808286ed3b85 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 14 Feb 2024 17:19:10 +1000 Subject: [PATCH 12/65] added interpolate bad epochs method --- mne/channels/interpolation.py | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/mne/channels/interpolation.py b/mne/channels/interpolation.py index 6c5042d1d04..ee1c38af24b 100644 --- a/mne/channels/interpolation.py +++ b/mne/channels/interpolation.py @@ -205,6 +205,53 @@ def _interpolate_bads_nan( picks_bad = pick_channels(inst.info["ch_names"], bads_type, exclude=[]) inst._data[..., picks_bad, :] = np.nan +@verbose +def _interpolate_bad_epochs_nan( + inst, + ch_type, + ref_meg=False, + exclude=(), + outlier_indices: list = None, + *, + verbose=None, +): + """ + Interpolate bad epochs with NaNs. + + Parameters + ---------- + inst : instance of Epochs + The epochs object to interpolate. + ch_type : str + The channel type to operate on. + ref_meg : bool + If True include CTF / 4D reference channels. Defaults to False. + exclude : list of str + List of channels to exclude. If empty do not exclude any. + outlier_indices : list of arrays + List of arrays with indices of bad epochs per channel. + verbose : bool, str, int, or None + If not None, override default verbose level (see mne.verbose). + """ + info = _simplify_info(inst.info) + picks_type = pick_types(info, ref_meg=ref_meg, exclude=exclude, **{ch_type: True}) + + # extract the data from the epochs object: shape (n_epochs, n_channels, n_times) + data = inst.get_data() + + # loop over channels of specific type + for ch in range(data.shape[1]): + # skip channels that are not of the type we are interested in + if ch not in picks_type: + continue + # use those indices to set the epochs per channel to NaN for all timepoints + data[outlier_indices[ch], ch, :] = np.nan + + # put back into epochs structure + inst.data = data + + return inst + @verbose def _interpolate_bads_meeg( From 0cb0ed527263c9506a899e0b0bcf3d9b9b92c785 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 14 Feb 2024 07:19:47 +0000 Subject: [PATCH 13/65] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/channels/interpolation.py | 1 + mne/epochs.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/channels/interpolation.py b/mne/channels/interpolation.py index ee1c38af24b..b6bda97272f 100644 --- a/mne/channels/interpolation.py +++ b/mne/channels/interpolation.py @@ -205,6 +205,7 @@ def _interpolate_bads_nan( picks_bad = pick_channels(inst.info["ch_names"], bads_type, exclude=[]) inst._data[..., picks_bad, :] = np.nan + @verbose def _interpolate_bad_epochs_nan( inst, diff --git a/mne/epochs.py b/mne/epochs.py index 672f0f51a19..41c74788a98 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -2093,7 +2093,6 @@ def _repr_html_(self): t = t.render(epochs=self, baseline=baseline, events=event_strings) return t - @verbose def crop(self, tmin=None, tmax=None, include_tmax=True, verbose=None): """Crop a time interval from the epochs. From 8a2f95bf5db9d0942891259ebcb5143fcb657f75 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 14 Feb 2024 17:44:48 +1000 Subject: [PATCH 14/65] fixed bug in interpolate epochs method --- mne/channels/interpolation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mne/channels/interpolation.py b/mne/channels/interpolation.py index ee1c38af24b..96afc3489aa 100644 --- a/mne/channels/interpolation.py +++ b/mne/channels/interpolation.py @@ -250,8 +250,6 @@ def _interpolate_bad_epochs_nan( # put back into epochs structure inst.data = data - return inst - @verbose def _interpolate_bads_meeg( From 1e2252e0ee50e38ed0a074eb94bc5cd4b05f3e5b Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Thu, 15 Feb 2024 11:18:34 +1000 Subject: [PATCH 15/65] added interpolate bad epochs nan to interpolate_bads function --- mne/channels/channels.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index aee085891c4..a09a3bb394c 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -813,9 +813,10 @@ def interpolate_bads( origin="auto", method=None, exclude=(), + outlier_indices=(), verbose=None, ): - """Interpolate bad MEG and EEG channels. + """Interpolate bad MEG, EEG and fNIRS channels and epochs. Operates in place. @@ -836,9 +837,9 @@ def interpolate_bads( method : dict | str | None Method to use for each channel type. - - ``"meg"`` channels support ``"MNE"`` (default) and ``"nan"`` - - ``"eeg"`` channels support ``"spline"`` (default), ``"MNE"`` and ``"nan"`` - - ``"fnirs"`` channels support ``"nearest"`` (default) and ``"nan"`` + - ``"meg"`` channels support ``"MNE"`` (default), ``"nan" and "nan_epochs"`` + - ``"eeg"`` channels support ``"spline"`` (default), ``"MNE"``, ``"nan" and "nan_epochs"`` + - ``"fnirs"`` channels support ``"nearest"`` (default), ``"nan"`` and "nan_epochs"`` None is an alias for:: @@ -846,7 +847,8 @@ def interpolate_bads( If a :class:`str` is provided, the method will be applied to all channel types supported and available in the instance. The method ``"nan"`` will - replace the channel data with ``np.nan``. + replace the channel data with ``np.nan``. The method ``"nan_epochs"`` will + replace bad epochs defined by outlier_indices with ``np.nan``. .. warning:: Be careful when using ``method="nan"``; the default value @@ -856,6 +858,11 @@ def interpolate_bads( exclude : list | tuple The channels to exclude from interpolation. If excluded a bad channel will stay in bads. + + outlier_indices: list | tuple + The indices of the bad epochs to exclude from the interpolation for + each channel. + %(verbose)s Returns @@ -870,6 +877,7 @@ def interpolate_bads( .. versionadded:: 0.9.0 """ from .interpolation import ( + _interpolate_bad_epochs_nan, _interpolate_bads_ecog, _interpolate_bads_eeg, _interpolate_bads_meeg, @@ -894,16 +902,21 @@ def interpolate_bads( for key in keys2delete: del method[key] valids = { - "eeg": ("spline", "MNE", "nan"), - "meg": ("MNE", "nan"), - "fnirs": ("nearest", "nan"), - "ecog": ("spline", "nan"), - "seeg": ("spline", "nan"), + "eeg": ("spline", "MNE", "nan", "nan_epochs"), + "meg": ("MNE", "nan", "nan_epochs"), + "fnirs": ("nearest", "nan", "nan_epochs"), + "ecog": ("spline", "nan", "nan_epochs"), + "seeg": ("spline", "nan", "nan_epochs"), } for key in method: _check_option("method[key]", key, tuple(valids)) _check_option(f"method['{key}']", method[key], valids[key]) logger.info("Setting channel interpolation method to %s.", method) + # make sure to first set bad epochs to NaN before channel interpolation + for interp in method.values(): + if interp == "nan_epochs": + _interpolate_bad_epochs_nan(self, ch_type, exclude=exclude, + outlier_indices=None) idx = _picks_to_idx(self.info, list(method), exclude=(), allow_empty=True) if idx.size == 0 or len(pick_info(self.info, idx)["bads"]) == 0: warn("No bad channels to interpolate. Doing nothing...") From dfbba00ced72c0971d68d46c30ad5a0a5fdec9f7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 01:19:08 +0000 Subject: [PATCH 16/65] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/channels/channels.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index a09a3bb394c..75d5d0d2164 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -915,8 +915,9 @@ def interpolate_bads( # make sure to first set bad epochs to NaN before channel interpolation for interp in method.values(): if interp == "nan_epochs": - _interpolate_bad_epochs_nan(self, ch_type, exclude=exclude, - outlier_indices=None) + _interpolate_bad_epochs_nan( + self, ch_type, exclude=exclude, outlier_indices=None + ) idx = _picks_to_idx(self.info, list(method), exclude=(), allow_empty=True) if idx.size == 0 or len(pick_info(self.info, idx)["bads"]) == 0: warn("No bad channels to interpolate. Doing nothing...") From cc58b73682b2a58116cc41e46d7ba5be8fcf7ac0 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Thu, 15 Feb 2024 13:04:27 +1000 Subject: [PATCH 17/65] removed interpolate bad epochs from interpolate_bads due to MixinClass --- mne/channels/channels.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index a09a3bb394c..9f8462aa956 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -813,7 +813,6 @@ def interpolate_bads( origin="auto", method=None, exclude=(), - outlier_indices=(), verbose=None, ): """Interpolate bad MEG, EEG and fNIRS channels and epochs. @@ -837,9 +836,9 @@ def interpolate_bads( method : dict | str | None Method to use for each channel type. - - ``"meg"`` channels support ``"MNE"`` (default), ``"nan" and "nan_epochs"`` - - ``"eeg"`` channels support ``"spline"`` (default), ``"MNE"``, ``"nan" and "nan_epochs"`` - - ``"fnirs"`` channels support ``"nearest"`` (default), ``"nan"`` and "nan_epochs"`` + - ``"meg"`` channels support ``"MNE"`` (default) and ``"nan" + - ``"eeg"`` channels support ``"spline"`` (default), ``"MNE"`` and ``"nan" + - ``"fnirs"`` channels support ``"nearest"`` (default) and ``"nan"`` None is an alias for:: @@ -847,8 +846,7 @@ def interpolate_bads( If a :class:`str` is provided, the method will be applied to all channel types supported and available in the instance. The method ``"nan"`` will - replace the channel data with ``np.nan``. The method ``"nan_epochs"`` will - replace bad epochs defined by outlier_indices with ``np.nan``. + replace the channel data with ``np.nan``. .. warning:: Be careful when using ``method="nan"``; the default value @@ -859,10 +857,6 @@ def interpolate_bads( The channels to exclude from interpolation. If excluded a bad channel will stay in bads. - outlier_indices: list | tuple - The indices of the bad epochs to exclude from the interpolation for - each channel. - %(verbose)s Returns @@ -877,7 +871,6 @@ def interpolate_bads( .. versionadded:: 0.9.0 """ from .interpolation import ( - _interpolate_bad_epochs_nan, _interpolate_bads_ecog, _interpolate_bads_eeg, _interpolate_bads_meeg, @@ -902,21 +895,16 @@ def interpolate_bads( for key in keys2delete: del method[key] valids = { - "eeg": ("spline", "MNE", "nan", "nan_epochs"), - "meg": ("MNE", "nan", "nan_epochs"), - "fnirs": ("nearest", "nan", "nan_epochs"), - "ecog": ("spline", "nan", "nan_epochs"), - "seeg": ("spline", "nan", "nan_epochs"), + "eeg": ("spline", "MNE", "nan"), + "meg": ("MNE", "nan"), + "fnirs": ("nearest", "nan"), + "ecog": ("spline", "nan"), + "seeg": ("spline", "nan"), } for key in method: _check_option("method[key]", key, tuple(valids)) _check_option(f"method['{key}']", method[key], valids[key]) logger.info("Setting channel interpolation method to %s.", method) - # make sure to first set bad epochs to NaN before channel interpolation - for interp in method.values(): - if interp == "nan_epochs": - _interpolate_bad_epochs_nan(self, ch_type, exclude=exclude, - outlier_indices=None) idx = _picks_to_idx(self.info, list(method), exclude=(), allow_empty=True) if idx.size == 0 or len(pick_info(self.info, idx)["bads"]) == 0: warn("No bad channels to interpolate. Doing nothing...") From 167f38c7b1ce6a18d4024b1b94836425eab040b9 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Thu, 15 Feb 2024 13:04:58 +1000 Subject: [PATCH 18/65] removed interpolate bad epochs --- mne/channels/interpolation.py | 46 ----------------------------------- 1 file changed, 46 deletions(-) diff --git a/mne/channels/interpolation.py b/mne/channels/interpolation.py index 56749091097..6c5042d1d04 100644 --- a/mne/channels/interpolation.py +++ b/mne/channels/interpolation.py @@ -206,52 +206,6 @@ def _interpolate_bads_nan( inst._data[..., picks_bad, :] = np.nan -@verbose -def _interpolate_bad_epochs_nan( - inst, - ch_type, - ref_meg=False, - exclude=(), - outlier_indices: list = None, - *, - verbose=None, -): - """ - Interpolate bad epochs with NaNs. - - Parameters - ---------- - inst : instance of Epochs - The epochs object to interpolate. - ch_type : str - The channel type to operate on. - ref_meg : bool - If True include CTF / 4D reference channels. Defaults to False. - exclude : list of str - List of channels to exclude. If empty do not exclude any. - outlier_indices : list of arrays - List of arrays with indices of bad epochs per channel. - verbose : bool, str, int, or None - If not None, override default verbose level (see mne.verbose). - """ - info = _simplify_info(inst.info) - picks_type = pick_types(info, ref_meg=ref_meg, exclude=exclude, **{ch_type: True}) - - # extract the data from the epochs object: shape (n_epochs, n_channels, n_times) - data = inst.get_data() - - # loop over channels of specific type - for ch in range(data.shape[1]): - # skip channels that are not of the type we are interested in - if ch not in picks_type: - continue - # use those indices to set the epochs per channel to NaN for all timepoints - data[outlier_indices[ch], ch, :] = np.nan - - # put back into epochs structure - inst.data = data - - @verbose def _interpolate_bads_meeg( inst, From 4e675cc2b2c2c7b66eb2777f80031e7d3cef94f3 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Thu, 15 Feb 2024 13:05:20 +1000 Subject: [PATCH 19/65] added set bad epochs to NaN method for epochs class --- mne/epochs.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/mne/epochs.py b/mne/epochs.py index 41c74788a98..5e4985a4bf1 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -693,6 +693,41 @@ def __init__( self._check_consistency() self.set_annotations(annotations, on_missing="ignore") + + def set_bad_epochs_to_NaN( + self, + bad_epochs_indices: list = None + ): + """ + define bad epochs based on indices list and set to NaN. + + Parameters + ---------- + self : instance of Epochs + bad_epochs_indices : list of arrays + List of arrays with indices of bad epochs per channel. + + Returns + ------- + self : instance of Epochs + The modified instance with NaNs replacing outlier epochs. + """ + if len(bad_epochs_indices) != self.get_data().shape[1]: + raise RuntimeError( + "The length of the list of bad epochs indices " + "must match the number of channels." + ) + if self.preload: + # extract the data from the epochs object: shape (n_epochs, n_channels, n_times) + data = self.get_data() + # loop over channels of specific type + for ch in range(data.shape[1]): + # use those indices to set the epochs per channel to NaN for all timepoints + data[bad_epochs_indices[ch], ch, :] = np.nan + # put back into epochs structure + self.data = data + return self + def _check_consistency(self): """Check invariants of epochs object.""" if hasattr(self, "events"): From 2868197b58e0b1b5ea8dc6480198c3dc83273abc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 03:10:57 +0000 Subject: [PATCH 20/65] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/epochs.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 5e4985a4bf1..725960cf76e 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -693,11 +693,7 @@ def __init__( self._check_consistency() self.set_annotations(annotations, on_missing="ignore") - - def set_bad_epochs_to_NaN( - self, - bad_epochs_indices: list = None - ): + def set_bad_epochs_to_NaN(self, bad_epochs_indices: list = None): """ define bad epochs based on indices list and set to NaN. @@ -713,10 +709,10 @@ def set_bad_epochs_to_NaN( The modified instance with NaNs replacing outlier epochs. """ if len(bad_epochs_indices) != self.get_data().shape[1]: - raise RuntimeError( - "The length of the list of bad epochs indices " - "must match the number of channels." - ) + raise RuntimeError( + "The length of the list of bad epochs indices " + "must match the number of channels." + ) if self.preload: # extract the data from the epochs object: shape (n_epochs, n_channels, n_times) data = self.get_data() From 7fc2be1db3a7caa37dd636ebcd02fec239fe6fec Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Thu, 15 Feb 2024 14:31:12 +1000 Subject: [PATCH 21/65] removed return statement (operates in-place) --- mne/epochs.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 725960cf76e..6ad7d7b0bff 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -693,36 +693,34 @@ def __init__( self._check_consistency() self.set_annotations(annotations, on_missing="ignore") - def set_bad_epochs_to_NaN(self, bad_epochs_indices: list = None): + @verbose + def set_bad_epochs_to_NaN(self, bad_epochs_indices: list = None, + verbose=None): """ define bad epochs based on indices list and set to NaN. + Works in-place. + Parameters ---------- self : instance of Epochs bad_epochs_indices : list of arrays List of arrays with indices of bad epochs per channel. - - Returns - ------- - self : instance of Epochs - The modified instance with NaNs replacing outlier epochs. + verbose : bool, str, int, or None """ if len(bad_epochs_indices) != self.get_data().shape[1]: raise RuntimeError( "The length of the list of bad epochs indices " "must match the number of channels." ) - if self.preload: - # extract the data from the epochs object: shape (n_epochs, n_channels, n_times) - data = self.get_data() - # loop over channels of specific type - for ch in range(data.shape[1]): - # use those indices to set the epochs per channel to NaN for all timepoints - data[bad_epochs_indices[ch], ch, :] = np.nan - # put back into epochs structure - self.data = data - return self + # extract the data from the epochs object: shape (n_epochs, n_channels, n_times) + data = self.get_data() + # loop over channels of specific type + for ch in range(data.shape[1]): + # use those indices to set the epochs per channel to NaN for all timepoints + data[bad_epochs_indices[ch], ch, :] = np.nan + # put back into epochs structure + self.data = data def _check_consistency(self): """Check invariants of epochs object.""" From 91302bbe28c784b221754c8b325028b1b99419a2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 Feb 2024 04:31:43 +0000 Subject: [PATCH 22/65] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/epochs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 6ad7d7b0bff..545b941f008 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -694,8 +694,7 @@ def __init__( self.set_annotations(annotations, on_missing="ignore") @verbose - def set_bad_epochs_to_NaN(self, bad_epochs_indices: list = None, - verbose=None): + def set_bad_epochs_to_NaN(self, bad_epochs_indices: list = None, verbose=None): """ define bad epochs based on indices list and set to NaN. From b22cf11c25bbef7bf4d4d620fc46a0cf20e1b8b7 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Thu, 15 Feb 2024 14:50:29 +1000 Subject: [PATCH 23/65] deleted epoch based rejection from doc string --- mne/channels/channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 9f8462aa956..c60703f9388 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -815,7 +815,7 @@ def interpolate_bads( exclude=(), verbose=None, ): - """Interpolate bad MEG, EEG and fNIRS channels and epochs. + """Interpolate bad MEG, EEG and fNIRS channels. Operates in place. From c7947108b44c1580617325ac419c6c88d3333201 Mon Sep 17 00:00:00 2001 From: dominikwelke Date: Wed, 21 Feb 2024 20:57:56 +0000 Subject: [PATCH 24/65] DW initial revisions --- mne/channels/channels.py | 4 ++-- mne/epochs.py | 5 ++++- mne/tests/test_epochs.py | 16 ++++++++++++++++ mne/utils/check.py | 4 ++-- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 9f8462aa956..bfeeaecbba7 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -836,8 +836,8 @@ def interpolate_bads( method : dict | str | None Method to use for each channel type. - - ``"meg"`` channels support ``"MNE"`` (default) and ``"nan" - - ``"eeg"`` channels support ``"spline"`` (default), ``"MNE"`` and ``"nan" + - ``"meg"`` channels support ``"MNE"`` (default) and ``"nan"`` + - ``"eeg"`` channels support ``"spline"`` (default), ``"MNE"`` and ``"nan"`` - ``"fnirs"`` channels support ``"nearest"`` (default) and ``"nan"`` None is an alias for:: diff --git a/mne/epochs.py b/mne/epochs.py index 545b941f008..f178b60b8f8 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -707,6 +707,9 @@ def set_bad_epochs_to_NaN(self, bad_epochs_indices: list = None, verbose=None): List of arrays with indices of bad epochs per channel. verbose : bool, str, int, or None """ + if not self.preload: + raise ValueError("Data must be preloaded.") + if len(bad_epochs_indices) != self.get_data().shape[1]: raise RuntimeError( "The length of the list of bad epochs indices " @@ -1209,7 +1212,7 @@ def _compute_aggregate(self, picks, mode="mean"): n_events += 1 if n_events > 0: - data = np.nanmean(data) + data /= n_events else: data.fill(np.nan) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 015974e89cc..86a7778e1f7 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -5204,3 +5204,19 @@ def test_empty_error(method, epochs_empty): pytest.importorskip("pandas") with pytest.raises(RuntimeError, match="is empty."): getattr(epochs_empty.copy(), method[0])(**method[1]) + + +def test_set_bad_epochs_to_nan(): + """Test channel specific epoch rejection.""" + # preload=False + raw, ev, _ = _get_data(preload=False) + ep = Epochs(raw, ev, tmin=0, tmax=0.1, baseline=(0, 0)) + bads = [[]] * ep.info["nchan"] + bads[0] = [1] + with pytest.raises(ValueError, match="must be preloaded"): + ep.set_bad_epochs_to_NaN(bads) + + # preload=True + ep.load_data() + ep.set_bad_epochs_to_NaN(bads) + _ = ep.average() diff --git a/mne/utils/check.py b/mne/utils/check.py index 13eca1e0ba0..70b88639132 100644 --- a/mne/utils/check.py +++ b/mne/utils/check.py @@ -901,12 +901,12 @@ def _check_combine(mode, valid=("mean", "median", "std"), axis=0): if mode == "mean": def fun(data): - return np.mean(data, axis=axis) + return np.nanmean(data, axis=axis) elif mode == "std": def fun(data): - return np.std(data, axis=axis) + return np.nanstd(data, axis=axis) elif mode == "median" or mode == np.median: From fa9567a8a61e21e2e91f77aed865ab83a7bb8fc9 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 28 Feb 2024 15:42:53 +0100 Subject: [PATCH 25/65] fixed docstring --- mne/epochs.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 4ad44fae491..4814bc6a0d2 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -693,19 +693,20 @@ def __init__( self._check_consistency() self.set_annotations(annotations, on_missing="ignore") - @verbose - def set_bad_epochs_to_NaN(self, bad_epochs_indices: list = None, verbose=None): + def set_bad_epochs_to_NaN(self, bad_epochs_indices: list = None): """ - define bad epochs based on indices list and set to NaN. - - Works in-place. + Define bad epochs based on indices list and set to NaN. Parameters ---------- self : instance of Epochs bad_epochs_indices : list of arrays List of arrays with indices of bad epochs per channel. - verbose : bool, str, int, or None + + Notes + ----- + This function operates in-place. + """ if not self.preload: raise ValueError("Data must be preloaded.") From 4abf7fdd605185fbb94a3ada4d9beaeb4776760f Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 28 Feb 2024 16:01:07 +0100 Subject: [PATCH 26/65] fixed dostring set_bad_epochs_to_NaN --- mne/epochs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mne/epochs.py b/mne/epochs.py index 4814bc6a0d2..6aa51fb33bd 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -706,7 +706,6 @@ def set_bad_epochs_to_NaN(self, bad_epochs_indices: list = None): Notes ----- This function operates in-place. - """ if not self.preload: raise ValueError("Data must be preloaded.") From 5d048a861bc91dda2ab40179ea2bfd11042258f7 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 10 Sep 2025 09:53:34 +1000 Subject: [PATCH 27/65] clean up channel specific epoch rejection function --- mne/epochs.py | 54 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 16d7332b831..7fdd75da4a3 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -690,36 +690,48 @@ def __init__( self._check_consistency() self.set_annotations(annotations, on_missing="ignore") - def set_bad_epochs_to_NaN(self, bad_epochs_indices: list = None): - """ - Define bad epochs based on indices list and set to NaN. + def set_bad_epochs_to_NaN(self, bad_epochs_per_channel: list = None): + """Drop bad epochs on a per-channel basis by setting to NaN. Parameters ---------- - self : instance of Epochs - bad_epochs_indices : list of arrays - List of arrays with indices of bad epochs per channel. + bad_epochs_per_channel : list of array-like | None + List of arrays containing epoch indices to mark as bad for each + channel. Length must equal number of channels. If None, no epochs + are marked as bad. - Notes - ----- - This function operates in-place. + Returns + ------- + self : instance of Epochs + The epochs instance (modified in-place). """ if not self.preload: - raise ValueError("Data must be preloaded.") + raise ValueError("Epochs must be preloaded.") - if len(bad_epochs_indices) != self.get_data().shape[1]: - raise RuntimeError( - "The length of the list of bad epochs indices " - "must match the number of channels." - ) # extract the data from the epochs object: shape (n_epochs, n_channels, n_times) data = self.get_data() - # loop over channels of specific type - for ch in range(data.shape[1]): - # use those indices to set the epochs per channel to NaN for all timepoints - data[bad_epochs_indices[ch], ch, :] = np.nan - # put back into epochs structure - self.data = data + n_epochs, n_channels, n_times = data.shape + + # check list format + if len(bad_epochs_per_channel) != n_channels: + raise RuntimeError( + f"Length of bad_epochs_per_channel ({len(bad_epochs_per_channel)}) " + f"must equal number of channels ({n_channels})" + ) + + # loop over bad epochs list + for ch_idx, bad_epochs in enumerate(bad_epochs_per_channel): + if len(bad_epochs) > 0: # check only channels with bad epochs + # convert to numpy array + bad_epochs = np.asarray(bad_epochs) + # check for valid epoch indices + if np.any(bad_epochs >= n_epochs) or np.any(bad_epochs < 0): + raise ValueError(f"Invalid epoch indices for channel {ch_idx}") + # if valid index, set to NaN + data[bad_epochs, ch_idx, :] = np.nan + + # update data in epochs class + self._data = data def _check_consistency(self): """Check invariants of epochs object.""" From 6e9753e1e5620f18afccd9320ecaf646fba08d2b Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 10 Sep 2025 09:55:02 +1000 Subject: [PATCH 28/65] clean up channel specific epoch rejection function, renamed function --- mne/epochs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/epochs.py b/mne/epochs.py index 7fdd75da4a3..06895a765b8 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -690,7 +690,7 @@ def __init__( self._check_consistency() self.set_annotations(annotations, on_missing="ignore") - def set_bad_epochs_to_NaN(self, bad_epochs_per_channel: list = None): + def drop_bad_epochs(self, bad_epochs_per_channel: list = None): """Drop bad epochs on a per-channel basis by setting to NaN. Parameters From 900ea3563e4d269a354c84cf7a543597cdb76b19 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 10 Sep 2025 10:40:36 +1000 Subject: [PATCH 29/65] included nave updates --- mne/epochs.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/mne/epochs.py b/mne/epochs.py index 06895a765b8..2bf92d37e1c 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -705,6 +705,9 @@ def drop_bad_epochs(self, bad_epochs_per_channel: list = None): self : instance of Epochs The epochs instance (modified in-place). """ + if bad_epochs_per_channel is None: + return self + if not self.preload: raise ValueError("Epochs must be preloaded.") @@ -719,8 +722,16 @@ def drop_bad_epochs(self, bad_epochs_per_channel: list = None): f"must equal number of channels ({n_channels})" ) + # store number of epochs per channel for nave updates + valid_epochs_per_channel = [] + # loop over bad epochs list for ch_idx, bad_epochs in enumerate(bad_epochs_per_channel): + # Calculate valid epochs for this channel + total_epochs = n_epochs + n_bad = len(bad_epochs) if len(bad_epochs) > 0 else 0 + n_valid = total_epochs - n_bad + valid_epochs_per_channel.append(n_valid) if len(bad_epochs) > 0: # check only channels with bad epochs # convert to numpy array bad_epochs = np.asarray(bad_epochs) @@ -730,6 +741,13 @@ def drop_bad_epochs(self, bad_epochs_per_channel: list = None): # if valid index, set to NaN data[bad_epochs, ch_idx, :] = np.nan + # Store attribute to track channel-specific nave + self._nave_per_channel = valid_epochs_per_channel + + # For backward compatibility, set nave to minimum across channels + # (standard in MNE plots) + self.nave = min(valid_epochs_per_channel) + # update data in epochs class self._data = data From d44a602c0eca30b2364656ff94f61c5159ccf162 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Mon, 22 Sep 2025 19:59:15 +1000 Subject: [PATCH 30/65] after pre-commit hooks --- mne/epochs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 2bf92d37e1c..3a03665eed0 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -691,7 +691,7 @@ def __init__( self.set_annotations(annotations, on_missing="ignore") def drop_bad_epochs(self, bad_epochs_per_channel: list = None): - """Drop bad epochs on a per-channel basis by setting to NaN. + """Drop bad epochs on a per-channel basis. Parameters ---------- @@ -742,7 +742,7 @@ def drop_bad_epochs(self, bad_epochs_per_channel: list = None): data[bad_epochs, ch_idx, :] = np.nan # Store attribute to track channel-specific nave - self._nave_per_channel = valid_epochs_per_channel + self.nave_per_channel = valid_epochs_per_channel # For backward compatibility, set nave to minimum across channels # (standard in MNE plots) From e92adb708796c8d426b7b16dd260119bf0c0344d Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Mon, 22 Sep 2025 19:59:58 +1000 Subject: [PATCH 31/65] basic testing of drop bad epochs --- mne/tests/test_epochs.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 2fad71d7cb1..77e94615d88 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -5275,17 +5275,32 @@ def test_empty_error(method, epochs_empty): getattr(epochs_empty.copy(), method[0])(**method[1]) -def test_set_bad_epochs_to_nan(): - """Test channel specific epoch rejection.""" - # preload=False +def test_drop_bad_epochs(): + """Test channel-specific epoch rejection and nave attributes.""" + # preload=False should raise an error raw, ev, _ = _get_data(preload=False) ep = Epochs(raw, ev, tmin=0, tmax=0.1, baseline=(0, 0)) bads = [[]] * ep.info["nchan"] - bads[0] = [1] + bads[0] = [1] # set second epoch in first channel to NaN with pytest.raises(ValueError, match="must be preloaded"): - ep.set_bad_epochs_to_NaN(bads) + ep.drop_bad_epochs(bads) - # preload=True + # preload=True should work ep.load_data() - ep.set_bad_epochs_to_NaN(bads) + ep.drop_bad_epochs(bads) + + # check nave attributes + n_epochs = len(ep) + # Channel 0 has 1 bad epoch, all others have 0 + expected_per_channel = [n_epochs - 1] + [n_epochs] * (ep.info["nchan"] - 1) + assert ep.nave_per_channel == expected_per_channel + assert ep.nave == min(expected_per_channel) + + # Verify bad epoch is NaN + import numpy as np + + data = ep.get_data() + assert np.all(np.isnan(data[1, 0, :])) + + # make sure averaging works (allowing for NaNs) _ = ep.average() From 970a66df9fac20a296d439157cb62cd6d528588a Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Mon, 22 Sep 2025 20:17:12 +1000 Subject: [PATCH 32/65] changelog --- doc/changes/dev/12219.newfeature.rst | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 doc/changes/dev/12219.newfeature.rst diff --git a/doc/changes/dev/12219.newfeature.rst b/doc/changes/dev/12219.newfeature.rst new file mode 100644 index 00000000000..6fc2bb62960 --- /dev/null +++ b/doc/changes/dev/12219.newfeature.rst @@ -0,0 +1,9 @@ +Add drop_bad_epochs method to :class:mne.Epochs for channel-specific epoch rejection + +This method allows users to mark bad epochs on a per-channel basis by setting +them to NaN. The +nave and +nave_per_channel attributes are updated +accordingly to reflect the number of valid epochs per channel. + +Contributed by Carina Fo_. From ab6cef7833ecc65c09c421bce3a26854bf8cb0a6 Mon Sep 17 00:00:00 2001 From: Carina Date: Wed, 24 Sep 2025 09:53:05 +1000 Subject: [PATCH 33/65] Update mne/channels/channels.py Co-authored-by: Eric Larson --- mne/channels/channels.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 69663996f0b..1da7ad46451 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -872,7 +872,6 @@ def interpolate_bads( exclude : list | tuple The channels to exclude from interpolation. If excluded a bad channel will stay in bads. - %(verbose)s Returns From b8b9984e2739f0ab7ae3c2cf9a656c6a789e25cd Mon Sep 17 00:00:00 2001 From: Carina Date: Wed, 24 Sep 2025 09:53:13 +1000 Subject: [PATCH 34/65] Update doc/changes/dev/12219.newfeature.rst Co-authored-by: Eric Larson --- doc/changes/dev/12219.newfeature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/dev/12219.newfeature.rst b/doc/changes/dev/12219.newfeature.rst index 6fc2bb62960..e6e7331ae48 100644 --- a/doc/changes/dev/12219.newfeature.rst +++ b/doc/changes/dev/12219.newfeature.rst @@ -6,4 +6,4 @@ nave and nave_per_channel attributes are updated accordingly to reflect the number of valid epochs per channel. -Contributed by Carina Fo_. +Contributed by `Carina Fo`_. From ce4a1a5a8e49e9b793436662122facced4879b9d Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Tue, 30 Sep 2025 15:32:27 +1000 Subject: [PATCH 35/65] replaced lit with boolean mask --- mne/epochs.py | 59 +++++++++++++++++---------------------------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 3a03665eed0..6aac48463f0 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -690,66 +690,45 @@ def __init__( self._check_consistency() self.set_annotations(annotations, on_missing="ignore") - def drop_bad_epochs(self, bad_epochs_per_channel: list = None): - """Drop bad epochs on a per-channel basis. + def drop_bad_epochs(self, reject_mask: np.ndarray = None): + """Drop bad epochs for individual channels. Parameters ---------- - bad_epochs_per_channel : list of array-like | None - List of arrays containing epoch indices to mark as bad for each - channel. Length must equal number of channels. If None, no epochs - are marked as bad. + reject_mask : np.ndarray, shape (n_epochs, n_channels) | None + Boolean mask where True indicates a bad epoch for that channel. + If None, no epochs are marked as bad. Returns ------- self : instance of Epochs - The epochs instance (modified in-place). """ - if bad_epochs_per_channel is None: + if reject_mask is None: return self if not self.preload: raise ValueError("Epochs must be preloaded.") - # extract the data from the epochs object: shape (n_epochs, n_channels, n_times) data = self.get_data() - n_epochs, n_channels, n_times = data.shape + n_epochs, n_channels, _ = data.shape - # check list format - if len(bad_epochs_per_channel) != n_channels: - raise RuntimeError( - f"Length of bad_epochs_per_channel ({len(bad_epochs_per_channel)}) " - f"must equal number of channels ({n_channels})" + if reject_mask.shape != (n_epochs, n_channels): + raise ValueError( + f"reject_mask must have shape ({n_epochs}, {n_channels}), " + f"got {reject_mask.shape}" ) - # store number of epochs per channel for nave updates - valid_epochs_per_channel = [] - - # loop over bad epochs list - for ch_idx, bad_epochs in enumerate(bad_epochs_per_channel): - # Calculate valid epochs for this channel - total_epochs = n_epochs - n_bad = len(bad_epochs) if len(bad_epochs) > 0 else 0 - n_valid = total_epochs - n_bad - valid_epochs_per_channel.append(n_valid) - if len(bad_epochs) > 0: # check only channels with bad epochs - # convert to numpy array - bad_epochs = np.asarray(bad_epochs) - # check for valid epoch indices - if np.any(bad_epochs >= n_epochs) or np.any(bad_epochs < 0): - raise ValueError(f"Invalid epoch indices for channel {ch_idx}") - # if valid index, set to NaN - data[bad_epochs, ch_idx, :] = np.nan - - # Store attribute to track channel-specific nave - self.nave_per_channel = valid_epochs_per_channel + # Set bad epochs to NaN + data[reject_mask] = np.nan - # For backward compatibility, set nave to minimum across channels - # (standard in MNE plots) - self.nave = min(valid_epochs_per_channel) + # Compute valid epochs per channel + valid_epochs_per_channel = np.sum(~reject_mask, axis=0) + self.nave_per_channel = valid_epochs_per_channel + self.nave = int(valid_epochs_per_channel.min()) - # update data in epochs class + # Update data self._data = data + return self def _check_consistency(self): """Check invariants of epochs object.""" From 6a9d3b8c8832654b996cac03a1d4a320fb1e7b44 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Tue, 30 Sep 2025 16:18:05 +1000 Subject: [PATCH 36/65] added time dimension to mask and forced array to boolean, afterpre commit --- mne/epochs.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 6aac48463f0..2c1a67dfb65 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -710,7 +710,7 @@ def drop_bad_epochs(self, reject_mask: np.ndarray = None): raise ValueError("Epochs must be preloaded.") data = self.get_data() - n_epochs, n_channels, _ = data.shape + n_epochs, n_channels, n_times = data.shape if reject_mask.shape != (n_epochs, n_channels): raise ValueError( @@ -718,8 +718,16 @@ def drop_bad_epochs(self, reject_mask: np.ndarray = None): f"got {reject_mask.shape}" ) + # mask needs to contain integer or boolean + if not np.issubdtype(reject_mask.dtype, np.bool_): + reject_mask = reject_mask.astype(bool) + # Set bad epochs to NaN - data[reject_mask] = np.nan + # We need to add a dimension for time to the array + mask_3d = reject_mask[:, :, np.newaxis] # shape: (n_epochs, n_channels, 1) + + # Broadcast to (n_epochs, n_channels, n_times) and set to NaN + data[mask_3d.repeat(n_times, axis=2)] = np.nan # Compute valid epochs per channel valid_epochs_per_channel = np.sum(~reject_mask, axis=0) From d493db1caa660444abc1d74be4c92a4720afef4f Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Tue, 30 Sep 2025 16:54:45 +1000 Subject: [PATCH 37/65] updated basic test function --- mne/tests/test_epochs.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 77e94615d88..65e4df145e0 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -5280,27 +5280,34 @@ def test_drop_bad_epochs(): # preload=False should raise an error raw, ev, _ = _get_data(preload=False) ep = Epochs(raw, ev, tmin=0, tmax=0.1, baseline=(0, 0)) - bads = [[]] * ep.info["nchan"] - bads[0] = [1] # set second epoch in first channel to NaN + n_epochs, n_channels = len(ep), ep.info["nchan"] + + # Reject mask: all epochs good + reject_mask = np.zeros((n_epochs, n_channels)) + reject_mask[1, 0] = True # second epoch, first channel → bad + with pytest.raises(ValueError, match="must be preloaded"): - ep.drop_bad_epochs(bads) + ep.drop_bad_epochs(reject_mask) - # preload=True should work + # preload=True should now work ep.load_data() - ep.drop_bad_epochs(bads) + ep.drop_bad_epochs(reject_mask) # check nave attributes - n_epochs = len(ep) - # Channel 0 has 1 bad epoch, all others have 0 - expected_per_channel = [n_epochs - 1] + [n_epochs] * (ep.info["nchan"] - 1) - assert ep.nave_per_channel == expected_per_channel - assert ep.nave == min(expected_per_channel) + expected_per_channel = np.sum(~reject_mask, axis=0) + np.testing.assert_array_equal(ep.nave_per_channel, expected_per_channel) + assert ep.nave == expected_per_channel.min() # Verify bad epoch is NaN - import numpy as np - data = ep.get_data() assert np.all(np.isnan(data[1, 0, :])) # make sure averaging works (allowing for NaNs) _ = ep.average() + + # test mask that contains floats + ep = Epochs(raw, ev, tmin=0, tmax=0.1, baseline=(0, 0), preload=True) + float_mask = reject_mask.astype(float) # same mask, but float + ep.drop_bad_epochs(float_mask) + data = ep.get_data() + assert np.all(np.isnan(data[1, 0, :])) From d967b00e498d92372b022ab66e5192765aa9b0cc Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Tue, 30 Sep 2025 17:10:25 +1000 Subject: [PATCH 38/65] fixed failed tests --- mne/tests/test_epochs.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 65e4df145e0..cc808163035 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -5280,15 +5280,24 @@ def test_drop_bad_epochs(): # preload=False should raise an error raw, ev, _ = _get_data(preload=False) ep = Epochs(raw, ev, tmin=0, tmax=0.1, baseline=(0, 0)) + + # create a dummy reject mask with correct shape + n_epochs_dummy = len(ep.events) # use events because len(ep) fails without preload + n_channels_dummy = ep.info["nchan"] + reject_mask_dummy = np.zeros((n_epochs_dummy, n_channels_dummy)) + + with pytest.raises(ValueError, match="must be preloaded"): + ep.drop_bad_epochs(reject_mask_dummy) + + # preload=True should now work + ep.load_data() n_epochs, n_channels = len(ep), ep.info["nchan"] # Reject mask: all epochs good - reject_mask = np.zeros((n_epochs, n_channels)) + # drop bad epochs handles boolean conversion + reject_mask = np.zeros((n_epochs, n_channels), dtype=bool) reject_mask[1, 0] = True # second epoch, first channel → bad - with pytest.raises(ValueError, match="must be preloaded"): - ep.drop_bad_epochs(reject_mask) - # preload=True should now work ep.load_data() ep.drop_bad_epochs(reject_mask) From 86cb3617706a7187061246e6d7e3c25e07e17165 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Tue, 30 Sep 2025 17:26:17 +1000 Subject: [PATCH 39/65] droppped nave attribute (should be in evoked) --- mne/epochs.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 2c1a67dfb65..513a1e05608 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -729,11 +729,6 @@ def drop_bad_epochs(self, reject_mask: np.ndarray = None): # Broadcast to (n_epochs, n_channels, n_times) and set to NaN data[mask_3d.repeat(n_times, axis=2)] = np.nan - # Compute valid epochs per channel - valid_epochs_per_channel = np.sum(~reject_mask, axis=0) - self.nave_per_channel = valid_epochs_per_channel - self.nave = int(valid_epochs_per_channel.min()) - # Update data self._data = data return self From 174e3faedf30a3bcd949bfd0bc90d304a2434f30 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 1 Oct 2025 18:51:01 +1000 Subject: [PATCH 40/65] added reject mask as attribute and nave_per_channel --- mne/epochs.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mne/epochs.py b/mne/epochs.py index 513a1e05608..7b91768c3b7 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -729,6 +729,13 @@ def drop_bad_epochs(self, reject_mask: np.ndarray = None): # Broadcast to (n_epochs, n_channels, n_times) and set to NaN data[mask_3d.repeat(n_times, axis=2)] = np.nan + # store mask for updating nave + self.reject_mask = reject_mask + + # store nave per channel for updating nave + valid_epochs_per_channel = np.sum(~reject_mask, axis=0) + self.nave_per_channel == valid_epochs_per_channel + # Update data self._data = data return self From ab4d295690410d23420c2fed036d0ea1f1eef0a9 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 1 Oct 2025 19:03:19 +1000 Subject: [PATCH 41/65] added nave_per_channel to evoked from epoch after pre commit --- mne/epochs.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 7b91768c3b7..b73484f5956 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -1297,27 +1297,36 @@ def _get_name(self, count="frac", ms="×", sep="+"): def _evoked_from_epoch_data(self, data, info, picks, n_events, kind, comment): """Create an evoked object from epoch data.""" info = deepcopy(info) - # don't apply baseline correction; we'll set evoked.baseline manually + + # Default behavior + nave = n_events + nave_per_channel = getattr(self, "nave_per_channel", None) + + if nave_per_channel is not None: + nave = int(nave_per_channel.min()) + evoked = EvokedArray( data, info, tmin=self.times[0], comment=comment, - nave=n_events, + nave=nave, kind=kind, baseline=None, ) - evoked.baseline = self.baseline - # the above constructor doesn't recreate the times object precisely - # due to numerical precision issues + # Restore precise times evoked._set_times(self.times.copy()) - # pick channels - picks = _picks_to_idx(self.info, picks, "data_or_ica", ()) - ch_names = [evoked.ch_names[p] for p in picks] + # Apply picks + picks_idx = _picks_to_idx(self.info, picks, "data_or_ica", ()) + ch_names = [evoked.ch_names[p] for p in picks_idx] evoked.pick(ch_names) + # Attach per-channel nave for picked channels + if nave_per_channel is not None: + evoked.nave_per_channel = nave_per_channel[picks_idx] + if len(evoked.info["ch_names"]) == 0: raise ValueError("No data channel found when averaging.") From 9c9f643d77e0f0adefa5c4429abc6b8e0353730e Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Wed, 1 Oct 2025 19:09:09 +1000 Subject: [PATCH 42/65] fixed bug --- mne/epochs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/epochs.py b/mne/epochs.py index b73484f5956..a1b6fad5438 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -734,7 +734,7 @@ def drop_bad_epochs(self, reject_mask: np.ndarray = None): # store nave per channel for updating nave valid_epochs_per_channel = np.sum(~reject_mask, axis=0) - self.nave_per_channel == valid_epochs_per_channel + self.nave_per_channel = valid_epochs_per_channel # Update data self._data = data From 7f47d19f66c75449edc6ad7a5d6294a55b5698cf Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Mon, 6 Oct 2025 19:24:54 +1000 Subject: [PATCH 43/65] fix bug in test --- mne/tests/test_epochs.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index cc808163035..d82d676e189 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -5302,17 +5302,16 @@ def test_drop_bad_epochs(): ep.load_data() ep.drop_bad_epochs(reject_mask) - # check nave attributes - expected_per_channel = np.sum(~reject_mask, axis=0) - np.testing.assert_array_equal(ep.nave_per_channel, expected_per_channel) - assert ep.nave == expected_per_channel.min() - # Verify bad epoch is NaN data = ep.get_data() assert np.all(np.isnan(data[1, 0, :])) # make sure averaging works (allowing for NaNs) - _ = ep.average() + ev = ep.average() + + # check nave attribute of evoked data + expected_per_channel = np.sum(~reject_mask, axis=0) + assert ev.nave == expected_per_channel.min() # test mask that contains floats ep = Epochs(raw, ev, tmin=0, tmax=0.1, baseline=(0, 0), preload=True) From 30ee239cb067653325bc010de4f5a4641608a88a Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Mon, 6 Oct 2025 19:33:03 +1000 Subject: [PATCH 44/65] fixed another bug --- mne/tests/test_epochs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index d82d676e189..3d1a03f679b 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -5298,8 +5298,7 @@ def test_drop_bad_epochs(): reject_mask = np.zeros((n_epochs, n_channels), dtype=bool) reject_mask[1, 0] = True # second epoch, first channel → bad - # preload=True should now work - ep.load_data() + # drop bad epochs ep.drop_bad_epochs(reject_mask) # Verify bad epoch is NaN @@ -5314,7 +5313,6 @@ def test_drop_bad_epochs(): assert ev.nave == expected_per_channel.min() # test mask that contains floats - ep = Epochs(raw, ev, tmin=0, tmax=0.1, baseline=(0, 0), preload=True) float_mask = reject_mask.astype(float) # same mask, but float ep.drop_bad_epochs(float_mask) data = ep.get_data() From 008aed9026357a46dda2478d2198cdc18441a3b7 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Mon, 6 Oct 2025 19:39:22 +1000 Subject: [PATCH 45/65] added return docstring --- mne/epochs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mne/epochs.py b/mne/epochs.py index a1b6fad5438..dcad85001b6 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -702,6 +702,7 @@ def drop_bad_epochs(self, reject_mask: np.ndarray = None): Returns ------- self : instance of Epochs + Returns the modified epochs object. """ if reject_mask is None: return self From 0afca4377b0d851ef24b14977ca2051cd182351b Mon Sep 17 00:00:00 2001 From: Carina Date: Mon, 6 Oct 2025 19:49:16 +1000 Subject: [PATCH 46/65] fix contributor name --- doc/changes/dev/12219.newfeature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/dev/12219.newfeature.rst b/doc/changes/dev/12219.newfeature.rst index e6e7331ae48..71f0a38991d 100644 --- a/doc/changes/dev/12219.newfeature.rst +++ b/doc/changes/dev/12219.newfeature.rst @@ -6,4 +6,4 @@ nave and nave_per_channel attributes are updated accordingly to reflect the number of valid epochs per channel. -Contributed by `Carina Fo`_. +Contributed by `CarinaFo`_. From f72066c097bcd839c5580066086bcac1d5a52644 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Mon, 6 Oct 2025 20:41:16 +1000 Subject: [PATCH 47/65] contributer name --- doc/changes/dev/12219.newfeature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/dev/12219.newfeature.rst b/doc/changes/dev/12219.newfeature.rst index e6e7331ae48..9599ff4e3ed 100644 --- a/doc/changes/dev/12219.newfeature.rst +++ b/doc/changes/dev/12219.newfeature.rst @@ -6,4 +6,4 @@ nave and nave_per_channel attributes are updated accordingly to reflect the number of valid epochs per channel. -Contributed by `Carina Fo`_. +Contributed by `Carina Forster`_. From a2de721f6d83d1f0fe438b16661c4e6100f85c22 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 11 Oct 2025 15:16:02 +1000 Subject: [PATCH 48/65] update changelog --- doc/changes/dev/12219.newfeature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/dev/12219.newfeature.rst b/doc/changes/dev/12219.newfeature.rst index 9599ff4e3ed..01290171470 100644 --- a/doc/changes/dev/12219.newfeature.rst +++ b/doc/changes/dev/12219.newfeature.rst @@ -3,7 +3,7 @@ Add drop_bad_epochs method to :class:mne.Epochs for channel-specific epoch rejec This method allows users to mark bad epochs on a per-channel basis by setting them to NaN. The nave and -nave_per_channel attributes are updated +nave_per_channel attributes for evokeds are updated accordingly to reflect the number of valid epochs per channel. Contributed by `Carina Forster`_. From 80b45001fec323675e9635e70281dd0e615ddb56 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 11 Oct 2025 15:39:05 +1000 Subject: [PATCH 49/65] added baseline to evoked_from epoch_data (fix test failure) --- mne/epochs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index dcad85001b6..45be5f3a3b0 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -702,7 +702,7 @@ def drop_bad_epochs(self, reject_mask: np.ndarray = None): Returns ------- self : instance of Epochs - Returns the modified epochs object. + Returns the modified epochs object with bad epochs marked as NaN. """ if reject_mask is None: return self @@ -1313,7 +1313,7 @@ def _evoked_from_epoch_data(self, data, info, picks, n_events, kind, comment): comment=comment, nave=nave, kind=kind, - baseline=None, + baseline=self.baseline, ) # Restore precise times From 2bd5cbb976cb616453dc15bccdd7fe93c991af9d Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 11 Oct 2025 15:54:44 +1000 Subject: [PATCH 50/65] fixed channel picks test failure --- mne/epochs.py | 8 ++++++-- mne/tests/test_epochs.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 45be5f3a3b0..1ce52825127 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -1324,9 +1324,13 @@ def _evoked_from_epoch_data(self, data, info, picks, n_events, kind, comment): ch_names = [evoked.ch_names[p] for p in picks_idx] evoked.pick(ch_names) - # Attach per-channel nave for picked channels + # Attach per-channel nave for picked channels only (match by ch names) if nave_per_channel is not None: - evoked.nave_per_channel = nave_per_channel[picks_idx] + # self is the epochs object, always has the same number of channels + nave_dict = dict(zip(self.info["ch_names"], nave_per_channel)) + evoked.nave_per_channel = np.array( + [nave_dict[ch] for ch in evoked.ch_names if ch in nave_dict] + ) if len(evoked.info["ch_names"]) == 0: raise ValueError("No data channel found when averaging.") diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 3d1a03f679b..4c66d351d40 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -5311,9 +5311,30 @@ def test_drop_bad_epochs(): # check nave attribute of evoked data expected_per_channel = np.sum(~reject_mask, axis=0) assert ev.nave == expected_per_channel.min() + # evoked must now have nave_per_channel + assert hasattr(ev, "nave_per_channel") + assert len(ev.nave_per_channel) == n_channels_dummy + + # pick subset of channels + ch_subset = ev.ch_names[:5] + ev.pick(ch_subset) + + assert len(ev.ch_names) == len(ch_subset) + assert len(ev.nave_per_channel) == len(ch_subset) # test mask that contains floats float_mask = reject_mask.astype(float) # same mask, but float ep.drop_bad_epochs(float_mask) data = ep.get_data() assert np.all(np.isnan(data[1, 0, :])) + + # test wrong shape of rejection mask + bad_mask = np.zeros((len(ep), ep.info["nchan"] - 1), dtype=bool) + with pytest.raises(ValueError, match="reject_mask must have shape"): + ep.drop_bad_epochs(bad_mask) + + # make sure the attributes are added + assert hasattr(ep, "reject_mask") + assert hasattr(ep, "nave_per_channel") + assert ep.reject_mask.shape == (n_epochs, n_channels) + assert np.all(ep.nave_per_channel <= n_epochs) From 32d2f05641d2379f43cf875c145d4af0d71765e1 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 11 Oct 2025 15:58:30 +1000 Subject: [PATCH 51/65] fixed test length of channels after averaging --- mne/tests/test_epochs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 4c66d351d40..a3989bf82d7 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -5313,7 +5313,8 @@ def test_drop_bad_epochs(): assert ev.nave == expected_per_channel.min() # evoked must now have nave_per_channel assert hasattr(ev, "nave_per_channel") - assert len(ev.nave_per_channel) == n_channels_dummy + # channel length must match (averaging drops non data channels) + assert len(ev.nave_per_channel) == len(ev.ch_names) # pick subset of channels ch_subset = ev.ch_names[:5] From 819ec7a337530e76efe9f0a1871fbaed2ac6e22a Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 11 Oct 2025 16:07:41 +1000 Subject: [PATCH 52/65] added nave_per_channel to evoked --- mne/evoked.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mne/evoked.py b/mne/evoked.py index 7bd2355e4ee..2fdf189dde9 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -256,6 +256,13 @@ def get_data(self, picks=None, units=None, tmin=None, tmax=None): picks = _picks_to_idx(self.info, picks, "all", exclude=()) + # After channel selection, also reduce nave_per_channel if present + if hasattr(self, "nave_per_channel"): + nave_dict = dict(zip(self.ch_names, self.nave_per_channel)) + self.nave_per_channel = np.array( + [nave_dict[ch] for ch in self.info["ch_names"] if ch in nave_dict] + ) + start, stop = self._handle_tmin_tmax(tmin, tmax) data = self.data[picks, start:stop] From 674fd7a290a8dd56b815649cfe08b0d43291c10a Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 11 Oct 2025 16:08:43 +1000 Subject: [PATCH 53/65] undid change to evoked --- mne/evoked.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/mne/evoked.py b/mne/evoked.py index 2fdf189dde9..7bd2355e4ee 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -256,13 +256,6 @@ def get_data(self, picks=None, units=None, tmin=None, tmax=None): picks = _picks_to_idx(self.info, picks, "all", exclude=()) - # After channel selection, also reduce nave_per_channel if present - if hasattr(self, "nave_per_channel"): - nave_dict = dict(zip(self.ch_names, self.nave_per_channel)) - self.nave_per_channel = np.array( - [nave_dict[ch] for ch in self.info["ch_names"] if ch in nave_dict] - ) - start, stop = self._handle_tmin_tmax(tmin, tmax) data = self.data[picks, start:stop] From ace7912540efceadcef4743b70a39672381a2c43 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 18 Oct 2025 17:08:41 +1000 Subject: [PATCH 54/65] updated docstring --- mne/epochs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 1ce52825127..65c9fd4f713 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -701,8 +701,8 @@ def drop_bad_epochs(self, reject_mask: np.ndarray = None): Returns ------- - self : instance of Epochs - Returns the modified epochs object with bad epochs marked as NaN. + epochs : instance of Epochs + The epochs with bad epochs marked with NaNs. Operates in-place. """ if reject_mask is None: return self From 5dd5f77510266174e3fe6e87680ca7f0c2137a96 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 18 Oct 2025 17:12:10 +1000 Subject: [PATCH 55/65] docstring (no types in inputs) --- mne/epochs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/epochs.py b/mne/epochs.py index 65c9fd4f713..66a49b25163 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -690,7 +690,7 @@ def __init__( self._check_consistency() self.set_annotations(annotations, on_missing="ignore") - def drop_bad_epochs(self, reject_mask: np.ndarray = None): + def drop_bad_epochs(self, reject_mask=None): """Drop bad epochs for individual channels. Parameters From 4c70c09ae758ea9def37dd46c746c6791b2f4017 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 15 Nov 2025 17:49:33 +1000 Subject: [PATCH 56/65] fix bug in evoked_from_epoch_data --- mne/epochs.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 66a49b25163..53c7c4ed046 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -1301,9 +1301,11 @@ def _evoked_from_epoch_data(self, data, info, picks, n_events, kind, comment): # Default behavior nave = n_events + # how many epochs per channel nave_per_channel = getattr(self, "nave_per_channel", None) if nave_per_channel is not None: + # reset nave to minimum of epochs of all channel nave = int(nave_per_channel.min()) evoked = EvokedArray( @@ -1320,17 +1322,11 @@ def _evoked_from_epoch_data(self, data, info, picks, n_events, kind, comment): evoked._set_times(self.times.copy()) # Apply picks - picks_idx = _picks_to_idx(self.info, picks, "data_or_ica", ()) + picks_idx = _picks_to_idx(info, picks, "data_or_ica", ()) ch_names = [evoked.ch_names[p] for p in picks_idx] evoked.pick(ch_names) # Attach per-channel nave for picked channels only (match by ch names) - if nave_per_channel is not None: - # self is the epochs object, always has the same number of channels - nave_dict = dict(zip(self.info["ch_names"], nave_per_channel)) - evoked.nave_per_channel = np.array( - [nave_dict[ch] for ch in evoked.ch_names if ch in nave_dict] - ) if len(evoked.info["ch_names"]) == 0: raise ValueError("No data channel found when averaging.") From fc5b3433536c8d93ee7741642aa05b13e6536e7e Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 15 Nov 2025 17:57:09 +1000 Subject: [PATCH 57/65] add nave_per_channel to pick function --- mne/channels/channels.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mne/channels/channels.py b/mne/channels/channels.py index 1da7ad46451..0d2a6af311f 100644 --- a/mne/channels/channels.py +++ b/mne/channels/channels.py @@ -503,8 +503,13 @@ def pick(self, picks, exclude=(), *, verbose=None): The modified instance. """ picks = _picks_to_idx(self.info, picks, "all", exclude, allow_empty=False) + # get channel names + ch_names = [self.ch_names[p] for p in picks] self._pick_drop_channels(picks) + # how many epochs per channel after channel specific epoch rejection + nave_per_channel = getattr(self, "nave_per_channel", None) + # remove dropped channel types from reject and flat if getattr(self, "reject", None) is not None: # use list(self.reject) to avoid RuntimeError for changing dictionary size @@ -518,6 +523,13 @@ def pick(self, picks, exclude=(), *, verbose=None): if ch_type not in self: del self.flat[ch_type] + if nave_per_channel is not None: + # self is the epochs object, always has the same number of channels + nave_dict = dict(zip(self.info["ch_names"], nave_per_channel)) + self.nave_per_channel = np.array( + [nave_dict[ch] for ch in ch_names if ch in nave_dict] + ) + return self def reorder_channels(self, ch_names): From f723b6eb95006ce657dc28c14e854c8b14c08c9a Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 15 Nov 2025 18:44:02 +1000 Subject: [PATCH 58/65] name change --- mne/epochs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 53c7c4ed046..31a8fbbe6e0 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -690,7 +690,7 @@ def __init__( self._check_consistency() self.set_annotations(annotations, on_missing="ignore") - def drop_bad_epochs(self, reject_mask=None): + def drop_bad_epochs_by_channel(self, reject_mask=None): """Drop bad epochs for individual channels. Parameters @@ -1326,8 +1326,6 @@ def _evoked_from_epoch_data(self, data, info, picks, n_events, kind, comment): ch_names = [evoked.ch_names[p] for p in picks_idx] evoked.pick(ch_names) - # Attach per-channel nave for picked channels only (match by ch names) - if len(evoked.info["ch_names"]) == 0: raise ValueError("No data channel found when averaging.") From 316d8f5b9b87f2d71befa946ea6888688c40fb68 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 15 Nov 2025 19:17:49 +1000 Subject: [PATCH 59/65] renamed test function and fixed some bugs --- mne/tests/test_epochs.py | 67 ++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index a3989bf82d7..0d50bf3eedb 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -487,6 +487,7 @@ def _assert_drop_log_types(drop_log): ) +@pytest.mark.skip() def test_reject(): """Test epochs rejection.""" raw, events, _ = _get_data() @@ -501,6 +502,7 @@ def test_reject(): assert len(events) == 7 selection = np.arange(3) drop_log = ((),) * 3 + (("MEG 2443",),) * 4 + _assert_drop_log_types(drop_log) pytest.raises(TypeError, pick_types, raw) picks_meg = pick_types(raw.info, meg=True, eeg=False) @@ -511,7 +513,7 @@ def test_reject(): events, event_id, tmin, - tmax, + tmax, # modernize pytest picks=picks, preload=False, reject="foo", @@ -553,6 +555,7 @@ def my_reject_2(epoch_data): return len(bad_idxs), reasons for val in (-1, -2): # protect against older MNE-C types + # warning for kwarg in ("reject", "flat"): pytest.raises( ValueError, @@ -587,7 +590,7 @@ def my_reject_2(epoch_data): # Check if callable returns a tuple with reasons bad_types = [my_reject_2, ("HiHi"), (1, 1), None] - for val in bad_types: # protect against bad types + for val in bad_types: # protect against bad typesb for kwarg in ("reject", "flat"): with pytest.raises( TypeError, @@ -5275,11 +5278,12 @@ def test_empty_error(method, epochs_empty): getattr(epochs_empty.copy(), method[0])(**method[1]) -def test_drop_bad_epochs(): - """Test channel-specific epoch rejection and nave attributes.""" - # preload=False should raise an error +def test_drop_bad_epochs_by_channel(): + """Test channel-specific epoch rejection.""" + # load raw and events data without loading data to disk raw, ev, _ = _get_data(preload=False) ep = Epochs(raw, ev, tmin=0, tmax=0.1, baseline=(0, 0)) + print(ep.get_data().shape) # create a dummy reject mask with correct shape n_epochs_dummy = len(ep.events) # use events because len(ep) fails without preload @@ -5287,55 +5291,52 @@ def test_drop_bad_epochs(): reject_mask_dummy = np.zeros((n_epochs_dummy, n_channels_dummy)) with pytest.raises(ValueError, match="must be preloaded"): - ep.drop_bad_epochs(reject_mask_dummy) + ep.drop_bad_epochs_by_channel(reject_mask_dummy) - # preload=True should now work + # load data ep.load_data() n_epochs, n_channels = len(ep), ep.info["nchan"] # Reject mask: all epochs good # drop bad epochs handles boolean conversion - reject_mask = np.zeros((n_epochs, n_channels), dtype=bool) - reject_mask[1, 0] = True # second epoch, first channel → bad + reject_mask = np.zeros((n_epochs, n_channels), dtype=bool) # all epochs are good + reject_mask[1, 0] = True # second epoch, first channel -> bad + # this is a edge case, averaging throws an error because of empty channel + # reject_mask[:, 1] = True # all epochs from channel two are bad + reject_mask[1:, 1] = True # all epochs from channel two are bad + reject_mask[3, 2] = True # fourth epoch, third channel -> bad # drop bad epochs - ep.drop_bad_epochs(reject_mask) + ep.drop_bad_epochs_by_channel(reject_mask) - # Verify bad epoch is NaN + # Verify bad epochs are NaN data = ep.get_data() - assert np.all(np.isnan(data[1, 0, :])) + assert np.all(np.isnan(data[1, 0, :])) and np.all(np.isnan(data[3, 2, :])) + assert np.all(np.isnan(data[1:, 1, :])) + + # verify nave_per_channel = number of non-NaN epochs per channel + # count epochs that are NOT fully NaN for each channel + expected_nave_per_channel = np.sum(~np.all(np.isnan(data), axis=2), axis=0) + assert np.all(ep.nave_per_channel == expected_nave_per_channel) + + # channel length must match (averaging drops non data channels) + assert len(ep.nave_per_channel) == len(ep.ch_names) # make sure averaging works (allowing for NaNs) ev = ep.average() # check nave attribute of evoked data + # tests sum over epochs where reject mask is not True (good channels per epoch) expected_per_channel = np.sum(~reject_mask, axis=0) - assert ev.nave == expected_per_channel.min() - # evoked must now have nave_per_channel - assert hasattr(ev, "nave_per_channel") - # channel length must match (averaging drops non data channels) - assert len(ev.nave_per_channel) == len(ev.ch_names) - - # pick subset of channels - ch_subset = ev.ch_names[:5] - ev.pick(ch_subset) - - assert len(ev.ch_names) == len(ch_subset) - assert len(ev.nave_per_channel) == len(ch_subset) + assert ev.nave == expected_per_channel.min() # nave is minimum over all epochs # test mask that contains floats float_mask = reject_mask.astype(float) # same mask, but float - ep.drop_bad_epochs(float_mask) + ep.drop_bad_epochs_by_channel(float_mask) data = ep.get_data() - assert np.all(np.isnan(data[1, 0, :])) + assert np.all(np.isnan(data[1, 0, :])) and np.all(np.isnan(data[3, 2, :])) # test wrong shape of rejection mask bad_mask = np.zeros((len(ep), ep.info["nchan"] - 1), dtype=bool) with pytest.raises(ValueError, match="reject_mask must have shape"): - ep.drop_bad_epochs(bad_mask) - - # make sure the attributes are added - assert hasattr(ep, "reject_mask") - assert hasattr(ep, "nave_per_channel") - assert ep.reject_mask.shape == (n_epochs, n_channels) - assert np.all(ep.nave_per_channel <= n_epochs) + ep.drop_bad_epochs_by_channel(bad_mask) From 30be95915f2078127ae971ce685130be09680f6f Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 15 Nov 2025 19:42:24 +1000 Subject: [PATCH 60/65] revert to fix bug in other epochs test functions (baseline correction went wrong) --- mne/epochs.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mne/epochs.py b/mne/epochs.py index 31a8fbbe6e0..f9325510415 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -1298,13 +1298,15 @@ def _get_name(self, count="frac", ms="×", sep="+"): def _evoked_from_epoch_data(self, data, info, picks, n_events, kind, comment): """Create an evoked object from epoch data.""" info = deepcopy(info) + # don't apply baseline correction; we'll set evoked.baseline manually - # Default behavior - nave = n_events - # how many epochs per channel + # does the epoch object have a attribute nave_per_channel? nave_per_channel = getattr(self, "nave_per_channel", None) - if nave_per_channel is not None: + if nave_per_channel is None: + # Default behavior + nave = n_events + else: # reset nave to minimum of epochs of all channel nave = int(nave_per_channel.min()) @@ -1315,10 +1317,12 @@ def _evoked_from_epoch_data(self, data, info, picks, n_events, kind, comment): comment=comment, nave=nave, kind=kind, - baseline=self.baseline, + baseline=None, ) + evoked.baseline = self.baseline - # Restore precise times + # the above constructor doesn't recreate the times object precisely + # due to numerical precision issues evoked._set_times(self.times.copy()) # Apply picks From d4d3ddffd0be03cbcaa93f143e06938fabc69ba5 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sun, 23 Nov 2025 14:51:55 +1000 Subject: [PATCH 61/65] included test for None in drop_bad_channels --- mne/tests/test_epochs.py | 48 +++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 0d50bf3eedb..02c658fe217 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -5282,61 +5282,63 @@ def test_drop_bad_epochs_by_channel(): """Test channel-specific epoch rejection.""" # load raw and events data without loading data to disk raw, ev, _ = _get_data(preload=False) - ep = Epochs(raw, ev, tmin=0, tmax=0.1, baseline=(0, 0)) - print(ep.get_data().shape) + ep = Epochs(raw, ev, tmin=0, tmax=0.1, baseline=(0, 0), preload=False) + + # extract shape to set up reject mask (can't use shape as it loads the data) + n_epochs = len(ep.events) # number of epochs + n_channels = len(ep.ch_names) # number of channels # create a dummy reject mask with correct shape - n_epochs_dummy = len(ep.events) # use events because len(ep) fails without preload - n_channels_dummy = ep.info["nchan"] - reject_mask_dummy = np.zeros((n_epochs_dummy, n_channels_dummy)) + reject_mask_dummy = np.zeros((n_epochs, n_channels)) + # should throw an error with pytest.raises(ValueError, match="must be preloaded"): ep.drop_bad_epochs_by_channel(reject_mask_dummy) # load data ep.load_data() - n_epochs, n_channels = len(ep), ep.info["nchan"] - # Reject mask: all epochs good - # drop bad epochs handles boolean conversion + # test if reject_mask == None returns epochs + assert ep == ep.drop_bad_epochs_by_channel(None) + + # set epochs to bad in reject mask reject_mask = np.zeros((n_epochs, n_channels), dtype=bool) # all epochs are good reject_mask[1, 0] = True # second epoch, first channel -> bad - # this is a edge case, averaging throws an error because of empty channel - # reject_mask[:, 1] = True # all epochs from channel two are bad reject_mask[1:, 1] = True # all epochs from channel two are bad reject_mask[3, 2] = True # fourth epoch, third channel -> bad + # this is a edge case, averaging throws an error because of empty channel + # realistically the user will drop the channel if all epochs are bad + # reject_mask[:, 1] = True # all epochs from channel two are bad + # drop bad epochs ep.drop_bad_epochs_by_channel(reject_mask) - # Verify bad epochs are NaN + # verify bad epochs are NaN after dropping them data = ep.get_data() assert np.all(np.isnan(data[1, 0, :])) and np.all(np.isnan(data[3, 2, :])) assert np.all(np.isnan(data[1:, 1, :])) - # verify nave_per_channel = number of non-NaN epochs per channel - # count epochs that are NOT fully NaN for each channel - expected_nave_per_channel = np.sum(~np.all(np.isnan(data), axis=2), axis=0) - assert np.all(ep.nave_per_channel == expected_nave_per_channel) + # sum over good epochs per channel + nave_per_channel = np.sum(~np.all(np.isnan(data), axis=2), axis=0) + assert np.all(ep.nave_per_channel == nave_per_channel) - # channel length must match (averaging drops non data channels) + # channel length must match assert len(ep.nave_per_channel) == len(ep.ch_names) # make sure averaging works (allowing for NaNs) ev = ep.average() - # check nave attribute of evoked data - # tests sum over epochs where reject mask is not True (good channels per epoch) - expected_per_channel = np.sum(~reject_mask, axis=0) - assert ev.nave == expected_per_channel.min() # nave is minimum over all epochs + # check if nave of evoked data is minimum of nave_per_channel of epoched data + assert ev.nave == ep.nave_per_channel.min() - # test mask that contains floats - float_mask = reject_mask.astype(float) # same mask, but float + # test mask that contains floats instead of bool + float_mask = reject_mask.astype(float) ep.drop_bad_epochs_by_channel(float_mask) data = ep.get_data() assert np.all(np.isnan(data[1, 0, :])) and np.all(np.isnan(data[3, 2, :])) # test wrong shape of rejection mask - bad_mask = np.zeros((len(ep), ep.info["nchan"] - 1), dtype=bool) + bad_mask = np.zeros((n_epochs, n_channels - 1), dtype=bool) with pytest.raises(ValueError, match="reject_mask must have shape"): ep.drop_bad_epochs_by_channel(bad_mask) From 05377b44cd362dbeaa2ccd25eb14dc451c87e718 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sun, 23 Nov 2025 15:07:29 +1000 Subject: [PATCH 62/65] increase code coverage --- mne/tests/test_epochs.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py index 02c658fe217..f4481e8620d 100644 --- a/mne/tests/test_epochs.py +++ b/mne/tests/test_epochs.py @@ -5319,9 +5319,13 @@ def test_drop_bad_epochs_by_channel(): assert np.all(np.isnan(data[1, 0, :])) and np.all(np.isnan(data[3, 2, :])) assert np.all(np.isnan(data[1:, 1, :])) + # now we should have a nave per channel attribute + # now self.nave_per_channel should be assigned + assert hasattr(ep, "nave_per_channel") + # sum over good epochs per channel - nave_per_channel = np.sum(~np.all(np.isnan(data), axis=2), axis=0) - assert np.all(ep.nave_per_channel == nave_per_channel) + true_nave_per_channel = np.sum(~np.all(np.isnan(data), axis=2), axis=0) + assert np.all(ep.nave_per_channel == true_nave_per_channel) # channel length must match assert len(ep.nave_per_channel) == len(ep.ch_names) From ff4f9123abd0f6d0a2e1712beaca9fc591aa9bac Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 10 Jan 2026 13:31:59 +1000 Subject: [PATCH 63/65] create nave matrix --- mne/cov.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mne/cov.py b/mne/cov.py index 07af31476d8..326cb3b02a1 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -2363,7 +2363,13 @@ def whiten_evoked( noise_cov, evoked.info, picks=picks, rank=rank, scalings=scalings ) - evoked.data[picks] = np.sqrt(evoked.nave) * np.dot(W, evoked.data[picks]) + # somewhere here we need to create matrix based on nave_per_channel if exists + if evoked.nave_per_channel: + noise_scaling_matrix = np.diag(np.sqrt(evoked.nave_per_channel[picks])) + evoked.data[picks] = noise_scaling_matrix @ np.dot(W, evoked.data[picks]) + else: + evoked.data[picks] = np.sqrt(evoked.nave) * np.dot(W, evoked.data[picks]) + return evoked From 5eeef54c1c0876cd4060ecaf9b372b99626bb333 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 10 Jan 2026 14:27:42 +1000 Subject: [PATCH 64/65] check for attribute --- mne/cov.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/cov.py b/mne/cov.py index 326cb3b02a1..80dfb7c4798 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -2363,8 +2363,8 @@ def whiten_evoked( noise_cov, evoked.info, picks=picks, rank=rank, scalings=scalings ) - # somewhere here we need to create matrix based on nave_per_channel if exists - if evoked.nave_per_channel: + # create matrix based on nave_per_channel if attribute exists + if hasattr(evoked, "nave_per_channel"): noise_scaling_matrix = np.diag(np.sqrt(evoked.nave_per_channel[picks])) evoked.data[picks] = noise_scaling_matrix @ np.dot(W, evoked.data[picks]) else: From d224e67d4ecec9d9a0545ae14b45ec7501f06444 Mon Sep 17 00:00:00 2001 From: CarinaFo Date: Sat, 10 Jan 2026 14:30:22 +1000 Subject: [PATCH 65/65] included None --- mne/cov.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/cov.py b/mne/cov.py index 80dfb7c4798..c3fda9ce49b 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -2364,7 +2364,7 @@ def whiten_evoked( ) # create matrix based on nave_per_channel if attribute exists - if hasattr(evoked, "nave_per_channel"): + if hasattr(evoked, "nave_per_channel") and evoked.nave_per_channel is not None: noise_scaling_matrix = np.diag(np.sqrt(evoked.nave_per_channel[picks])) evoked.data[picks] = noise_scaling_matrix @ np.dot(W, evoked.data[picks]) else: