From ed47b4f1a738086ddd2c727b41d65bcaab8fb20d Mon Sep 17 00:00:00 2001 From: Valerii Date: Tue, 6 Apr 2021 03:19:25 +0200 Subject: [PATCH 01/15] collapsible additional info --- mne/data/html_templates.py | 45 +++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/mne/data/html_templates.py b/mne/data/html_templates.py index e1943fe0e0d..c2e9ac6783a 100644 --- a/mne/data/html_templates.py +++ b/mne/data/html_templates.py @@ -1,7 +1,42 @@ from ..externals.tempita import Template -info_template = Template(""" +html_style = """ + +""" + +info_template = Template(html_style + """ @@ -22,6 +57,10 @@ {{endif}} {{else}}{{endif}} +
Measurement dateUnknown
+ + + {{if info['dig'] is not None}} @@ -63,7 +102,7 @@
Digitized points
""") -raw_template = Template(""" +raw_template = Template(html_style + """ {{info_repr[:-9]}} Filenames @@ -76,7 +115,7 @@ """) -epochs_template = Template(""" +epochs_template = Template(html_style + """ From 2b77cb8d0402942dec811252d33766e0a152344a Mon Sep 17 00:00:00 2001 From: Valerii Date: Tue, 6 Apr 2021 03:34:34 +0200 Subject: [PATCH 02/15] make plot_report.py visible for checks --- tutorials/misc/plot_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/misc/plot_report.py b/tutorials/misc/plot_report.py index 5db79181b14..29a64e34724 100644 --- a/tutorials/misc/plot_report.py +++ b/tutorials/misc/plot_report.py @@ -56,7 +56,7 @@ # will open the HTML in a new tab in the browser. To disable this, use the # ``open_browser=False`` parameter of :meth:`~mne.Report.save`. # -# For our first example, we'll generate a barebones report for all the +# For our first example, we will generate a barebones report for all the # :file:`.fif` files containing raw data in the sample dataset, by passing the # pattern ``*raw.fif`` to :meth:`~mne.Report.parse_folder`. We'll omit the # ``subject`` and ``subjects_dir`` parameters from the :class:`~mne.Report` From a6926825338a4ccb15b1d840d1b2b5c5f9de1752 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Mon, 5 Apr 2021 16:16:31 -0500 Subject: [PATCH 03/15] expose colorbar label parameter in plot_sensors_connectivity (#9248) * expose cbar label param; update test * update changelog * Update mne/viz/_3d.py Co-authored-by: Eric Larson * fix error message grammar * Update mne/viz/tests/test_3d.py Co-authored-by: Eric Larson Co-authored-by: Eric Larson --- doc/changes/latest.inc | 4 +++- mne/viz/_3d.py | 9 ++++++--- mne/viz/tests/test_3d.py | 23 +++++++++++++++-------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 5a612bbca22..9a60e9def0c 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -175,7 +175,7 @@ Bugs - Fix bug with :func:`mne.io.read_raw_edf` where µV was not correctly recognized (:gh:`9187` **by new contributor** |Sumalyo Datta|_) - Fix bug with :func:`mne.viz.plot_compare_evokeds` did not check type of combine. (:gh:`9151` **by new contributor** |Matteo Anelli|_) - + - Fix bug with :func:`mne.viz.plot_evoked_topo` where ``ylim`` was only being applied to the first channel in the dataset (:gh:`9162` **by new contributor** |Ram Pari|_ ) - Fix bug with :func:`mne.Epochs.plot_image` allowing interactive zoom to work properly (:gh:`9152` by **by new contributor** |Maggie Clarke|_ and `Daniel McCloy`_) @@ -278,6 +278,8 @@ Bugs API changes ~~~~~~~~~~~ +- `mne.viz.plot_sensors_connectivity` now allows setting the colorbar label via the ``cbar_label`` parameter (:gh:`9248` by `Daniel McCloy`_) + - Introduced new ``'auto'`` settings for ``ICA.max_iter``. The old default ``max_iter=200`` will be removed in MNE-Python 0.24 (:gh:`9099` **by new contributor** |Cora Kim|_) - ``mne.read_selection`` has been deprecated in favor of `mne.read_vectorview_selection`. ``mne.read_selection`` will be removed in MNE-Python 0.24 (:gh:`8870` by `Richard Höchenberger`_) diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index 597589ef183..7427154a6f8 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -2943,7 +2943,8 @@ def snapshot_brain_montage(fig, montage, hide_sensors=True): @fill_doc -def plot_sensors_connectivity(info, con, picks=None): +def plot_sensors_connectivity(info, con, picks=None, + cbar_label='Connectivity'): """Visualize the sensor connectivity in 3D. Parameters @@ -2954,6 +2955,8 @@ def plot_sensors_connectivity(info, con, picks=None): The computed connectivity measure(s). %(picks_good_data)s Indices of selected channels. + cbar_label : str + Label for the colorbar. Returns ------- @@ -2969,7 +2972,7 @@ def plot_sensors_connectivity(info, con, picks=None): picks = _picks_to_idx(info, picks) if len(picks) != len(con): raise ValueError('The number of channels picked (%s) does not ' - 'correspond the size of the connectivity data ' + 'correspond to the size of the connectivity data ' '(%s)' % (len(picks), len(con))) # Plot the sensor locations @@ -3007,7 +3010,7 @@ def plot_sensors_connectivity(info, con, picks=None): vmin=vmin, vmax=vmax, reverse_lut=True) - renderer.scalarbar(source=tube, title='Phase Lag Index (PLI)') + renderer.scalarbar(source=tube, title=cbar_label) # Add the sensor names for the connections shown nodes_shown = list(set([n[0] for n in con_nodes] + diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index 957e0ca4d57..aba0903a32c 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -717,14 +717,21 @@ def test_plot_sensors_connectivity(renderer): n_channels = len(picks) con = np.random.RandomState(42).randn(n_channels, n_channels) info = raw.info - with pytest.raises(TypeError): - plot_sensors_connectivity(info='foo', con=con, - picks=picks) - with pytest.raises(ValueError): - plot_sensors_connectivity(info=info, con=con[::2, ::2], - picks=picks) - - plot_sensors_connectivity(info=info, con=con, picks=picks) + with pytest.raises(TypeError, match='must be an instance of Info'): + plot_sensors_connectivity(info='foo', con=con, picks=picks) + with pytest.raises(ValueError, match='does not correspond to the size'): + plot_sensors_connectivity(info=info, con=con[::2, ::2], picks=picks) + + fig = plot_sensors_connectivity(info=info, con=con, picks=picks) + if renderer._get_3d_backend() == 'pyvista': + title = fig.plotter.scalar_bar.GetTitle() + else: + assert renderer._get_3d_backend() == 'mayavi' + # the last thing we add is the Tube, so we need to go + # vtkDataSource->Stripper->Tube->ModuleManager + mod_man = fig.children[-1].children[0].children[0].children[0] + title = mod_man.scalar_lut_manager.scalar_bar.title + assert title == 'Connectivity' @pytest.mark.parametrize('orientation', ('horizontal', 'vertical')) From e17a82d619fe1e073258910371b4e64f570e1781 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 6 Apr 2021 03:56:16 -0500 Subject: [PATCH 04/15] fix: error when passing axes to plot_compare_evokeds (#9252) * fix comparing array to string * update changelog * fix: prev changelog entry in wrong place --- doc/changes/latest.inc | 6 ++++-- mne/viz/evoked.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 9a60e9def0c..1caefe053c0 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -272,16 +272,18 @@ Bugs - Fix bug with :func:`mne.io.Raw.pick` where incorrect fnirs types were returned (:gh:`9178` by `Robert Luke`_) +- Fix bug when passing both axes and picks to `mne.viz.plot_compare_evokeds` (:gh:`9252` by `Daniel McCloy`_) + - Improved string representation of `~mne.Epochs` containing multiple event types; improved (and more mathematically correct) ``evoked.comment`` in the `mne.combine_evoked` output; and better (and often more concise) legend labels in the figures created via `~mne.viz.plot_compare_evokeds` (:gh:`9027` by `Richard Höchenberger`_) - :func:`mne.preprocessing.find_ecg_events` now correctly handles situation where no ECG activity could be detected, and correctly returns an empty array of ECG events (:gh:`9236` by `Richard Höchenberger`_) API changes ~~~~~~~~~~~ -- `mne.viz.plot_sensors_connectivity` now allows setting the colorbar label via the ``cbar_label`` parameter (:gh:`9248` by `Daniel McCloy`_) - - Introduced new ``'auto'`` settings for ``ICA.max_iter``. The old default ``max_iter=200`` will be removed in MNE-Python 0.24 (:gh:`9099` **by new contributor** |Cora Kim|_) +- `mne.viz.plot_sensors_connectivity` now allows setting the colorbar label via the ``cbar_label`` parameter (:gh:`9248` by `Daniel McCloy`_) + - ``mne.read_selection`` has been deprecated in favor of `mne.read_vectorview_selection`. ``mne.read_selection`` will be removed in MNE-Python 0.24 (:gh:`8870` by `Richard Höchenberger`_) - ``mne.beamformer.tf_dics`` has been deprecated and will be removed in MNE-Python 0.24 (:gh:`9122` by `Britta Westner`_) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 0f710232d58..f275829fd62 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -2251,7 +2251,8 @@ def plot_compare_evokeds(evokeds, picks=None, colors=None, warn('Only {} channel in "picks"; cannot combine by method "{}".' .format(len(picks), combine)) # `combine` defaults to GFP unless picked a single channel or axes='topo' - if combine is None and len(picks) > 1 and axes != 'topo': + do_topo = isinstance(axes, str) and axes == 'topo' + if combine is None and len(picks) > 1 and not do_topo: combine = 'gfp' # convert `combine` into callable (if None or str) combine_func = _make_combine_callable(combine) @@ -2261,7 +2262,6 @@ def plot_compare_evokeds(evokeds, picks=None, colors=None, ch_names=ch_names, combine=combine) # setup axes - do_topo = (axes == 'topo') if do_topo: show_sensors = False if len(picks) > 70: From 329eafe23c5d389b28af6a75d9e49f97a5e0fb94 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 6 Apr 2021 08:17:02 -0500 Subject: [PATCH 05/15] fix for MPL 3.4.x (#9253) --- mne/viz/_figure.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py index 38d804979f9..96c531b2c6c 100644 --- a/mne/viz/_figure.py +++ b/mne/viz/_figure.py @@ -2275,6 +2275,7 @@ def _browse_figure(inst, **kwargs): # initialize zen mode (can't do in __init__ due to get_position() calls) fig.canvas.draw() fig._update_zen_mode_offsets() + fig._resize(None) # needed for MPL >=3.4 # if scrollbars are supposed to start hidden, set to True and then toggle if not fig.mne.scrollbars_visible: fig.mne.scrollbars_visible = True From eeb550dcd2ef7c1df0c2d56ba6d5f138620b3604 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 6 Apr 2021 11:33:51 -0500 Subject: [PATCH 06/15] Fix html table scrolling again (#9254) * remove cruft * fix dataframe x-scrolling for latest docutils [skip azp][skip github] * touch tutorial [skip github][skip azp] * fix acronyms in BibTeX * remove month fields * remove keyword fields * whitespace * cruft * remove language fields * touch tutorial again (why?) * FIX: Cats Co-authored-by: Eric Larson --- .circleci/config.yml | 2 +- doc/_static/style.css | 3 +- doc/_templates/layout.html | 12 - doc/references.bib | 238 ++++++++---------- .../epochs/plot_40_autogenerate_metadata.py | 8 +- 5 files changed, 111 insertions(+), 152 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7b434af12b0..b6c9203c1c2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -203,7 +203,7 @@ jobs: - run: name: make html command: | - PATTERN=$(cat ../pattern.txt) make -C doc $(cat ../build.txt); + PATTERN=$(cat pattern.txt) make -C doc $(cat build.txt); - run: name: Show profiling output when: always diff --git a/doc/_static/style.css b/doc/_static/style.css index d762df6a515..2b69a39b156 100644 --- a/doc/_static/style.css +++ b/doc/_static/style.css @@ -57,7 +57,8 @@ iframe.sg_report { } /* ******************************** make HTML'd pandas dataframes scrollable */ -output_html { +table.dataframe { + display: block; overflow: auto; } diff --git a/doc/_templates/layout.html b/doc/_templates/layout.html index 30f7d3bbb03..59638326f56 100755 --- a/doc/_templates/layout.html +++ b/doc/_templates/layout.html @@ -1,20 +1,8 @@ {%- extends "pydata_sphinx_theme/layout.html" %} -{% block fonts %} - - - - - -{% endblock %} - {% block extrahead %} - {{ super() }} {% endblock %} diff --git a/doc/references.bib b/doc/references.bib index bd55edeb543..6bc3dd91521 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -146,7 +146,7 @@ @inproceedings{BigdelyShamloEtAl2013 doi = {10.1109/GlobalSIP.2013.6736796}, booktitle = {2013 IEEE Global Conference on Signal and Information Processing}, pages = {1--4}, - title = {Hierarchical event descriptor (HED) tags for analysis of event-related EEG studies}, + title = {Hierarchical event descriptor {(HED)} tags for analysis of event-related {EEG} studies}, organization = {IEEE}, year = {2013}, } @@ -186,15 +186,15 @@ @article{BrookesEtAl2008 } @article{BrunaEtAl2018, - doi = {10.1088/1741-2552/aacfe4}, - year = {2018}, - publisher = {{IOP} Publishing}, - volume = {15}, - number = {5}, - pages = {056011}, - author = {Ricardo Bru{\~{n}}a, Fernando Maest{\'{u}}, Ernesto Pereda}, - title = {Phase locking value revisited: teaching new tricks to an old dog}, - journal = {Journal of Neural Engineering}, + doi = {10.1088/1741-2552/aacfe4}, + year = {2018}, + publisher = {{IOP} Publishing}, + volume = {15}, + number = {5}, + pages = {056011}, + author = {Ricardo Bru{\~{n}}a, Fernando Maest{\'{u}}, Ernesto Pereda}, + title = {Phase locking value revisited: teaching new tricks to an old dog}, + journal = {Journal of Neural Engineering}, } @techreport{BurdakovMerkulov2001, @@ -438,7 +438,7 @@ @article{FischlEtAl2004 } @article{FishburnEtAl2019, - title={Temporal derivative distribution repair (TDDR): a motion correction method for fNIRS}, + title={Temporal derivative distribution repair (TDDR): a motion correction method for {fNIRS}}, doi = {10.1016/j.neuroimage.2018.09.025}, author={Fishburn, Frank A and Ludlum, Ruth S and Vaidya, Chandan J and Medvedev, Andrei V}, journal={NeuroImage}, @@ -685,13 +685,13 @@ @article{HaufeEtAl2014 } @article{HaufeEtAl2014b, - author = {Haufe, Stefan and D{\"a}hne, Sven and Nikulin, Vadim V}, - doi = {https://doi.org/10.1016/j.neuroimage.2014.06.073}, - journal = {NeuroImage}, - pages = {583-597}, - title = {Dimensionality reduction for the analysis of brain oscillations}, - volume = {101}, - year = {2014} + author = {Haufe, Stefan and D{\"a}hne, Sven and Nikulin, Vadim V}, + doi = {https://doi.org/10.1016/j.neuroimage.2014.06.073}, + journal = {NeuroImage}, + pages = {583-597}, + title = {Dimensionality reduction for the analysis of brain oscillations}, + volume = {101}, + year = {2014} } @article{HaukEtAl2006, @@ -706,12 +706,12 @@ @article{HaukEtAl2006 } @article {HaukEtAl2019, - author = {Hauk, Olaf and Stenroos, Matti and Treder, Matthias}, - title = {Towards an Objective Evaluation of EEG/MEG Source Estimation Methods: The Linear Tool Kit}, - year = {2019}, - doi = {10.1101/672956}, - publisher = {Cold Spring Harbor Laboratory}, - journal = {bioRxiv} + author = {Hauk, Olaf and Stenroos, Matti and Treder, Matthias}, + title = {Towards an Objective Evaluation of {EEG/MEG} Source Estimation Methods: The Linear Tool Kit}, + year = {2019}, + doi = {10.1101/672956}, + publisher = {Cold Spring Harbor Laboratory}, + journal = {bioRxiv} } @book{Heiman2002, @@ -1028,21 +1028,17 @@ @article{LinEtAl2006 year = {2006} } - @article{LinEtAl2006a, - title = {Assessing and improving the spatial accuracy in {MEG} source localization by depth-weighted minimum-norm estimates}, - volume = {31}, - issn = {1053-8119}, - doi = {10.1016/j.neuroimage.2005.11.054}, - language = {en}, - number = {1}, - urldate = {2021-01-28}, - journal = {NeuroImage}, - author = {Lin, Fa-Hsuan and Witzel, Thomas and Ahlfors, Seppo P. and Stufflebeam, Steven M. and Belliveau, John W. and Hämäläinen, Matti S.}, - month = may, - year = {2006}, - keywords = {Brain, Depth, Inverse problem, MEG, Minimum-norm}, - pages = {160--171} + title = {Assessing and improving the spatial accuracy in {MEG} source localization by depth-weighted minimum-norm estimates}, + volume = {31}, + issn = {1053-8119}, + doi = {10.1016/j.neuroimage.2005.11.054}, + number = {1}, + urldate = {2021-01-28}, + journal = {NeuroImage}, + author = {Lin, Fa-Hsuan and Witzel, Thomas and Ahlfors, Seppo P. and Stufflebeam, Steven M. and Belliveau, John W. and Hämäläinen, Matti S.}, + year = {2006}, + pages = {160--171} } @article{LiuEtAl1998, @@ -1133,7 +1129,7 @@ @article{MolinsEtAl2008 journal = {Neuroimage}, number = {3}, pages = {1069-1077}, - title = {Quantification of the benefit from integrating MEG and EEG data in + title = {Quantification of the benefit from integrating {MEG} and {EEG} data in minimum l2-norm estimation}, volume = {42}, year = {2008} @@ -1233,14 +1229,14 @@ @article{NicholsHolmes2002 } @article{NikulinEtAl2011, - author = {Nikulin, Vadim V and Nolte, Guido and Curio, Gabriel}, - doi = {10.1016/j.neuroimage.2011.01.057}, - journal={NeuroImage}, - title = {A novel method for reliable and fast extraction of neuronal {EEG/MEG} oscillations on the basis of spatio-spectral decomposition}, - pages={1528-1535}, - volume={55}, - number={4}, - year={2011} + author = {Nikulin, Vadim V and Nolte, Guido and Curio, Gabriel}, + doi = {10.1016/j.neuroimage.2011.01.057}, + journal={NeuroImage}, + title = {A novel method for reliable and fast extraction of neuronal {EEG/MEG} oscillations on the basis of spatio-spectral decomposition}, + pages={1528-1535}, + volume={55}, + number={4}, + year={2011} } @article{NolteEtAl2004, @@ -1315,7 +1311,6 @@ @article{Pascual-Marqui2011 number = {1952}, journal = {Philosophical Transactions of the Royal Society A: Mathematical, Physical and Engineering Sciences}, author = {Pascual-Marqui, Roberto D. and Lehmann, Dietrich and Koukkou, Martha and Kochi, Kieko and Anderer, Peter and Saletu, Bernd and Tanaka, Hideaki and Hirata, Koichi and John, E. Roy and Prichep, Leslie and Biscay-Lirio, Rolando and Kinoshita, Toshihiko}, - month = oct, year = {2011}, pages = {3768--3784} } @@ -1340,7 +1335,6 @@ @article{PerrinEtAl1987 number = {4}, journal = {IEEE Transactions on Biomedical Engineering}, author = {Perrin, F. and Bertrand, O. and Pernier, J.}, - month = apr, year = {1987}, pages = {283--288} } @@ -1906,7 +1900,6 @@ @article{KayserTenke2015 number = {3}, journal = {International journal of psychophysiology : official journal of the International Organization of Psychophysiology}, author = {Kayser, Jürgen and Tenke, Craig E.}, - month = sep, year = {2015}, pmid = {26071227}, pmcid = {PMC4610715}, @@ -1914,67 +1907,58 @@ @article{KayserTenke2015 } @article{GrattonEtAl1983, - title = {A new method for off-line removal of ocular artifact}, - volume = {55}, - issn = {0013-4694}, - doi = {10.1016/0013-4694(83)90135-9}, - language = {en}, - number = {4}, - urldate = {2020-08-03}, - journal = {Electroencephalography and Clinical Neurophysiology}, - author = {Gratton, Gabriele and Coles, Michael G. H and Donchin, Emanuel}, - month = apr, - year = {1983}, - pages = {468--484} + title = {A new method for off-line removal of ocular artifact}, + volume = {55}, + issn = {0013-4694}, + doi = {10.1016/0013-4694(83)90135-9}, + number = {4}, + urldate = {2020-08-03}, + journal = {Electroencephalography and Clinical Neurophysiology}, + author = {Gratton, Gabriele and Coles, Michael G. H and Donchin, Emanuel}, + year = {1983}, + pages = {468--484} } @book{OppenheimEtAl1999, - address = {Upper Saddle River, NJ}, - edition = {2 edition}, - title = {Discrete-{Time} {Signal} {Processing}}, - isbn = {978-0-13-754920-7}, - language = {English}, - publisher = {Prentice Hall}, - author = {Oppenheim, Alan V. and Schafer, Ronald W. and Buck, John R.}, - month = jan, - year = {1999} + address = {Upper Saddle River, NJ}, + edition = {2 edition}, + title = {Discrete-{Time} {Signal} {Processing}}, + isbn = {978-0-13-754920-7}, + publisher = {Prentice Hall}, + author = {Oppenheim, Alan V. and Schafer, Ronald W. and Buck, John R.}, + year = {1999} } - @book{CrochiereRabiner1983, - address = {Englewood Cliffs, NJ}, - edition = {1 edition}, - title = {Multirate {Digital} {Signal} {Processing}}, - isbn = {978-0-13-605162-6}, - language = {English}, - publisher = {Pearson}, - author = {Crochiere, Ronald E. and Rabiner, Lawrence R.}, - month = dec, - year = {1983} + address = {Englewood Cliffs, NJ}, + edition = {1 edition}, + title = {Multirate {Digital} {Signal} {Processing}}, + isbn = {978-0-13-605162-6}, + publisher = {Pearson}, + author = {Crochiere, Ronald E. and Rabiner, Lawrence R.}, + year = {1983} } @article{Yao2001, - title = {A method to standardize a reference of scalp {EEG} recordings to a point at infinity}, - volume = {22}, - issn = {0967-3334}, - doi = {10.1088/0967-3334/22/4/305}, - number = {4}, - journal = {Physiological Measurement}, - author = {Yao, D.}, - month = nov, - year = {2001}, - pmid = {11761077}, - pages = {693--711} + title = {A method to standardize a reference of scalp {EEG} recordings to a point at infinity}, + volume = {22}, + issn = {0967-3334}, + doi = {10.1088/0967-3334/22/4/305}, + number = {4}, + journal = {Physiological Measurement}, + author = {Yao, D.}, + year = {2001}, + pmid = {11761077}, + pages = {693--711} } @inproceedings{StrohmeierEtAl2015, - title = {{MEG}/{EEG} {Source} {Imaging} with a {Non}-{Convex} {Penalty} in the {Time}-{Frequency} {Domain}}, - doi = {10.1109/PRNI.2015.14}, - booktitle = {2015 {International} {Workshop} on {Pattern} {Recognition} in {NeuroImaging}}, - author = {Strohmeier, Daniel and Gramfort, Alexandre and Haueisen, Jens}, - month = jun, - year = {2015}, - pages = {21--24} + title = {{MEG}/{EEG} {Source} {Imaging} with a {Non}-{Convex} {Penalty} in the {Time}-{Frequency} {Domain}}, + doi = {10.1109/PRNI.2015.14}, + booktitle = {2015 {International} {Workshop} on {Pattern} {Recognition} in {NeuroImaging}}, + author = {Strohmeier, Daniel and Gramfort, Alexandre and Haueisen, Jens}, + year = {2015}, + pages = {21--24} } @misc{WikipediaSI, @@ -1992,37 +1976,30 @@ @misc{BIDSdocs urldate = "12-October-2020" } - @article{OReillyEtAl2021, - title = {Structural templates for imaging {EEG} cortical sources in infants}, - volume = {227}, - issn = {1053-8119}, - url = {http://www.sciencedirect.com/science/article/pii/S1053811920311678}, - doi = {10.1016/j.neuroimage.2020.117682}, - language = {en}, - urldate = {2021-01-12}, - journal = {NeuroImage}, - author = {O'Reilly, Christian and Larson, Eric and Richards, John E. and Elsabbagh, Mayada}, - month = feb, - year = {2021}, - keywords = {Electroencephalography, Forward model, Infant, Neurodevelopment, Population template, Source reconstruction}, - pages = {117682} + title = {Structural templates for imaging {EEG} cortical sources in infants}, + volume = {227}, + issn = {1053-8119}, + url = {http://www.sciencedirect.com/science/article/pii/S1053811920311678}, + doi = {10.1016/j.neuroimage.2020.117682}, + urldate = {2021-01-12}, + journal = {NeuroImage}, + author = {O'Reilly, Christian and Larson, Eric and Richards, John E. and Elsabbagh, Mayada}, + year = {2021}, + pages = {117682} } @article{RichardsEtAl2016, - series = {Sharing the wealth: {Brain} {Imaging} {Repositories} in 2015}, - title = {A database of age-appropriate average {MRI} templates}, - volume = {124}, - issn = {1053-8119}, - url = {http://www.sciencedirect.com/science/article/pii/S1053811915003559}, - doi = {10.1016/j.neuroimage.2015.04.055}, - language = {en}, - journal = {NeuroImage}, - author = {Richards, John E. and Sanchez, Carmen and Phillips-Meek, Michelle and Xie, Wanze}, - month = jan, - year = {2016}, - keywords = {Average MRI templates, Brain development, Lifespan MRI, Neurodevelopmental MRI Database}, - pages = {1254--1259} + series = {Sharing the wealth: {Brain} {Imaging} {Repositories} in 2015}, + title = {A database of age-appropriate average {MRI} templates}, + volume = {124}, + issn = {1053-8119}, + url = {http://www.sciencedirect.com/science/article/pii/S1053811915003559}, + doi = {10.1016/j.neuroimage.2015.04.055}, + journal = {NeuroImage}, + author = {Richards, John E. and Sanchez, Carmen and Phillips-Meek, Michelle and Xie, Wanze}, + year = {2016}, + pages = {1254--1259} } @Article{Lehmann1980, @@ -2031,7 +2008,6 @@ @Article{Lehmann1980 title = {Reference-free identification of components of checkerboard-evoked multichannel potential fields}, year = {1980}, issn = {0013-4694}, - month = {jun}, number = {6}, pages = {609--621}, volume = {48}, @@ -2045,7 +2021,6 @@ @Article{Lehmann1984 title = {Spatial analysis of evoked potentials in man—a review}, year = {1984}, issn = {0301-0082}, - month = {jan}, number = {3}, pages = {227--250}, volume = {23}, @@ -2059,7 +2034,6 @@ @Article{Murray2008 title = {Topographic {ERP} Analyses: {A} Step-by-Step Tutorial Review}, year = {2008}, issn = {0896-0267}, - month = {mar}, number = {4}, pages = {249--264}, volume = {20}, @@ -2073,7 +2047,6 @@ @Article{Kappenman2021 title = {{ERP} {CORE}: An open resource for human event-related potential research}, year = {2021}, issn = {1053-8119}, - month = {jan}, pages = {117465}, volume = {225}, doi = {10.1016/j.neuroimage.2020.117465}, @@ -2091,13 +2064,10 @@ @article{GenoveseEtAl2002 doi = {https://doi.org/10.1006/nimg.2001.1037}, url = {https://www.sciencedirect.com/science/article/pii/S1053811901910377}, author = {Christopher R. Genovese and Nicole A. Lazar and Thomas Nichols}, -keywords = {functional neuroimaging, false discovery rate, multiple testing, Bonferroni correction} } -@Comment{jabref-meta: databaseType:bibtex;} - @article{YaoEtAl2019, - title={Which reference should we use for EEG and ERP practice?}, + title={Which reference should we use for {EEG} and {ERP} practice?}, author={Yao, Dezhong and Qin, Yun and Hu, Shiang and Dong, Li and Vega, Maria L Bringas and Sosa, Pedro A Vald{\'e}s}, journal={Brain topography}, volume={32}, diff --git a/tutorials/epochs/plot_40_autogenerate_metadata.py b/tutorials/epochs/plot_40_autogenerate_metadata.py index 1a6da469ead..5fb66fdd80f 100644 --- a/tutorials/epochs/plot_40_autogenerate_metadata.py +++ b/tutorials/epochs/plot_40_autogenerate_metadata.py @@ -70,9 +70,9 @@ # or it might include events that occurred well outside a given epoch. # # Let us look at a concrete example. In the Flankers task of the ERP CORE -# dataset, participants were required to respond to visual stimuli by -# pressing a button. We're interested in looking at the visual evoked responses -# (ERPs) of trials with correct responses. Assume that based on literature +# dataset, participants were required to respond to visual stimuli by pressing +# a button. We're interested in looking at the visual evoked responses (ERPs) +# of trials with correct responses. Assume that based on literature # studies, we decide that responses later than 1500 ms after stimulus onset are # to be considered invalid, because they don't capture the neuronal processes # of interest here. We can approach this in the following way with the help of @@ -362,7 +362,7 @@ epochs.metadata.loc[epochs.metadata['last_stimulus'].isna(), :] ############################################################################### -# Bummer! ☹️ It seems the very first two responses were recorded before the +# Bummer! It seems the very first two responses were recorded before the # first stimulus appeared: the values in the ``stimulus`` column are ``None``. # There is a very simple way to select only those epochs that **do** have a # stimulus (i.e., are not ``None``): From 338ef6004ad5eb08b69c5460bd1a22cbb911b6bb Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 6 Apr 2021 12:34:21 -0400 Subject: [PATCH 07/15] MRG, MAINT: Better notebook tests (#9034) * MAINT: Encapsulate and separate notebook tests * FIX: Fixture * MAINT: ipytest requirement * ENH: Add * WIP: Closer-ish * FIX * FIX: Scope? * Fix conftest * TST: Skip Windows Co-authored-by: Guillaume Favelier --- MANIFEST.in | 1 - environment.yml | 3 + mne/conftest.py | 8 +- mne/viz/_brain/tests/test.ipynb | 123 ------------------------ mne/viz/_brain/tests/test_notebook.py | 124 +++++++++++++++++++++---- mne/viz/conftest.py | 129 ++++++++++++++++++++++++++ mne/viz/tests/conftest.py | 45 --------- requirements.txt | 2 + requirements_testing.txt | 2 + 9 files changed, 249 insertions(+), 188 deletions(-) delete mode 100644 mne/viz/_brain/tests/test.ipynb create mode 100644 mne/viz/conftest.py delete mode 100644 mne/viz/tests/conftest.py diff --git a/MANIFEST.in b/MANIFEST.in index 8e126e4c718..3a72e165d28 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -36,7 +36,6 @@ recursive-include mne mne/datasets *.csv include mne/io/edf/gdf_encodes.txt include mne/datasets/sleep_physionet/SHA1SUMS include mne/externals/tqdm/_tqdm/tqdm.1 -include mne/viz/_brain/tests/test.ipynb ### Exclude diff --git a/environment.yml b/environment.yml index 6fe7b1bd6b4..a5cbdea0ed2 100644 --- a/environment.yml +++ b/environment.yml @@ -34,3 +34,6 @@ dependencies: - pyqt!=5.15.3 - mne - mffpy>=0.5.7 +- ipywidgets +- pip: + - ipyvtk-simple diff --git a/mne/conftest.py b/mne/conftest.py index 8375c0ba51a..ec4f14f2093 100644 --- a/mne/conftest.py +++ b/mne/conftest.py @@ -332,15 +332,17 @@ def _use_backend(backend_name, interactive): def _check_skip_backend(name): from mne.viz.backends.tests._utils import (has_mayavi, has_pyvista, has_pyqt5, has_imageio_ffmpeg) + check_pyvista = name in ('pyvista', 'notebook') + check_pyqt5 = name in ('mayavi', 'pyvista') if name == 'mayavi': if not has_mayavi(): pytest.skip("Test skipped, requires mayavi.") elif name == 'pyvista': - if not has_pyvista(): - pytest.skip("Test skipped, requires pyvista.") if not has_imageio_ffmpeg(): pytest.skip("Test skipped, requires imageio-ffmpeg") - if not has_pyqt5(): + if check_pyvista and not has_pyvista(): + pytest.skip("Test skipped, requires pyvista.") + if check_pyqt5 and not has_pyqt5(): pytest.skip("Test skipped, requires PyQt5.") diff --git a/mne/viz/_brain/tests/test.ipynb b/mne/viz/_brain/tests/test.ipynb deleted file mode 100644 index 66c4c8772de..00000000000 --- a/mne/viz/_brain/tests/test.ipynb +++ /dev/null @@ -1,123 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import mne\n", - "from mne.datasets import testing\n", - "data_path = testing.data_path()\n", - "raw_fname = data_path + '/MEG/sample/sample_audvis_trunc_raw.fif'\n", - "subjects_dir = data_path + '/subjects'\n", - "subject = 'sample'\n", - "trans = data_path + '/MEG/sample/sample_audvis_trunc-trans.fif'\n", - "info = mne.io.read_info(raw_fname)\n", - "mne.viz.set_3d_backend('notebook')\n", - "fig = mne.viz.plot_alignment(info, trans, subject=subject, dig=True,\n", - " meg=['helmet', 'sensors'], subjects_dir=subjects_dir,\n", - " surfaces=['head-dense'])\n", - "assert fig.display is not None" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from contextlib import contextmanager\n", - "import os\n", - "from numpy.testing import assert_allclose\n", - "from ipywidgets import Button\n", - "import matplotlib.pyplot as plt\n", - "import mne\n", - "from mne.datasets import testing\n", - "data_path = testing.data_path()\n", - "sample_dir = os.path.join(data_path, 'MEG', 'sample')\n", - "subjects_dir = os.path.join(data_path, 'subjects')\n", - "fname_stc = os.path.join(sample_dir, 'sample_audvis_trunc-meg')\n", - "stc = mne.read_source_estimate(fname_stc, subject='sample')\n", - "initial_time = 0.13\n", - "mne.viz.set_3d_backend('notebook')\n", - "brain_class = mne.viz.get_brain_class()\n", - "\n", - "\n", - "@contextmanager\n", - "def interactive(on):\n", - " old = plt.isinteractive()\n", - " plt.interactive(on)\n", - " try:\n", - " yield\n", - " finally:\n", - " plt.interactive(old)\n", - "\n", - "with interactive(False):\n", - " brain = stc.plot(subjects_dir=subjects_dir, initial_time=initial_time,\n", - " clim=dict(kind='value', pos_lims=[3, 6, 9]),\n", - " time_viewer=True,\n", - " show_traces=True,\n", - " hemi='lh', size=300)\n", - " assert isinstance(brain, brain_class)\n", - " assert brain._renderer.figure.notebook\n", - " assert brain._renderer.figure.display is not None\n", - " brain._renderer._update()\n", - " total_number_of_buttons = len([k for k in brain._renderer.actions.keys() if '_field' not in k])\n", - " number_of_buttons = 0\n", - " for action in brain._renderer.actions.values():\n", - " if isinstance(action, Button):\n", - " action.click()\n", - " number_of_buttons += 1\n", - " assert number_of_buttons == total_number_of_buttons\n", - " img_nv = brain.screenshot()\n", - " assert img_nv.shape == (300, 300, 3), img_nv.shape\n", - " img_v = brain.screenshot(time_viewer=True)\n", - " assert img_v.shape[1:] == (300, 3), img_v.shape\n", - " # XXX This rtol is not very good, ideally would be zero\n", - " assert_allclose(img_v.shape[0], img_nv.shape[0] * 1.25, err_msg=img_nv.shape, rtol=0.1)\n", - " brain.close()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import mne\n", - "mne.viz.set_3d_backend('notebook')\n", - "rend = mne.viz.create_3d_figure(size=(100, 100), scene=False)\n", - "fig = rend.scene()\n", - "mne.viz.set_3d_title(fig, 'Notebook testing')\n", - "mne.viz.set_3d_view(fig, 200, 70, focalpoint=[0, 0, 0])\n", - "assert fig.display is None\n", - "rend.show()\n", - "total_number_of_buttons = len([k for k in rend.actions.keys() if '_field' not in k])\n", - "number_of_buttons = 0\n", - "for action in rend.actions.values():\n", - " if isinstance(action, Button):\n", - " action.click()\n", - " number_of_buttons += 1\n", - "assert number_of_buttons == total_number_of_buttons\n", - "assert fig.display is not None" - ] - } - ], - "metadata": { - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.5" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/mne/viz/_brain/tests/test_notebook.py b/mne/viz/_brain/tests/test_notebook.py index 7c159326b74..8b67bf73667 100644 --- a/mne/viz/_brain/tests/test_notebook.py +++ b/mne/viz/_brain/tests/test_notebook.py @@ -1,22 +1,114 @@ -import os +# -*- coding: utf-8 -*- +# +# Authors: Guillaume Favelier +# Eric Larson +# NOTE: Tests in this directory must be self-contained because they are +# executed in a separate IPython kernel. + +import sys +import pytest from mne.datasets import testing -from mne.utils import requires_version -PATH = os.path.dirname(os.path.realpath(__file__)) + +# This will skip all tests in this scope +pytestmark = pytest.mark.skipif( + sys.platform.startswith('win'), reason='nbexec does not work on Windows') + + +@testing.requires_testing_data +def test_notebook_alignment(renderer_notebook, brain_gc, nbexec): + """Test plot alignment in a notebook.""" + import mne + data_path = mne.datasets.testing.data_path() + raw_fname = data_path + '/MEG/sample/sample_audvis_trunc_raw.fif' + subjects_dir = data_path + '/subjects' + subject = 'sample' + trans = data_path + '/MEG/sample/sample_audvis_trunc-trans.fif' + info = mne.io.read_info(raw_fname) + mne.viz.set_3d_backend('notebook') + fig = mne.viz.plot_alignment( + info, trans, subject=subject, dig=True, + meg=['helmet', 'sensors'], subjects_dir=subjects_dir, + surfaces=['head-dense']) + assert fig.display is not None + + +@testing.requires_testing_data +def test_notebook_interactive(renderer_notebook, brain_gc, nbexec): + """Test interactive modes.""" + import os + from contextlib import contextmanager + from numpy.testing import assert_allclose + from ipywidgets import Button + import matplotlib.pyplot as plt + import mne + from mne.datasets import testing + data_path = testing.data_path() + sample_dir = os.path.join(data_path, 'MEG', 'sample') + subjects_dir = os.path.join(data_path, 'subjects') + fname_stc = os.path.join(sample_dir, 'sample_audvis_trunc-meg') + stc = mne.read_source_estimate(fname_stc, subject='sample') + initial_time = 0.13 + mne.viz.set_3d_backend('notebook') + brain_class = mne.viz.get_brain_class() + + @contextmanager + def interactive(on): + old = plt.isinteractive() + plt.interactive(on) + try: + yield + finally: + plt.interactive(old) + + with interactive(False): + brain = stc.plot(subjects_dir=subjects_dir, initial_time=initial_time, + clim=dict(kind='value', pos_lims=[3, 6, 9]), + time_viewer=True, + show_traces=True, + hemi='lh', size=300) + assert isinstance(brain, brain_class) + assert brain._renderer.figure.notebook + assert brain._renderer.figure.display is not None + brain._renderer._update() + total_number_of_buttons = sum( + '_field' not in k for k in brain._renderer.actions.keys()) + number_of_buttons = 0 + for action in brain._renderer.actions.values(): + if isinstance(action, Button): + action.click() + number_of_buttons += 1 + assert number_of_buttons == total_number_of_buttons + img_nv = brain.screenshot() + assert img_nv.shape == (300, 300, 3), img_nv.shape + img_v = brain.screenshot(time_viewer=True) + assert img_v.shape[1:] == (300, 3), img_v.shape + # XXX This rtol is not very good, ideally would be zero + assert_allclose( + img_v.shape[0], img_nv.shape[0] * 1.25, err_msg=img_nv.shape, + rtol=0.1) + brain.close() @testing.requires_testing_data -@requires_version('nbformat') -@requires_version('nbclient') -@requires_version('ipympl') -def test_notebook_3d_backend(renderer_notebook, brain_gc): - """Test executing a notebook that should not fail.""" - import nbformat - from nbclient import NotebookClient - - notebook_filename = os.path.join(PATH, "test.ipynb") - with open(notebook_filename) as f: - nb = nbformat.read(f, as_version=4) - client = NotebookClient(nb) - client.execute() +def test_notebook_button_counts(renderer_notebook, brain_gc, nbexec): + """Test button counts.""" + import mne + from ipywidgets import Button + mne.viz.set_3d_backend('notebook') + rend = mne.viz.create_3d_figure(size=(100, 100), scene=False) + fig = rend.scene() + mne.viz.set_3d_title(fig, 'Notebook testing') + mne.viz.set_3d_view(fig, 200, 70, focalpoint=[0, 0, 0]) + assert fig.display is None + rend.show() + total_number_of_buttons = sum( + '_field' not in k for k in rend.actions.keys()) + number_of_buttons = 0 + for action in rend.actions.values(): + if isinstance(action, Button): + action.click() + number_of_buttons += 1 + assert number_of_buttons == total_number_of_buttons + assert fig.display is not None diff --git a/mne/viz/conftest.py b/mne/viz/conftest.py new file mode 100644 index 00000000000..6576bc5ee26 --- /dev/null +++ b/mne/viz/conftest.py @@ -0,0 +1,129 @@ +# Authors: Robert Luke +# Eric Larson +# Alexandre Gramfort +# +# License: BSD (3-clause) + +import inspect +from textwrap import dedent + +import pytest +import numpy as np +import os.path as op + +from mne import create_info, EvokedArray, events_from_annotations, Epochs +from mne.channels import make_standard_montage +from mne.datasets.testing import data_path, _pytest_param +from mne.preprocessing.nirs import optical_density, beer_lambert_law +from mne.io import read_raw_nirx +from mne.utils import Bunch + + +@pytest.fixture() +def fnirs_evoked(): + """Create an fnirs evoked structure.""" + montage = make_standard_montage('biosemi16') + ch_names = montage.ch_names + ch_types = ['eeg'] * 16 + info = create_info(ch_names=ch_names, sfreq=20, ch_types=ch_types) + evoked_data = np.random.randn(16, 30) + evoked = EvokedArray(evoked_data, info=info, tmin=-0.2, nave=4) + evoked.set_montage(montage) + evoked.set_channel_types({'Fp1': 'hbo', 'Fp2': 'hbo', 'F4': 'hbo', + 'Fz': 'hbo'}, verbose='error') + return evoked + + +@pytest.fixture(params=[_pytest_param()]) +def fnirs_epochs(): + """Create an fnirs epoch structure.""" + fname = op.join(data_path(download=False), + 'NIRx', 'nirscout', 'nirx_15_2_recording_w_overlap') + raw_intensity = read_raw_nirx(fname, preload=False) + raw_od = optical_density(raw_intensity) + raw_haemo = beer_lambert_law(raw_od) + evts, _ = events_from_annotations(raw_haemo, event_id={'1.0': 1}) + evts_dct = {'A': 1} + tn, tx = -1, 2 + epochs = Epochs(raw_haemo, evts, event_id=evts_dct, tmin=tn, tmax=tx) + return epochs + + +# Create one nbclient and reuse it +@pytest.fixture(scope='session') +def _nbclient(): + try: + import nbformat + from jupyter_client import AsyncKernelManager + from nbclient import NotebookClient + from ipywidgets import Button # noqa + import ipyvtk_simple # noqa + except Exception as exc: + return pytest.skip(f'Skipping Notebook test: {exc}') + km = AsyncKernelManager(config=None) + nb = nbformat.reads(""" +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata":{}, + "outputs": [], + "source":[] + } + ], + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version":3}, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.5" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}""", as_version=4) + client = NotebookClient(nb, km=km) + yield client + client._cleanup_kernel() + + +@pytest.fixture(scope='function') +def nbexec(_nbclient): + """Execute Python code in a notebook.""" + # Adapted/simplified from nbclient/client.py (BSD 3-clause) + _nbclient._cleanup_kernel() + + def execute(code, reset=False): + _nbclient.reset_execution_trackers() + with _nbclient.setup_kernel(): + assert _nbclient.kc is not None + cell = Bunch(cell_type='code', metadata={}, source=dedent(code)) + _nbclient.execute_cell(cell, 0, execution_count=0) + _nbclient.set_widgets_metadata() + + yield execute + + +def pytest_runtest_call(item): + """Run notebook code written in Python.""" + if 'nbexec' in getattr(item, 'fixturenames', ()): + nbexec = item.funcargs['nbexec'] + code = inspect.getsource(getattr(item.module, item.name.split('[')[0])) + code = code.splitlines() + ci = 0 + for ci, c in enumerate(code): + if c.startswith(' '): # actual content + break + code = '\n'.join(code[ci:]) + + def run(nbexec=nbexec, code=code): + nbexec(code) + + item.runtest = run + return diff --git a/mne/viz/tests/conftest.py b/mne/viz/tests/conftest.py deleted file mode 100644 index e5e6cc06b36..00000000000 --- a/mne/viz/tests/conftest.py +++ /dev/null @@ -1,45 +0,0 @@ -# Authors: Robert Luke -# Eric Larson -# Alexandre Gramfort -# -# License: BSD (3-clause) - -import pytest -import numpy as np -import os.path as op - -from mne import create_info, EvokedArray, events_from_annotations, Epochs -from mne.channels import make_standard_montage -from mne.datasets.testing import data_path, _pytest_param -from mne.preprocessing.nirs import optical_density, beer_lambert_law -from mne.io import read_raw_nirx - - -@pytest.fixture() -def fnirs_evoked(): - """Create an fnirs evoked structure.""" - montage = make_standard_montage('biosemi16') - ch_names = montage.ch_names - ch_types = ['eeg'] * 16 - info = create_info(ch_names=ch_names, sfreq=20, ch_types=ch_types) - evoked_data = np.random.randn(16, 30) - evoked = EvokedArray(evoked_data, info=info, tmin=-0.2, nave=4) - evoked.set_montage(montage) - evoked.set_channel_types({'Fp1': 'hbo', 'Fp2': 'hbo', 'F4': 'hbo', - 'Fz': 'hbo'}, verbose='error') - return evoked - - -@pytest.fixture(params=[_pytest_param()]) -def fnirs_epochs(): - """Create an fnirs epoch structure.""" - fname = op.join(data_path(download=False), - 'NIRx', 'nirscout', 'nirx_15_2_recording_w_overlap') - raw_intensity = read_raw_nirx(fname, preload=False) - raw_od = optical_density(raw_intensity) - raw_haemo = beer_lambert_law(raw_od) - evts, _ = events_from_annotations(raw_haemo, event_id={'1.0': 1}) - evts_dct = {'A': 1} - tn, tx = -1, 2 - epochs = Epochs(raw_haemo, evts, event_id=evts_dct, tmin=tn, tmax=tx) - return epochs diff --git a/requirements.txt b/requirements.txt index 3c39ef0f241..577ab76a427 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,3 +30,5 @@ pyvista>=0.24 pyvistaqt>=0.2.0 tqdm mffpy>=0.5.7 +ipywidgets +ipyvtk-simple diff --git a/requirements_testing.txt b/requirements_testing.txt index 6525253ec2f..34eaa3ccdfe 100644 --- a/requirements_testing.txt +++ b/requirements_testing.txt @@ -2,6 +2,7 @@ pytest!=4.6.0 pytest-cov pytest-timeout pytest-harvest +ipytest flake8 flake8-array-spacing sphinx-gallery @@ -11,3 +12,4 @@ pydocstyle check-manifest twine wheel +nbclient From 7070a602436bcc6cd8117670ba846b54d78bc979 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 6 Apr 2021 14:45:56 -0400 Subject: [PATCH 08/15] BUG: Fix coordinate frame (#9251) * BUG: Fix coordinate frame * FIX: Better test * FIX: Test --- mne/gui/tests/test_file_traits.py | 8 +-- mne/io/_digitization.py | 2 +- mne/io/egi/egimff.py | 74 +++++++++------------ mne/io/egi/tests/test_egi.py | 106 +++++++++++++++++++----------- mne/io/meas_info.py | 2 +- 5 files changed, 105 insertions(+), 87 deletions(-) diff --git a/mne/gui/tests/test_file_traits.py b/mne/gui/tests/test_file_traits.py index 3fc48a774da..8b094785a96 100644 --- a/mne/gui/tests/test_file_traits.py +++ b/mne/gui/tests/test_file_traits.py @@ -90,10 +90,10 @@ def test_digitization_source(tmpdir): # EGI MFF inst.file = op.join(data_path, 'EGI', 'test_egi.mff') assert len(inst.points) == 0 - assert len(inst.eeg_points) == 129 - assert_allclose(inst.lpa * 1000, [[-67.1, 0.5, -37.1]], atol=0.1) - assert_allclose(inst.nasion * 1000, [[0.0, 103.6, -26.9]], atol=0.1) - assert_allclose(inst.rpa * 1000, [[67.1, 0.5, -37.1]], atol=0.1) + assert len(inst.eeg_points) == 130 + assert_allclose(inst.lpa * 1000, [[-67.1, 0, 0]], atol=0.1) + assert_allclose(inst.nasion * 1000, [[0.0, 103.6, 0]], atol=0.1) + assert_allclose(inst.rpa * 1000, [[67.1, 0, 0]], atol=0.1) # CTF inst.file = op.join(data_path, 'CTF', 'testdata_ctf.ds') diff --git a/mne/io/_digitization.py b/mne/io/_digitization.py index 19c07a7cb8c..f010e24e629 100644 --- a/mne/io/_digitization.py +++ b/mne/io/_digitization.py @@ -406,7 +406,7 @@ def _make_dig_points(nasion=None, lpa=None, rpa=None, hpi=None, 'coord_frame': coord_frame}) if extra_points is not None: extra_points = np.asarray(extra_points) - if extra_points.shape[1] != 3: + if len(extra_points) and extra_points.shape[1] != 3: raise ValueError('Points should have the shape (n_points, 3) ' 'instead of %s' % (extra_points.shape,)) for idx, point in enumerate(extra_points): diff --git a/mne/io/egi/egimff.py b/mne/io/egi/egimff.py index 2b647bea7e8..3d024e83153 100644 --- a/mne/io/egi/egimff.py +++ b/mne/io/egi/egimff.py @@ -1,5 +1,6 @@ """EGI NetStation Load Function.""" +from collections import OrderedDict import datetime import math import os.path as op @@ -12,10 +13,9 @@ from .events import _read_events, _combine_triggers from .general import (_get_signalfname, _get_ep_info, _extract, _get_blocks, _get_gains, _block_r) -from .._digitization import DigPoint from ..base import BaseRaw from ..constants import FIFF -from ..meas_info import _empty_info, create_info +from ..meas_info import _empty_info, create_info, _ensure_meas_date_none_or_dt from ..proj import setup_proj from ..utils import _create_chs, _mult_cal_one from ...annotations import Annotations @@ -252,28 +252,22 @@ def _get_eeg_calibration_info(filepath, egi_info): def _read_locs(filepath, chs, egi_info): """Read channel locations.""" + from ...channels.montage import make_dig_montage fname = op.join(filepath, 'coordinates.xml') if not op.exists(fname): - return chs, [] + return chs, None reference_names = ('VREF', 'Vertex Reference') - dig_kind_map = { - '': FIFF.FIFFV_POINT_EEG, - 'VREF': FIFF.FIFFV_POINT_EEG, - 'Vertex Reference': FIFF.FIFFV_POINT_EEG, - 'Left periauricular point': FIFF.FIFFV_POINT_CARDINAL, - 'Right periauricular point': FIFF.FIFFV_POINT_CARDINAL, - 'Nasion': FIFF.FIFFV_POINT_CARDINAL, - } dig_ident_map = { - 'Left periauricular point': FIFF.FIFFV_POINT_LPA, - 'Right periauricular point': FIFF.FIFFV_POINT_RPA, - 'Nasion': FIFF.FIFFV_POINT_NASION, + 'Left periauricular point': 'lpa', + 'Right periauricular point': 'rpa', + 'Nasion': 'nasion', } numbers = np.array(egi_info['numbers']) coordinates = parse(fname) sensors = coordinates.getElementsByTagName('sensor') - dig_points = [] - dig_reference = None + ch_pos = OrderedDict() + hsp = list() + nlr = dict() for sensor in sensors: name_element = sensor.getElementsByTagName('name')[0].firstChild name = '' if name_element is None else name_element.data @@ -282,27 +276,19 @@ def _read_locs(filepath, chs, egi_info): for coord in 'xyz'] loc = np.array(coords) / 100 # cm -> m # create dig entry - kind = dig_kind_map[name] - if kind == FIFF.FIFFV_POINT_CARDINAL: - ident = dig_ident_map[name] + if name in dig_ident_map: + nlr[dig_ident_map[name]] = loc else: - ident = int(nr) - dig_point = DigPoint(kind=kind, ident=ident, r=loc, - coord_frame=FIFF.FIFFV_COORD_HEAD) - dig_points.append(dig_point) - if name in reference_names: - dig_reference = dig_point - # add location to channel entry - id = np.flatnonzero(numbers == nr) - if len(id) == 0: - continue - chs[id[0]]['loc'][:3] = loc - # Insert reference location into channel location - if dig_reference is not None: - for ch in chs: - if ch['kind'] == FIFF.FIFFV_EEG_CH: - ch['loc'][3:6] = dig_reference['r'] - return chs, dig_points + if name in reference_names: + ch_pos['EEG000'] = loc + # add location to channel entry + id_ = np.flatnonzero(numbers == nr) + if len(id_) == 0: + hsp.append(loc) + else: + ch_pos[chs[id_[0]]['ch_name']] = loc + mon = make_dig_montage(ch_pos=ch_pos, hsp=hsp, **nlr) + return chs, mon def _add_pns_channel_info(chs, egi_info, ch_names): @@ -443,7 +429,8 @@ def __init__(self, input_fname, eog=None, misc=None, if isinstance(v, list): for k in v: if k not in event_codes: - raise ValueError('Could find event named "%s"' % k) + raise ValueError( + f'Could not find event named {repr(k)}') elif v is not None: raise ValueError('`%s` must be None or of type list' % kk) logger.info(' Synthesizing trigger channel "STI 014" ...') @@ -468,7 +455,7 @@ def __init__(self, input_fname, eog=None, misc=None, egi_info['year'], egi_info['month'], egi_info['day'], egi_info['hour'], egi_info['minute'], egi_info['second']) my_timestamp = time.mktime(my_time.timetuple()) - info['meas_date'] = (my_timestamp, 0) + info['meas_date'] = _ensure_meas_date_none_or_dt((my_timestamp, 0)) # First: EEG ch_names = [channel_naming % (i + 1) for i in @@ -490,7 +477,7 @@ def __init__(self, input_fname, eog=None, misc=None, ch_coil = FIFF.FIFFV_COIL_EEG ch_kind = FIFF.FIFFV_EEG_CH chs = _create_chs(ch_names, cals, ch_coil, ch_kind, eog, (), (), misc) - chs, dig = _read_locs(input_fname, chs, egi_info) + chs, mon = _read_locs(input_fname, chs, egi_info) sti_ch_idx = [i for i, name in enumerate(ch_names) if name.startswith('STI') or name in event_codes] for idx in sti_ch_idx: @@ -500,10 +487,10 @@ def __init__(self, input_fname, eog=None, misc=None, 'coil_type': FIFF.FIFFV_COIL_NONE, 'unit': FIFF.FIFF_UNIT_NONE}) chs = _add_pns_channel_info(chs, egi_info, ch_names) - info['chs'] = chs - info['dig'] = dig info._update_redundant() + if mon is not None: + info.set_montage(mon, on_missing='ignore') file_bin = op.join(input_fname, egi_info['eeg_fname']) egi_info['egi_events'] = egi_events @@ -864,11 +851,12 @@ def _read_evoked_mff(fname, condition, channel_naming='E%d', verbose=None): ch_coil = FIFF.FIFFV_COIL_EEG ch_kind = FIFF.FIFFV_EEG_CH chs = _create_chs(ch_names, cals, ch_coil, ch_kind, (), (), (), ()) - chs, dig = _read_locs(fname, chs, egi_info) + chs, mon = _read_locs(fname, chs, egi_info) # Update PNS channel info chs = _add_pns_channel_info(chs, egi_info, ch_names) info['chs'] = chs - info['dig'] = dig + if mon is not None: + info.set_montage(mon, on_missing='ignore') # Add bad channels to info info['description'] = category diff --git a/mne/io/egi/tests/test_egi.py b/mne/io/egi/tests/test_egi.py index 92580718520..2b476cb47ad 100644 --- a/mne/io/egi/tests/test_egi.py +++ b/mne/io/egi/tests/test_egi.py @@ -9,16 +9,16 @@ import shutil import numpy as np -from numpy.testing import assert_array_equal, assert_allclose, assert_equal +from numpy.testing import assert_array_equal, assert_allclose import pytest from scipy import io as sio - from mne import find_events, pick_types from mne.io import read_raw_egi, read_evokeds_mff -from mne.io.tests.test_raw import _test_raw_reader +from mne.io.constants import FIFF from mne.io.egi.egi import _combine_triggers -from mne.utils import run_tests_if_main, requires_version, object_diff +from mne.io.tests.test_raw import _test_raw_reader +from mne.utils import requires_version, object_diff from mne.datasets.testing import data_path, requires_testing_data base_dir = op.join(op.dirname(op.abspath(__file__)), 'data') @@ -115,33 +115,38 @@ def test_io_egi_mff(): test_scaling=False, # XXX probably some bug ) assert raw.info['sfreq'] == 1000. - assert len(raw.info['dig']) == 132 # 128 eeg + 1 ref + 3 cardinal points + # The ref here is redundant, but we don't currently have a way in + # DigMontage to mark that a given channel is actually the ref so... + assert len(raw.info['dig']) == 133 # 129 eeg + 1 ref + 3 cardinal points assert raw.info['dig'][0]['ident'] == 1 # EEG channel E1 - assert raw.info['dig'][128]['ident'] == 129 # Reference channel - ref_loc = raw.info['dig'][128]['r'] - for i in pick_types(raw.info, eeg=True): - assert_equal(raw.info['chs'][i]['loc'][3:6], ref_loc) - - assert_equal('eeg' in raw, True) + assert raw.info['dig'][3]['ident'] == 0 # Reference channel + assert raw.info['dig'][-1]['ident'] == 129 # Reference channel + ref_loc = raw.info['dig'][3]['r'] + eeg_picks = pick_types(raw.info, eeg=True) + assert len(eeg_picks) == 129 + for i in eeg_picks: + loc = raw.info['chs'][i]['loc'] + assert loc[:3].any(), loc[:3] + assert_array_equal(loc[3:6], ref_loc, err_msg=f'{i}') + + assert 'eeg' in raw eeg_chan = [c for c in raw.ch_names if 'EEG' in c] - assert_equal(len(eeg_chan), 129) - picks = pick_types(raw.info, eeg=True) - assert_equal(len(picks), 129) - assert_equal('STI 014' in raw.ch_names, True) + assert len(eeg_chan) == 129 + assert 'STI 014' in raw.ch_names events = find_events(raw, stim_channel='STI 014') - assert_equal(len(events), 8) - assert_equal(np.unique(events[:, 1])[0], 0) - assert (np.unique(events[:, 0])[0] != 0) - assert (np.unique(events[:, 2])[0] != 0) - - pytest.raises(ValueError, read_raw_egi, egi_mff_fname, include=['Foo'], - preload=False) - pytest.raises(ValueError, read_raw_egi, egi_mff_fname, exclude=['Bar'], - preload=False) + assert len(events) == 8 + assert np.unique(events[:, 1])[0] == 0 + assert np.unique(events[:, 0])[0] != 0 + assert np.unique(events[:, 2])[0] != 0 + + with pytest.raises(ValueError, match='Could not find event'): + read_raw_egi(egi_mff_fname, include=['Foo']) + with pytest.raises(ValueError, match='Could not find event'): + read_raw_egi(egi_mff_fname, exclude=['Bar']) for ii, k in enumerate(include, 1): - assert (k in raw.event_id) - assert (raw.event_id[k] == ii) + assert k in raw.event_id + assert raw.event_id[k] == ii def test_io_egi(): @@ -171,19 +176,19 @@ def test_io_egi(): test_scaling=False, # XXX probably some bug ) - assert_equal('eeg' in raw, True) + assert 'eeg' in raw eeg_chan = [c for c in raw.ch_names if c.startswith('E')] - assert_equal(len(eeg_chan), 256) + assert len(eeg_chan) == 256 picks = pick_types(raw.info, eeg=True) - assert_equal(len(picks), 256) - assert_equal('STI 014' in raw.ch_names, True) + assert len(picks) == 256 + assert 'STI 014' in raw.ch_names events = find_events(raw, stim_channel='STI 014') - assert_equal(len(events), 2) # ground truth - assert_equal(np.unique(events[:, 1])[0], 0) - assert (np.unique(events[:, 0])[0] != 0) - assert (np.unique(events[:, 2])[0] != 0) + assert len(events) == 2 # ground truth + assert np.unique(events[:, 1])[0] == 0 + assert np.unique(events[:, 0])[0] != 0 + assert np.unique(events[:, 2])[0] != 0 triggers = np.array([[0, 1, 1, 0], [0, 0, 1, 0]]) # test trigger functionality @@ -208,7 +213,7 @@ def test_io_egi_pns_mff(tmpdir): verbose='error') assert ('RawMff' in repr(raw)) pns_chans = pick_types(raw.info, ecg=True, bio=True, emg=True) - assert_equal(len(pns_chans), 7) + assert len(pns_chans) == 7 names = [raw.ch_names[x] for x in pns_chans] pns_names = ['Resp. Temperature', 'Resp. Pressure', @@ -222,7 +227,7 @@ def test_io_egi_pns_mff(tmpdir): test_rank='less', test_scaling=False, # XXX probably some bug ) - assert_equal(names, pns_names) + assert names == pns_names mat_names = [ 'Resp_Temperature', 'Resp_Pressure', @@ -364,7 +369,7 @@ def test_io_egi_evokeds_mff(idx, cond, tmax, signals, bads): assert evoked_cond.info['nchan'] == 259 assert evoked_cond.info['sfreq'] == 250.0 assert not evoked_cond.info['custom_ref_applied'] - assert len(evoked_cond.info['dig']) == 0 # coordinates.xml missing + assert evoked_cond.info['dig'] is None @requires_version('mffpy', '0.5.7') @@ -384,4 +389,29 @@ def test_read_evokeds_mff_bad_input(): assert str(exc_info.value) == message -run_tests_if_main() +@requires_testing_data +def test_egi_coord_frame(): + """Test that EGI coordinate frame is changed to head.""" + info = read_raw_egi(egi_mff_fname).info + want_idents = ( + FIFF.FIFFV_POINT_LPA, + FIFF.FIFFV_POINT_NASION, + FIFF.FIFFV_POINT_RPA, + ) + for ii, want in enumerate(want_idents): + d = info['dig'][ii] + assert d['kind'] == FIFF.FIFFV_POINT_CARDINAL + assert d['ident'] == want + loc = d['r'] + if ii == 0: + assert 0.05 < -loc[0] < 0.1, 'LPA' + assert_allclose(loc[1:], 0, atol=1e-7, err_msg='LPA') + elif ii == 1: + assert 0.05 < loc[1] < 0.11, 'Nasion' + assert_allclose(loc[::2], 0, atol=1e-7, err_msg='Nasion') + else: + assert ii == 2 + assert 0.05 < loc[0] < 0.1, 'RPA' + assert_allclose(loc[1:], 0, atol=1e-7, err_msg='RPA') + for d in info['dig'][3:]: + assert d['kind'] == FIFF.FIFFV_POINT_EEG diff --git a/mne/io/meas_info.py b/mne/io/meas_info.py index 4cf789ac4fb..153578dc288 100644 --- a/mne/io/meas_info.py +++ b/mne/io/meas_info.py @@ -729,7 +729,7 @@ def _check_consistency(self, prepend_error=''): self['meas_date'].tzinfo is None or self['meas_date'].tzinfo is not datetime.timezone.utc): raise RuntimeError('%sinfo["meas_date"] must be a datetime ' - 'object in UTC or None, got "%r"' + 'object in UTC or None, got %r' % (prepend_error, repr(self['meas_date']),)) chs = [ch['ch_name'] for ch in self['chs']] From 880fbdf8ea5c66ada67faa554a33884bd0b8f048 Mon Sep 17 00:00:00 2001 From: eioe Date: Tue, 6 Apr 2021 22:29:31 +0200 Subject: [PATCH 09/15] ENH: add to_data_frame method to EpochsTFR (#9124) * implement to_df method; missing selection attr * add to_data_frame method * Apply suggestions from code review Co-authored-by: Britta Westner * cosmetics * add test_to_data_frame * fix tests _index and _time_format * fix codespell * fix codespell (I now learned to run flake locally) * add selection, drop_log to EpochsTFR * forward selection and drop_log to EpochsTFR * assign selection and drop_log also in else case * consistency checks * add tests for selection, drop_log, rw epochstfr * doc: add EpochsTFR to params of read_ write_tfrs * add doc string for test_init_EpochsTFR * fix doc str test * move to_data_frame to _BaseTFR for inheritance * extend tests * simplify Co-authored-by: Daniel McCloy * upgrade coding style Co-authored-by: Daniel McCloy * shorten doc string Co-authored-by: Daniel McCloy * shorten doc string Co-authored-by: Daniel McCloy * Apply suggestions from code review Co-authored-by: eioe Co-authored-by: Britta Westner Co-authored-by: Daniel McCloy --- mne/time_frequency/tests/test_tfr.py | 317 +++++++++++++++++++++------ mne/time_frequency/tfr.py | 141 +++++++++++- 2 files changed, 387 insertions(+), 71 deletions(-) diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index 88f206393be..0c2aafbddf9 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -471,16 +471,53 @@ def test_io(): events[:, 0] = np.arange(n_events) events[:, 2] = np.ones(n_events) event_id = {'a/b': 1} + # fake selection + n_dropped_epochs = 3 + selection = np.arange(n_events + n_dropped_epochs)[n_dropped_epochs:] + drop_log = tuple([('IGNORED',) for i in range(n_dropped_epochs)] + + [() for i in range(n_events)]) tfr = EpochsTFR(info, data=data, times=times, freqs=freqs, comment='test', method='crazy-tfr', events=events, - event_id=event_id, metadata=meta) - tfr.save(fname, True) - read_tfr = read_tfrs(fname)[0] - assert_array_equal(tfr.data, read_tfr.data) - assert_metadata_equal(tfr.metadata, read_tfr.metadata) - assert_array_equal(tfr.events, read_tfr.events) - assert tfr.event_id == read_tfr.event_id + event_id=event_id, selection=selection, drop_log=drop_log, + metadata=meta) + fname_save = fname + tfr.save(fname_save, True) + fname_write = op.join(tempdir, 'test3-tfr.h5') + write_tfrs(fname_write, tfr, overwrite=True) + for fname in [fname_save, fname_write]: + read_tfr = read_tfrs(fname)[0] + assert_array_equal(tfr.data, read_tfr.data) + assert_metadata_equal(tfr.metadata, read_tfr.metadata) + assert_array_equal(tfr.events, read_tfr.events) + assert tfr.event_id == read_tfr.event_id + assert_array_equal(tfr.selection, read_tfr.selection) + assert tfr.drop_log == read_tfr.drop_log + with pytest.raises(NotImplementedError, match='condition not supported'): + tfr = read_tfrs(fname, condition='a') + + +def test_init_EpochsTFR(): + """Test __init__ for EpochsTFR.""" + # Create fake data: + data = np.zeros((3, 3, 3, 3)) + times = np.array([.1, .2, .3]) + freqs = np.array([.10, .20, .30]) + info = mne.create_info(['MEG 001', 'MEG 002', 'MEG 003'], 1000., + ['mag', 'mag', 'mag']) + data_x = data[:, :, :, 0] + with pytest.raises(ValueError, match='data should be 4d. Got 3'): + tfr = EpochsTFR(info, data=data_x, times=times, freqs=freqs) + data_x = data[:, :-1, :, :] + with pytest.raises(ValueError, match="channels and data size don't"): + tfr = EpochsTFR(info, data=data_x, times=times, freqs=freqs) + times_x = times[:-1] + with pytest.raises(ValueError, match="times and data size don't match"): + tfr = EpochsTFR(info, data=data, times=times_x, freqs=freqs) + freqs_x = freqs[:-1] + with pytest.raises(ValueError, match="frequencies and data size don't"): + tfr = EpochsTFR(info, data=data, times=times_x, freqs=freqs_x) + del(tfr) def test_plot(): @@ -784,69 +821,219 @@ def test_getitem_epochsTFR(): # Setup for reading the raw data and select a few trials raw = read_raw_fif(raw_fname) events = read_events(event_fname) - n_events = 10 - - # create fake metadata - rng = np.random.RandomState(42) - rt = rng.uniform(size=(n_events,)) - trialtypes = np.array(['face', 'place']) - trial = trialtypes[(rng.uniform(size=(n_events,)) > .5).astype(int)] - meta = DataFrame(dict(RT=rt, Trial=trial)) - event_id = dict(a=1, b=2, c=3, d=4) - epochs = Epochs(raw, events[:n_events], event_id=event_id, metadata=meta, - decim=1) - - freqs = np.arange(12., 17., 2.) # define frequencies of interest - n_cycles = freqs / 2. # 0.5 second time windows for all frequencies - - # Choose time x (full) bandwidth product - time_bandwidth = 4.0 # With 0.5 s time windows, this gives 8 Hz smoothing - kwargs = dict(freqs=freqs, n_cycles=n_cycles, use_fft=True, - time_bandwidth=time_bandwidth, return_itc=False, - average=False, n_jobs=1) - power = tfr_multitaper(epochs, **kwargs) + # create fake data, test with and without dropping epochs + for n_drop_epochs in [0, 2]: + n_events = 12 + # create fake metadata + rng = np.random.RandomState(42) + rt = rng.uniform(size=(n_events,)) + trialtypes = np.array(['face', 'place']) + trial = trialtypes[(rng.uniform(size=(n_events,)) > .5).astype(int)] + meta = DataFrame(dict(RT=rt, Trial=trial)) + event_id = dict(a=1, b=2, c=3, d=4) + epochs = Epochs(raw, events[:n_events], event_id=event_id, + metadata=meta, decim=1) + epochs.drop(np.arange(n_drop_epochs)) + n_events -= n_drop_epochs + + freqs = np.arange(12., 17., 2.) # define frequencies of interest + n_cycles = freqs / 2. # 0.5 second time windows for all frequencies + + # Choose time x (full) bandwidth product + time_bandwidth = 4.0 + # With 0.5 s time windows, this gives 8 Hz smoothing + kwargs = dict(freqs=freqs, n_cycles=n_cycles, use_fft=True, + time_bandwidth=time_bandwidth, return_itc=False, + average=False, n_jobs=1) + power = tfr_multitaper(epochs, **kwargs) + + # Check that power and epochs metadata is the same + assert_metadata_equal(epochs.metadata, power.metadata) + assert_metadata_equal(epochs[::2].metadata, power[::2].metadata) + assert_metadata_equal(epochs['RT < .5'].metadata, + power['RT < .5'].metadata) + assert_array_equal(epochs.selection, power.selection) + assert epochs.drop_log == power.drop_log + + # Check that get power is functioning + assert_array_equal(power[3:6].data, power.data[3:6]) + assert_array_equal(power[3:6].events, power.events[3:6]) + assert_array_equal(epochs.selection[3:6], power.selection[3:6]) + + indx_check = (power.metadata['Trial'] == 'face') + try: + indx_check = indx_check.to_numpy() + except Exception: + pass # older Pandas + indx_check = indx_check.nonzero() + assert_array_equal(power['Trial == "face"'].events, + power.events[indx_check]) + assert_array_equal(power['Trial == "face"'].data, + power.data[indx_check]) + + # Check that the wrong Key generates a Key Error for Metadata search + with pytest.raises(KeyError): + power['Trialz == "place"'] + + # Test length function + assert len(power) == n_events + assert len(power[3:6]) == 3 + + # Test iteration function + for ind, power_ep in enumerate(power): + assert_array_equal(power_ep, power.data[ind]) + if ind == 5: + break + + # Test that current state is maintained + assert_array_equal(power.next(), power.data[ind + 1]) # Check decim affects sfreq power_decim = tfr_multitaper(epochs, decim=2, **kwargs) assert power.info['sfreq'] / 2. == power_decim.info['sfreq'] - # Check that power and epochs metadata is the same - assert_metadata_equal(epochs.metadata, power.metadata) - assert_metadata_equal(epochs[::2].metadata, power[::2].metadata) - assert_metadata_equal(epochs['RT < .5'].metadata, - power['RT < .5'].metadata) - - # Check that get power is functioning - assert_array_equal(power[3:6].data, power.data[3:6]) - assert_array_equal(power[3:6].events, power.events[3:6]) - - indx_check = (power.metadata['Trial'] == 'face') - try: - indx_check = indx_check.to_numpy() - except Exception: - pass # older Pandas - indx_check = indx_check.nonzero() - assert_array_equal(power['Trial == "face"'].events, - power.events[indx_check]) - assert_array_equal(power['Trial == "face"'].data, - power.data[indx_check]) - - # Check that the wrong Key generates a Key Error for Metadata search - with pytest.raises(KeyError): - power['Trialz == "place"'] - - # Test length function - assert len(power) == n_events - assert len(power[3:6]) == 3 - - # Test iteration function - for ind, power_ep in enumerate(power): - assert_array_equal(power_ep, power.data[ind]) - if ind == 5: - break - - # Test that current state is maintained - assert_array_equal(power.next(), power.data[ind + 1]) + +@requires_pandas +def test_to_data_frame(): + """Test EpochsTFR Pandas exporter.""" + # Create fake EpochsTFR data: + n_epos = 3 + ch_names = ['EEG 001', 'EEG 002', 'EEG 003', 'EEG 004'] + n_picks = len(ch_names) + ch_types = ['eeg'] * n_picks + n_freqs = 5 + n_times = 6 + data = np.random.rand(n_epos, n_picks, n_freqs, n_times) + times = np.arange(6) + srate = 1000. + freqs = np.arange(5) + events = np.zeros((n_epos, 3), dtype=int) + events[:, 0] = np.arange(n_epos) + events[:, 2] = np.arange(5, 5 + n_epos) + event_id = {k: v for v, k in zip(events[:, 2], ['ha', 'he', 'hu'])} + info = mne.create_info(ch_names, srate, ch_types) + tfr = mne.time_frequency.EpochsTFR(info, data, times, freqs, + events=events, event_id=event_id) + # test index checking + with pytest.raises(ValueError, match='options. Valid index options are'): + tfr.to_data_frame(index=['foo', 'bar']) + with pytest.raises(ValueError, match='"qux" is not a valid option'): + tfr.to_data_frame(index='qux') + with pytest.raises(TypeError, match='index must be `None` or a string '): + tfr.to_data_frame(index=np.arange(400)) + # test wide format + df_wide = tfr.to_data_frame() + assert all(np.in1d(tfr.ch_names, df_wide.columns)) + assert all(np.in1d(['time', 'condition', 'freq', 'epoch'], + df_wide.columns)) + # test long format + df_long = tfr.to_data_frame(long_format=True) + expected = ('condition', 'epoch', 'freq', 'time', 'channel', 'ch_type', + 'value') + assert set(expected) == set(df_long.columns) + assert set(tfr.ch_names) == set(df_long['channel']) + assert(len(df_long) == tfr.data.size) + # test long format w/ index + df_long = tfr.to_data_frame(long_format=True, index=['freq']) + del df_wide, df_long + # test whether data is in correct shape + df = tfr.to_data_frame(index=['condition', 'epoch', 'freq', 'time']) + data = tfr.data + assert_array_equal(df.values[:, 0], + data[:, 0, :, :].reshape(1, -1).squeeze()) + # compare arbitrary observation: + assert df.loc[('he', slice(None), freqs[1], times[2] * srate), + ch_names[3]].iloc[0] == data[1, 3, 1, 2] + + # Check also for AverageTFR: + tfr = tfr.average() + with pytest.raises(ValueError, match='options. Valid index options are'): + tfr.to_data_frame(index=['epoch', 'condition']) + with pytest.raises(ValueError, match='"epoch" is not a valid option'): + tfr.to_data_frame(index='epoch') + with pytest.raises(TypeError, match='index must be `None` or a string '): + tfr.to_data_frame(index=np.arange(400)) + # test wide format + df_wide = tfr.to_data_frame() + assert all(np.in1d(tfr.ch_names, df_wide.columns)) + assert all(np.in1d(['time', 'freq'], df_wide.columns)) + # test long format + df_long = tfr.to_data_frame(long_format=True) + expected = ('freq', 'time', 'channel', 'ch_type', 'value') + assert set(expected) == set(df_long.columns) + assert set(tfr.ch_names) == set(df_long['channel']) + assert(len(df_long) == tfr.data.size) + # test long format w/ index + df_long = tfr.to_data_frame(long_format=True, index=['freq']) + del df_wide, df_long + # test whether data is in correct shape + df = tfr.to_data_frame(index=['freq', 'time']) + data = tfr.data + assert_array_equal(df.values[:, 0], + data[0, :, :].reshape(1, -1).squeeze()) + # compare arbitrary observation: + assert df.loc[(freqs[1], times[2] * srate), ch_names[3]] == \ + data[3, 1, 2] + + +@requires_pandas +@pytest.mark.parametrize('index', ('time', ['condition', 'time', 'freq'], + ['freq', 'time'], ['time', 'freq'], None)) +def test_to_data_frame_index(index): + """Test index creation in epochs Pandas exporter.""" + # Create fake EpochsTFR data: + n_epos = 3 + ch_names = ['EEG 001', 'EEG 002', 'EEG 003', 'EEG 004'] + n_picks = len(ch_names) + ch_types = ['eeg'] * n_picks + n_freqs = 5 + n_times = 6 + data = np.random.rand(n_epos, n_picks, n_freqs, n_times) + times = np.arange(6) + freqs = np.arange(5) + events = np.zeros((n_epos, 3), dtype=int) + events[:, 0] = np.arange(n_epos) + events[:, 2] = np.arange(5, 8) + event_id = {k: v for v, k in zip(events[:, 2], ['ha', 'he', 'hu'])} + info = mne.create_info(ch_names, 1000., ch_types) + tfr = mne.time_frequency.EpochsTFR(info, data, times, freqs, + events=events, event_id=event_id) + df = tfr.to_data_frame(picks=[0, 2, 3], index=index) + # test index order/hierarchy preservation + if not isinstance(index, list): + index = [index] + assert (df.index.names == index) + # test that non-indexed data were present as columns + non_index = list(set(['condition', 'time', 'freq', 'epoch']) - set(index)) + if len(non_index): + assert all(np.in1d(non_index, df.columns)) + + +@requires_pandas +@pytest.mark.parametrize('time_format', (None, 'ms', 'timedelta')) +def test_to_data_frame_time_format(time_format): + """Test time conversion in epochs Pandas exporter.""" + from pandas import Timedelta + n_epos = 3 + ch_names = ['EEG 001', 'EEG 002', 'EEG 003', 'EEG 004'] + n_picks = len(ch_names) + ch_types = ['eeg'] * n_picks + n_freqs = 5 + n_times = 6 + data = np.random.rand(n_epos, n_picks, n_freqs, n_times) + times = np.arange(6) + freqs = np.arange(5) + events = np.zeros((n_epos, 3), dtype=int) + events[:, 0] = np.arange(n_epos) + events[:, 2] = np.arange(5, 8) + event_id = {k: v for v, k in zip(events[:, 2], ['ha', 'he', 'hu'])} + info = mne.create_info(ch_names, 1000., ch_types) + tfr = mne.time_frequency.EpochsTFR(info, data, times, freqs, + events=events, event_id=event_id) + # test time_format + df = tfr.to_data_frame(time_format=time_format) + dtypes = {None: np.float64, 'ms': np.int64, 'timedelta': Timedelta} + assert isinstance(df['time'].iloc[0], dtypes[time_format]) run_tests_if_main() diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 6060ac3cd95..eaecc035eda 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -24,7 +24,9 @@ sizeof_fmt, GetEpochsMixin, _prepare_read_metadata, fill_doc, _prepare_write_metadata, _check_event_id, _gen_events, SizeMixin, _is_numeric, _check_option, - _validate_type, _check_combine) + _validate_type, _check_combine, _check_pandas_installed, + _check_pandas_index_arguments, _check_time_format, + _convert_times, _build_data_frame) from ..channels.channels import ContainsMixin, UpdateChannelsMixin from ..channels.layout import _merge_ch_data, _pair_grad_sensors from ..io.pick import (pick_info, _picks_to_idx, channel_type, _pick_inst, @@ -657,12 +659,15 @@ def _tfr_aux(method, inst, freqs, decim, return_itc, picks, average, meta = deepcopy(inst._metadata) evs = deepcopy(inst.events) ev_id = deepcopy(inst.event_id) + selection = deepcopy(inst.selection) + drop_log = deepcopy(inst.drop_log) else: # if the input is of class Evoked - meta = evs = ev_id = None + meta = evs = ev_id = selection = drop_log = None out = EpochsTFR(info, power, times, freqs, method='%s-power' % method, - events=evs, event_id=ev_id, metadata=meta) + events=evs, event_id=ev_id, selection=selection, + drop_log=drop_log, metadata=meta) return out @@ -1012,6 +1017,80 @@ def save(self, fname, overwrite=False, *, verbose=None): """ write_tfrs(fname, self, overwrite=overwrite) + @fill_doc + def to_data_frame(self, picks=None, index=None, long_format=False, + time_format='ms'): + """Export data in tabular structure as a pandas DataFrame. + + Channels are converted to columns in the DataFrame. By default, + additional columns ``'time'``, ``'freq'``, ``'epoch'``, and + ``'condition'`` (epoch event description) are added, unless ``index`` + is not ``None`` (in which case the columns specified in ``index`` will + be used to form the DataFrame's index instead). ``'epoch'``, and + ``'condition'`` are not supported for ``AverageTFR``. + + Parameters + ---------- + %(picks_all)s + %(df_index_epo)s + Valid string values are ``'time'``, ``'freq'``, ``'epoch'``, and + ``'condition'`` for ``EpochsTFR`` and ``'time'`` and ``'freq'`` + for ``AverageTFR``. + Defaults to ``None``. + %(df_longform_epo)s + %(df_time_format)s + + .. versionadded:: 0.23 + + Returns + ------- + %(df_return)s + """ + # check pandas once here, instead of in each private utils function + pd = _check_pandas_installed() # noqa + # arg checking + valid_index_args = ['time', 'freq'] + if isinstance(self, EpochsTFR): + valid_index_args.extend(['epoch', 'condition']) + valid_time_formats = ['ms', 'timedelta'] + index = _check_pandas_index_arguments(index, valid_index_args) + time_format = _check_time_format(time_format, valid_time_formats) + # get data + times = self.times + picks = _picks_to_idx(self.info, picks, 'all', exclude=()) + if isinstance(self, EpochsTFR): + data = self.data[:, picks, :, :] + else: + data = self.data[np.newaxis, picks] # add singleton "epochs" axis + n_epochs, n_picks, n_freqs, n_times = data.shape + # reshape to (epochs*freqs*times) x signals + data = np.moveaxis(data, 1, -1) + data = data.reshape(n_epochs * n_freqs * n_times, n_picks) + # prepare extra columns / multiindex + mindex = list() + times = np.tile(times, n_epochs * n_freqs) + times = _convert_times(self, times, time_format) + mindex.append(('time', times)) + freqs = self.freqs + freqs = np.tile(np.repeat(freqs, n_times), n_epochs) + mindex.append(('freq', freqs)) + if isinstance(self, EpochsTFR): + mindex.append(('epoch', np.repeat(self.selection, + n_times * n_freqs))) + rev_event_id = {v: k for k, v in self.event_id.items()} + conditions = [rev_event_id[k] for k in self.events[:, 2]] + mindex.append(('condition', np.repeat(conditions, + n_times * n_freqs))) + assert all(len(mdx) == len(mindex[0]) for mdx in mindex) + # build DataFrame + if isinstance(self, EpochsTFR): + default_index = ['condition', 'epoch', 'freq', 'time'] + else: + default_index = ['freq', 'time'] + df = _build_data_frame(self, data, picks, long_format, mindex, index, + default_index=default_index) + return df + @fill_doc class AverageTFR(_BaseTFR): @@ -2012,6 +2091,16 @@ class EpochsTFR(_BaseTFR, GetEpochsMixin): associated events. If None, all events will be used and a dict is created with string integer names corresponding to the event id integers. + selection : iterable | None + Iterable of indices of selected epochs. If ``None``, will be + automatically generated, corresponding to all non-zero events. + + .. versionadded:: 0.23 + drop_log : tuple | None + Tuple of tuple of strings indicating which epochs have been marked to + be ignored. + + .. versionadded:: 0.23 metadata : instance of pandas.DataFrame | None A :class:`pandas.DataFrame` containing pertinent information for each trial. See :class:`mne.Epochs` for further details. @@ -2037,6 +2126,26 @@ class EpochsTFR(_BaseTFR, GetEpochsMixin): Array containing sample information as event_id event_id : dict | None Names of conditions correspond to event_ids + selection : array + List of indices of selected events (not dropped or ignored etc.). For + example, if the original event array had 4 events and the second event + has been dropped, this attribute would be np.array([0, 2, 3]). + drop_log : tuple of tuple + A tuple of the same length as the event array used to initialize the + ``EpochsTFR`` object. If the i-th original event is still part of the + selection, drop_log[i] will be an empty tuple; otherwise it will be + a tuple of the reasons the event is not longer in the selection, e.g.: + + - ``'IGNORED'`` + If it isn't part of the current subset defined by the user + - ``'NO_DATA'`` or ``'TOO_SHORT'`` + If epoch didn't contain enough data names of channels that + exceeded the amplitude threshold + - ``'EQUALIZED_COUNTS'`` + See :meth:`~mne.Epochs.equalize_event_counts` + - ``'USER'`` + For user-defined reasons (see :meth:`~mne.Epochs.drop`). + metadata : pandas.DataFrame, shape (n_events, n_cols) | None DataFrame containing pertinent information for each trial Notes @@ -2046,7 +2155,8 @@ class EpochsTFR(_BaseTFR, GetEpochsMixin): @verbose def __init__(self, info, data, times, freqs, comment=None, method=None, - events=None, event_id=None, metadata=None, verbose=None): + events=None, event_id=None, selection=None, + drop_log=None, metadata=None, verbose=None): # noqa: D102 self.info = info if data.ndim != 4: @@ -2064,12 +2174,29 @@ def __init__(self, info, data, times, freqs, comment=None, method=None, if events is None: n_epochs = len(data) events = _gen_events(n_epochs) + if selection is None: + n_epochs = len(data) + selection = np.arange(n_epochs) + if drop_log is None: + n_epochs_prerejection = max(len(events), max(selection) + 1) + drop_log = tuple( + () if k in selection else ('IGNORED',) + for k in range(n_epochs_prerejection)) + else: + drop_log = drop_log + # check consistency: + assert len(selection) == len(events) + assert len(drop_log) >= len(events) + assert len(selection) == sum( + (len(dl) == 0 for dl in drop_log)) event_id = _check_event_id(event_id, events) self.data = data self.times = np.array(times, dtype=float) self.freqs = np.array(freqs, dtype=float) self.events = events self.event_id = event_id + self.selection = selection + self.drop_log = drop_log self.comment = comment self.method = method self.preload = True @@ -2298,7 +2425,7 @@ def write_tfrs(fname, tfr, overwrite=False, *, verbose=None): ---------- fname : str The file name, which should end with ``-tfr.h5``. - tfr : AverageTFR instance, or list of AverageTFR instances + tfr : AverageTFR | list of AverageTFR | EpochsTFR The TFR dataset, or list of TFR datasets, to save in one file. Note. If .comment is not None, a name will be generated on the fly, based on the order in which the TFR objects are passed. @@ -2332,6 +2459,8 @@ def _prepare_write_tfr(tfr, condition): elif hasattr(tfr, 'events'): # if EpochsTFR attributes['events'] = tfr.events attributes['event_id'] = tfr.event_id + attributes['selection'] = tfr.selection + attributes['drop_log'] = tfr.drop_log attributes['metadata'] = _prepare_write_metadata(tfr.metadata) return condition, attributes @@ -2349,7 +2478,7 @@ def read_tfrs(fname, condition=None): Returns ------- - tfrs : list of instances of AverageTFR | instance of AverageTFR + tfr : AverageTFR | list of AverageTFR | EpochsTFR Depending on ``condition`` either the TFR object or a list of multiple TFR objects. From 76785fb6992eaaf0d4c4adae48bc3781139f19ee Mon Sep 17 00:00:00 2001 From: vagechirkov Date: Wed, 7 Apr 2021 00:56:41 +0200 Subject: [PATCH 10/15] make plot_20_events_from_raw.py visible for checks --- tutorials/intro/plot_20_events_from_raw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/intro/plot_20_events_from_raw.py b/tutorials/intro/plot_20_events_from_raw.py index dca39323377..af621811b4d 100644 --- a/tutorials/intro/plot_20_events_from_raw.py +++ b/tutorials/intro/plot_20_events_from_raw.py @@ -11,7 +11,7 @@ In the :ref:`introductory tutorial ` we saw an example of reading experimental events from a :term:`"STIM" channel `; here we'll discuss :term:`events` and :term:`annotations` more +channel>`; here we will discuss :term:`events` and :term:`annotations` more broadly, give more detailed information about reading from STIM channels, and give an example of reading events that are in a marker file or included in the data file as an embedded array. The tutorials :ref:`tut-event-arrays` and From c75d225f84c5afc60a5da707f38aed9f68da0407 Mon Sep 17 00:00:00 2001 From: vagechirkov Date: Wed, 7 Apr 2021 16:40:34 +0200 Subject: [PATCH 11/15] Add collapsible sections for Info and BaseRaw --- mne/data/html_templates.py | 136 ++++++++++++++++++++++--------------- mne/io/base.py | 7 +- mne/io/meas_info.py | 12 +++- 3 files changed, 92 insertions(+), 63 deletions(-) diff --git a/mne/data/html_templates.py b/mne/data/html_templates.py index c2e9ac6783a..ae31a394746 100644 --- a/mne/data/html_templates.py +++ b/mne/data/html_templates.py @@ -1,55 +1,31 @@ +import uuid from ..externals.tempita import Template +# style, section_ids=section_ids, sections=sections, +info_template = Template(""" +{{style}} -html_style = """ - -""" - -info_template = Template(html_style + """
Number of events
+ + + + + {{if meas_date is not None}} {{else}}{{endif}} - + {{if info['experimenter'] is not None}} {{else}}{{endif}} + {{if info['subject_info'] is not None}} {{if 'his_id' in info['subject_info'].keys()}} @@ -57,11 +33,14 @@ {{endif}} {{else}}{{endif}} -
+ +
Measurement date{{meas_date}}Unknown
Experimenter{{info['experimenter']}}Unknown
ParticipantUnknown
- - - + + + + + {{if info['dig'] is not None}} @@ -69,53 +48,59 @@ {{endif}} - + - + {{if info['bads'] is not None}} {{else}}{{endif}} - + - + + + + + + - + - + -
+ +
Digitized points{{len(info['dig'])}} pointsNot available
Good channels {{n_mag}} magnetometer, {{n_grad}} gradiometer, and {{n_eeg}} EEG channels
Bad channels{{', '.join(info['bads'])}}None
EOG channels {{eog}}
ECG channels {{ecg}}
+ +
Sampling frequency {{u'%0.2f' % info['sfreq']}} Hz
Highpass {{u'%0.2f' % info['highpass']}} Hz
Lowpass {{u'%0.2f' % info['lowpass']}} Hz
-""") - -raw_template = Template(html_style + """ -{{info_repr[:-9]}} - + {{if filenames is not None}} + Filenames {{', '.join(filenames)}} - + {{endif}} + {{if duration is not None}} + Duration {{duration}} (HH:MM:SS) + {{endif}} """) -epochs_template = Template(html_style + """ +epochs_template = Template(""" @@ -139,3 +124,44 @@
Number of events
""") + + +def _section_style(section_id): + html = f"""#{section_id} ~ table [for="{section_id}"]::before {{ + display: inline-block; + content: "►"; + font-size: 11px; + width: 15px; + text-align: left; + }} + + #{section_id}:checked ~ table [for="{section_id}"]::before {{ + content: "▼"; + }} + + #{section_id} ~ table tr.{section_id} {{ + visibility: collapse; + }} + + #{section_id}:checked ~ table tr.{section_id} {{ + visibility: visible; + }} + """ + return html + + +def collapsible_sections_reprt_html(sections): + style = "" + + for i in ids_: + style += f""" + + """ + + return style, ids_ diff --git a/mne/io/base.py b/mne/io/base.py index d12ba267652..df33b35ba95 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -52,8 +52,6 @@ from ..viz import plot_raw, plot_raw_psd, plot_raw_psd_topo, _RAW_CLIP_DEF from ..event import find_events, concatenate_events from ..annotations import Annotations, _combine_annotations, _sync_onset -from ..data.html_templates import raw_template - class TimeMixin(object): """Class to add sfreq and time_as_index capabilities to certain classes.""" @@ -1709,9 +1707,8 @@ def _repr_html_(self, caption=None): m, s = divmod(self._last_time - self.first_time, 60) h, m = divmod(m, 60) duration = f'{int(h):02d}:{int(m):02d}:{int(s):02d}' - return raw_template.substitute( - info_repr=self.info._repr_html_(caption=caption), - filenames=basenames, duration=duration) + return self.info._repr_html_( + caption=caption, filenames=basenames, duration=duration) def add_events(self, events, stim_channel=None, replace=False): """Add events to stim channel. diff --git a/mne/io/meas_info.py b/mne/io/meas_info.py index 153578dc288..773c189ea8e 100644 --- a/mne/io/meas_info.py +++ b/mne/io/meas_info.py @@ -39,7 +39,8 @@ _dig_kind_rev, _dig_kind_ints, _read_dig_fif) from ._digitization import write_dig as _dig_write_dig from .compensator import get_current_comp -from ..data.html_templates import info_template +from ..data.html_templates import (info_template, + collapsible_sections_reprt_html) b = bytes # alias @@ -809,7 +810,8 @@ def pick_channels(self, ch_names, ordered=False): def ch_names(self): return self['ch_names'] - def _repr_html_(self, caption=None): + def _repr_html_(self, caption=None, filenames=None, + duration=None): if isinstance(caption, str): html = f'

{caption}

' else: @@ -831,9 +833,13 @@ def _repr_html_(self, caption=None): if meas_date is not None: meas_date = meas_date.strftime("%B %d, %Y %H:%M:%S") + ' GMT' + sections = ['General', 'Channels', 'Data'] + style, section_ids = collapsible_sections_reprt_html(sections) html += info_template.substitute( + style=style, section_ids=section_ids, sections=sections, caption=caption, info=self, meas_date=meas_date, n_eeg=n_eeg, - n_grad=n_grad, n_mag=n_mag, eog=eog, ecg=ecg) + n_grad=n_grad, n_mag=n_mag, eog=eog, ecg=ecg, filenames=filenames, + duration=duration) return html From 1cb735c6aef6899a20dcf282d0ad7e660b3c00b5 Mon Sep 17 00:00:00 2001 From: vagechirkov Date: Wed, 7 Apr 2021 16:44:27 +0200 Subject: [PATCH 12/15] fix flack8 style --- mne/io/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mne/io/base.py b/mne/io/base.py index df33b35ba95..36001804429 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -53,6 +53,7 @@ from ..event import find_events, concatenate_events from ..annotations import Annotations, _combine_annotations, _sync_onset + class TimeMixin(object): """Class to add sfreq and time_as_index capabilities to certain classes.""" From b846075cdfa6b3ac30dcf14b8dbd39dba251183e Mon Sep 17 00:00:00 2001 From: vagechirkov Date: Thu, 8 Apr 2021 16:14:06 +0200 Subject: [PATCH 13/15] add colspan for sections' labels --- mne/data/html_templates.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/data/html_templates.py b/mne/data/html_templates.py index ae31a394746..a719db53d6c 100644 --- a/mne/data/html_templates.py +++ b/mne/data/html_templates.py @@ -8,7 +8,7 @@ - @@ -35,7 +35,7 @@ - @@ -69,7 +69,7 @@ - From da534581e73a646c94a7feda0778b955e6d1a441 Mon Sep 17 00:00:00 2001 From: vagechirkov Date: Fri, 9 Apr 2021 14:53:00 +0200 Subject: [PATCH 14/15] fix pydocstyle --- mne/data/html_templates.py | 4 +++- mne/io/meas_info.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mne/data/html_templates.py b/mne/data/html_templates.py index a719db53d6c..f2d0c1ef0b4 100644 --- a/mne/data/html_templates.py +++ b/mne/data/html_templates.py @@ -127,6 +127,7 @@ def _section_style(section_id): + """Set CSS style for all collapsible section.""" html = f"""#{section_id} ~ table [for="{section_id}"]::before {{ display: inline-block; content: "►"; @@ -150,7 +151,8 @@ def _section_style(section_id): return html -def collapsible_sections_reprt_html(sections): +def _collapsible_sections_reprt_html(sections): + """Set style and unique ID for each collapsible section.""" style = "
+
+
+