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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches: [main]
pull_request:
branches: [main]
schedule:
# Keep the trusted documentation dataset cache warm.
- cron: "17 5 * * 1,4"
workflow_dispatch:

concurrency:
Expand All @@ -14,6 +17,7 @@ concurrency:
jobs:
lint:
name: Lint
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -64,6 +68,8 @@ jobs:
docs:
name: Documentation
runs-on: ubuntu-latest
env:
MNE_DATA: ${{ github.workspace }}/.mne-data
permissions:
contents: write
steps:
Expand All @@ -76,10 +82,38 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -e .[docs]
- name: Restore documentation datasets
id: docs-data-cache
uses: actions/cache/restore@v5
with:
path: ${{ env.MNE_DATA }}
key: docs-data-${{ runner.os }}-${{ hashFiles('scripts/prefetch_docs_data.py', 'pyproject.toml') }}
restore-keys: |
docs-data-${{ runner.os }}-
- name: Prefetch and verify documentation datasets
run: python scripts/prefetch_docs_data.py --data-dir "$MNE_DATA"
- name: Save trusted documentation dataset cache
if: >-
steps.docs-data-cache.outputs.cache-hit != 'true' &&
(github.event_name == 'push' ||
github.event_name == 'schedule' ||
github.event_name == 'workflow_dispatch')
uses: actions/cache/save@v5
with:
path: ${{ env.MNE_DATA }}
key: docs-data-${{ runner.os }}-${{ hashFiles('scripts/prefetch_docs_data.py', 'pyproject.toml') }}
- name: Build documentation
run: make -C docs html SPHINXOPTS="-W --keep-going"
- name: Add .nojekyll to the build
run: touch docs/_build/html/.nojekyll
- name: Verify the build produced a complete site
run: |
set -e
test -s docs/_build/html/index.html
test -s docs/_build/html/auto_examples/index.html
test -s docs/_build/html/auto_examples/zapline/plot_02_parameter_tuning.html
test -s docs/_build/html/auto_examples/zapline/plot_03_epoched_data.html
echo "$(find docs/_build/html -name '*.html' | wc -l) HTML pages built"
- name: Upload documentation artifact
uses: actions/upload-artifact@v6
with:
Expand Down
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ Documentation is built with Sphinx and hosted on GitHub Pages.
# Build HTML documentation
make -C docs html

# Populate the real-data cache before a full gallery build
python scripts/prefetch_docs_data.py

# View in browser
open docs/_build/html/index.html # macOS
xdg-open docs/_build/html/index.html # Linux
Expand All @@ -289,6 +292,12 @@ start docs/_build/html/index.html # Windows
- `docs/dss.md` - DSS module guide
- `examples/` - Gallery examples (rendered by sphinx-gallery)

Documentation CI executes the complete gallery. Its MNE Sample, Somato, and
EEGBCI downloads are prefetched and cached using an inventory-derived key; a
twice-weekly trusted build keeps that cache available to pull requests. When
adding a new real dataset to an example, also add it to
`scripts/prefetch_docs_data.py` so failures happen before Sphinx starts.

### Adding Examples

Examples are Python scripts in the `examples/` directory:
Expand Down
4 changes: 4 additions & 0 deletions docs/changes/devel/72.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Made documentation builds resilient without dropping real-data gallery output:
CI now prefetches and caches the complete MNE dataset inventory, ZapLine uses a
maintained MNE recording instead of the unavailable NoiseTools host, and an
incomplete build cannot replace the published site.
3 changes: 3 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@
"../examples/zapline",
"../examples/asr",
"../examples/spectrum_interpolation",
# Keep new example sections buildable until they receive an
# intentional position above this fallback.
"*",
]
),
"within_subsection_order": FileNameSortKey,
Expand Down
66 changes: 16 additions & 50 deletions examples/zapline/plot_02_parameter_tuning.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
=========================================

This example shows how the main ZapLine tuning parameters change the cleaning
behavior on synthetic data, then applies the same workflow to a real NoiseTools
MEG recording.
behavior on synthetic data, then applies the same workflow to the real MNE
Sample MEG recording.

Authors: Sina Esmaeili (sina.esmaeili@umontreal.ca)
Hamza Abdelhedi (hamza.abdelhedi@umontreal.ca)
Expand All @@ -13,14 +13,10 @@
# %%
# Imports
# -------
from pathlib import Path
from urllib.request import urlretrieve

import matplotlib.pyplot as plt
import mne
import numpy as np
from scipy import signal
from scipy.io import loadmat

from mne_denoise.viz import (
plot_component_cleaning_summary,
Expand All @@ -30,40 +26,6 @@
)
from mne_denoise.zapline import ZapLine

NOISETOOLS_BASE_URL = "http://audition.ens.fr/adc/NoiseTools/DATA"


def _find_repo_root():
"""Return the repository root for this example."""
starts = []
if "__file__" in globals():
starts.append(Path(__file__).resolve())
starts.append(Path.cwd().resolve())

for start in starts:
current = start if start.is_dir() else start.parent
for candidate in (current, *current.parents):
mne_ok = (candidate / "mne_denoise").exists()
ex_ok = (candidate / "examples").exists()
if mne_ok and ex_ok:
return candidate

raise FileNotFoundError("Could not locate the repository root.")


def _load_or_fetch_data_file(name):
"""Return one ZapLine example data file, downloading it if needed."""
path = _find_repo_root() / "examples" / "zapline" / "data" / name
if path.exists():
return path

path.parent.mkdir(parents=True, exist_ok=True)
url = f"{NOISETOOLS_BASE_URL}/{name}"
print(f"Downloading {name} to {path}...")
urlretrieve(url, str(path))
return path


# %%
# Part 1: n_remove Parameter
# --------------------------
Expand Down Expand Up @@ -243,24 +205,28 @@ def _load_or_fetch_data_file(name):
plt.show()

# %%
# Part 4: Real MEG Data (NoiseTools)
# Part 4: Real MEG Data (MNE Sample)
# ----------------------------------
# Apply ZapLine to real MEG data from NoiseTools dataset.
# The file is cached under ``examples/zapline/data`` after the first run.
# Apply ZapLine to a real MEG recording managed by MNE's dataset fetcher. MNE
# verifies the dataset archive and reuses its local copy across examples.

print("\nPart 4: Real MEG Data")

# Load data1.mat (MEG with large near-DC fluctuations)
data1_path = _load_or_fetch_data_file("data1.mat")
mat = loadmat(str(data1_path))
meg_data = mat["data"].T # Transpose to (channels, times)
sfreq_meg = float(mat["sr"].flatten()[0])
# Load 30 seconds of gradiometer data and resample to keep the gallery build
# compact. The MNE Sample recording was acquired in a 60 Hz mains environment.
sample_path = mne.datasets.sample.data_path()
raw_path = sample_path / "MEG" / "sample" / "sample_audvis_raw.fif"
raw_meg = mne.io.read_raw_fif(raw_path, preload=False, verbose="ERROR")
grad_picks = mne.pick_types(raw_meg.info, meg="grad", exclude="bads")
raw_meg.pick(grad_picks).crop(0, 30).load_data().resample(300, verbose="ERROR")
meg_data = raw_meg.get_data() * 1e12 # Constant scale for readable values
sfreq_meg = raw_meg.info["sfreq"]

# Demean
meg_data = meg_data - np.mean(meg_data, axis=1, keepdims=True)

print(f"Loaded data1.mat: {meg_data.shape}, sfreq={sfreq_meg} Hz")
print("MEG data with large near-DC fluctuations")
print(f"Loaded MNE Sample data: {meg_data.shape}, sfreq={sfreq_meg} Hz")
print("Real gradiometer data with 60 Hz line interference")

# %%
# Apply ZapLine to MEG Data
Expand Down
Loading
Loading