Skip to content

Commit 01aa3f0

Browse files
committed
final formatting (ask Dan regarding ouputs)
1 parent 722692e commit 01aa3f0

1 file changed

Lines changed: 52 additions & 47 deletions

File tree

tutorials/inverse/80_brainstorm_phantom_elekta.py

Lines changed: 52 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
that generates known magnetic signals,
1313
allowing validation and benchmarking of MEG system accuracy and analysis methods.
1414
15-
The aim of this tutorial is to learn how to use phantom recordings to
16-
evaluate source localisation methods by comparing estimated vs real dipole positions.
15+
The aim of this tutorial is to demonstrate how phantom recordings can be used to
16+
evaluate source localisation methods by comparing estimated and true dipole positions.
1717
1818
For comparison, see :footcite:`TadelEtAl2011` and
1919
`the original Brainstorm tutorial
@@ -28,7 +28,6 @@
2828
# Copyright the MNE-Python contributors.
2929

3030
# %%
31-
3231
import matplotlib.pyplot as plt
3332
import numpy as np
3433
from scipy.signal import find_peaks
@@ -39,60 +38,61 @@
3938
from mne.datasets.brainstorm import bst_phantom_elekta
4039
from mne.io import read_raw_fif
4140

42-
print(__doc__)
4341
# %%
4442
# Load and prepare the data
4543
# -------------------------------
44+
4645
# The data were collected with an Elekta Neuromag VectorView system
4746
# at 1000 Hz and low-pass filtered at 330 Hz.
48-
# The dataset has recordings at 3 different current amplitudes (20, 200, and 2000 nAm),
49-
# here we will load the medium-amplitude current.
47+
#
48+
# The dataset contains recordings at three current amplitudes (20, 200, and 2000 nAm).
49+
# Here we load the medium-amplitude condition.
5050
data_path = bst_phantom_elekta.data_path(verbose=True)
5151
raw_fname = data_path / "kojak_all_200nAm_pp_no_chpi_no_ms_raw.fif"
5252
raw = read_raw_fif(raw_fname)
53-
raw.info["bads"] = ["MEG1933", "MEG2421"] # known bad channels
53+
54+
# Mark known bad channels
55+
raw.info["bads"] = ["MEG1933", "MEG2421"]
56+
5457
events = find_events(raw, "STI201")
55-
# The first 32 events are the dipole phantoms recorded.
56-
# The remaining event IDs are not relevant for this tutorial.
58+
59+
# The first 32 events correspond to dipole activations.
60+
5761
# %%
58-
# Here we epoch the data around dipole events
59-
# and baseline correct the epochs from -100 ms to -0.05 ms before stimulus onset.
62+
# Epoch the data and plot evokeds
63+
# ---------------
64+
#
65+
# We epoch the data around dipole events and apply baseline correction.
66+
6067
bmax = -0.05
6168
tmin, tmax = -0.1, 0.8
6269
event_id = list(range(1, 33))
70+
6371
epochs = mne.Epochs(
6472
raw, events, event_id, tmin, tmax, baseline=(None, bmax), preload=False
6573
)
66-
# %%
67-
# Dipole fitting on phantom data
68-
# -------------------------------
69-
# Next, we fit dipoles based on the epoched phantom recordings and
70-
# compare the estimated to the known dipole locations built into the phantom.
71-
72-
# We use the first dipole event to plot the evoked signal
73-
# and skip the first and the last epoch as they are corrupted.
74-
74+
# Plot evoked response for the first clean dipole
7575
epochs["1"][1:-1].average().plot(time_unit="s")
76-
77-
# We average over 18 simulated events for the first dipole.
76+
# %%
7877
# In this data the phantom was set to produce 20 Hz sinusoidal bursts of current.
7978
# You can see that the burst envelope repeats at approximately 3 Hz.
80-
# %%
81-
# First, we need to determine the timepoint of the peak amplitude.
82-
# To find this timepoint, we compute Global Field Power (GFP),
83-
# which is the standard deviation of all EEG electrode voltages at a
84-
# specific timepoint.
79+
#
80+
# Determine peak activation using Global Field Power (GFP)
81+
# --------------------------------------------------------
82+
83+
# GFP is the standard deviation across sensors at each time
84+
# point, providing a reference-independent measure of signal strength.
8585
evoked_tmp = epochs["1"][1:-1].average()
8686
gfp = np.std(evoked_tmp.data, axis=0)
8787
times = evoked_tmp.times
8888

89-
# We restrict the time window to find the peak
90-
# for the first bursting event.
89+
# Restrict to first burst window
9190
time_mask = (times > 0) & (times <= 0.1)
91+
9292
peaks, _ = find_peaks(gfp[time_mask])
93-
# Convert to original indices
9493
peak_indices = np.where(time_mask)[0][peaks]
95-
# Select the strongest peak (max GFP)
94+
95+
# Select the strongest peak
9696
strongest_peak_idx = peak_indices[np.argmax(gfp[peak_indices])]
9797
t_peak = times[strongest_peak_idx]
9898

@@ -107,7 +107,8 @@
107107
# %%
108108
# Next, we need to compute the noise covariance to capture the sensor noise structure.
109109
# We use the baseline window to estimate covariance.
110-
# You can explore the covariance tutorial for details :ref:`tut-compute-covariance`.
110+
#
111+
# You can explore the covariance tutorial for details: :ref:`tut-compute-covariance`.
111112

112113
cov = mne.compute_covariance(epochs, tmax=bmax)
113114

@@ -120,37 +121,38 @@
120121
sphere = mne.make_sphere_model(r0=(0.0, 0.0, 0.0), head_radius=0.08)
121122

122123
# %%
123-
# We finally fit all 32 phantom dipoles and store them
124-
# as well as the residuals in a list.
124+
# Fit dipoles
125+
# -----------
125126

127+
# We fit dipoles for each phantom and store them in a list.
126128
dip_all, residuals_all = [], []
127129

128130
for evoked in evokeds:
129131
dip, residual = fit_dipole(evoked, cov, sphere, n_jobs=1)
130132
dip_all.append(dip)
131133
residuals_all.append(residual)
132134
# %%
133-
# Let's visualize the explained variance.
134-
# The dipole object stores the goodness of fit (GOF) for each dipole.
135-
gof = [dip.gof[0] for dip in dip_all] # array of GOF values
135+
# Evaluate goodness of fit
136+
# -----------------------
136137

137-
# Define colorblind-friendly colors
138+
# The dipole object stores the goodness of fit (GOF) for each dipole.
139+
gof = [dip.gof[0] for dip in dip_all]
138140
colors = ["#E69F00" if val < 60 else "#0072B2" for val in gof]
139-
# orange = low, blue = high (from Okabe-Ito palette)
140-
141141
plt.bar(event_id, gof, color=colors)
142142
plt.xlabel("Phantom dipole estimation")
143143
plt.ylabel("Goodness of fit (%)")
144144
plt.show()
145+
145146
# %%
146-
# We can see that GOF varies between 50 and more than 95 %
147-
# variance explained for dipoles.
147+
#
148+
# We can see that GOF varies between 50 % and up to 95 %.
149+
#
150+
# Compare estimated and true dipoles
151+
# --------------------------------
148152

149-
# Estimated vs true dipole locations
150-
# -------------------------------
151-
# Finally, we compare the estimated to the true dipole locations.
152153
actual_pos, actual_ori = mne.dipole.get_phantom_dipoles()
153154
actual_amp = 200.0 # nAm
155+
154156
# estimated dipoles
155157
dip_pos = [dip.pos[0] for dip in dip_all]
156158
dip_ori = [dip.ori[0] for dip in dip_all]
@@ -186,8 +188,9 @@
186188
# The dipole fits closely match the true phantom data,
187189
# achieving sub-centimeter accuracy (mean position error 2.7mm).
188190
#
189-
# Finally, we can plot the positions and the orientations
190-
# of the estimated and true dipoles.
191+
# Visualise estimated and true dipoles
192+
# -----------------------
193+
191194
actual_amp = np.ones(len(dip)) # fake amp, needed to create Dipole instance
192195
actual_gof = np.ones(len(dip)) # fake goodness-of-fit (GOF)
193196
# setup dipole objects for true and estimated dipoles
@@ -208,10 +211,12 @@
208211
show_axes=True,
209212
subjects_dir=subjects_dir,
210213
)
214+
211215
# Plot the position and the orientation of the true dipole in black
212216
fig = mne.viz.plot_dipole_locations(
213217
dipoles=dip_true, mode="arrow", subject=subject, color=(0.0, 0.0, 0.0), fig=fig
214218
)
219+
215220
# Plot the position and the orientation of the estimated dipole in green
216221
fig = mne.viz.plot_dipole_locations(
217222
dipoles=dip_estimated, mode="arrow", subject=subject, color=(0.2, 1.0, 0.5), fig=fig

0 commit comments

Comments
 (0)