@@ -561,6 +561,9 @@ def _lite_src(folder, rel):
561561 "SSS/ct_sparse_mgh.fif" ,
562562 "subjects/sample/mri/T1.mgz" ,
563563 "subjects/sample/mri/aseg.mgz" ,
564+ # read_talxfm builds this path itself, so nothing in the tutorials
565+ # names it; plot_alignment needs it to estimate MRI fiducials
566+ "subjects/sample/mri/transforms/talairach.xfm" ,
564567 "subjects/sample/bem/sample-oct-6-src.fif" ,
565568 # Head and skull surfaces for plot_alignment. outer_skin.surf is what
566569 # MNE picks first, so serving it makes the browser figure match the
@@ -589,6 +592,12 @@ def _lite_src(folder, rel):
589592 "subjects/sample/surf/lh.curv" ,
590593 "subjects/sample/label/lh.aparc.annot" ,
591594 "subjects/sample/label/rh.aparc.annot" ,
595+ # the auditory/visual ROIs; about nine notebooks build these names with
596+ # an f-string, so a scan of the tutorial text never sees them
597+ "MEG/sample/labels/Aud-lh.label" ,
598+ "MEG/sample/labels/Aud-rh.label" ,
599+ "MEG/sample/labels/Vis-lh.label" ,
600+ "MEG/sample/labels/Vis-rh.label" ,
592601 ]
593602 for req in required_files :
594603 s = _lite_src ("MNE-sample-data" , req )
@@ -677,6 +686,10 @@ def _lite_copy_tree(folder, rel_dir):
677686 for f in sorted (src .rglob ("*" )):
678687 if not f .is_file ():
679688 continue
689+ # zero-byte members (an .mff carries a couple of lock files) do not
690+ # survive the artifact upload, so listing them only yields a 404
691+ if f .stat ().st_size == 0 :
692+ continue
680693 size_mb = f .stat ().st_size / 1e6
681694 if size_mb > LITE_MAX_FILE_MB :
682695 print (f"[JupyterLite] SKIPPED { folder } /{ rel_dir } ({ size_mb :.1f} MB)" )
@@ -753,10 +766,17 @@ def _lite_copy_tree(folder, rel_dir):
753766 ("mTRF_1.5" , ["speech_data.mat" ]),
754767 (
755768 "MNE-eegbci-data" ,
769+ # exactly the runs tools/circleci_download.sh fetches: subject 1 runs
770+ # 3/6/10/14 and run 3 for subjects 2-4. Notebooks wanting run 1 or 2 are
771+ # excluded instead, since that data never reaches the CI box.
756772 [
773+ "files/eegmmidb/1.0.0/S001/S001R03.edf" ,
757774 "files/eegmmidb/1.0.0/S001/S001R06.edf" ,
758775 "files/eegmmidb/1.0.0/S001/S001R10.edf" ,
759776 "files/eegmmidb/1.0.0/S001/S001R14.edf" ,
777+ "files/eegmmidb/1.0.0/S002/S002R03.edf" ,
778+ "files/eegmmidb/1.0.0/S003/S003R03.edf" ,
779+ "files/eegmmidb/1.0.0/S004/S004R03.edf" ,
760780 ],
761781 ),
762782):
@@ -799,7 +819,8 @@ def _lite_copy_tree(folder, rel_dir):
799819 "# matplotlib/scipy/numpy are older than MNE's declared minimums.\n "
800820 "await piplite.install(\n "
801821 " ['mne', 'scikit-learn', 'joblib', 'pandas', 'seaborn', "
802- "'mne-connectivity', 'nibabel', 'pyvista-js', 'pyxdf'],\n "
822+ "'mne-connectivity', 'nibabel', 'pyvista-js', 'pyxdf', 'mffpy', "
823+ "'python-picard'],\n "
803824 " keep_going=True,\n "
804825 ")\n "
805826 "\n "
@@ -1090,6 +1111,48 @@ def _lite_copy_tree(folder, rel_dir):
10901111 " _lite_fetch_if_under_mne_data(fname), *_a, **_kw\n "
10911112 " )\n "
10921113 "mne.read_source_spaces = _lite_read_source_spaces\n "
1114+ "# Nearly every MNE reader validates its filename through\n "
1115+ "# _check_fname(must_exist=True) before opening it, so hooking that one\n "
1116+ "# function covers read_info, read_evokeds, read_cov, read_label and the\n "
1117+ "# rest without a wrapper each. Failures stay silent here so MNE still\n "
1118+ "# raises its own, clearer error for a file that genuinely is missing.\n "
1119+ "import mne.utils.check as _mne_check\n "
1120+ "_orig_check_fname = _mne_check._check_fname\n "
1121+ "def _lite_check_fname(fname, overwrite=False, must_exist=False,\n "
1122+ " *_a, **_kw):\n "
1123+ " if must_exist:\n "
1124+ " try:\n "
1125+ " _lite_fetch_if_under_mne_data(fname)\n "
1126+ " except Exception:\n "
1127+ " pass\n "
1128+ " return _orig_check_fname(fname, overwrite, must_exist, *_a, **_kw)\n "
1129+ "_mne_check._check_fname = _lite_check_fname\n "
1130+ "# modules that imported it before now hold their own reference; ones\n "
1131+ "# loaded later (mne lazy-loads most of itself) pick up the patch\n "
1132+ "for _m in list(sys.modules.values()):\n "
1133+ " if (getattr(_m, '__name__', '').startswith('mne')\n "
1134+ " and getattr(_m, '_check_fname', None) is _orig_check_fname):\n "
1135+ " _m._check_fname = _lite_check_fname\n "
1136+ "# read_label, read_epochs and read_raw_edf open their file directly\n "
1137+ "# rather than validating it first, so the hook above never sees them\n "
1138+ "_orig_read_label = mne.read_label\n "
1139+ "def _lite_read_label(filename, *_a, **_kw):\n "
1140+ " return _orig_read_label(\n "
1141+ " _lite_fetch_if_under_mne_data(filename), *_a, **_kw\n "
1142+ " )\n "
1143+ "mne.read_label = _lite_read_label\n "
1144+ "_orig_read_epochs = mne.read_epochs\n "
1145+ "def _lite_read_epochs(fname, *_a, **_kw):\n "
1146+ " return _orig_read_epochs(\n "
1147+ " _lite_fetch_if_under_mne_data(fname), *_a, **_kw\n "
1148+ " )\n "
1149+ "mne.read_epochs = _lite_read_epochs\n "
1150+ "_orig_read_raw_edf = mne.io.read_raw_edf\n "
1151+ "def _lite_read_raw_edf(input_fname, *_a, **_kw):\n "
1152+ " return _orig_read_raw_edf(\n "
1153+ " _lite_fetch_if_under_mne_data(input_fname), *_a, **_kw\n "
1154+ " )\n "
1155+ "mne.io.read_raw_edf = _lite_read_raw_edf\n "
10931156 "_orig_read_bem_solution = mne.read_bem_solution\n "
10941157 "def _lite_read_bem_solution(fname, *_a, **_kw):\n "
10951158 " return _orig_read_bem_solution(\n "
@@ -1121,7 +1184,12 @@ def _lite_copy_tree(folder, rel_dir):
11211184 " with open(_manifest) as _fh:\n "
11221185 " _names = [_n.strip() for _n in _fh if _n.strip()]\n "
11231186 " for _name in _names:\n "
1124- " _lite_fetch_rel(_rel + '/' + _name)\n "
1187+ " # one unreachable member must not abandon the rest of the\n "
1188+ " # recording; the reader complains if it needed that file\n "
1189+ " try:\n "
1190+ " _lite_fetch_rel(_rel + '/' + _name)\n "
1191+ " except Exception as _e:\n "
1192+ " print('[JupyterLite] skipped ' + _name + ': ' + repr(_e))\n "
11251193 " return mne_data_path + '/' + _rel\n "
11261194 "def _lite_dir_reader(_orig):\n "
11271195 " def _read(fname, *_a, **_kw):\n "
@@ -1136,36 +1204,6 @@ def _lite_copy_tree(folder, rel_dir):
11361204 " return _read\n "
11371205 "mne.io.read_raw_nirx = _lite_dir_reader(mne.io.read_raw_nirx)\n "
11381206 "mne.io.read_raw_egi = _lite_dir_reader(mne.io.read_raw_egi)\n "
1139- "# mne.Report writes an HTML file and opens a browser, neither of which\n "
1140- "# means anything here. Writes to a temp dir do work in Pyodide's\n "
1141- "# in-memory filesystem, so save there, read the HTML back and show it\n "
1142- "# in an isolated iframe -- the report's own CSS/JS then cannot clash\n "
1143- "# with the notebook page.\n "
1144- "_orig_report_save = mne.Report.save\n "
1145- "def _lite_report_save(self, fname=None, *_a, **_kw):\n "
1146- " # only the HTML form is previewed; an .hdf5 round-trip still has to\n "
1147- " # write the file the caller asked for\n "
1148- " if fname is not None and str(fname).lower().endswith(('.h5', '.hdf5')):\n "
1149- " return _orig_report_save(self, fname, *_a, **_kw)\n "
1150- " try:\n "
1151- " import html as _htmllib\n "
1152- " import tempfile\n "
1153- " from IPython.display import HTML as _HTML, display as _display\n "
1154- " _tmp = os.path.join(tempfile.mkdtemp(), 'report.html')\n "
1155- " _orig_report_save(self, _tmp, open_browser=False, overwrite=True)\n "
1156- " with open(_tmp, encoding='utf-8') as _fh:\n "
1157- " _doc = _fh.read()\n "
1158- " # wrap in a div so the payload does not start with '<iframe',\n "
1159- " # which trips IPython's 'use IPython.display.IFrame' warning\n "
1160- " _display(_HTML(\n "
1161- " '<div><iframe srcdoc=\" ' + _htmllib.escape(_doc) + '\" '\n "
1162- ' \' width="100%" height="500" \' \n '
1163- " 'style=\" border:1px solid #ccc;\" ></iframe></div>'\n "
1164- " ))\n "
1165- " except Exception as _e:\n "
1166- " print('(report preview unavailable: ' + repr(_e) + ')')\n "
1167- " return fname\n "
1168- "mne.Report.save = _lite_report_save\n "
11691207 "# the logging tutorial reads a KIT file from inside the installed\n "
11701208 "# package; the wheel excludes mne/**/tests, so stage the served copy\n "
11711209 "# into the path the tutorial builds rather than editing the tutorial\n "
@@ -1857,12 +1895,19 @@ def _lite_copy_tree(folder, rel_dir):
18571895 # copy step and the badge would have nothing to load.
18581896 "examples/datasets/kernel_phantom.py" ,
18591897 "examples/io/elekta_epochs.py" ,
1860- # Report renders its forward/inverse/source-estimate sections through the
1861- # Brain screenshot path, which the browser renderer has no equivalent for,
1862- # and three sections round-trip the report through HDF5. The parts that do
1863- # work are not worth a badge that half-renders, so the whole page is hidden
1864- # (mne.Report.save itself still previews inline -- see the setup cell).
1898+ # These want EEGBCI runs 1 and 2, which tools/circleci_download.sh never
1899+ # fetches (it takes subject 1 runs 3/6/10/14 and run 3 for subjects 2-4),
1900+ # so the data is not on the machine that builds the docs. eeg_bridging
1901+ # alone would need run 1 for ten subjects.
1902+ "examples/visualization/onionskin.py" ,
1903+ "examples/preprocessing/muscle_ica.py" ,
1904+ "examples/preprocessing/eeg_bridging.py" ,
1905+ # Both Report tutorials build their figures by screenshotting a 3D scene
1906+ # (Report._itv calls backend._take_3d_screenshot), and vtk.js cannot hand a
1907+ # framebuffer back to Python, so those sections would embed blank images.
1908+ # 70_report additionally round-trips a report through HDF5.
18651909 "tutorials/intro/70_report.py" ,
1910+ "tutorials/preprocessing/14_quality_control_report.py" ,
18661911)
18671912
18681913import sphinx_gallery .gen_rst as _sg_gen_rst # noqa: E402
0 commit comments