Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
python: '3.13'
kind: pip
- os: ubuntu-latest
python: '3.13'
python: '3.14'
kind: pip-pre
- os: ubuntu-latest
python: '3.13'
Expand Down
1 change: 1 addition & 0 deletions doc/changes/dev/13579.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix bug with montage test error message checking, by `Eric Larson`_.
2 changes: 2 additions & 0 deletions doc/sphinxext/mne_doc_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ def reset_warnings(gallery_conf, fname):
for message in (
# Matplotlib
".*is non-interactive, and thus cannot.*",
# nilearn
r"You are using the.*matplotlib backend that[.\n]*",
# pybtex
".*pkg_resources is deprecated as an API.*",
):
Expand Down
3 changes: 1 addition & 2 deletions examples/inverse/mixed_source_space_inverse.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
Compute MNE inverse solution on evoked data with a mixed source space
=====================================================================

Create a mixed source space and compute an MNE inverse solution on an
evoked dataset.
Create a mixed source space and compute an MNE inverse solution on an evoked dataset.
"""
# Author: Annalisa Pascarella <[email protected]>
#
Expand Down
4 changes: 2 additions & 2 deletions mne/channels/tests/test_standard_montage.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ def test_set_montage_artinis_basic():
)
new = RawArray(np.random.normal(size=(2, len(raw))), info_new)
raw.add_channels([new], force_update_info=True)
with pytest.raises(ValueError, match="is not in list"):
with pytest.raises(ValueError, match="not in list"):
raw.set_montage("artinis-brite23")

# Detector not in montage: fail
Expand All @@ -302,5 +302,5 @@ def test_set_montage_artinis_basic():
)
new = RawArray(np.random.normal(size=(2, len(raw))), info_new)
raw.add_channels([new], force_update_info=True)
with pytest.raises(ValueError, match="is not in list"):
with pytest.raises(ValueError, match="not in list"):
raw.set_montage("artinis-brite23")
2 changes: 1 addition & 1 deletion mne/report/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -3546,7 +3546,7 @@ def _add_projs(
evoked = None
if isinstance(info, Info): # no-op
pass
elif hasattr(info, "info"): # try to get the file name
elif isinstance(getattr(info, "info", None), Info): # try to get the file name
if isinstance(info, Evoked):
evoked = info
info = info.info
Expand Down
6 changes: 4 additions & 2 deletions tools/install_pre_requirements.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ python -m pip install $STD_ARGS --only-binary ":all:" --default-timeout=60 \
"scipy>=1.15.0.dev0" \
"scikit-learn>=1.6.dev0" \
"pandas>=3.0.0.dev0" \
"dipy>=1.10.0.dev0" \
"tables>=3.10.3.dev0" \
"statsmodels>=0.15.0.dev697" \
"pyarrow>=22.0.0.dev0" \
"matplotlib>=3.11.0.dev0" \
"h5py>=3.13.0"
# Needs https://github.com/dipy/dipy/pull/3678
# "dipy>=1.12.0.dev0"
# Needs https://github.com/PyTables/PyTables/pull/1286
# "tables>=3.10.3.dev0"
echo "::endgroup::"
# No Numba because it forces an old NumPy version

Expand Down
267 changes: 267 additions & 0 deletions tutorials/preprocessing/14_quality_control_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
"""
.. _tut-qc-report:

============================================
Quality control (QC) reports with mne.Report
============================================

Quality control (QC) is the process of systematically inspecting M/EEG data
**throughout all stages of an analysis pipeline**, including raw data,
intermediate preprocessing steps, and derived results.

While QC often begins with an initial inspection of the raw recording,
it is equally important to verify that signals continue to "look reasonable"
after operations such as filtering, artifact correction, epoching, and
averaging. Issues introduced or missed at any stage can propagate downstream
and invalidate later analyses.

This tutorial demonstrates how to create a **single, narrative QC report**
using :class:`mne.Report`, focusing on **what should be inspected and how the
results should be interpreted**, rather than exhaustively covering the API.

For clarity and reproducibility, the examples below focus on common QC checks
applied at representative stages of an analysis pipeline. The same reporting
approach can—and should—be reused whenever new processing steps are applied.

We use the MNE sample dataset for demonstration. Not all QC sections are
applicable to every dataset (e.g., continuous head-position tracking), and
this tutorial explicitly handles such cases.

.. note:: For several additional examples of complete reports, see the
`MNE-BIDS-Pipeline QC reports <https://mne.tools/mne-bids-pipeline/stable/examples/examples.html>`_.
""" # noqa: E501

# Authors: The MNE-Python contributors
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.

# %%

from pathlib import Path

import mne
from mne.preprocessing import ICA, create_eog_epochs

# %%
# Load the sample dataset
# -----------------------
# We load a pre-filtered MEG/EEG recording from the MNE sample dataset.
# Only channels relevant for QC (MEG, EEG, EOG, stimulus) are retained.

data_path = Path(mne.datasets.sample.data_path(verbose=False))
subject = "sample"
sample_dir = data_path / "MEG" / subject
subjects_dir = data_path / "subjects"

raw_path = sample_dir / "sample_audvis_filt-0-40_raw.fif"


raw = mne.io.read_raw(raw_path)

# We will also crop the dataset for speed
raw.crop(0, 60).load_data()

# Retain only channels relevant for QC to simplify visualization and
# focus inspection on signals typically reviewed during data quality checks.
raw.pick(["meg", "eeg", "eog", "stim"])

sfreq = raw.info["sfreq"] # Sampling Frequency (Hz)

# %%
# Create the QC report
# --------------------
# The report acts as a container that collects figures, tables, and text
# into a single HTML document.

report = mne.Report(
title="Sample dataset - Quality Control report",
subject=subject,
subjects_dir=subjects_dir,
)

# %%
# Dataset overview
# ----------------
# A brief overview helps the reviewer immediately understand the scale and
# basic properties of the dataset.

html_overview = """
This report presents a quality control (QC) overview of the MNE sample dataset.<br><br>
For information about the paradigm, see
<a href="https://mne.tools/stable/documentation/datasets.html#sample">the MNE docs</a>.
"""

report.add_html(
title="Overview",
html=html_overview,
tags=("overview"),
)

# %%
# Raw data inspection
# -------------------
# Visual inspection of raw data is the single most important QC step.
# Here we inspect both the time series and the power spectral density (PSD).
#
# - Look for channels with unusually large amplitudes or flat signals.
# - In the PSD, check for excessive low-frequency drift, strong line noise,
# or abnormal spectral shapes compared to neighboring channels.

report.add_raw(
raw,
title="Raw data overview",
psd=False, # omit just for speed here
)

# %%
# Events and stimulus timing
# --------------------------
# Correct event detection is crucial for all subsequent epoch-based analyses.
#
# - Verify that the number of events matches expectations.
# - Check that event timing is plausible and evenly distributed.
# - Missing or duplicated events often indicate trigger channel issues.

events = mne.find_events(raw)

report.add_events(
events,
sfreq=sfreq,
title="Detected events",
)

# %%
# Epoching and rejection statistics
# ---------------------------------
# Epoching allows inspection of data segments time-locked to events, along
# with automated rejection based on amplitude thresholds.

event_id = {
"auditory/left": 1,
"auditory/right": 2,
"visual/left": 3,
"visual/right": 4,
}

epochs = mne.Epochs(
raw,
events,
event_id=event_id,
tmin=-0.2,
tmax=0.5,
baseline=(None, 0),
reject=dict(eeg=150e-6),
preload=True,
)

report.add_epochs(
epochs,
title="Epochs and rejection statistics",
)

# %%
# Evoked responses
# ----------------
# Averaged responses should show physiologically plausible waveforms and
# reasonable signal-to-noise ratios.
#
# - Check that evoked responses have the expected polarity and timing.
# - Absence of clear evoked structure may indicate poor data quality or
# incorrect event definitions.

cov_path = sample_dir / "sample_audvis-cov.fif"
evoked = mne.read_evokeds(
sample_dir / "sample_audvis-ave.fif",
baseline=(None, 0),
)[0] # just one for speed
evoked.decimate(4) # also for speed

report.add_evokeds(
evokeds=evoked,
noise_cov=cov_path,
n_time_points=5,
)


# %%
# ICA for artifact inspection
# ---------------------------
# Independent Component Analysis (ICA) can be used during QC to identify
# stereotypical artifacts such as eye blinks and eye movements.
#
# For QC purposes, ICA is typically run with a lightweight configuration
# (e.g., fewer components or temporal decimation) to provide rapid feedback
# on data quality, rather than an optimized decomposition for final analysis.
#
# - Use the topographic maps to identify spatial patterns characteristic
# of artifacts (e.g., frontal patterns for eye blinks).
# - The component property viewer is intended for detailed inspection of
# individual components and is most informative when combined with
# epoched data or explicit artifact scoring.
# - Components correlated with EOG should show frontal topographies and
# stereotyped time courses.
# - Only components clearly associated with artifacts should be excluded.

ica = ICA(
n_components=15,
random_state=97,
max_iter=50, # just for speed!
)

# Fit ICA using a decimated signal for speed
ica.fit(raw, picks=("meg", "eeg"), decim=10, verbose="error")


# Identify EOG-related components
eog_epochs = create_eog_epochs(raw)
eog_inds, eog_scores = ica.find_bads_eog(eog_epochs)
ica.exclude = eog_inds

report.add_ica(
ica=ica,
inst=epochs,
eog_evoked=eog_epochs.average(),
eog_scores=eog_scores,
title="ICA components (artifact inspection)",
)

# %%
# MEG–MRI coregistration
# ----------------------
# Accurate coregistration is critical for source localization.
#
# - Head shape points should align well with the MRI scalp surface.
# - Systematic misalignment indicates digitization or transformation errors.


trans = sample_dir / "sample_audvis_raw-trans.fif"
report.add_trans(
trans,
info=raw.info,
title="MEG–MRI-head coregistration",
subject=subject,
subjects_dir=subjects_dir,
)

# %%
# MRI and BEM surfaces
# --------------------
# Boundary Element Method (BEM) surfaces define the head model used for
# forward and inverse solutions.
#
# - Surfaces should be smooth, closed, and non-intersecting.
# - Poorly formed surfaces can severely degrade source estimates.

report.add_bem(
subject,
subjects_dir=subjects_dir,
title="BEM surfaces",
decim=20, # for speed
)

# %%
# View the final report
# ---------------------
# You can set ``open_browser=True`` to have it pop open a browser tab if you want:

report.save("qc_report.html", overwrite=True, open_browser=False)
Loading