From 35a6c93c980a88b55b7556557e464480ff80cd7c Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Fri, 29 May 2026 15:47:10 +0100 Subject: [PATCH 1/2] Add PyScript examples for mne Generated by apply_llm_response.py from prompts/mne/response.toml. Examples included: - raw_eeg_basics: Synthetic EEG with RawArray - filtering_and_psd: Filtering and power spectra - events_and_epochs: Events, epochs, and evoked responses Generated-By: apply_llm_response.py --- examples/mne/README.md | 18 ++++ examples/mne/events_and_epochs/code.py | 98 ++++++++++++++++++++++ examples/mne/events_and_epochs/config.toml | 1 + examples/mne/events_and_epochs/setup.py | 28 +++++++ examples/mne/filtering_and_psd/code.py | 75 +++++++++++++++++ examples/mne/filtering_and_psd/config.toml | 1 + examples/mne/filtering_and_psd/setup.py | 29 +++++++ examples/mne/order.json | 5 ++ examples/mne/raw_eeg_basics/code.py | 65 ++++++++++++++ examples/mne/raw_eeg_basics/config.toml | 1 + examples/mne/raw_eeg_basics/setup.py | 51 +++++++++++ 11 files changed, 372 insertions(+) create mode 100644 examples/mne/README.md create mode 100644 examples/mne/events_and_epochs/code.py create mode 100644 examples/mne/events_and_epochs/config.toml create mode 100644 examples/mne/events_and_epochs/setup.py create mode 100644 examples/mne/filtering_and_psd/code.py create mode 100644 examples/mne/filtering_and_psd/config.toml create mode 100644 examples/mne/filtering_and_psd/setup.py create mode 100644 examples/mne/order.json create mode 100644 examples/mne/raw_eeg_basics/code.py create mode 100644 examples/mne/raw_eeg_basics/config.toml create mode 100644 examples/mne/raw_eeg_basics/setup.py diff --git a/examples/mne/README.md b/examples/mne/README.md new file mode 100644 index 0000000..41eaf29 --- /dev/null +++ b/examples/mne/README.md @@ -0,0 +1,18 @@ +# mne Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/mne/events_and_epochs/code.py b/examples/mne/events_and_epochs/code.py new file mode 100644 index 0000000..6e4258d --- /dev/null +++ b/examples/mne/events_and_epochs/code.py @@ -0,0 +1,98 @@ +# --------------------------------------------------------------------- +# Cutting continuous data into epochs around stimulus events and +# averaging them into an evoked response. +# --------------------------------------------------------------------- + +heading("From continuous data to evoked responses") +note( + "Most M/EEG analysis revolves around three structures: Raw " + "(continuous), Epochs (segments around events), and Evoked " + "(epoch averages). Here we simulate an experiment with two " + "conditions and walk through that pipeline." +) + +# Build continuous data with a brief positive deflection 200 ms after +# each 'target' event and a smaller deflection after each 'standard'. +sampling_freq = 500.0 +duration_s = 60.0 +n_samples = int(sampling_freq * duration_s) + +# One event every ~2 seconds, alternating standard/target. +event_samples = np.arange(500, n_samples - 500, 1000) +event_codes = np.where(np.arange(len(event_samples)) % 2 == 0, 1, 2) + +def response_kernel(amplitude): + # 300 ms Gaussian bump, peak around sample 100 (= 200 ms post event). + t = np.arange(150) + return amplitude * np.exp(-((t - 100) ** 2) / (2 * 20 ** 2)) + +signal = rng.normal(0, 0.3, size=n_samples) +for sample, code in zip(event_samples, event_codes): + amp = 2.0 if code == 2 else 0.6 # targets evoke a bigger response + end = sample + 150 + signal[sample:end] += response_kernel(amp) + +# In MNE, an events array has shape (n_events, 3): sample, prev id, id. +events = np.column_stack([ + event_samples, + np.zeros_like(event_samples), + event_codes, +]).astype(int) +event_id = {"standard": 1, "target": 2} + +info = mne.create_info(ch_names=["Cz"], sfreq=sampling_freq, ch_types="eeg") +raw = mne.io.RawArray(signal[np.newaxis, :] * 1e-6, info) + +note(f"Generated {len(events)} events ({np.sum(event_codes == 2)} targets).") + +# `Epochs` slices Raw into windows aligned to events. `tmin`/`tmax` are +# relative to each event (in seconds). `baseline` subtracts the mean of +# the pre-stimulus interval from each epoch. +epochs = mne.Epochs( + raw, + events, + event_id=event_id, + tmin=-0.1, + tmax=0.5, + baseline=(-0.1, 0.0), + preload=True, +) + +note("Epochs object summary:") +display(HTML(f"
{epochs}
"), append=True) + +# Indexing by condition name returns the matching subset; `.average()` +# collapses across trials to produce an Evoked response. +evoked_standard = epochs["standard"].average() +evoked_target = epochs["target"].average() + +heading("Evoked waveforms") +note( + "Averaging across many noisy trials reveals the stereotyped " + "response. Targets show a clear peak ~200 ms after the event; " + "standards show a smaller one." +) + +fig, ax = plt.subplots(figsize=(9, 4)) +ax.plot( + evoked_standard.times * 1000, + evoked_standard.data[0] * 1e6, + label="Standard", + color="steelblue", + linewidth=2, +) +ax.plot( + evoked_target.times * 1000, + evoked_target.data[0] * 1e6, + label="Target", + color="crimson", + linewidth=2, +) +ax.axvline(0, color="black", linestyle="--", linewidth=0.8) +ax.axhline(0, color="gray", linewidth=0.5) +ax.set_xlabel("Time relative to event (ms)") +ax.set_ylabel("Amplitude (µV)") +ax.set_title("Average evoked response at Cz") +ax.legend() +fig.tight_layout() +display(fig, append=True) diff --git a/examples/mne/events_and_epochs/config.toml b/examples/mne/events_and_epochs/config.toml new file mode 100644 index 0000000..2e1a863 --- /dev/null +++ b/examples/mne/events_and_epochs/config.toml @@ -0,0 +1 @@ +packages = ["mne", "numpy", "matplotlib"] diff --git a/examples/mne/events_and_epochs/setup.py b/examples/mne/events_and_epochs/setup.py new file mode 100644 index 0000000..31d5ca6 --- /dev/null +++ b/examples/mne/events_and_epochs/setup.py @@ -0,0 +1,28 @@ +"""Setup for the epoching example.""" + +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import numpy as np +import matplotlib.pyplot as plt +import mne + +mne.set_log_level("WARNING") +rng = np.random.default_rng(7) diff --git a/examples/mne/filtering_and_psd/code.py b/examples/mne/filtering_and_psd/code.py new file mode 100644 index 0000000..c19d5cd --- /dev/null +++ b/examples/mne/filtering_and_psd/code.py @@ -0,0 +1,75 @@ +# --------------------------------------------------------------------- +# Filtering a Raw object and inspecting its frequency content. +# --------------------------------------------------------------------- + +heading("Cleaning a noisy signal with band-pass filtering") +note( + "We'll build a single-channel Raw with a 10 Hz alpha rhythm, " + "a 50 Hz line-noise contaminant, and slow drift. Then we apply " + "a band-pass filter and compare power spectra before and after." +) + +sampling_freq = 500.0 +duration_s = 30.0 +n_samples = int(sampling_freq * duration_s) +times = np.arange(n_samples) / sampling_freq + +alpha = 1.0 * np.sin(2 * np.pi * 10.0 * times) +line_noise = 0.8 * np.sin(2 * np.pi * 50.0 * times) +slow_drift = 1.2 * np.sin(2 * np.pi * 0.3 * times) +white_noise = rng.normal(0, 0.5, size=n_samples) + +# Combined signal in microvolt-range, scaled to volts for MNE. +signal = (alpha + line_noise + slow_drift + white_noise) * 1e-6 + +info = mne.create_info(ch_names=["Oz"], sfreq=sampling_freq, ch_types="eeg") +raw = mne.io.RawArray(signal[np.newaxis, :], info) + +# `copy()` keeps the original around so we can compare. `filter` is +# an in-place band-pass between 1 Hz and 40 Hz, removing both slow +# drift and the 50 Hz line noise. +raw_filtered = raw.copy().filter(l_freq=1.0, h_freq=40.0) + +heading("Comparing the time series") +note("First two seconds before and after filtering:") + +n_show = int(2 * sampling_freq) +raw_data, raw_times = raw[0, :n_show] +filt_data, _ = raw_filtered[0, :n_show] + +fig, axes = plt.subplots(2, 1, figsize=(9, 5), sharex=True) +axes[0].plot(raw_times, raw_data[0] * 1e6, color="gray") +axes[0].set_title("Raw signal (µV)") +axes[1].plot(raw_times, filt_data[0] * 1e6, color="darkgreen") +axes[1].set_title("Band-pass 1-40 Hz (µV)") +axes[1].set_xlabel("Time (s)") +fig.tight_layout() +display(fig, append=True) + +heading("Power spectral density") +note( + "MNE's `compute_psd` uses Welch's method by default. The alpha " + "peak around 10 Hz survives filtering, while the 50 Hz line and " + "low-frequency drift are strongly attenuated." +) + +psd_raw = raw.compute_psd(fmin=0.1, fmax=80.0) +psd_filt = raw_filtered.compute_psd(fmin=0.1, fmax=80.0) + +freqs = psd_raw.freqs +power_raw = psd_raw.get_data()[0] +power_filt = psd_filt.get_data()[0] + +fig, ax = plt.subplots(figsize=(9, 4)) +ax.semilogy(freqs, power_raw, color="gray", label="Raw") +ax.semilogy(freqs, power_filt, color="darkgreen", label="Filtered") +ax.axvline(10, color="steelblue", linestyle="--", linewidth=1, + label="Alpha (10 Hz)") +ax.axvline(50, color="crimson", linestyle="--", linewidth=1, + label="Line (50 Hz)") +ax.set_xlabel("Frequency (Hz)") +ax.set_ylabel("Power (V²/Hz)") +ax.set_title("Power spectral density") +ax.legend() +fig.tight_layout() +display(fig, append=True) diff --git a/examples/mne/filtering_and_psd/config.toml b/examples/mne/filtering_and_psd/config.toml new file mode 100644 index 0000000..2e1a863 --- /dev/null +++ b/examples/mne/filtering_and_psd/config.toml @@ -0,0 +1 @@ +packages = ["mne", "numpy", "matplotlib"] diff --git a/examples/mne/filtering_and_psd/setup.py b/examples/mne/filtering_and_psd/setup.py new file mode 100644 index 0000000..bc457f1 --- /dev/null +++ b/examples/mne/filtering_and_psd/setup.py @@ -0,0 +1,29 @@ +"""Setup for the filtering example. Mirrors the names established in +the first example without re-registering the IPython shim.""" + +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import numpy as np +import matplotlib.pyplot as plt +import mne + +mne.set_log_level("WARNING") +rng = np.random.default_rng(7) diff --git a/examples/mne/order.json b/examples/mne/order.json new file mode 100644 index 0000000..c6bac16 --- /dev/null +++ b/examples/mne/order.json @@ -0,0 +1,5 @@ +[ + "raw_eeg_basics", + "filtering_and_psd", + "events_and_epochs" +] diff --git a/examples/mne/raw_eeg_basics/code.py b/examples/mne/raw_eeg_basics/code.py new file mode 100644 index 0000000..295cab9 --- /dev/null +++ b/examples/mne/raw_eeg_basics/code.py @@ -0,0 +1,65 @@ +""" +A first look at MNE-Python: build a synthetic multi-channel EEG +recording, wrap it in an MNE Raw object, and inspect it. + +MNE-Python (https://mne.tools/) is the de facto toolkit for +M/EEG analysis. Its central data structures are `Info` (metadata +about channels and sampling) and `Raw` (continuous time series). +Here we build both from scratch using NumPy arrays. +""" +from IPython.core.display import display, HTML + +heading("1. Building a synthetic EEG recording") +note( + "We'll simulate 60 seconds of data on five EEG channels at " + "250 Hz: a noisy background with an alpha-band (10 Hz) " + "oscillation that is stronger over the occipital channels." +) + +sampling_freq = 250.0 # Hz +duration_s = 60.0 +n_samples = int(sampling_freq * duration_s) +times = np.arange(n_samples) / sampling_freq + +channel_names = ["Fz", "Cz", "Pz", "O1", "O2"] +# Occipital channels (O1, O2) get a stronger 10 Hz alpha rhythm. +alpha_gain = np.array([0.2, 0.4, 0.8, 1.5, 1.5]) + +alpha = np.sin(2 * np.pi * 10.0 * times) +background = rng.normal(0, 1.0, size=(len(channel_names), n_samples)) +# MNE expects EEG signals in volts; scale to microvolt-range. +data = (alpha_gain[:, None] * alpha + background) * 1e-6 + +# `create_info` describes the channels; combined with the data array +# it yields a Raw object that behaves like a real recording. +info = mne.create_info( + ch_names=channel_names, + sfreq=sampling_freq, + ch_types="eeg", +) +raw = mne.io.RawArray(data, info) + +note("The Raw object summarizes the recording:") +display(HTML(f"
{raw}
"), append=True) +display(HTML(f"
{raw.info}
"), append=True) + +heading("2. Plotting the first few seconds") +note("Pulling out the first 3 seconds and plotting each channel.") + +window_data, window_times = raw[:, : int(3 * sampling_freq)] + +fig, ax = plt.subplots(figsize=(9, 4)) +offset = 6e-6 # vertical spacing between channels, in volts +for i, name in enumerate(channel_names): + ax.plot( + window_times, + window_data[i] + i * offset, + linewidth=0.8, + label=name, + ) +ax.set_yticks([i * offset for i in range(len(channel_names))]) +ax.set_yticklabels(channel_names) +ax.set_xlabel("Time (s)") +ax.set_title("Synthetic EEG: first 3 seconds") +fig.tight_layout() +display(fig, append=True) diff --git a/examples/mne/raw_eeg_basics/config.toml b/examples/mne/raw_eeg_basics/config.toml new file mode 100644 index 0000000..2e1a863 --- /dev/null +++ b/examples/mne/raw_eeg_basics/config.toml @@ -0,0 +1 @@ +packages = ["mne", "numpy", "matplotlib"] diff --git a/examples/mne/raw_eeg_basics/setup.py b/examples/mne/raw_eeg_basics/setup.py new file mode 100644 index 0000000..d379915 --- /dev/null +++ b/examples/mne/raw_eeg_basics/setup.py @@ -0,0 +1,51 @@ +""" +Shim IPython's display API onto PyScript so example code written in a +Jupyter/IPython idiom runs unmodified in the browser. +""" + +import sys +import types +import js +from pyscript import window, HTML, display as _display + +js.alert = window.alert + + +def display(*args, **kwargs): + """Wrap pyscript.display so output lands in the example target.""" + return _display( + *args, **kwargs, target=__pyscript_display_target__, + ) + + +ipython = types.ModuleType("IPython") +core = types.ModuleType("IPython.core") +core_display = types.ModuleType("IPython.core.display") +core_display.display = display +core_display.HTML = HTML +ipython.core = core +core.display = core_display +ipython.get_ipython = lambda: None +ipython.display = core_display +sys.modules["IPython"] = ipython +sys.modules["IPython.core"] = core +sys.modules["IPython.core.display"] = core_display +sys.modules["IPython.display"] = core_display + + +def heading(text, level=2): + display(HTML(f"{text}"), append=True) + + +def note(text): + display(HTML(f"

{text}

"), append=True) + + +import numpy as np +import matplotlib.pyplot as plt +import mne + +# Quiet MNE's default INFO chatter so the example output stays focused. +mne.set_log_level("WARNING") + +rng = np.random.default_rng(7) From c2328106cb316a72fceeaed3898189af98447656 Mon Sep 17 00:00:00 2001 From: "Nicholas H.Tollervey" Date: Thu, 4 Jun 2026 16:30:17 +0100 Subject: [PATCH 2/2] Fix imports. --- examples/mne/raw_eeg_basics/code.py | 9 +++++++++ examples/mne/raw_eeg_basics/setup.py | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/mne/raw_eeg_basics/code.py b/examples/mne/raw_eeg_basics/code.py index 295cab9..7baccec 100644 --- a/examples/mne/raw_eeg_basics/code.py +++ b/examples/mne/raw_eeg_basics/code.py @@ -8,6 +8,15 @@ Here we build both from scratch using NumPy arrays. """ from IPython.core.display import display, HTML +import numpy as np +import matplotlib.pyplot as plt +import mne + +# Quiet MNE's default INFO chatter so the example output stays focused. +mne.set_log_level("WARNING") + +rng = np.random.default_rng(7) + heading("1. Building a synthetic EEG recording") note( diff --git a/examples/mne/raw_eeg_basics/setup.py b/examples/mne/raw_eeg_basics/setup.py index d379915..07879f9 100644 --- a/examples/mne/raw_eeg_basics/setup.py +++ b/examples/mne/raw_eeg_basics/setup.py @@ -40,12 +40,3 @@ def heading(text, level=2): def note(text): display(HTML(f"

{text}

"), append=True) - -import numpy as np -import matplotlib.pyplot as plt -import mne - -# Quiet MNE's default INFO chatter so the example output stays focused. -mne.set_log_level("WARNING") - -rng = np.random.default_rng(7)